kdocs
GitHub
SC - Software Architecture
SC - Software Architecture
  • About
    • Architectural Requirements (RAs)
    • Architectural Perspectives
  • Software Design
    • Microservices
      • Patterns
    • Monolithic
    • C4 Model
  • Software Architectures
    • Clean Architecture
    • DDD (Domain Driven Design)
      • Strategic Modeling
      • Tactical Modeling
    • Event Driven Architecture
      • CAP Theorem
    • Hexagonal Architecture (Ports and Adapters)
  • Design Patterns
    • Behavioral
    • Creational
    • Data Access
    • Structural
  • Practices
    • Clean Code
    • SOLID
  • Others
    • CQRS
Powered by GitBook
On this page
  • S (Single Responsibility Principle - SRP)
  • O (Open Closed Principle - OCP)
  • L (Liskov Substitution Principle - LSP)
  • Parameters and Returns
  • Pre & Post conditions
  • Invariance (Internal State)
  • I (Interface Segregation Principle - ISP)
  • D (Dependency Inversion Principle - DIP)
  1. Practices

SOLID

The applicability of SOLID principles, has the objective of valuing:

  • Object Oriented Design.

    • High cohesion.

    • Low coupling.

      • The intimacy, Object A knows B, B knows C and A, etc...

      • The problem is not only knowing, but to know the inner characteristics.

  • Fragility reduction.

  • Increase reuse.

  • Reduce rigidity.

S (Single Responsibility Principle - SRP)

A class should have one responsability.

A class must have one reason to change, if there are many reasons, they must be separated to other classes.

O (Open Closed Principle - OCP)

Classes should be open to extension and closed for modification.

The behavior of a module can be extended without modifying its source code.

Whenever you need to add new functionality to a class, don't just modify the existent class.

Instead create a base class, and for each added functionality you make it in a new class to extend the base one.

Using Factories to create similar implementations.

L (Liskov Substitution Principle - LSP)

Objects of a Super-class should be able to be replaced with objects of a Sub-class, without any breaks.

If Class S extends Class T, then Object<T> could be replaced by Object<S> without any breaks.

Garantees that subclasses can be exchanged between themselves without any breaks.

Easy practical example

In practice have S as AxiosAdapter or FetchAdapter implements T HttpClient, you have to be able to change between Axios or Fetch without breaking execution/expectation.

interface HttpClient {
    get(url: string): Promise<any>;
    post(url: string, data: any): Promise<any>;
}

// Returns `response.data`
class AxiosAdapter implements HttpClient {
    get(url: string): Promise<any> {
        return axios.get(url);
    }
    
    post(url: string, data: any): Promise<any> {
        return axios.post(url, data);
    }
}

// Returns `response.body`
class FetchAdapter implements HttpClient {
    async get(url: string): Promise<any> {
        const response = await fetch(url);
        return response.json();
    }
    
    async post(url: string, data: any): Promise<any> {
        const response = await fetch(url, {
            method: 'post',
            headers: { "content-type": "application/json" },
            body: JSON.stringify(data)
        });
        return response.json();
    }
}

Defining subclasses only garantees syntax, but does not implies in keeping coherence and execution semantics.

From this principle we can derive some needed restrictions and requirements when making a Sub-class to avoid breaking LSP:

Parameters and Returns

Example Base

class AnimalShelter {
    putAnimal(Animal animal): void { ... }
    takeAnimal(): Animal { ... }
}

class CatShelter extends AnimalShelter {
    ...
}

Contravariance of method parameters

It is type safe and preferred to allow an overriding method of a Sub-class to accept more general arguments than the method in the Super-class.

(Parameters passed cannot be strenghthened - more restrictive - in Sub-classes)

It might seem intuitive that a subclass should be “more restrictive” since it’s specialized, but contravariance exists to ensure that the subclass can accept anything the superclass can and still operate correctly.

This makes substitutability possible while still allowing the Sub-class to handle inputs in more specific ways if needed (like the credit card limit check in the example).

LSP requires contravariant parameters because:

  • It ensures that a subclass can be used wherever the Super-class is expected without introducing type conflicts.

  • It keeps the behavior general and interchangeable at the point of use, which is fundamental to achieving polymorphism.

// If overriding `putAnimal`
class CatShelter extends AnimalShelter {
    /*
        I could pass a LivingBeing, but I should NOT pass a Cat.
    */
    putAnimal(LivingBeing lb): void { ... }
    putAnimal(Cat cat): void { ... }  // WRONG
}

Covariance of method return types

An overriding method of a Sub-class may return more specific types than the method in the Super-class.

(Returned values cannot be weakened - less restrictive - in Sub-classes)

// If overriding `takeAnimal`
class CatShelter extends AnimalShelter {
    /*
        I can return a Cat, but I should NOT return an LivingBeing.
        (Considering LivingBeing something more general than Animal)
    */
    takeAnimal(): Cat { ... }
    takeAnimal(): LivingBeing { ... }  // WRONG
}

Exceptions thrown

New exceptions cannot be thrown by the methods in the Sub-class, except if they are Sub-classes of exceptions thrown by the methods of the Super-class.

New exceptions in Sub-classes may introduce unexpected behavior, that the Super-class did not have, violating the substitutability.

Pre & Post conditions

Ex.:
class PaymentProcessor {
    // Preconditions: amount should be a positive number.
    // Postconditions: returns a success message with the processed amount.
    process(amount: number): string {
        if (amount <= 0) throw new Error("Amount must be positive.");
        return `Processed payment of $${amount}`;
    }
}

Preconditions

Refer to the conditions that must be true before a method is called.

Cannot be strenghthened in the Sub-class.

Sub-classes should not impose stricter requirements than the Super-class. If they do, the Sub-class won't be usable in all contexts where the Super-class is expected.

Preconditions can be modified in redefined routines, but may only be weakened. (May lessen the obligation of the client, but not increase it)

An example of Preconditions being violated:

Ex.:
class CreditCardProcessor extends PaymentProcessor {
    private limit: number = 5000;
    
    process(amount: number): string {
        if (amount <= 0) throw new Error("Amount must be positive.");
        /*
            Here we are strengthening the method's preconditions.
            
            By throwing an Error, thus any code that expects `process` to handle
            numbers over 5000 will encounter unexpected behavior.
        */
        if (amount > this.limit) throw new Error("Amount exceeds limits");
        return `Processed payment of $${amount}`;
    }
}

A resolution to this problem would be:

  • To use Composition instead of Inheritance.

  • Or define Interfaces with specific Contracts.

Postconditions

Refer to the conditions that should be true after a method completes sucessfully.

Cannot be weakened in the Sub-class.

Sub-classes must meet all the guarantees (outcomes or states) promised by the Super-class. If a Sub-class does less than what the Super-class guarantees, it will break client expectations.

Postconditions can be modified in redefined routines, but may only be strenghthened. (May increase the benefits it provides to the client, but not decrease those benefits)

An example of Postconditions being violated:

Ex.:
class CreditCardProcessor extends PaymentProcessor {
    private limit: number = 5000;
    
    process(amount: number): string {
        if (amount <= 0) throw new Error("Amount must be positive.");
        /*
            Here we are weakening the method's postconditions.
            
            By returning a new type of message (Error:) that is unexpected, remember that the
            postcondition from the Super-class was to return a success message only.
        */
        if (amount > this.limit) return `Error: Amount exceeds limits.`;
        return `Processed payment of $${amount}`;
    }
}

Invariance (Internal State)

Cannot be weakened in the Sub-class. (Should be preserved)

I (Interface Segregation Principle - ISP)

A class should not implement interfaces that it won't use.

Create interfaces based only on what the client needs, avoiding dependencies on things it won't use.

Usually impacts reduction of unecessary re-compilation.

Example

In this example, the Exec class, which is the class that uses the method, specifies what it wants from ClassA.

So now, Exec don't know ClassA anymore, it only knows Interface2. And if ClassA changes in the future, Exec won't need to be re-compiled (Since there is no more import A from '.../ClassA').

Main.ts
Registry.getInstance().provide('classA', new A());
Exec.ts
class Exec {
    @inject('classA')
    classA: Interface2;
}

export interface2 {
    method2(): void;
}
ClassA.ts
interface ARepo extends Interface1, Interface2 {
    method1(): void;
    method2(): void;
    method3(): void;
}

class A implements ARepo {
    method1(): void { ... }
    method2(): void { ... }
    method3(): void { ... }
}

D (Dependency Inversion Principle - DIP)

The closest to business rules, the higher the level.

High level modules must not depend in low level ones. Both must depend on abstractions and not implementations.

  • Separated (independent) modules, must not have direct dependencies between them.

  • A class must not know the implementation of other classes methods, but know only the interface of that class.

PreviousClean CodeNextCQRS

Last updated 4 months ago

The following code breaks LSP since you cannot change between adapters, because AxiosAdapter returns different data object than FetchAdapter. (So the Class are broken)

Post Conditions
Drawing
Drawing
Drawing