SOLID explicado com TypeScript
Published: 2023-05-21 23:16:30

Alguns princípios foram criados para ajudar pessoas desenvolvedoras a criar sistemas que sejam fáceis de entender, manter e evoluir ao longo do tempo.

## Princípios

1. **Single Responsibility Principle** ou **Princípio da Responsabilidade Única** diz que uma classe deve ter apenas uma única responsabilidade e assim, um único motivo para mudar.
2. **Open-Closed Principle** ou **Princípio do Aberto-Fechado** diz que uma classe deve estar aberta para novos cenários, mas fechada para modificação.
3. **Liskov Substitution Principle** ou **Princípio da Substituição de Liskov** diz que os objetos de uma classe derivada devem ser substituíveis por objetos de sua classe base sem alterar o comportamento do software.
4. **Interface Segregation Principle** ou **Princípio da Segregação de Interface** diz que interfaces devem ser separadas para que classes possam depender apenas dos métodos que utilizam.
5. **Dependency Inversion Principle** ou **Princípio da Inversão de Dependência** diz que módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.

Ao seguir esses princípios, conseguimos criar sistemas mais flexíveis, robustos e fáceis de manter, alterar e evoluir ao longo do tempo.

Vamos mergulhar um pouco mais fundo.

### Single Responsibility Principle ou Princípio da Responsabilidade Única

Ter uma responsabilidade única significa que uma classe deve ter apenas um motivo para mudar. Se uma classe tiver mais de uma responsabilidade, mudanças em uma responsabilidade podem afetar outras responsabilidades, o que pode levar a um código mais complexo e difícil de manter.

Darei um exemplo mais claro sobre o que podem ser consideradas responsabilidades diferentes.

Suponha que tenhamos uma classe chamada **`User`** que é responsável tanto pela autenticação do usuário quanto pelo envio de notificações por e-mail.

```tsx
abstract class User {
  abstract login(email: string, password: string): void
  abstract sendCode(email: string, code: string): void
}
```

Ela possui responsabilidades diferentes e não segue o princípo **SRP**. Podemos dividir as responsabilidades em duas classes diferentes.

```tsx
abstract class Auth {
  abstract login(email: string, password: string): void
}

abstract class Email {
  abstract send(email: string, message: string): void
}
```

Assim, caso haja uma mudança em uma das responsabilidades, a outra não será afetada, o código estará mais organizado, fácil de entender, testar e manter. O que facilita sua reutilização, pois as classes que têm apenas uma responsabilidade podem ser usadas em diferentes partes.

### Open-Closed Principle ou Princípio do Aberto-Fechado

Uma classe deve permitir ser estendida para atender a novos requisitos, sem necessidade de alteração, ou seja, deve permitir adicionar novas funcionalidades usando herança, sem afetar o comportamento existente. Por exemplo, temos uma classe abstrata contendo alguns métodos básicos para comunicação com alguma fonte de dados.

```tsx
abstract class Repository<T, K extends keyof T> {
  abstract findAll(): T[];
  abstract findOne(key: T[K]): T[];
  abstract create(entity: Omit<T, K>): void;
  abstract update(key: T[K], entity: Partial<T>): void;
  abstract remove(key: T[K]): void;
}
```

Então criamos uma abstração específica com um método contextual, sem alterar `Repository`.

```tsx
abstract class UserRepository extends Repository<User, 'id'> {
  abstract findOneByEmail(email: string): User
}
```

Para seguir o **OCP**, a classe deve depender de abstrações em vez de implementações concretas. Isso significa que a classe deve depender de interfaces ou classes abstratas em vez de depender diretamente de classes concretas. As extensões podem ser adicionadas por meio da criação de novas implementações de interfaces ou classes abstratas, sem afetar o existente.

### Liskov Substitution Principle ou Princípio da Substituição de Liskov

Uma classe derivada deve ser capaz de substituir sua classe base sem introduzir erros ou comportamentos inesperados.  Classes derivadas devem manter a mesma semântica que suas classes base, ou seja, implementações devem cumprir os mesmos contratos definidos em suas abstrações. Caso uma subclasse não possa cumprir essas condições, ela não deve ser considerada apta de implementação da classe base.

Por exemplo, criamos um caso de uso para autenticação.

```tsx
class SignInUseCase {
  constructor(private repository: AuthRepository) {}

  execute(credential: UserCredential) {
    return this.repository.signIn(credential);
  }
}
```

Em seguida, criamos uma abstração que será o contrato para garantir que a classe concreta de `AuthRepository` cumpra o acordo de implementação para o método `signIn`.

```tsx
abstract class AuthRepository {
  abstract signIn(value: UserCredential): Observable<AuthResponse>;
}
```

Agora podemos criar implementações diferentes para `AuthRepository` que cumpram o contrato, recebendo `UserCredential` e retornando `Observable<AuthResponse>`.

Como uma implementação que envia uma [requisição *HTTP*](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Methods).

```tsx
class AuthHttpRepositoryImpl implements AuthRepository {
  constructor(private http: HttpClient) {}

  signIn(value: UserCredential) {
    return this.http.post<AuthResponse>('/api/auth', value);
  }
}
```

E outra como *[stub](https://www.notion.so/C-digo-respons-vel-2ad92eebdb444596890e3003bf9f4fcb),* usada apenas durante a execução de testes automatizados.

```tsx
class AuthStubRepositoryImpl implements AuthRepository {
  signIn(value: UserCredential) {
    return of(
      { accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2Vybm...' }
    );
  }
}
```

Então se uma classe **A** depende da classe base **B**, então uma classe derivada **C** de **B** deve ser capaz de ser usada como **B**, sem afetar o software.

- **A**: `SignInUseCase`
- **B**: `AuthRepository`
- **C**: `AuthMockRepositoryImpl` e `AuthMockRepositoryImpl`

Assim o código se torna mais flexível e escalável, pois novas classes derivadas podem ser adicionadas sem efeitos colaterais indesejados, além de ajudar na prevenção de bugs, pois classes derivadas devem ser testadas com relação aos contratos definidos nas classes base.

### Princípio da Segregação de Interface

Uma classe deve ter interfaces específicas para as suas funcionalidades, em vez de depender de uma única interface que contenha todas as funcionalidades possíveis.

Por exemplo, se uma classe precisa apenas de um método de leitura de dados, ela deve implementar uma interface que contenha apenas esse método, em vez de depender de uma interface que contenha também métodos de escrita e exclusão de dados.

```tsx
abstract class UserRepository implements FindOneBy<User> {
  abstract findOneBy<K extends keyof User>(
    key: K,
    value: User[K]
  ): Observable<User>;
}
```

O código se torna mais coeso e menos acoplado, pois dependem apenas de interfaces que precisam, pois temos interfaces mais granulares e específicas, o que facilita a manutenção e reutilização de código. O **ISP** incentiva a modularidade para que interfaces possam ser adicionadas para atender a novas funcionalidades, sem afetar classes existentes.

### **Dependency Inversion Principle** ou **Princípio da Inversão de Dependência**

Uma classe de alto nível deve depender de uma interface ou classe abstrata ao invés de depender de uma classe concreta de baixo nível.

Um bom exemplo é a implementação que apresentei no [Princípio da Substituição de Liskov](#liskov-substitution-principle-ou-princ%C3%ADpio-da-substitui%C3%A7%C3%A3o-de-liskov) em que a classe `SignInUseCase` depende de `AuthRepository`, que é uma classe abstrata. A substituição entre `AuthHttpRepositoryImpl` e `AuthStubRepositoryImpl` quando conveniente, é possível utilizando a técnica de injeção de dependências.

Então, em vez de uma classe de alto nível depender diretamente de uma classe de acesso a dados específica, ela deve depender de uma abstração genérica que represente as funcionalidades que ela precisa.

Alguns frameworks trabalham desta forma por padrão, como é o caso do [Angular](https://angular.io/guide/dependency-injection-in-action), quando este não é o padrão do framework que utilizamos podemos usar alguma biblioteca, como a [Inversify](https://github.com/inversify/InversifyJS) ou [tsyringe](https://github.com/microsoft/tsyringe) da Microsoft.

Assim o código se torna mais flexível e reutilizável, pois os módulos de alto nível são desacoplados dos módulos de baixo nível. Permitindo que diferentes implementações de baixo nível possam ser usadas com a mesma classe de alto nível, sem afetar o código a classe.

Este princípio recomenda um design orientado a interfaces, que é uma abordagem mais modular e escalável para o desenvolvimento de software. Permitindo que equipes diferentes trabalhem sem que uma dependa diretamente do código da outra.

Comenta aqui se você gostaria de ler uma publicação mostrando como funciona esta técnica, implementando o pattern dependency injection do zero. 🙂