SOLID
solid-subtitle
solid-origin-note
Single Responsibility
solid-srp-desc
code-origin-oop
// โ BAD: Single Responsibility Principle Violation// This class has multiple responsibilities:// 1. Order validation// 2. Price calculation// 3. Payment processing// 4. Email notificationsclass OrderProcessor {fun validateOrder(order: Order): Boolean {// Validation logicreturn order.items.isNotEmpty()}fun calculateTotal(order: Order): Double {// Price calculation logicvar total = order.items.sumOf { it.price }total += total * 0.08 // Taxreturn total}fun processPayment(order: Order, amount: Double): Boolean {// Payment processing logicprintln("Processing payment: $$amount")return true}fun sendConfirmationEmail(order: Order) {// Email logicprintln("Sending email for order: ${order.id}")}}// โ GOOD: Single Responsibility Applied// Each class has ONE responsibilityclass OrderValidator {fun validate(order: Order): Boolean {return order.items.isNotEmpty()}}class PriceCalculator {fun calculateTotal(order: Order): Double {var total = order.items.sumOf { it.price }total += total * 0.08return total}}class PaymentProcessor {fun processPayment(order: Order, amount: Double): Boolean {println("Processing payment: $$amount")return true}}class EmailService {fun sendConfirmation(order: Order) {println("Sending email for order: ${order.id}")}}// Orchestrator coordinates but doesn't implement logicclass OrderService {private val validator = OrderValidator()private val calculator = PriceCalculator()private val paymentProcessor = PaymentProcessor()private val emailService = EmailService()fun processOrder(order: Order) {if (!validator.validate(order)) returnval total = calculator.calculateTotal(order)if (paymentProcessor.processPayment(order, total)) {emailService.sendConfirmation(order)}}}// Benefits:// โ Each class has one reason to change// โ Easy to test in isolation// โ Easy to maintain and modify// โ Clear responsibilities
// โ WRONG: Multiple responsibilities in one componentexport function OrderProcessor() {// Responsibility 1: Validationconst validateOrder = (order: Order) => {return order.items.length > 0;};// Responsibility 2: Price calculationconst calculateTotal = (order: Order) => {let total = order.items.reduce((sum, item) => sum + item.price, 0);total += total * 0.08; // Taxreturn total;};// Responsibility 3: Payment processingconst processPayment = async (order: Order, amount: number) => {console.log("Processing payment:", amount);// Payment logic};// Responsibility 4: Email notificationsconst sendEmail = async (order: Order) => {console.log("Sending email for order:", order.id);// Email logic};// All mixed together!const handleOrder = async (order: Order) => {if (!validateOrder(order)) return;const total = calculateTotal(order);if (await processPayment(order, total)) {await sendEmail(order);}};// Problems:// - Multiple reasons to change// - Hard to test// - Violates SRP// - Difficult to maintainreturn <div>...</div>;}
Preview
Console
// โ GOOD: Single Responsibility Applied// Each function/service has ONE responsibility// Responsibility 1: Validation onlyclass OrderValidator {validate(order: Order): boolean {return order.items.length > 0;}}// Responsibility 2: Price calculation onlyclass PriceCalculator {calculateTotal(order: Order): number {let total = order.items.reduce((sum, item) => sum + item.price, 0);total += total * 0.08; // Taxreturn total;}}// Responsibility 3: Payment processing onlyclass PaymentProcessor {async processPayment(order: Order, amount: number): Promise<boolean> {console.log("Processing payment:", amount);// Payment logicreturn true;}}// Responsibility 4: Email notifications onlyclass EmailService {async sendConfirmation(order: Order): Promise<void> {console.log("Sending email for order:", order.id);// Email logic}}// Orchestrator coordinates but doesn't implement logicclass OrderService {private validator = new OrderValidator();private calculator = new PriceCalculator();private paymentProcessor = new PaymentProcessor();private emailService = new EmailService();async processOrder(order: Order): Promise<void> {if (!this.validator.validate(order)) return;const total = this.calculator.calculateTotal(order);if (await this.paymentProcessor.processPayment(order, total)) {await this.emailService.sendConfirmation(order);}}}// Component only handles UIexport function OrderForm() {const orderService = new OrderService();const handleSubmit = async (order: Order) => {await orderService.processOrder(order);};return (<form onSubmit={handleSubmit}>{/* Form UI */}</form>);}// Benefits:// โ Each class/function has one reason to change// โ Easy to test in isolation// โ Easy to maintain and modify// โ Clear separation of concerns// โ Reusable components
Preview
Console
Open/Closed Principle (OCP)
Software entities should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
code-origin-oop
// โ BAD: Open/Closed Principle Violation// Must modify existing code to add new shapesclass AreaCalculator {fun calculateArea(shape: Any): Double {return when (shape) {is Circle -> Math.PI * shape.radius * shape.radiusis Rectangle -> shape.width * shape.height// โ Must modify this class to add Triangle!else -> throw IllegalArgumentException("Unknown shape")}}}// โ GOOD: Open/Closed Principle Applied// Open for extension, closed for modificationinterface Shape {fun area(): Double}class Circle(val radius: Double) : Shape {override fun area() = Math.PI * radius * radius}class Rectangle(val width: Double, val height: Double) : Shape {override fun area() = width * height}// โ Can add new shapes without modifying existing codeclass Triangle(val base: Double, val height: Double) : Shape {override fun area() = 0.5 * base * height}class AreaCalculator {fun calculateTotalArea(shapes: List<Shape>): Double {return shapes.sumOf { it.area() }}}// Benefits:// โ Add new shapes without modifying calculator// โ Follows Open/Closed Principle// โ Easy to extend
// โ WRONG: Must modify existing code to add new button typesconst Button = ({ type, ...props }: { type: string; [key: string]: any }) => {// Must modify this component to add new button typesif (type === "primary") {return <button className="bg-blue-500" {...props} />;} else if (type === "secondary") {return <button className="bg-gray-500" {...props} />;} else if (type === "danger") {return <button className="bg-red-500" {...props} />;}// โ Must add new if-else for each new type!return <button {...props} />;};// Problems:// - Must modify component for new types// - Violates Open/Closed Principle// - Hard to extend
Preview
Console
// โ GOOD: Open/Closed Principle Applied// Open for extension, closed for modificationinterface ButtonProps {onClick?: () => void;children: React.ReactNode;className?: string;}interface ButtonVariant {getClassName: () => string;}// Base button component (closed for modification)const Button = ({ variant, ...props }: ButtonProps & { variant: ButtonVariant }) => {return (<button className={variant.getClassName()} {...props}>{props.children}</button>);};// Variants (open for extension)const PrimaryButtonVariant: ButtonVariant = {getClassName: () => "bg-blue-500 hover:bg-blue-600 text-white",};const SecondaryButtonVariant: ButtonVariant = {getClassName: () => "bg-gray-500 hover:bg-gray-600 text-white",};const DangerButtonVariant: ButtonVariant = {getClassName: () => "bg-red-500 hover:bg-red-600 text-white",};// โ Can add new variants without modifying Button componentconst SuccessButtonVariant: ButtonVariant = {getClassName: () => "bg-green-500 hover:bg-green-600 text-white",};// Usage<Button variant={PrimaryButtonVariant}>Click me</Button><Button variant={SuccessButtonVariant}>Success</Button>// Benefits:// โ Add new variants without modifying Button// โ Follows Open/Closed Principle// โ Easy to extend// โ Testable in isolation
Preview
Console
Liskov Substitution Principle (LSP)
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application. Subtypes must be substitutable for their base types.
code-origin-oop
// โ BAD: Liskov Substitution Principle Violation// Square cannot substitute Rectangle without breaking behavioropen class Rectangle {open var width: Double = 0.0open var height: Double = 0.0fun area(): Double = width * height}class Square : Rectangle() {override var width: Double = 0.0set(value) {field = valueheight = value // โ Breaks rectangle behavior!}override var height: Double = 0.0set(value) {field = valuewidth = value // โ Breaks rectangle behavior!}}// This breaks!fun testRectangle(rect: Rectangle) {rect.width = 5.0rect.height = 4.0println(rect.area()) // Expects 20, but Square gives 16!}// โ GOOD: Liskov Substitution Applied// Both can substitute Shape without breaking behaviorinterface Shape {fun area(): Double}class Rectangle(val width: Double, val height: Double) : Shape {override fun area() = width * height}class Square(val side: Double) : Shape {override fun area() = side * side}// Both are substitutablefun calculateTotalArea(shapes: List<Shape>): Double {return shapes.sumOf { it.area() }}// Benefits:// โ Subtypes are truly substitutable// โ No unexpected behavior// โ Follows Liskov Substitution Principle
// โ WRONG: Components not substitutableconst Input = ({ type, ...props }: { type: string; [key: string]: any }) => {if (type === "text") {return <input type="text" {...props} />;} else if (type === "number") {// โ Number input might not accept all text input propsreturn <input type="number" {...props} />;}// Not substitutable - breaks when swapped};// Problems:// - Not truly substitutable// - Breaks when used interchangeably// - Violates LSP
Preview
Console
// โ GOOD: Liskov Substitution Applied// All input types are substitutableinterface InputProps {value: string;onChange: (value: string) => void;placeholder?: string;disabled?: boolean;}// Base input componentconst BaseInput = ({ value, onChange, placeholder, disabled, type }: InputProps & { type: string }) => {return (<inputtype={type}value={value}onChange={(e) => onChange(e.target.value)}placeholder={placeholder}disabled={disabled}/>);};// All are substitutable - same interfaceconst TextInput = (props: InputProps) => (<BaseInput {...props} type="text" />);const NumberInput = (props: InputProps) => (<BaseInput {...props} type="number" />);const EmailInput = (props: InputProps) => (<BaseInput {...props} type="email" />);// Can use any input type interchangeablyfunction FormField({ InputComponent, ...props }: { InputComponent: React.ComponentType<InputProps> } & InputProps) {return <InputComponent {...props} />;}// All work the same way!<FormField InputComponent={TextInput} value={text} onChange={setText} /><FormField InputComponent={NumberInput} value={number} onChange={setNumber} /><FormField InputComponent={EmailInput} value={email} onChange={setEmail} />// Benefits:// โ All input types are substitutable// โ Same interface guarantees compatibility// โ Follows Liskov Substitution Principle// โ No unexpected behavior
Preview
Console
solid-dip-title
solid-dip-desc
code-origin-oop
// โ GOOD: Dependency Inversion Principle Applied// High-level modules depend on abstractions, not concrete implementations// Step 1: Define Abstractions (Interfaces)data class User(val name: String,val email: String,val dateOfBirth: java.util.Date)interface ExportUser {fun export(user: User)}// Step 2: Implement Concrete Classesclass ExportUserToCSV : ExportUser {override fun export(user: User) {// Logic to export user to a CSV fileprintln("Exported ${user.name} to CSV")}}class ExportUserToPDF : ExportUser {override fun export(user: User) {// Logic to export user to a PDF fileprintln("Exported ${user.name} to PDF")}}// Step 3: Use Dependency Injection// High-level module depends on abstraction, not concrete implementationclass ExportUserUseCase(private val exportUser: ExportUser // Depends on abstraction) {fun execute(user: User) {// Export the user data using the injected dependencyexportUser.export(user)}}// Usage examples:// With CSVval exportUsertoCSV = ExportUserToCSV()val exportUserUseCaseCSV = ExportUserUseCase(exportUsertoCSV)exportUserUseCaseCSV.execute(User(name = "Bobson Paebi",email = "paebi@bubstack.tech",dateOfBirth = java.util.Date()))// With PDFval exportUsertoPDF = ExportUserToPDF()val exportUserUseCasePDF = ExportUserUseCase(exportUsertoPDF)exportUserUseCasePDF.execute(User(name = "Bobson Paebi",email = "paebi@bubstack.tech",dateOfBirth = java.util.Date()))// Benefits:// โ High-level modules don't depend on low-level modules// โ Both depend on abstractions// โ Easy to test (can inject mocks)// โ Easy to extend (add new export formats)// โ Follows Dependency Inversion Principle
// โ WRONG: High-level module depends on low-level modules// Direct dependency on concrete implementationstype User = {name: string;email: string;dateOfBirth: Date;};// Low-level module: CSV exportclass ExportUserToCSV {export(user: User) {console.log(`Exported ${user.name} to CSV`);}}// Low-level module: PDF exportclass ExportUserToPDF {export(user: User) {console.log(`Exported ${user.name} to PDF`);}}// โ High-level module directly depends on concrete implementationsclass ExportUserUseCase {private csvExporter = new ExportUserToCSV(); // Direct dependency!private pdfExporter = new ExportUserToPDF(); // Direct dependency!executeCSV(user: User) {this.csvExporter.export(user);}executePDF(user: User) {this.pdfExporter.export(user);}}// Problems:// - High-level depends on low-level modules// - Hard to test (can't inject mocks)// - Hard to extend (must modify UseCase for new formats)// - Violates Dependency Inversion Principle// - Tight coupling
Preview
Console
// โ GOOD: Dependency Inversion Principle Applied// High-level modules depend on abstractions, not concrete implementations// Step 1: Define Abstractions (Interfaces)type User = {name: string;email: string;dateOfBirth: Date;};interface ExportUser {export(user: User): void;}// Step 2: Implement Concrete Classesclass ExportUserToCSV implements ExportUser {export(user: User) {// Logic to export user to a CSV fileconsole.log(`Exported ${user.name} to CSV`);}}class ExportUserToPDF implements ExportUser {export(user: User) {// Logic to export user to a PDF fileconsole.log(`Exported ${user.name} to PDF`);}}// Step 3: Use Dependency Injection// High-level module depends on abstraction, not concrete implementationclass ExportUserUseCase {constructor(private exportUser: ExportUser) {} // Depends on abstractionexecute(user: User) {// Export the user data using the injected dependencythis.exportUser.export(user);}}// Usage examples:// With CSVconst exportUsertoCSV = new ExportUserToCSV();const exportUserUseCaseCSV = new ExportUserUseCase(exportUsertoCSV);exportUserUseCaseCSV.execute({name: 'Bobson Paebi',email: 'paebi@bubstack.tech',dateOfBirth: new Date(),});// With PDFconst exportUsertoPDF = new ExportUserToPDF();const exportUserUseCasePDF = new ExportUserUseCase(exportUsertoPDF);exportUserUseCasePDF.execute({name: 'Bobson Paebi',email: 'paebi@bubstack.tech',dateOfBirth: new Date(),});// React Component Example:export function UserExportForm() {const [exportFormat, setExportFormat] = useState<'csv' | 'pdf'>('csv');// Create the appropriate exporter based on selectionconst exporter: ExportUser = exportFormat === 'csv'? new ExportUserToCSV(): new ExportUserToPDF();const exportUserUseCase = new ExportUserUseCase(exporter);const handleExport = (user: User) => {exportUserUseCase.execute(user);};return (<form onSubmit={(e) => {e.preventDefault();handleExport({ name: 'John', email: 'john@example.com', dateOfBirth: new Date() });}}><select value={exportFormat} onChange={(e) => setExportFormat(e.target.value as 'csv' | 'pdf')}><option value="csv">CSV</option><option value="pdf">PDF</option></select><button type="submit">Export User</button></form>);}// Benefits:// โ High-level modules don't depend on low-level modules// โ Both depend on abstractions// โ Easy to test (can inject mocks)// โ Easy to extend (add new export formats without modifying UseCase)// โ Follows Dependency Inversion Principle// โ Loose coupling
Preview
Console