Skip to Content
Course content

24: Implementing Traits on Structs and Enums

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was reviewing a PR for a teammate who was building a telemetry system. He had created three different structs—SystemLog, NetworkLog, and ApplicationLog—and for every single one, he'd written a nearly identical method called to_summary_string(). When he wanted to print a list of all logs regardless of their type, he ended up writing a massive, nested match statement that was frankly a nightmare to maintain. He asked me why he couldn't just call a single method on a collection of different log types. The answer was simple: he had the data, but he hadn't defined the behavior as a trait.

Giving Your Structs New Abilities

When you define a trait, you're essentially creating a contract. You're saying, "Any type that implements this trait promises to provide these specific functions." Implementing a trait on a struct is where the magic happens because it allows your code to stop caring about what a type is and start caring about what it can do.

Let's look at a more concrete example. Suppose we're building a notification system. We have different ways to send alerts, and we want a unified way to "dispatch" them.

trait Dispatchable {
    fn dispatch(&self);
}

struct EmailNotification {
    email_address: String,
    subject: String,
    body: String,
}

struct SMSNotification {
    phone_number: String,
    message: String,
}

impl Dispatchable for EmailNotification {
    fn dispatch(&self) {
        println!("Sending email to {}: {}", self.email_address, self.subject);
    }
}

impl Dispatchable for SMSNotification {
    fn dispatch(&self) {
        println!("Sending SMS to {}: {}", self.phone_number, self.message);
    }
}

By implementing Dispatchable for both, we've decoupled the logic. Now, I can write a function that takes any type that implements Dispatchable, and it doesn't matter if it's an email, a text, or some future Slack integration we haven't even thought of yet. I've personally found that this is the single best way to avoid "spaghetti code" as a project grows.

Handling Variants with Trait Implementations

Now, you might wonder if this works for enums. It does, and it's actually incredibly powerful. When you implement a trait on an enum, you typically use a match statement inside the trait method to define how each variant should behave. This is where Rust really shines compared to traditional OOP inheritance.

Let's say our notification system has different priority levels, and we want to format a priority label for a dashboard. Instead of a standalone function, we'll use a trait.

trait Labelable {
    fn get_label(&self) -> String;
}

enum Priority {
    Low,
    Medium,
    High,
    Critical,
}

impl Labelable for Priority {
    fn get_label(&self) -> String {
        match self {
            Priority::Low => "🟢 Low Priority".to_string(),
            Priority::Medium => "🟡 Medium Priority".to_string(),
            Priority::High => "🟠 High Priority".to_string(),
            Priority::Critical => "🔴 CRITICAL".to_string(),
        }
    }
}

Notice that the impl block is for the Priority enum as a whole, not for individual variants. In Rust, variants aren't separate types; they are values of the enum type. This means the trait implementation acts as a centralized dispatcher for all those variants. It keeps your logic grouped together, which makes it much easier to track down bugs when a specific variant starts behaving weirdly.




📋 Practical Task

Exercise: Implementing a Unified Payment Processor

You are building a checkout system that needs to handle multiple payment methods. Your goal is to ensure that regardless of the payment method, the system can call a single method to process the transaction.

Requirements:

  • Define a trait called PaymentProcessor with a method process_payment(&self, amount: f64) -> bool.
  • Create a struct CreditCard with fields for card_number (String) and expiry_date (String).
  • Create a struct CryptoWallet with a field for wallet_address (String).
  • Implement the PaymentProcessor trait for both structs.
    • The CreditCard implementation should print "Processing credit card [number] for $[amount]" and return true.
    • The CryptoWallet implementation should print "Processing crypto payment from [address] for $[amount]" and return true.
  • Create an enum called PaymentStatus with variants Pending, Completed, and Failed.
  • Implement a trait called StatusDisplay for the PaymentStatus enum that returns a human-readable string (e.g., "Payment is currently Pending").
  • In your main function, create an instance of a CreditCard and a CryptoWallet, and call process_payment on both.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.