Best Practices
SOLID Principles
Single Responsibility Principle
Don't
Pack multiple functionalities into one class. This creates:
- Poor conceptual cohesion
- Multiple reasons for the class to change
- Difficult to understand dependencies
- Hard to modify without affecting other parts
DoClick to view
Separate concerns into focused classes. This provides:
- Clear single responsibility for each class
- Easier maintenance and testing
- Better code organization and reusability
- Reduced coupling between components
class UserSettings {constructor(private readonly user: User) {}changeSettings(settings: UserSettings) {if (this.verifyCredentials()) {// Update settings logic}}verifyCredentials() {// Authentication logic}}
Open/Closed Principle
Don't
Modify existing code to add new functionality. This approach:
- Violates the open/closed principle
- Requires changing tested code
- Increases risk of introducing bugs
- Creates tight coupling between components
DoClick to view
Use abstraction to enable extension without modification. This provides:
- Open for extension through inheritance
- Closed for modification of existing code
- Consistent interface for all implementations
- Easy addition of new functionality
class HttpRequester {constructor(private readonly adapter: Adapter) {}async fetch<T>(url: string): Promise<T> {if (this.adapter instanceof AjaxAdapter) {return await makeAjaxCall<T>(url);} else if (this.adapter instanceof NodeAdapter) {return await makeHttpCall<T>(url);}// Need to modify this class for each new adapter type}}
Liskov Substitution Principle
Don't
Create subclasses that violate parent class contracts. This causes:
- Unexpected behavior when substituting objects
- Breaking the "is-a" relationship assumption
- Runtime errors and incorrect results
- Violation of polymorphism principles
DoClick to view
Design inheritance that maintains behavioral contracts. This ensures:
- Proper substitutability of parent and child objects
- Consistent behavior across the inheritance hierarchy
- Reliable polymorphism without surprises
- Maintainable code with clear contracts
class Rectangle {constructor(protected width = 0, protected height = 0) {}setWidth(width: number): this {this.width = width;return this;}setHeight(height: number): this {this.height = height;return this;}getArea(): number {return this.width * this.height;}}class Square extends Rectangle {setWidth(width: number): this {this.width = this.height = width; // Violates LSPreturn this;}setHeight(height: number): this {this.width = this.height = height; // Violates LSPreturn this;}}// BAD: Square returns 25, Rectangle returns 20const shapes = [new Rectangle(), new Square()];shapes.forEach(shape => {console.log(shape.setWidth(4).setHeight(5).getArea());});
Interface Segregation Principle
Don't
Create large interfaces with many unrelated methods. This forces:
- Clients to implement methods they don't use
- Unnecessary dependencies on unused functionality
- Violation of single responsibility at interface level
- Difficult maintenance and testing
DoClick to view
Create focused interfaces with related methods. This provides:
- Clients depend only on methods they use
- Flexible composition of functionality
- Easy implementation without unused methods
- Better maintainability and testability
interface SmartPrinter {print();fax();scan();}class EconomicPrinter implements SmartPrinter {print() { /* Print logic */ }fax() {throw new Error("Fax not supported."); // Forced to implement}scan() {throw new Error("Scan not supported."); // Forced to implement}}
Dependency Inversion Principle
Don't
Depend directly on concrete implementations. This creates:
- Tight coupling between modules
- Difficulty in testing with mocks
- Hard to change implementations
- Reduced flexibility and reusability
DoClick to view
Depend on abstractions and inject dependencies. This enables:
- Loose coupling between modules
- Easy testing with dependency injection
- Flexible implementations without code changes
- Better maintainability and extensibility
class XmlFormatter {parse<T>(content: string): T {// XML parsing logic}}class ReportReader {private readonly formatter = new XmlFormatter(); // Tightly coupledasync read(path: string): Promise<ReportData> {const text = await readFile(path, "UTF8");return this.formatter.parse<ReportData>(text);// Hard to test or change formatter}}