SOLID Principle
SOLID is an acronym that stands for five key design principles that help software developers create code that is reusable, maintainable, and easy to understand. These principles are:
- Single Responsibility Principle: This principle states that a class should have only one reason to change, meaning it should do only one thing. For example, a class that handles user authentication should not also handle logging or sending emails. This way, the code is easier to test, debug, and modify without affecting other functionalities.
// A bad example of SRP violation
class User {
public:
// Constructor, getters, setters, etc.
void authenticate(); // Handles user authentication
void log(); // Handles logging
void sendEmail(); // Handles sending emails
};
// A good example of SRP compliance
class User {
public:
// Constructor, getters, setters, etc.
};
class Authenticator {
public:
void authenticate(User user); // Handles user authentication
};
class Logger {
public:
void log(User user); // Handles logging
};
class EmailSender {
public:
void sendEmail(User user); // Handles sending emails};
- Open-Closed Principle: This principle states that a class should be open for extension, but closed for modification. This means that the code should allow adding new features or behaviors without changing the existing ones. For example, a class that calculates the area of different shapes can be extended by adding new subclasses for new shapes, but the original class should not be modified to accommodate them. This way, the code is more flexible and less prone to errors.
// A bad example of OCP violation
class Shape {
public:
enum Type {circle, square};
Type type;
};
class AreaCalculator {
public:
double calculateArea(Shape shape) {
if (shape.type == Shape::circle) {
// Calculate area of circle
}
else if (shape.type == Shape::square) {
// Calculate area of square
}
// What if we want to add more shapes?
}
};
// A good example of OCP compliance
class Shape {
public:
virtual double calculateArea() = 0; // An abstract method
};
class Circle : public Shape {
public:
double calculateArea() override {
// Calculate area of circle
}
};
class Square : public Shape {
public:
double calculateArea() override {
// Calculate area of square
}
};
// We can add more subclasses for different shapes without modifying the base class
class AreaCalculator {
public:
double calculateArea(Shape shape) {
return shape.calculateArea(); // Polymorphism
}
};
- Liskov Substitution Principle: This principle states that a subclass should be able to replace its superclass without breaking the functionality of the program. This means that the subclass should follow the contract or specification of the superclass and not violate its assumptions or expectations. For example, a class that represents a rectangle should not inherit from a class that represents a square, because a rectangle is not always a square and may have different properties or behaviors. This way, the code is more consistent and reliable.
// A bad example of LSP violation
class Square {
protected:
int side;
public:
void setSide(int side) {
this->side = side;
}
int getSide() {
return side;
}
};
class Rectangle : public Square {
protected:
int width;
int height;
public:
void setWidth(int width) {
this->width = width;
}
void setHeight(int height) {
this->height = height;
}
int getWidth() {
return width;
}
int getHeight() {
return height;
}
};
void process(Rectangle& r) {
r.setWidth(5);
r.setHeight(4);
assert(r.getWidth() == r.getHeight()); // This will fail if r is a Square object
}
// A good example of LSP compliance
class Shape {
public:
virtual int getArea() = 0; // An abstract method
};
class Rectangle : public Shape {
protected:
int width;
int height;
public:
void setWidth(int width) {
this->width = width;
}
void setHeight(int height) {
this->height = height;
}
int getWidth() {
return width;
}
int getHeight() {
return height;
}
int getArea() override {
return width * height;
}
};
class Square : public Shape {
protected:
int side;
public:
void setSide(int side) {
this->side = side;
}
int getSide() {
return side;
}
int getArea() override {
return side * side;
}
};
void process(Shape& s) {
// Do something with the shape object
}
- Interface Segregation Principle: This principle states that a class should not depend on methods or properties that it does not use. This means that the interface or contract of a class should be as small and specific as possible, and not include unnecessary or irrelevant details. For example, a class that implements a printer interface should not have methods for scanning or faxing if it does not support those features. This way, the code is more cohesive and less coupled.
// A bad example of ISP violation
class Printer {
public:
virtual void print() = 0;
virtual void scan() = 0;
virtual void fax() = 0;
};
class LaserPrinter : public Printer {
public:
void print() override {
// Print using laser
}
void scan() override {
// Scan using laser
}
void fax() override {
// Fax using laser
}
};
class InkjetPrinter : public Printer {
public:
void print() override {
// Print using ink
}
void scan() override {
// Scan using ink
}
void fax() override {
// Fax using ink
}
};
class DotMatrixPrinter : public Printer {
public:
void print() override {
// Print using dot matrix
}
void scan() override {
// Do nothing, this printer does not support scanning
}
void fax() override {
// Do nothing, this printer does not support faxing
}
};// A good example of ISP compliance
class Printer {
public:
virtual void print() = 0;
};
class Scanner {
public:
virtual void scan() = 0;
};
class FaxMachine {
public:
virtual void fax() = 0;
};
class LaserPrinter : public Printer, public Scanner, public FaxMachine {
public:
void print() override {
// Print using laser
}
void scan() override {
// Scan using laser
}
void fax() override {
// Fax using laser
}
};
class InkjetPrinter : public Printer, public Scanner, public FaxMachine {
public:
void print() override {
// Print using ink
}
void scan() override {
// Scan using ink
}
void fax() override {
// Fax using ink
}
};
class DotMatrixPrinter : public Printer {
public:
void print() override {
// Print using dot matrix
}
};
- Dependency Inversion Principle: This principle states that a class should depend on abstractions, not concretions. This means that the high-level modules or classes should not depend on the low-level ones, but rather on their interfaces or abstract classes. For example, a class that performs some business logic should not depend on a specific database implementation, but rather on an abstract data access layer. This way, the code is more decoupled and easier to change.
// A bad example of DIP violation
class MySQLConnection {public:void connect();void executeQuery();// Other methods related to MySQL connection};class BusinessLogic {private:MySQLConnection connection; // A concrete dependencypublic:BusinessLogic(MySQLConnection connection) {this->connection = connection;}void doSomeWork() {connection.connect();connection.executeQuery();// Other business logic operations}};// A good example of DIP compliance
class DatabaseConnection { // An abstract class or interfacepublic:virtual void connect() = 0;virtual void executeQuery() = 0;// Other abstract methods related to database connection};class MySQLConnection : public DatabaseConnection {public:void connect() override {// Connect to MySQL database}void executeQuery() override {// Execute query on MySQL database}};class BusinessLogic {private:DatabaseConnection connection; // An abstract dependencypublic:BusinessLogic(DatabaseConnection connection) {this->connection = connection;}void doSomeWork() {connection.connect();connection.executeQuery();// Other business logic operations}};