Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

08: Interfaces and Protocols

Audience: All
Time: 120 minutes
Prerequisites: 07-Classes-and-Instances
You'll learn: Define interfaces, implement contracts, polymorphism, real-world patterns


The Big Picture

Interfaces define contracts—promises about what an object can do, without specifying how. Instead of depending on concrete classes, you depend on interfaces. This enables:

  • Swappable implementations — Change how something works without changing callers
  • Polymorphism — Treat different objects the same way
  • Testing — Mock implementations easily
  • Flexibility — Add new implementations without modifying existing code

Defining Interfaces

Simple Interface

# file: 08_interface_basic.zbr
# teaches: interface definition
# chapter: 08-Interfaces-and-Protocols

interface Animal
    def speak(): str
    def move()

class Dog implements Animal
    def speak(): str
        return "Woof!"

    def move()
        print("Running on four legs")

class Bird implements Animal
    def speak(): str
        return "Tweet!"

    def move()
        print("Flying through the air")

def main()
    var dog: Animal = Dog()
    print(dog.speak())  # Woof!
    dog.move()             # Running on four legs

    var bird: Animal = Bird()
    print(bird.speak())  # Tweet!
    bird.move()            # Flying through the air

Key points:

  • interface Animal — Define what any animal must do
  • implements Animal goes on the class line itself (class Dog implements Animal) — Promise to implement all methods
  • To implement more than one interface, list them on the same line: class Dog implements Animal, Trainable
  • Treat objects as their interface type (var dog: Animal)

Multiple Methods

# file: 08_interface_methods.zbr
# teaches: interface with multiple methods
# chapter: 08-Interfaces-and-Protocols

interface PaymentProcessor
    def process(amount: float): bool
    def refund(transaction_id: str): bool
    def get_status(transaction_id: str): str

class CreditCardProcessor implements PaymentProcessor
    def process(amount: float): bool
        print("Processing credit card: ${amount}")
        return true
        
    def refund(transaction_id: str): bool
        print("Refunding transaction: ${transaction_id}")
        return true
        
    def get_status(transaction_id: str): str
        return "completed"

class PayPalProcessor implements PaymentProcessor
    def process(amount: float): bool
        print("Processing PayPal: ${amount}")
        return true
        
    def refund(transaction_id: str): bool
        print("PayPal refund: ${transaction_id}")
        return true
        
    def get_status(transaction_id: str): str
        return "pending"

Polymorphism in Action

Using Different Implementations

# file: 08_polymorphism.zbr
# teaches: polymorphic behavior
# chapter: 08-Interfaces-and-Protocols

def process_payment(processor: PaymentProcessor, amount: float)
    if processor.process(amount)
        print("Payment successful")
    else
        print("Payment failed")

def main()
    var cc_processor = CreditCardProcessor()
    var paypal_processor = PayPalProcessor()

    # Same code, different behavior
    process_payment(cc_processor, 99.99)
    process_payment(paypal_processor, 49.99)

Collections of Interface Types

# file: 08_collection_interface.zbr
# teaches: storing different implementations
# chapter: 08-Interfaces-and-Protocols

class Zoo
    var animals: List(Animal) = List(Animal)()

    def add_animal(animal: Animal)
        this.animals.add(animal)

    def make_them_speak()
        for animal in this.animals
            print(animal.speak())

    def exercise_all()
        for animal in this.animals
            animal.move()

def main()
    var zoo = Zoo()
    zoo.add_animal(Dog())
    zoo.add_animal(Bird())
    zoo.add_animal(Dog())

    zoo.make_them_speak()
    zoo.exercise_all()

Real World: Logging System

# file: 08_logger_system.zbr
# teaches: realistic interface use
# chapter: 08-Interfaces-and-Protocols

interface Logger
    def debug(message: str)
    def info(message: str)
    def warn(message: str)
    def log_error(message: str)

class ConsoleLogger implements Logger
    def debug(message: str)
        print("[DEBUG] ${message}")
        
    def info(message: str)
        print("[INFO] ${message}")
        
    def warn(message: str)
        print("[WARN] ${message}")
        
    def log_error(message: str)
        print("[ERROR] ${message}")

class FileLogger implements Logger
    def debug(message: str)
        # Write to file: [DEBUG] message
        
    def info(message: str)
        # Write to file: [INFO] message
        
    def warn(message: str)
        # Write to file: [WARN] message
        
    def log_error(message: str)
        # Write to file: [ERROR] message

class Application
    var logger: Logger

    def set_logger(l: Logger)
        this.logger = l

    def do_work()
        this.logger.info("Starting work")
        # Do work
        this.logger.info("Work complete")

def main()
    var app = Application()

    # Use console logger
    app.set_logger(ConsoleLogger())
    app.do_work()

    # Switch to file logger (same code, different output)
    app.set_logger(FileLogger())
    app.do_work()

Why log_error, not error? error is a plain identifier in Zebra itself, but an interface's methods are also emitted as a Zig vtable field name, and error is a Zig keyword there — interface Logger with a plain def error(...) type-checks but fails at build with a Zig parse error (expected '.', found ':'). It's a gap in the compiler's keyword-escaping for interface vtables specifically (BUG to file), not a rule of the language — but until it's fixed, avoid error as an interface method name.


Common Patterns

Strategy Pattern

interface SortStrategy
    def sort(items: List(int))

class AscendingSort implements SortStrategy
    def sort(items: List(int))
        # Sort ascending

class DescendingSort implements SortStrategy
    def sort(items: List(int))
        # Sort descending

class Sorter
    var strategy: SortStrategy

    def set_strategy(s: SortStrategy)
        this.strategy = s

    def sort(items: List(int))
        this.strategy.sort(items)

Adapter Pattern

interface NewSystem
    def process(data: str)

class OldSystem
    def old_process(input: str)
        # Old implementation
        pass

class OldSystemAdapter implements NewSystem
    var old_system: OldSystem = OldSystem()

    def process(data: str)
        # Adapt new interface to old system
        this.old_system.old_process(data)

If you're new to programming

An interface is like a promise or contract. When you say a class "implements" an interface, you promise that it has all the methods the interface requires.

This lets you write code that works with any object that implements that interface, without knowing exactly which class it is.

Polymorphism means "many forms"—the same code can work with different types of objects.

If you know Python

# Python (using duck typing)
class Dog:
    def speak(self):
        return "Woof!"

class Bird:
    def speak(self):
        return "Tweet!"

def get_sound(animal):
    return animal.speak()
# Zebra (using interfaces)
interface Animal
    def speak(): str

class Dog implements Animal
    def speak(): str
        return "Woof!"

class Bird implements Animal
    def speak(): str
        return "Tweet!"

def get_sound(animal: Animal): str
    return animal.speak()

Python relies on duck typing ("if it quacks like a duck"). Zebra makes the contract explicit with interfaces.


Common Mistakes

❌ Mistake: Not implementing all interface methods

interface Animal
    def speak(): str
    def move()

class Dog implements Animal
    def speak(): str
        return "Woof!"
    # ❌ Missing: def move

💡 Why: The compiler requires all methods. You're breaking the contract.

✅ Better:

class Dog implements Animal
    def speak(): str
        return "Woof!"
    def move()
        print("Running")

❌ Mistake: Wrong method signature

interface PaymentProcessor
    def process(amount: float): bool

class CreditCard implements PaymentProcessor
    def process(amount: int): bool  # ❌ int, not float
        return true

✅ Better:

class CreditCard implements PaymentProcessor
    def process(amount: float): bool  # ✅ Matches interface
        return true

❌ Mistake: Forgetting to declare implementation

class Dog  # ❌ Doesn't say implements Animal
    def speak(): str
        return "Woof!"

✅ Better:

class Dog implements Animal  # ✅ Explicit contract
    def speak(): str
        return "Woof!"

Exercises

Exercise 1: Shape Interface

Create an interface for shapes and multiple implementations:

Solution
interface Shape
    def area(): float
    def perimeter(): float

class Circle implements Shape
    var radius: float = 0.0

    def area(): float
        return 3.14 * this.radius * this.radius
    def perimeter(): float
        return 2.0 * 3.14 * this.radius

class Rectangle implements Shape
    var width: float = 0.0
    var height: float = 0.0

    def area(): float
        return this.width * this.height
    def perimeter(): float
        return 2.0 * (this.width + this.height)

def print_shape_info(shape: Shape)
    print("Area: ${shape.area()}")
    print("Perimeter: ${shape.perimeter()}")

def main()
    var circle = Circle()
    circle.radius = 5.0
    print_shape_info(circle)

    var rect = Rectangle()
    rect.width = 10.0
    rect.height = 5.0
    print_shape_info(rect)

Exercise 2: Database Interface

Create a database interface with multiple implementations:

Solution
interface Database
    def save(key: str, value: str): bool
    def load(key: str): str?
    def delete(key: str): bool

class MemoryDatabase implements Database
    var data: HashMap(str, str) = HashMap(str, str)()

    def save(key: str, value: str): bool
        this.data.put(key, value)
        return true
    def load(key: str): str?
        return this.data.get(key)
    def delete(key: str): bool
        this.data.remove(key)
        return true

def main()
    var db: Database = MemoryDatabase()
    db.save("user1", "Alice")
    db.save("user2", "Bob")

    if db.load("user1") as user
        print("Found: ${user}")

    db.delete("user1")

Exercise 3: Document Processor

Create an interface for document processors:

Solution
interface DocumentProcessor
    def process(content: str): str
    def validate(content: str): bool

class MarkdownProcessor implements DocumentProcessor
    def process(content: str): str
        # Convert markdown to HTML
        return "<html>${content}</html>"
    def validate(content: str): bool
        return content.len > 0

class JSONValidator implements DocumentProcessor
    def process(content: str): str
        return content  # Already valid JSON
    def validate(content: str): bool
        return content.contains("{") and content.contains("}")

def process_document(processor: DocumentProcessor, doc: str)
    if processor.validate(doc)
        var processed = processor.process(doc)
        print("Processed: ${processed}")
    else
        print("Invalid document")

def main()
    var md = MarkdownProcessor()
    process_document(md, "# Hello")

    var json = JSONValidator()
    process_document(json, "{}")

Next Steps

  • → 09-Composition-and-Mixins — Sharing behaviour without inheritance
  • → 14-Contracts — More complex interfaces
  • 🏋 Project-2-HTTP-Server — Interfaces for handlers and middleware

Key Takeaways

  • Interfaces define contracts — What methods must exist, not how they work
  • Polymorphism lets you write code once, work with many types
  • Swap implementations without changing callers
  • Testing is easier — Mock implementations implement the interface
  • Makes code flexible — Add new implementations without modifying existing code
  • Forces clear design — Interfaces make relationships explicit

Next: Head to 09-Composition-and-Mixins for code reuse without classical inheritance.