Course contentsShow
← Rust
Lesson 628 of 1,9105. Type System and TraitsFree lesson

Designing for Extension and Modification

Build systems that remain open for improvement but closed for breaking changes.

In this lesson, you will learn how to structure your code so that new features can be added without modifying existing, tested logic.

The Open-Closed Principle

The core of designing for extension is the Open-Closed Principle: software entities should be open for extension but closed for modification. In Rust, we achieve this by relying on abstractions (traits) rather than concrete implementations. If you write your functions to accept a trait instead of a specific struct, you allow users of your code to extend functionality by defining their own types that implement that trait, without ever touching your source code.

Decoupling Logic from Implementation

Consider a simple reporting system. If your function is hardcoded to work with a JsonReporter, you must modify that function every time you want to support a new format, like XML or CSV. Instead, define an Exporter trait:

trait Exporter {
    fn export(&self, data: &str);
}

// Your system remains "closed" to changes
fn run_report<T: Exporter>(exporter: T, data: &str) {
    exporter.export(data);
}

By using generics (<T: Exporter>), your run_report function is now "closed" to modification—you don't need to change it when you add new export formats. It is, however, "open" to extension; a developer can implement Exporter for a DatabaseExporter or a CloudExporter in their own crate.

Why This Matters

When your logic is locked to concrete types, every new requirement forces a modification to your existing code. This creates a "fragile" system where a change for a new feature risks breaking legacy functionality. By designing your architecture around traits, you ensure that your code acts as a stable foundation that others can build upon, rather than a monolithic block that needs constant alteration. You shift the burden of "adding features" from changing your core logic to implementing new trait instances, keeping your codebase clean, decoupled, and safe from regressions.

Key Takeaway: Design your systems to depend on traits rather than concrete types so that your core logic remains stable while your functionality grows through new implementations.