Skip to Content
Course content

34: Default Trait Implementations

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

I've seen this happen in a lot of production codebases: a developer defines a trait, then spends the next three hours writing the exact same logic over and over again for every single struct that implements that trait. It's not a compiler error, but it's a "maintenance bug"—the kind of debt that bites you the moment you decide to change a single string or a logic gate in ten different places.

The Redundancy Trap

trait Logger {
    fn log(&self, message: &str);
    fn log_warning(&self, message: &str);
}

struct ConsoleLogger;
impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("[LOG]: {}", message);
    }
    fn log_warning(&self, message: &str) {
        println!("[WARNING]: {}", message);
    }
}

struct FileLogger;
impl Logger for FileLogger {
    fn log(&self, message: &str) {
        // Imagine complex file writing logic here
        println!("Writing to file: {}", message);
    }
    fn log_warning(&self, message: &str) {
        println!("[WARNING]: {}", message);
    }
}

Take a look at log_warning. In both ConsoleLogger and FileLogger, it's doing the exact same thing: wrapping the message in a [WARNING] tag. If your boss comes to you tomorrow and says, "Actually, we need warnings to start with ⚠️ ALERT:", you now have to hunt down every single implementation of Logger in your project and change that string. That's a recipe for missing one and creating an inconsistent UI.

Centralizing Logic with Default Implementations

Rust lets you provide a default body for a method directly inside the trait definition. If a struct implements the trait but doesn't provide its own version of that method, Rust just uses the default. This effectively allows you to define "standard" behavior while still leaving the door open for customization.

Here is how we clean up that Logger mess:

trait Logger {
    // This remains required because every logger writes differently
    fn log(&self, message: &str);

    // This is now a default implementation
    fn log_warning(&self, message: &str) {
        self.log(&format!("[WARNING]: {}", message));
    }
}

struct ConsoleLogger;
impl Logger for ConsoleLogger {
    fn log(&self, message: &str) {
        println!("[LOG]: {}", message);
    }
    // No need to implement log_warning here!
}

struct FileLogger;
impl Logger for FileLogger {
    fn log(&self, message: &str) {
        println!("Writing to file: {}", message);
    }
    // No need to implement log_warning here either!
}

Notice what I did in the default implementation: I called self.log(). This is a powerful pattern. The default method handles the "formatting" logic, but it delegates the actual "writing" (the part that changes between structs) to the method that must be implemented. Now, if you want to change the warning prefix, you change it in one line, and every logger in your system is updated instantly.

Overriding the Default for Special Cases

Now, you might be thinking, "What if one specific logger needs to do something totally different for warnings?" Maybe your SmsLogger needs to send a high-priority flag to an API instead of just adding a string prefix.

The beauty of default implementations is that they are optional. You can simply override them in the impl block:

struct SmsLogger;
impl Logger for SmsLogger {
    fn log(&self, message: &str) {
        println!("Sending SMS: {}", message);
    }

    // We don't like the default [WARNING] prefix for SMS, 
    // so we provide our own specific logic.
    fn log_warning(&self, message: &str) {
        println!("Sending HIGH-PRIORITY SMS: {}", message);
    }
}

By doing this, SmsLogger ignores the default logic and uses its own, while ConsoleLogger and FileLogger continue to use the shared logic. You've essentially created a "sane default" that stays out of your way until you actually need it.




📋 Practical Task

Building a Customizable Game Character Ability System

You are building a combat system for an RPG. Every character has a Character trait. All characters must implement a basic attack method, but you want most characters to share a default taunt method that prints "I'm going to win!". However, some characters should have unique taunts.

Your Task:

  • Define a trait Character with a required method attack(&self) and a default method taunt(&self).
  • Create a struct Warrior that implements Character using the default taunt.
  • Create a struct Rogue that implements Character but overrides the default taunt with something more stealthy (e.g., "You won't even see me coming...").
  • In your main function, create instances of both and call both attack() and taunt() on each to verify that the Warrior uses the default and the Rogue uses the override.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.