SOLID Principles in Large-Scale C++
SOLID is an acronym for five design principles intended to make software designs more understandable, flexible, and maintainable. In senior C++ interviews, you must know how to refactor "Bad" code into "Good" code using these principles.
1. Single Responsibility Principle (SRP)
A class should have one, and only one, reason to change. If a class handles two completely different responsibilities, a change in one requirement forces a recompilation and potential bug in the other.
Bad Design (Violation):
class Document {
private:
std::string content;
public:
void addText(const std::string& text) { content += text; }
// VIOLATION: Document handles data AND formatting/printing
void printAsPDF() {
// complex PDF rendering logic
}
};Good Design (Refactored):
class Document {
private:
std::string content;
public:
void addText(const std::string& text) { content += text; }
std::string getContent() const { return content; }
};
class PDFPrinter {
public:
// Single responsibility: Only handles printing.
void print(const Document& doc) {
// complex PDF rendering logic using doc.getContent()
}
};2. Open/Closed Principle (OCP)
Software entities (classes, modules, functions) should be open for extension, but closed for modification. You should be able to add new functionality without touching existing, tested code.
Bad Design (Violation):
class GraphicEditor {
public:
// VIOLATION: Every time we add a new shape, we MUST modify this function!
void drawShape(int shapeType) {
if (shapeType == 1) drawCircle();
else if (shapeType == 2) drawSquare();
// else if (shapeType == 3) drawTriangle(); // Requires editing existing code
}
};Good Design (Refactored using Polymorphism):
class Shape {
public:
virtual void draw() = 0; // Interface
virtual ~Shape() = default;
};
class Circle : public Shape {
public:
void draw() override { /* draw circle */ }
};
class GraphicEditor {
public:
// OCP ACHIEVED: We never need to modify this function again,
// even if we add 100 new shapes!
void drawShape(Shape& s) {
s.draw();
}
};3. Liskov Substitution Principle (LSP)
Objects of a superclass shall be replaceable with objects of its subclasses without breaking the application. A child class must honor the contract established by the base class.
The Classic Rectangle/Square Violation:
class Rectangle {
protected:
int width, height;
public:
virtual void setWidth(int w) { width = w; }
virtual void setHeight(int h) { height = h; }
int getArea() const { return width * height; }
};
class Square : public Rectangle {
public:
// VIOLATION: A Square must have equal sides.
// But overriding this breaks the behavior a client expects from a Rectangle!
void setWidth(int w) override { width = height = w; }
void setHeight(int h) override { width = height = h; }
};
// Client Code Expectation
void scaleRectangle(Rectangle& r) {
r.setWidth(10);
r.setHeight(5);
// Client expects area to be 50.
// If we passed a Square, height=5 overwrites width=5. Area becomes 25!
// The program is broken. LSP is violated.
}4. Interface Segregation Principle (ISP)
No client should be forced to depend on methods it does not use. Break fat interfaces into smaller, highly cohesive interfaces.
Bad Design (Violation):
class IMachine {
public:
virtual void print() = 0;
virtual void scan() = 0;
virtual void fax() = 0;
};
class SimplePrinter : public IMachine {
public:
void print() override { /* printing logic */ }
// VIOLATION: Forced to implement useless methods!
void scan() override { throw std::logic_error("Cannot scan"); }
void fax() override { throw std::logic_error("Cannot fax"); }
};Good Design (Refactored):
class IPrinter { public: virtual void print() = 0; };
class IScanner { public: virtual void scan() = 0; };
class IFax { public: virtual void fax() = 0; };
class SimplePrinter : public IPrinter {
public:
void print() override { /* printing logic */ }
};
class SuperCopier : public IPrinter, public IScanner {
public:
void print() override { /* printing logic */ }
void scan() override { /* scanning logic */ }
};5. Dependency Inversion Principle (DIP)
Depend upon abstractions, not concretions. High-level modules should not depend on low-level modules. Both should depend on abstractions.
Bad Design (Violation):
class V8Engine {
public:
void start() {}
};
class Car {
private:
V8Engine engine; // VIOLATION: Tightly coupled to a specific engine.
public:
void turnKey() { engine.start(); }
};Good Design (Refactored with Dependency Injection):
class IEngine {
public:
virtual void start() = 0;
virtual ~IEngine() = default;
};
class V8Engine : public IEngine {
public:
void start() override { /* start V8 */ }
};
class ElectricEngine : public IEngine {
public:
void start() override { /* start Electric */ }
};
class Car {
private:
IEngine* engine; // DEPENDS ON ABSTRACTION
public:
// Dependency Injection
Car(IEngine* e) : engine(e) {}
void turnKey() { engine->start(); }
};