Interfaces & Traits
Interfaces and traits provide ways to share behavior and define contracts in Strata.
Interfaces
Interfaces define contracts that classes must implement. They specify what methods a class must have, but not how to implement them.
Basic Syntax
interface Greeter {
public fn greet(): String;
}
class User(name: String) impl Greeter {
public fn greet(): String {
return "Hello, ${this.name}!";
}
}Multiple Interfaces
A class can implement multiple interfaces:
interface Greeter {
public fn greet(): String;
}
interface Named {
public fn getName(): String;
}
class User(name: String) impl Greeter, Named {
public fn greet(): String {
return "Hello, ${this.name}!";
}
public fn getName(): String {
return this.name;
}
}Interface Methods
Interface methods have no body:
interface Drawable {
public fn draw(): Void;
public fn getArea(): Float;
}Traits
Traits provide reusable behavior that can be shared across classes. Unlike interfaces, traits can have method implementations.
Basic Syntax
trait Greeter {
public fn greet(): String {
return "Hello!";
}
}
class User(name: String) with Greeter {
// User now has the greet() method
}
let user = User(name: "Luna");
print(user.greet()); // "Hello!"Trait Rules
Traits have specific rules:
- No state: Traits cannot have properties
- Behavior only: Traits provide methods, not data
// valid trait
trait Loggable {
public fn log(): Void;
public static fn info(message: String): Void;
}
// invalid: traits cannot have properties
trait InvalidTrait {
property: String; // error
}Multiple Traits
Classes can use multiple traits:
trait Greeter {
public fn greet(): String {
return "Hello!";
}
}
trait Logger {
public fn log(message: String): Void {
print("[LOG] ${message}");
}
}
class User(name: String) with Greeter, Logger {
// User has both greet() and log() methods
}Overriding Trait Methods
Classes can override trait methods:
trait Greeter {
public fn greet(): String {
return "Hello!";
}
}
class User(name: String) with Greeter {
public fn greet(): String {
return "Hello, ${this.name}!";
}
}Interfaces vs Traits
| Feature | Interface | Trait |
|---|---|---|
| Method implementations | No | Yes |
| Properties | No | No |
| Multiple inheritance | Yes | Yes |
| Purpose | Define contracts | Share behavior |
Combining Interfaces and Traits
You can use both interfaces and traits together:
interface Identifiable {
public fn getId(): Int;
}
trait Timestamps {
public fn getCreatedAt(): String {
return "2025-01-01";
}
}
class User(name: String, id: Int) with Timestamps impl Identifiable {
public fn getId(): Int {
return this.id;
}
}For classes that implement many interfaces or use multiple traits, you can use multi-line declarations:
class MyController
with Logger, Greeter, Validator
impl Controller, JsonSerializable, Dispatchable
{
// ...
}Constants
Similarly to classes, both interfaces and traits can define constants:
interface Protocol {
const PORT = 8000;
const TIMEOUT = 30;
}
trait Config {
const MAX_RETRIES = 3;
}These constants are accessible through any class that implements the interface or uses the trait:
class MyServer with Config impl Protocol {
}
print(MyServer::PORT); // 8000
print(MyServer::MAX_RETRIES); // 3Real-World Examples
Example: Repository Pattern
interface Repository<T> {
public fn find(id: Int): Option<T>;
public fn save(entity: T): Void;
public fn delete(id: Int): Void;
}
trait SoftDeletes {
public fn softDelete(id: Int): Void {
// implementation
}
public fn restore(id: Int): Void {
// implementation
}
}
class UserRepository with SoftDeletes impl Repository<User> {
public fn find(id: Int): Option<User> {
// implementation
}
public fn save(entity: User): Void {
// implementation
}
public fn delete(id: Int): Void {
// implementation
}
}Example: Validation
class ValidationError(message: String) impl Failure {
}
interface Validatable {
public fn validate(): Result<Void, ValidationError>;
}
// ...
class User(email: String) with EmailValidator impl Validatable {
public fn validate(): Result<Void, ValidationError> {
if !this.isValidEmail(email: this.email) {
return Err(ValidationError(message: "Invalid email"));
}
return Ok(());
}
}Best Practices
- Use interfaces to define contracts and ensure classes implement required methods
- Use traits to share common behavior across multiple classes
- Keep traits focused on a single responsibility
- Prefer composition: use traits and interfaces over deep inheritance
- Document interfaces: clearly state what implementing classes must do
Next Steps
- Classes - How classes work with interfaces and traits
- Functions - Method syntax
- Error Handling - Using Result types in interfaces