127. Refactor Driver–Bus Relationship

You are given a program that models drivers communicating over a hardware bus.

The current design is incorrect because:

  • Each driver creates its own bus
  • In real embedded systems, a single hardware bus is shared
  • Drivers incorrectly behave like buses

Your task is to fix the design without changing the behavior or output.

What the Program Must Achieve (End Goal):

  • There must be exactly one Bus
  • Two drivers must share the same Bus
  • Drivers must use the bus but must not be buses
  • The bus must start once and stop once

What Is Given to You:

  • A working but incorrectly designed program
  • Correct output format
  • An incorrect class relationship

What You Must Change (Very Explicit):

  • Driver must NOT inherit from Bus
  • Driver must contain or reference a Bus
  • ✅ Both drivers must use the same Bus object
  • ❌ Do NOT change input/output format
  • ❌ Do NOT add new features

In short:
Replace inheritance with composition so the bus can be shared.

Program Flow:

  1. Read four integers
    • Driver-1 address and value
    • Driver-2 address and value
  2. Create one Bus
  3. Create two Driver objects using the same Bus
  4. Each driver performs one write
  5. Program ends
  6. Bus shuts down once

Input:
Four integers

addr1 val1
addr2 val2

Output (Exact Order):

Bus ready
Driver started
Driver started
Bus write: <addr1> <val1>
Bus write: <addr2> <val2>
Driver stopped
Driver stopped
Bus stopped

Constraints:

  • Only one Bus object may exist
  • Bus must NOT be created inside Driver
  • Driver must NOT expose write() directly
  • No dynamic allocation
  • No smart pointers
  • No inheritance
  • Output must match exactly

 

 

Need Help? Refer to the Quick Guide below

These three concepts form the backbone of robust C++ architecture.

  1. Composition ("Has-a"): Instead of inheriting from classes ("Is-a"), you build complex objects by combining smaller, simpler objects. A Car has an Engine, it is not an Engine.
  2. RAII (Resource Acquisition Is Initialization): The "Golden Rule" of C++. Resources (Memory, Locks, Files) are acquired in the Constructor and released in the Destructor. This ties resource management to the Scope, guaranteeing cleanup.
  3. Ownership: Defines who is responsible for the lifetime of a resource. "If I own it, I delete it."

Syntax & Usage

1. Composition (Building Blocks)

Composition allows you to swap parts easily and keeps dependencies clear.

class PWM { 
public: 
    void set(int duty) { /* ... */ } 
};

class Encoder { 
public: 
    int read() { /* ... */ } 
};

// Composition: MotorController "Has-a" PWM and "Has-a" Encoder
class MotorController {
    PWM pwm_driver;      // Sub-component 1
    Encoder enc_driver;  // Sub-component 2

public:
    void set_speed(int speed) {
        int current = enc_driver.read(); // Delegate work
        pwm_driver.set(speed);
    }
};

2. RAII (The Scope Guard)

The most common embedded use case is managing interrupts or mutexes.

class ScopedIntLock {
public:
    ScopedIntLock()  { __disable_irq(); } // Acquire
    ~ScopedIntLock() { __enable_irq(); }  // Release
};

void critical_function() {
    ScopedIntLock lock; // Interrupts disabled HERE
    
    // ... do critical work ...
    
    if (error) return;  // Safe! Destructor re-enables interrupts automatically.
} // Destructor runs here naturally.

Ownership Model

Ownership TypeConceptC++ Tool
Owner"I control this life. When I die, it dies."std::unique_ptr or Member Variable.
Shared Owner"We keep this alive as long as one of us needs it."std::shared_ptr.
Observer"I just look at it. I hope it's still there."Raw Pointer (T*) or Reference (T&).

Relevance in Embedded/Firmware

1. Composition over Inheritance

Embedded hierarchies can get messy ("Is a SPI driver a Stream? Is it a Bus?").

Composition is cleaner. A Sensor class shouldn't inherit from I2C. It should contain a pointer/reference to an I2C driver. This allows you to easily swap the I2C driver for a SPI driver without rewriting the Sensor class.

2. Leak-Free Error Handling

In C, if an error occurs in the middle of a function, you must remember to free() memory or unlock() mutexes before every return.

With RAII, you just return. The compiler inserts the cleanup code for you (Stack Unwinding), making firmware crash-proof.

3. Clear Lifetimes

By using Composition and RAII, you avoid "dangling pointers" where a driver tries to access a hardware object that has already been de-initialized. If the parent object (Robot) dies, its children (Motors) are destroyed automatically and in the correct order.

Common Pitfalls (Practical Tips)

PitfallDetails
❌ Deep InheritanceAvoid creating 5-layer deep class hierarchies (Driver -> Serial -> UART -> STM32_UART). It increases code size (v-tables) and makes debugging a nightmare. Prefer Composition.
❌ Raw Pointers as Owners

Never use a raw pointer (T*) to own data.

drivers.add(new UART()); is a leak waiting to happen. Use unique_ptr for ownership, raw pointers only for observing.

❌ Circular DependencyIf Object A owns B, and Object B owns A (via shared_ptr), they will keep each other alive forever (Memory Leak). Use weak_ptr or raw references to break the cycle.
✅ Dependency Injection

Pass the "Sub-components" into the Constructor rather than creating them inside.

Motor(PWM& pwm) is better than creating a new PWM inside Motor, because it allows you to pass a MockPWM for testing.