SOLID Principles: Comprehensive Knowledge Guide¶
Overview¶
SOLID is an acronym representing five fundamental design principles in object-oriented programming and software engineering. These principles, introduced by Robert C. Martin, help create maintainable, scalable, and robust software systems. The five principles are:
- Single Responsibility Principle (SRP)
- Open/Closed Principle (OCP)
- Liskov Substitution Principle (LSP)
- Interface Segregation Principle (ISP)
- Dependency Inversion Principle (DIP)
1. Single Responsibility Principle (SRP)¶
Definition¶
Each class should have a single, well-defined responsibility. A class should have only one reason to change, meaning it should handle only one aspect of the application's functionality.
Core Concepts¶
- Single Concern: A class should focus on doing one thing and doing it well
- Separation of Concerns: Different responsibilities should be separated into different classes
- Cohesion: Classes with a single responsibility tend to have higher cohesion (internal consistency)
Problems Without SRP¶
When classes have multiple responsibilities:
- Poor Separation of Concerns: Multiple unrelated functionalities are mixed together
- Low Coherence: The class lacks internal consistency and clear purpose
- Difficult to Fragment and Reuse: Cannot extract and reuse individual functionalities in other projects
- Harder to Test: Testing becomes complex when a single class handles multiple concerns
- Increased Coupling: Multiple responsibilities lead to increased dependencies and coupling
Example: Violation of SRP¶
public class WordProcessor {
public void saveDocument(Document doc) {
// File handling logic
}
public Document loadDocument(String fileName) {
// File loading logic
return null;
}
public void spellCheckDocument(Document doc) {
// Spell checking logic
}
}
In this example, the WordProcessor class has three responsibilities:
1. Saving documents (file persistence)
2. Loading documents (file persistence)
3. Spell checking (document analysis)
Example: SRP Compliant Design¶
public class FileHandler {
public void saveDocument(Document doc, String format) {
// Only handles file persistence
}
public Document loadDocument(String fileName) {
// Only handles file loading
return null;
}
}
public class SpellChecker {
public void spellCheckDocument(Document doc, String lang) {
// Only handles spell checking
}
}
Benefits of SRP¶
- Easier Collaboration: Multiple developers can work on different components without conflict
- Code Reusability: Modules with single responsibilities can be easily shared among projects
- Improved Readability: Classes are easier to understand when they have a clear, focused purpose
- Better Testability: Single-responsibility classes are simpler to unit test
- Reduced Coupling: Changes to one responsibility don't cascade to others
- Maintainability: Bugs are localized and easier to fix
- Flexibility: New requirements can be addressed without modifying existing classes
2. Open/Closed Principle (OCP)¶
Definition¶
Software classes should be closed for modification but open for extension. This means you should be able to add new functionality without changing existing code.
Core Philosophy¶
The principle was introduced by Bertrand Meyer and addresses the challenge of creating stable, extensible systems:
- Closed for Modification: Existing, tested code should remain unchanged to avoid introducing bugs
- Open for Extension: New functionality should be added through extension mechanisms without modifying the original code
- Stability with Flexibility: Balances the need for stable code with the ability to accommodate new requirements
Implementation Strategies¶
1. Interface-Based Design¶
Define class behavior through abstract interface definitions:
public interface ICardPaymentProvider {
CardPaymentResponseCode takePaymentFromCard(String cardName,
String cardNumber,
Date start, Date end,
String cvv,
String postcode);
}
public abstract class CardPaymentProviderBase implements ICardPaymentProvider {
public final CardPaymentResponseCode takePaymentFromCard(String cardName,
String cardNumber,
Date start, Date end,
String cvv,
String postcode) {
// Closed for modification - contains critical validation logic
boolean numOK = isCardNumValid(cardNumber);
if (!numOK) {
return CardPaymentResponseCode.INVALID_CARDNUM;
}
// Delegates to overrideable method for extension
return onTakePayment();
}
protected CardPaymentResponseCode onTakePayment() {
// Open for extension - subclasses override this method
return CardPaymentResponseCode.PAYMENT_PROVIDER_UNIMPLEMENTED;
}
}
2. Dynamic Polymorphism¶
Runtime type checking allows the same code to work with different implementations:
public interface Printable {
void doPrint();
}
class Circle implements Printable {
public void doPrint() {
// Circle-specific printing implementation
}
}
class Rectangle implements Printable {
public void doPrint() {
// Rectangle-specific printing implementation
}
}
// Use polymorphism - implementation determined at runtime
Printable shape = new Circle();
shape.doPrint(); // Calls Circle's implementation
3. Static Polymorphism (Generics/Templates)¶
Compile-time type checking with flexible type parameters:
public class Stack<E> {
private E[] stackData;
public Stack() {
stackData = (E[]) new Object[MAX_SIZE];
}
public void push(E element) {
// Implementation
}
public E pop() {
// Implementation
return null;
}
}
// Type is fixed at compile time
Stack<Integer> intStack = new Stack<>();
Stack<String> stringStack = new Stack<>();
Type Erasure: During compilation, generic type parameters are erased and replaced with Object:
- Stack<Integer> becomes Stack with Object[] internally
- Type safety is checked at compile-time but erased at runtime
- Restrictions: Cannot create instances like new E() or new E[]
4. The final Keyword¶
Prevents modification at the code level:
// Final class - cannot be subclassed
public final class ImmutablePaymentProvider {
// ...
}
// Final method - cannot be overridden
public final boolean makePayment(String number) {
// Implementation
}
// Final attribute - cannot be changed
public static final double PI = 3.1415926;
5. Protected Methods for Extension¶
Non-final protected methods allow extension while maintaining stability:
public class PaypalPaymentProvider extends CardPaymentProviderBase {
protected CardPaymentResponseCode onTakePayment() {
// This method is open for extension
System.out.println("Attempting to take payment from PayPal");
// Implementation specific to PayPal
return CardPaymentResponseCode.OK;
}
}
Practical Example: Shape Calculation System¶
Problem: Create a system that can calculate areas in different units (meters squared, millimeters squared) without modifying the scaling code when new shapes are added.
// Interface defines contract
public interface IShape {
double calculateArea();
}
// Abstract base class provides stable interface
public abstract class Shape implements IShape {
protected abstract double calculateArea();
public final double getArea() {
// Closed for modification - stable scaling logic here
return calculateArea();
}
}
// Concrete implementations - open for extension
public final class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
protected final double calculateArea() {
return radius * radius * Math.PI;
}
}
public final class Rectangle extends Shape {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
protected final double calculateArea() {
return length * width;
}
}
// Usage
IShape shape1 = new Rectangle(40, 50);
IShape shape2 = new Circle(200);
System.out.println("Area of shape1 is " + shape1.getArea());
System.out.println("Area of shape2 is " + shape2.getArea());
Facade Pattern with Package Access¶
The Facade pattern works with OCP by encapsulating implementation details:
// All internal classes use default (package-private) access
class InternalClass1 {
// Hidden from external use
}
class InternalClass2 {
// Hidden from external use
}
// Only the facade is public
public class PublicFacade {
public void publicMethod() {
// Coordinates internal classes
}
}
Benefits: - Internal implementation can change without affecting external clients - Only the public interface needs to remain stable - Package acts as a single unit that can be extended without modification
Design by Contract¶
Formal specifications of class behavior that should not be violated:
// Contract: if you set focus1 and focus2 to different points,
// they should remain different when retrieved
public class Ellipse {
private Point focus1, focus2;
public void setFocus1(Point focus) {
this.focus1 = focus;
}
public void setFocus2(Point focus) {
this.focus2 = focus;
}
public Point getFocus1() {
return focus1;
}
public Point getFocus2() {
return focus2;
}
}
Implementation Checklist for OCP¶
- Define clear interface definitions (public contracts)
- Use the
finalkeyword on methods that should not be modified - Provide protected overrideable methods as extension points
- Isolate modification-prone code from extension code
- Use package-private access for internal implementation
- Create abstract base classes with template methods
- Apply appropriate access modifiers (public, protected, private)
3. Liskov Substitution Principle (LSP)¶
Definition¶
Derived classes (subclasses) must be substitutable for their base classes without breaking the application. If a derived class violates the base class's contract, it violates LSP.
Core Concept¶
Substitutability: Any code written to work with a base class should work correctly with any of its subclasses, without knowing which subclass is actually being used.
If S is a subtype of T, then objects of type S may be substituted
for objects of type T without altering any of the desirable properties
of that program.
Example: LSP Violation¶
public class Ellipse {
protected Point focus1, focus2;
public Point getFocus1() {
return focus1;
}
public void setFocus1(Point focus1) {
this.focus1 = focus1;
}
public Point getFocus2() {
return focus2;
}
public void setFocus2(Point focus2) {
this.focus2 = focus2;
}
}
// Violation: Circle breaks Ellipse's contract
public class Circle extends Ellipse {
public void setFocus1(Point focus) {
// A circle has only one focus point, so set both
this.focus1 = focus;
this.focus2 = focus; // Violates the implicit contract!
}
public void setFocus2(Point focus) {
// Same violation
this.focus1 = focus;
this.focus2 = focus;
}
}
LSP Violation Detection¶
private static void checkShape(Ellipse e) {
Point a = new Point(-1, 0);
Point b = new Point(1, 0);
e.setFocus1(a);
e.setFocus2(b);
// These assertions should pass for any Ellipse
assert(e.getFocus1().equals(a)); // Fails if e is a Circle!
assert(e.getFocus2().equals(b)); // Fails if e is a Circle!
}
The problem occurs because Circle violates the implicit contract of Ellipse. When you set two different focus points on an Ellipse, they should remain different. A Circle cannot maintain this contract because a circle has only one focus point (the center).
Design by Contract¶
Definition: Each method has an implicit or explicit contract specifying: - Preconditions: What conditions must be true before calling the method - Postconditions: What conditions will be true after the method executes - Invariants: What conditions remain true throughout the method's execution
LSP Requirement: Subclasses must not weaken preconditions or postconditions
Practical Solutions to LSP Violations¶
1. Use the final Keyword¶
Prevents problematic method overriding:
public class Ellipse {
protected Point focus1, focus2;
public final void setFocus1(Point focus1) {
this.focus1 = focus1;
}
public final void setFocus2(Point focus2) {
this.focus2 = focus2;
}
}
// Now Circle cannot override these methods
2. Correct the Inheritance Hierarchy¶
Use composition instead of inheritance if true substitutability doesn't exist:
public interface Shape {
double getArea();
}
public class Ellipse implements Shape {
private Point focus1, focus2;
public double getArea() {
// Calculate ellipse area
return 0;
}
public void setFocus1(Point focus) {
this.focus1 = focus;
}
public void setFocus2(Point focus) {
this.focus2 = focus;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public double getArea() {
return radius * radius * Math.PI;
}
// No inheritance relationship - no contract violation
}
3. Extract Common Interface¶
If classes share some behavior but not all:
public interface GeometricShape {
double getArea();
}
public class Circle implements GeometricShape {
private double radius;
public double getArea() {
return radius * radius * Math.PI;
}
}
public class Rectangle implements GeometricShape {
private double length, width;
public double getArea() {
return length * width;
}
}
LSP and Inheritance Rules¶
When to Use Inheritance (to maintain LSP):
- True is-a relationship exists
- Subclass can fulfill all contracts of the base class
- Example: Doctor is-a Person, Apple is-a Fruit
When NOT to Use Inheritance:
- Only need to reuse functionality without a true is-a relationship
- Example: Person is-NOT-an EncryptionHelper
- Solution: Use composition instead
Benefits of Following LSP¶
- Predictability: Code behaves consistently with base class expectations
- Reliability: Substitution doesn't introduce unexpected bugs
- Maintainability: Inheritance hierarchies remain coherent and logical
- Extensibility: Safe to add new subclasses without breaking existing code
- Testing: Can write tests for base classes that work for all subclasses
4. Interface Segregation Principle (ISP)¶
Definition¶
No client should be forced to depend on methods it does not use. Create fine-grained, client-specific interfaces rather than one large, general-purpose interface.
Core Philosophy¶
- Small, Focused Interfaces: Interfaces should be designed around specific client needs
- Avoid Fat Interfaces: Large interfaces with many methods can force clients to implement unused methods
- Client-Specific Abstractions: Different clients have different needs; provide interfaces tailored to those needs
Problems with Fat Interfaces¶
// BAD: One large interface for SQL operations
public interface ISQLHelper {
boolean insert(String table, Map<String, Object> data);
boolean update(String table, Map<String, Object> data);
boolean delete(String table, String condition);
ResultSet select(String query);
boolean createTable(String schema);
boolean dropTable(String tableName);
}
// A client that only needs to insert data is forced to implement
// all methods, even those it doesn't use
public class InsertOnlyClient implements ISQLHelper {
public boolean insert(String table, Map<String, Object> data) {
// Actual implementation
return true;
}
public boolean update(String table, Map<String, Object> data) {
throw new UnsupportedOperationException();
}
public boolean delete(String table, String condition) {
throw new UnsupportedOperationException();
}
public ResultSet select(String query) {
throw new UnsupportedOperationException();
}
public boolean createTable(String schema) {
throw new UnsupportedOperationException();
}
public boolean dropTable(String tableName) {
throw new UnsupportedOperationException();
}
}
ISP Compliant Design¶
// GOOD: Segregated interfaces for specific clients
public interface IInsertHelper {
boolean insert(String table, Map<String, Object> data);
}
public interface IUpdateHelper {
boolean update(String table, Map<String, Object> data);
}
public interface IDeleteHelper {
boolean delete(String table, String condition);
}
public interface ISelectHelper {
ResultSet select(String query);
}
public interface ISchemaHelper {
boolean createTable(String schema);
boolean dropTable(String tableName);
}
// Clients implement only the interfaces they need
public class InsertClient implements IInsertHelper {
public boolean insert(String table, Map<String, Object> data) {
// Implementation
return true;
}
}
public class SelectClient implements ISelectHelper {
public ResultSet select(String query) {
// Implementation
return null;
}
}
public class FullDatabaseManager implements IInsertHelper, IUpdateHelper,
IDeleteHelper, ISelectHelper,
ISchemaHelper {
// Implements all interfaces - has full functionality
}
Real-World Example: Document Processing System¶
// Segregated interfaces for different document operations
public interface IRenderable {
void render();
}
public interface IPDFExportable {
byte[] exportToPDF();
}
public interface IShapeProvider {
Shape getShape();
}
// Different document types implement only needed interfaces
public class Circle implements IShapeProvider, IRenderable {
public Shape getShape() {
// Return circle shape
return null;
}
public void render() {
// Render circle
}
}
public class Image implements IRenderable, IPDFExportable {
public void render() {
// Render image
}
public byte[] exportToPDF() {
// Export to PDF
return null;
}
}
public class Text implements IRenderable, IPDFExportable {
public void render() {
// Render text
}
public byte[] exportToPDF() {
// Export to PDF
return null;
}
}
public class Rectangle implements IShapeProvider, IRenderable {
public Shape getShape() {
// Return rectangle shape
return null;
}
public void render() {
// Render rectangle
}
}
Benefits of ISP¶
- Reduced Coupling: Clients depend only on the methods they use
- Flexibility: Easy to add new implementations with specific interfaces
- Cleaner Implementations: No need for empty/stub method implementations
- Better Testability: Easier to create test doubles with minimal interface
- Single Responsibility: Interfaces have focused responsibilities
- Evolutionary Design: Easy to modify interfaces without breaking all clients
ISP Implementation Checklist¶
- Analyze client needs and group them logically
- Create focused interfaces for each group
- Avoid large, general-purpose interfaces
- Use interface composition when multiple behaviors are needed
- Review existing implementations for unused methods (indicator of ISP violation)
- Consider the actual usage patterns of classes
5. Dependency Inversion Principle (DIP)¶
Definition¶
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.
Core Philosophy¶
- Depend on Abstractions, Not Concretions: Use interfaces and abstract classes rather than concrete implementations
- Reduce Coupling: High-level logic should not be tightly coupled to implementation details
- Enable Flexibility: Easy to swap implementations without changing high-level code
- Inversion of Control: The dependency flow is inverted from the traditional top-down approach
Traditional (Incorrect) Dependency Flow¶
In this traditional approach, high-level modules directly depend on low-level modules, creating tight coupling.
Inverted Dependency Flow¶
Both high-level and low-level modules depend on the abstraction, not on each other.
Example: Violation of DIP¶
// BAD: High-level module depends directly on low-level module
public class UserService {
private MySQLDatabase database;
public UserService() {
this.database = new MySQLDatabase(); // Direct dependency on concrete class
}
public void saveUser(User user) {
database.save(user);
}
}
public class MySQLDatabase {
public void save(User user) {
// MySQL-specific saving logic
}
}
Problems:
- UserService is tightly coupled to MySQLDatabase
- Changing database technology requires modifying UserService
- Cannot use different database implementations without code changes
- Difficult to test (cannot use mock database)
DIP Compliant Design¶
// Interface defines abstraction
public interface IDatabase {
void save(User user);
User getUserById(int id);
void delete(int id);
}
// Low-level module implements the interface
public class MySQLDatabase implements IDatabase {
public void save(User user) {
// MySQL-specific implementation
}
public User getUserById(int id) {
// MySQL-specific implementation
return null;
}
public void delete(int id) {
// MySQL-specific implementation
}
}
// Alternative implementation
public class PostgreSQLDatabase implements IDatabase {
public void save(User user) {
// PostgreSQL-specific implementation
}
public User getUserById(int id) {
// PostgreSQL-specific implementation
return null;
}
public void delete(int id) {
// PostgreSQL-specific implementation
}
}
// High-level module depends on abstraction
public class UserService {
private IDatabase database;
// Dependency injection - provides flexibility
public UserService(IDatabase database) {
this.database = database;
}
public void saveUser(User user) {
database.save(user);
}
public User getUser(int id) {
return database.getUserById(id);
}
public void deleteUser(int id) {
database.delete(id);
}
}
// Usage - can easily swap implementations
public class Application {
public static void main(String[] args) {
// Use MySQL
IDatabase mysqlDb = new MySQLDatabase();
UserService serviceWithMySQL = new UserService(mysqlDb);
// Use PostgreSQL - same service, different implementation
IDatabase pgDb = new PostgreSQLDatabase();
UserService serviceWithPostgreSQL = new UserService(pgDb);
// Use mock for testing
IDatabase mockDb = new MockDatabase();
UserService serviceForTesting = new UserService(mockDb);
}
}
public class MockDatabase implements IDatabase {
public void save(User user) {
// Mock implementation for testing
}
public User getUserById(int id) {
// Mock implementation
return new User(id, "Test User");
}
public void delete(int id) {
// Mock implementation
}
}
Dependency Injection¶
A key technique for implementing DIP:
Constructor Injection¶
public class UserService {
private IDatabase database;
// Dependency provided through constructor
public UserService(IDatabase database) {
this.database = database;
}
}
Setter Injection¶
public class UserService {
private IDatabase database;
// Dependency provided through setter
public void setDatabase(IDatabase database) {
this.database = database;
}
}
Interface Injection¶
public interface IDatabaseInjectable {
void injectDatabase(IDatabase database);
}
public class UserService implements IDatabaseInjectable {
private IDatabase database;
public void injectDatabase(IDatabase database) {
this.database = database;
}
}
Advanced Example: Multi-Service System¶
// Abstractions (interfaces)
public interface IAuthentication {
boolean authenticate(String username, String password);
}
public interface IAuthorization {
boolean isAuthorized(User user, String action);
}
public interface IEmailService {
void sendEmail(String to, String subject, String body);
}
// Concrete implementations
public class ActiveDirectoryAuth implements IAuthentication {
public boolean authenticate(String username, String password) {
// Active Directory authentication logic
return true;
}
}
public class RoleBasedAuthorization implements IAuthorization {
public boolean isAuthorized(User user, String action) {
// Role-based authorization logic
return user.getRole().canPerform(action);
}
}
public class SMTPEmailService implements IEmailService {
public void sendEmail(String to, String subject, String body) {
// SMTP email sending logic
}
}
// High-level service depends on abstractions
public class UserManagementService {
private IAuthentication authentication;
private IAuthorization authorization;
private IEmailService emailService;
public UserManagementService(IAuthentication auth,
IAuthorization authz,
IEmailService email) {
this.authentication = auth;
this.authorization = authz;
this.emailService = email;
}
public boolean createUser(String username, String password, User newUser) {
if (!authentication.authenticate(username, password)) {
return false;
}
if (!authorization.isAuthorized(new User(username, null), "create_user")) {
return false;
}
// Create user logic...
emailService.sendEmail(newUser.getEmail(), "Welcome",
"Your account has been created");
return true;
}
}
Benefits of DIP¶
- Loose Coupling: High-level modules are not dependent on low-level implementation details
- Flexibility: Easy to swap implementations without changing business logic
- Testability: Easy to create mock implementations for unit testing
- Maintainability: Changes to implementations don't affect high-level code
- Scalability: Can extend functionality by providing new implementations
- Reusability: High-level modules can be reused with different implementations
DIP Implementation Checklist¶
- Identify dependencies in high-level modules
- Create interfaces for each dependency
- Make high-level modules depend on interfaces, not concrete classes
- Implement dependency injection to provide concrete implementations
- Avoid direct instantiation of dependencies (use constructor/setter injection)
- Use factory patterns or service locators for complex object creation
- Ensure concrete implementations are only created at the application boundaries
Relationships Between SOLID Principles¶
The five SOLID principles work together to create well-designed object-oriented systems:
| Principle | Focuses On | Key Benefit |
|---|---|---|
| SRP | Class responsibilities | Single reason to change; better testability |
| OCP | Code extension | Stable, existing code remains untouched |
| LSP | Inheritance contracts | Predictable substitutability |
| ISP | Interface granularity | Clients depend only on needed methods |
| DIP | Dependency direction | Depend on abstractions, not concretions |
Hierarchical Application¶
- Start with DIP: Design your system using abstractions (interfaces)
- Apply SRP: Each class should have one responsibility
- Apply OCP: Make classes open for extension, closed for modification
- Apply ISP: Ensure interfaces are focused and client-specific
- Respect LSP: Maintain proper inheritance hierarchies
Common Anti-Patterns and Solutions¶
Rigid Design¶
Problem: Hard to modify functionality without major rework (e.g., hardcoded values)
Example:
Solution:
// GOOD
String yes = Strings.getInstance().getString("YES");
String no = Strings.getInstance().getString("NO");
Fragility¶
Problem: High probability of errors after modification
Example:
Solution:
Immobility¶
Problem: Code is difficult to reuse in other projects
Solution: Apply SRP and reduce coupling through proper abstraction
Viscosity of Design¶
Problem: Difficult to maintain the original design philosophy when making changes
Solution: Enforce design principles consistently; use code reviews
Data Duplication¶
Problem: Storing the same data in multiple places (violates DRY principle)
Example:
// BAD - Balance calculated and stored separately
double balance = 1000;
// Later: transactions modify the stored balance incorrectly
Solution:
// GOOD - Calculate balance from transaction history
double balance = calculateBalance(); // SELECT SUM(amount) FROM transactions
Implementation Best Practices¶
1. Use Access Modifiers Correctly¶
public class MyClass {
// Private: internal implementation details
private int internalState;
// Protected: extension points for subclasses
protected void extensionPoint() {
// Can be overridden
}
// Public: stable interface
public final void publicAPI() {
// Cannot be overridden - maintains contract
}
}
2. Prefer Composition Over Inheritance¶
// BAD - Violates LSP and creates unnecessary coupling
class Person extends EncryptionHelper {
// Person is not-an EncryptionHelper
}
// GOOD - Composition preserves single responsibility
class Person {
private EncryptionHelper encrypter = new EncryptionHelper();
public String encryptData(String data) {
return encrypter.encrypt(data);
}
}
3. Use Interfaces for Abstraction¶
// Define services through interfaces
public interface IPaymentProcessor {
PaymentResult processPayment(PaymentDetails details);
}
// Implement for different providers
public class CreditCardProcessor implements IPaymentProcessor {
// Implementation
}
public class PayPalProcessor implements IPaymentProcessor {
// Implementation
}
// High-level code depends on interface
public class CheckoutService {
private IPaymentProcessor paymentProcessor;
public CheckoutService(IPaymentProcessor processor) {
this.paymentProcessor = processor;
}
}
4. Apply Template Method Pattern for OCP¶
public abstract class DataProcessor {
// Template method - closed for modification
public final void process(String data) {
String validated = validate(data);
String processed = transform(validated);
save(processed);
}
// Extension points - open for extension
protected abstract String validate(String data);
protected abstract String transform(String data);
protected abstract void save(String data);
}
public class CSVProcessor extends DataProcessor {
protected String validate(String data) {
// CSV-specific validation
return data;
}
protected String transform(String data) {
// CSV-specific transformation
return data;
}
protected void save(String data) {
// CSV-specific saving
}
}
5. Use Enum for Type-Safe Constants¶
public enum PaymentStatus {
OK,
INVALID_CARD_NUMBER,
INVALID_EXPIRY_DATE,
INSUFFICIENT_FUNDS,
PROVIDER_ERROR;
}
public class PaymentResult {
private PaymentStatus status;
public PaymentResult(PaymentStatus status) {
this.status = status;
}
public boolean isSuccess() {
return status == PaymentStatus.OK;
}
}
Conclusion¶
The SOLID principles provide a comprehensive framework for writing high-quality, maintainable object-oriented software. They work together to:
- Reduce coupling between modules
- Improve code flexibility and extensibility
- Make systems easier to test and maintain
- Facilitate team collaboration
- Create systems that can evolve with changing requirements
- Prevent common design problems (rigidity, fragility, immobility)
Applying these principles requires practice and experience, but the long-term benefits in code quality, maintainability, and team productivity make the effort worthwhile. Remember that these are guidelines, not rigid rules—apply them judiciously based on your specific context and requirements.