SOLID Principles and DRY: Complete Development Guide
๐ฏ What are SOLID Principles?โ
SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin (Uncle Bob) in the early 2000s, building on work from the 1980s and 1990s.
The Five SOLID Principles:โ
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
๐ Historical Background and Namingโ
Single Responsibility Principle (SRP)โ
- Origin: Introduced by Tom DeMarco in 1979
- Popularized by: Robert C. Martin
- Why "Single Responsibility": Each class should have only one reason to change, meaning one responsibility
Open/Closed Principle (OCP)โ
- Origin: Bertrand Meyer in 1988 in his book "Object-Oriented Software Construction"
- Why "Open/Closed": Software entities should be open for extension but closed for modification
Liskov Substitution Principle (LSP)โ
- Origin: Barbara Liskov in a 1987 paper titled "Data Abstraction and Hierarchy"
- Named after: Barbara Liskov, computer scientist and Turing Award winner
- Why "Substitution": Objects of a superclass should be substitutable with objects of subclasses without breaking functionality
Interface Segregation Principle (ISP)โ
- Origin: Robert C. Martin in the 1990s
- Why "Segregation": Clients shouldn't be forced to depend on interfaces they don't use - interfaces should be segregated (separated)
Dependency Inversion Principle (DIP)โ
- Origin: Robert C. Martin in 1996
- Why "Inversion": Traditional dependency flow (high-level โ low-level) is inverted - both depend on abstractions
๐ What Does Dependency Inversion Really Mean?โ
Traditional Dependency Flow (Without DIP):โ
High-Level Module โ Low-Level Module
(Business Logic) โ (Database Implementation)
Inverted Dependency Flow (With DIP):โ
High-Level Module โ Abstraction โ Low-Level Module
(Business Logic) โ (Interface) โ (Database Implementation)
The "Inversion" Explained:โ
// โ Traditional Flow: High-level depends on low-level
class OrderService { // High-level
private database: MySQLDatabase; // Depends on concrete low-level
constructor() {
this.database = new MySQLDatabase(); // Direct dependency
}
}
// โ
Inverted Flow: Both depend on abstraction
interface Database { // Abstraction
save(data: any): void;
}
class OrderService { // High-level
constructor(private database: Database) {} // Depends on abstraction
}
class MySQLDatabase implements Database { // Low-level depends on abstraction
save(data: any): void {
// Implementation
}
}
The dependency arrow is "inverted" - instead of high-level modules depending directly on low-level modules, both depend on abstractions.
๐ป SOLID Principles in JavaScript/TypeScriptโ
Are SOLID Principles Applicable to JavaScript/TypeScript?โ
Absolutely YES! Here's why and how:
1. JavaScript/TypeScript Support:โ
// โ
SRP in JavaScript
class UserValidator {
static validateEmail(email) {
return email && email.includes('@');
}
}
class UserService {
createUser(userData) {
if (!UserValidator.validateEmail(userData.email)) {
throw new Error('Invalid email');
}
// Create user logic
}
}
// โ
OCP in TypeScript
interface PaymentMethod {
process(amount: number): void;
}
class PaymentProcessor {
private methods = new Map<string, PaymentMethod>();
addMethod(type: string, method: PaymentMethod) {
this.methods.set(type, method);
}
}
2. TypeScript Advantages for SOLID:โ
// TypeScript interfaces enable better SOLID compliance
interface Readable {
read(id: string): any;
}
interface Writable {
write(data: any): void;
}
// ISP: Segregated interfaces
class ReadOnlyService implements Readable {
read(id: string): any {
return `Data for ${id}`;
}
// Not forced to implement write()
}
class ReadWriteService implements Readable, Writable {
read(id: string): any {
return `Data for ${id}`;
}
write(data: any): void {
console.log('Writing data');
}
}
3. Dependency Injection in JavaScript/TypeScript:โ
// DIP with dependency injection
class EmailService {
send(message: string, recipient: string): void {
console.log(`Sending email to ${recipient}: ${message}`);
}
}
class NotificationService {
constructor(private emailService: EmailService) {}
notify(message: string, recipient: string): void {
this.emailService.send(message, recipient);
}
}
// Usage
const emailService = new EmailService();
const notificationService = new NotificationService(emailService);
๐๏ธ Programming Languages and SOLID Principlesโ
Languages Where SOLID is Most Critical:โ
| Language | SOLID Importance | Reason |
|---|---|---|
| Java | โญโญโญโญโญ Very High | Strong typing, large enterprise applications, OOP-focused |
| C# | โญโญโญโญโญ Very High | Enterprise development, .NET ecosystem, strong OOP |
| C++ | โญโญโญโญ High | Complex systems, manual memory management |
| TypeScript | โญโญโญโญ High | Large-scale applications, strong typing benefits |
| Python | โญโญโญ Medium-High | Duck typing makes some principles less critical |
| JavaScript | โญโญโญ Medium | Dynamic nature, but still valuable for large apps |
| Go | โญโญโญ Medium | Interface-based design, but simpler OOP model |
| Rust | โญโญโญ Medium | Trait system supports principles, but ownership model differs |
Why Some Languages Need SOLID More:โ
Java/C# - Highest Need:โ
// Java enterprise applications benefit greatly from SOLID
public interface PaymentProcessor {
boolean processPayment(BigDecimal amount, PaymentDetails details);
}
@Service
public class OrderService {
private final PaymentProcessor paymentProcessor;
private final NotificationService notificationService;
public OrderService(PaymentProcessor paymentProcessor,
NotificationService notificationService) {
this.paymentProcessor = paymentProcessor;
this.notificationService = notificationService;
}
}
Python - Medium Need:โ
# Python's duck typing makes LSP less critical
class Bird:
def move(self):
pass
class Duck(Bird):
def move(self):
print("Flying and swimming")
class Penguin(Bird):
def move(self):
print("Swimming and walking")
# Both work without explicit interfaces
def make_bird_move(bird):
bird.move() # Works with any object that has move()
JavaScript - Growing Need:โ
// Modern JavaScript applications benefit from SOLID
class ApiService {
constructor(httpClient, logger) {
this.httpClient = httpClient;
this.logger = logger;
}
async fetchData(url) {
this.logger.log(`Fetching data from ${url}`);
return await this.httpClient.get(url);
}
}
// Easy to test and maintain
const apiService = new ApiService(mockHttpClient, mockLogger);
โ Where to Use SOLID Principlesโ
1. Large Enterprise Applicationsโ
// Enterprise order management system
interface OrderRepository {
save(order: Order): Promise<void>;
findById(id: string): Promise<Order>;
}
interface PaymentGateway {
charge(amount: number, token: string): Promise<PaymentResult>;
}
interface NotificationService {
sendOrderConfirmation(order: Order): Promise<void>;
}
class OrderProcessingService {
constructor(
private orderRepo: OrderRepository,
private paymentGateway: PaymentGateway,
private notificationService: NotificationService
) {}
async processOrder(orderData: OrderData): Promise<string> {
// Complex business logic with multiple dependencies
const order = new Order(orderData);
await this.orderRepo.save(order);
const payment = await this.paymentGateway.charge(
order.total,
orderData.paymentToken
);
await this.notificationService.sendOrderConfirmation(order);
return order.id;
}
}
2. Microservices Architectureโ
// User service following SOLID principles
interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
}
interface EventPublisher {
publish(event: DomainEvent): Promise<void>;
}
class UserService {
constructor(
private userRepo: UserRepository,
private eventPublisher: EventPublisher
) {}
async updateUser(id: string, updates: UserUpdates): Promise<void> {
const user = await this.userRepo.findById(id);
user.update(updates);
await this.userRepo.save(user);
await this.eventPublisher.publish(
new UserUpdatedEvent(user.id, updates)
);
}
}
3. Libraries and Frameworksโ
// HTTP client library following OCP
interface HttpInterceptor {
intercept(request: HttpRequest): HttpRequest;
}
class HttpClient {
private interceptors: HttpInterceptor[] = [];
addInterceptor(interceptor: HttpInterceptor): void {
this.interceptors.push(interceptor);
}
async get(url: string): Promise<Response> {
let request = new HttpRequest(url);
// Apply all interceptors
for (const interceptor of this.interceptors) {
request = interceptor.intercept(request);
}
return await fetch(request.url, request.options);
}
}
// Easy to extend with new interceptors
class AuthInterceptor implements HttpInterceptor {
intercept(request: HttpRequest): HttpRequest {
request.headers['Authorization'] = 'Bearer token';
return request;
}
}
4. Testing Scenariosโ
// SOLID principles make testing easier
interface EmailProvider {
send(to: string, subject: string, body: string): Promise<void>;
}
class UserRegistrationService {
constructor(
private userRepo: UserRepository,
private emailProvider: EmailProvider
) {}
async registerUser(userData: UserData): Promise<void> {
const user = new User(userData);
await this.userRepo.save(user);
await this.emailProvider.send(
user.email,
'Welcome!',
'Welcome to our platform'
);
}
}
// Easy to test with mocks
describe('UserRegistrationService', () => {
it('should register user and send welcome email', async () => {
const mockUserRepo = {
save: jest.fn()
};
const mockEmailProvider = {
send: jest.fn()
};
const service = new UserRegistrationService(
mockUserRepo,
mockEmailProvider
);
await service.registerUser({ email: 'test@example.com' });
expect(mockUserRepo.save).toHaveBeenCalled();
expect(mockEmailProvider.send).toHaveBeenCalledWith(
'test@example.com',
'Welcome!',
'Welcome to our platform'
);
});
});
โ Where NOT to Use SOLID Principlesโ
1. Simple Scripts and Utilitiesโ
// โ OVER-ENGINEERING: Don't use SOLID for simple scripts
// Simple file processing script
const fs = require('fs');
// This is fine - no need for abstraction
function processLogFile(filename) {
const content = fs.readFileSync(filename, 'utf8');
const lines = content.split('\n');
const errors = lines.filter(line => line.includes('ERROR'));
console.log(`Found ${errors.length} errors`);
errors.forEach(error => console.log(error));
}
processLogFile('app.log');
2. Rapid Prototypingโ
// โ OVER-ENGINEERING: Don't use SOLID for prototypes
// Quick prototype for testing an idea
function quickWeatherApp() {
// Direct API call - no abstraction needed
fetch('https://api.weather.com/current')
.then(response => response.json())
.then(data => {
document.getElementById('weather').innerHTML =
`Temperature: ${data.temp}ยฐC`;
});
}
// This is perfectly fine for a prototype
3. Configuration Filesโ
// โ OVER-ENGINEERING: Simple config doesn't need SOLID
// Application configuration
const config = {
database: {
host: 'localhost',
port: 5432,
name: 'myapp'
},
api: {
timeout: 5000,
retries: 3
}
};
module.exports = config;
4. Small Pure Functionsโ
// โ OVER-ENGINEERING: Pure utility functions don't need SOLID
// Simple utility functions
function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}
function calculateTax(price, rate) {
return price * rate;
}
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
// These are fine as-is
5. One-off Data Processingโ
# โ OVER-ENGINEERING: One-time data migration script
import csv
import json
# Simple data conversion - no need for classes/interfaces
with open('users.csv', 'r') as csv_file:
reader = csv.DictReader(csv_file)
users = list(reader)
with open('users.json', 'w') as json_file:
json.dump(users, json_file, indent=2)
print(f"Converted {len(users)} users to JSON")
๐ When to Apply SOLID: Decision Matrixโ
| Scenario | Code Size | Team Size | Maintenance Period | Apply SOLID? |
|---|---|---|---|---|
| Enterprise App | Large (10K+ lines) | Large (5+ devs) | Years | โ YES |
| Startup MVP | Medium (1K-5K lines) | Small (2-3 devs) | Months | ๐ค MAYBE |
| Personal Project | Small (<1K lines) | Solo | Weeks | โ NO |
| Library/Framework | Any size | Any size | Long-term | โ YES |
| Data Script | Small | Solo | One-time | โ NO |
| Microservice | Medium | Medium (3-5 devs) | Long-term | โ YES |
| Prototype | Any size | Any size | Days | โ NO |
๐ฏ Gradual Adoption Strategyโ
Phase 1: Start Simpleโ
// Begin with working code
class OrderService {
async createOrder(orderData: any) {
// Validation
if (!orderData.items || orderData.items.length === 0) {
throw new Error('Order must have items');
}
// Save to database
const order = await database.save(orderData);
// Send notification
await emailService.send(orderData.customerEmail, 'Order created');
return order;
}
}
Phase 2: Extract Responsibilities (SRP)โ
// Extract validation
class OrderValidator {
static validate(orderData: any): void {
if (!orderData.items || orderData.items.length === 0) {
throw new Error('Order must have items');
}
}
}
class OrderService {
async createOrder(orderData: any) {
OrderValidator.validate(orderData);
const order = await database.save(orderData);
await emailService.send(orderData.customerEmail, 'Order created');
return order;
}
}
Phase 3: Add Abstractions (DIP)โ
// Add interfaces when you need flexibility
interface OrderRepository {
save(orderData: any): Promise<Order>;
}
interface NotificationService {
send(email: string, message: string): Promise<void>;
}
class OrderService {
constructor(
private orderRepo: OrderRepository,
private notificationService: NotificationService
) {}
async createOrder(orderData: any) {
OrderValidator.validate(orderData);
const order = await this.orderRepo.save(orderData);
await this.notificationService.send(
orderData.customerEmail,
'Order created'
);
return order;
}
}
Phase 4: Enable Extension (OCP)โ
// Add plugin system when needed
interface OrderProcessor {
process(order: Order): Promise<void>;
}
class InventoryProcessor implements OrderProcessor {
async process(order: Order): Promise<void> {
// Reserve inventory
}
}
class PaymentProcessor implements OrderProcessor {
async process(order: Order): Promise<void> {
// Process payment
}
}
class OrderService {
private processors: OrderProcessor[] = [];
addProcessor(processor: OrderProcessor): void {
this.processors.push(processor);
}
async createOrder(orderData: any) {
const order = await this.orderRepo.save(orderData);
// Process with all registered processors
for (const processor of this.processors) {
await processor.process(order);
}
return order;
}
}
๐ Key Principles Summaryโ
SOLID + DRY Guidelines:โ
- Start simple - Don't over-engineer from the beginning
- Refactor when you feel pain - Code duplication, hard to test, hard to change
- Apply gradually - One principle at a time
- Focus on value - Principles should solve real problems
- Team size matters - Larger teams benefit more from SOLID
- Maintenance duration matters - Long-term projects need SOLID more
- JavaScript/TypeScript fully support SOLID - Especially with TypeScript's type system
- Enterprise languages (Java/C#) benefit most - But all OOP languages can use SOLID
The Bottom Line:โ
SOLID principles are tools, not rules. Use them when they add value, not because you should. Start with working code, then refactor when complexity grows. The goal is maintainable, testable, and flexible code - not perfect adherence to principles.
JavaScript and TypeScript are excellent languages for applying SOLID principles, especially in larger applications where the benefits of good architecture become apparent. The dynamic nature of JavaScript doesn't prevent good design - it just requires more discipline.
