Reflexões sobre SOLID - A Letra "S"
Tags: solidprinciples, programming, designpatterns, typescript
Published: 2024-05-08 02:02:39

Continuando minha série de reflexões, assim como a maioria das pessoas que começa a se aprofundar em SOLID, gostaria de começar com a letra "S". Trabalharei com TypeScript nos exemplos.

---

## Neste artigo

[Single Responsibility Principle](#single-responsibility-principle)
[Exemplo abstrato](#exemplo-abstrato)
[Exemplo técnico (Front-End)](#exemplo-técnico-frontend)
[Exemplo técnico (Back-End)](#exemplo-técnico-backend)
[Exemplo pessoal](#exemplo-pessoal)
[Exemplo funcional](#exemplo-funcional)
[As aplicabilidades](#as-aplicabilidades)
[Reflexões finais](#reflexões-finais)

---

## Single Responsibility Principle

O Single Responsibility Principle (ou _Princípio da Responsabilidade Única_) determina que uma classe (ou entidade, como preferir) deve ter somente **uma única responsabilidade, e um único motivo para ser alterada**. Essa última parte é a chave para o entendimento profundo do princípio.

Ou seja, o grande propósito por trás desta ideia é garantir que não tenhamos classes com propósitos misturados. Tendo uma classe A que foi desenvolvida para o propósito X, caso uma nova funcionalidade Y surja, é necessário uma análise para identificar se faz sentido Y ser implementado na classe A, ou se faria mais sentido em uma nova classe B.

E como fazer esse análise? Quais os critérios? Bom, vamos aos exemplos.

---

## Exemplo abstrato

Vamos utilizar uma **biblioteca** como exemplo. Suponhamos que eu desejo controlar os livros que existem em uma biblioteca, e também pesquisá-los. Nossas funcionalidades poderiam ser descritas da seguinte forma:

- **Cadastrar livros.** Permitir adicionar livros com o título e o autor.
- **Remover livros.** Permitir remover livros pelo número de identificação.
- **Listar todos os livros.**
- **Buscar livros por autor.**
- **Buscar livros por título.**

**🔴 Implementação Incorreta**

```typescript
// Apenas a interface dos nossos livros.
// Interface simples para o livro, utilizando constructor shorthand.
class Book {
  constructor(public id: string, public title: string, public author: string) {}
}

class Library {
  public books: Book[];

  constructor() {}

  // Método para adicionar um livro na biblioteca.
  public addBook(book: Book) {
    this.books.push(book);
  }

  // Método para remover um livro da biblioteca.
  // Ignore a falta de validação e uso puro do id - é para ser mais didático.
  public removeBook(id: string): void {
    const index = this.books.findIndex((book) => (book.id = id));
    this.books.splice(index, 1);
  }

  // Método para listar os livros existentes na biblioteca.
  public listBooks(): void {
    this.books.forEach((book) => {
      console.log(`Livro "${book.title}", autor "${book.author}"`);
    });
  }

  // Método para buscar por título do livro.
  // VIOLAÇÃO DO PRINCÍPIO: A classe tem a responsabilidade de controlar o cadastro de livros.
  // Com esse método, ela passa a controlar também as buscas e algoritmos vinculados a sua manipulação.
  // Portanto, ela passa a ter mais de um motivo para ser modificada.
  public findBookByTitle(titleToSearch: string): Book[] {
    // Supondo que o algoritmo de busca abaixo seja modificado ou evoluído, ocasionaria uma alteração em uma classe
    // que, originalmente, deveria se encarregar somente de controlar o cadastro de livros.
    return this.books.filter((book) => book.title.includes(titleToSearch));
  }

  // Método para buscar por autor do livro.
  // VIOLAÇÃO DO PRINCÍPIO: Mesmo de cima.
  public findBookByAuthor(authorToSearch: string): Book[] {
    return this.books.filter((book) => book.author.includes(authorToSearch));
  }
}

// Criando objeto do primeiro livro.
const viagemAoCentroDaTerra = new Book(
  "123",
  "Viagem ao Centro da Terra",
  "Julio Verne"
);

// Criando objeto do segundo livro.
const aMaquinaDoTempo = new Book(
  "456",
  "A Máquina do Tempo",
  "Herbert George Wells"
);

// Criando nossa biblioteca.
const library = new Library();

// Adicionando os livros.
library.addBook(viagemAoCentroDaTerra);
library.addBook(aMaquinaDoTempo);

// Listando os livros.
library.listBooks();
// OUTPUT:
// Livro "Viagem ao Centro da Terra", autor "Julio Verne"
// Livro "A Máquina do Tempo", autor "Herbert George Wells"

// Buscando por autor.
const search = library.findBookByAuthor('Julio');
console.log(search);
// OUTPUT:
// [ Book { id: '123', title: 'Viagem ao Centro da Terra', author: 'Julio Verne' } ]
```

**🟢 Implementação Correta**

```typescript
class Book {
  constructor(public id: string, public title: string, public author: string) {}
}

// Removemos a lógica de pesquisa dessa classe.
class Library {
  public books: Book[];

  constructor() {}

  public addBook(book: Book) {
    this.books.push(book);
  }

  public removeBook(id: string): void {
    const index = this.books.findIndex((book) => (book.id = id));
    this.books.splice(index, 1);
  }

  public listBooks(): void {
    this.books.forEach((book) => {
      console.log(`Livro "${book.title}", autor "${book.author}"`);
    });
  }
}

// E criamos uma nova, que se responsabiliza pelas pesquisas.
class LibrarySearcher {
  // Instanciamos com o objeto da biblioteca.
  constructor(private library: Library) {}

  // E, por fim, fazemos nossas pesquisas.
  public findBookByTitle(titleToSearch: string): Book[] {
    return this.library.books.filter((book) => book.title.includes(titleToSearch));
  }

  // Isso até nos abre a oportunidade de otimizarmos o código repetido entre as pesquisas.
  public findBookByAuthor(authorToSearch: string): Book[] {
    return this.library.books.filter((book) => book.author.includes(authorToSearch));
  }
}

const viagemAoCentroDaTerra = new Book(
  "123",
  "Viagem ao Centro da Terra",
  "Julio Verne"
);

const aMaquinaDoTempo = new Book(
  "456",
  "A Máquina do Tempo",
  "Herbert George Wells"
);

const library = new Library();

library.addBook(viagemAoCentroDaTerra);
library.addBook(aMaquinaDoTempo);

// Instanciando o objeto de pesquisa.
const searcher = new LibrarySearcher(library);

// Buscando por autor.
const search = searcher.findBookByAuthor("Julio");
console.log(search);
// OUTPUT:
// [ Book { id: '123', title: 'Viagem ao Centro da Terra', author: 'Julio Verne' } ]
```

---

## Exemplo técnico (Front-End)

Vamos imaginar que temos uma classe de componente, e que queremos ter o seguinte comportamento:

- **Exibir o componente.** Esse componente exibirá um texto.
- **Envelopar o componente com outro componente pai.** Apenas um _container_ simples.

**🔴 Implementação Incorreta**

```typescript
// Imaginamos, de forma um pouco abstrata, que temos uma classe de componente.
class Component {
  // Em seu construtor, passamos um texto, e um indicador de envelopamento.
  constructor(private text: string, private wrapper?: boolean) {}

  // Temos um método para renderizar.
  public render() {
    // VIOLAÇÃO DO PRINCÍPIO: Agora, o componente não se responsabiliza somente pela exibição
    // do texto, mas também pelo envelopador. Se ele mudar, a classe também precisa sofrer alteração.
    if (this.wrapper) {
      return <div><p>{this.text}</p></div>
    }

    return <p>{this.text}</p>
  }
}
```

**🟢 Implementação Correta**

```typescript
class Component {
  constructor(private text: string) {}

  // Agora, a renderização do Component se preocupa somente com o componente de texto em si.
  public render() {
    return <p>{this.text}</p>
  }
}

// E criamos uma classe Wrapper que se encarregará de envelopar o componente.
class Wrapper {
  constructor(private component: Component) {}

  public wrap() {
    return <div>{this.component}</div>
  }
}

// Não precisamos mais do boolean "wrapper", pois o `new Wrapper(...)` é opcional.
const myText = new Component('Hello!');
const myWrapper = new Wrapper(myText);
```

---

## Exemplo técnico (Back-End)

Considere que temos uma classe para controlar o acesso ao **Banco de Dados**, e que queremos ter as seguintes funcionalidades:

- **Conectar com o banco de dados.**
- **Obter dados de um usuário.** Busca específica de um domínio.

**🔴 Implementação Incorreta**

```typescript
// Vamos simular uma classe que controla o uso do banco de dados.
class Database {
  constructor() {}

  // Temos um método que se conecta com o banco de dados.
  public connect() {
    console.log('Connected to the database!');
  }

  // VIOLAÇÃO DO PRINCÍPIO: A classe deveria se preocupar somente com o uso do banco de dados,
  // e não com a obtenção de dados de uma domínio tão específico.
  // Se o provedor do banco de dados mudar, a classe seria alterada. Se a forma de obtenção do usuário mudar, também.
  public getUser(id: string) {
    this.connect();
    console.log(`Got user ${id}`);
  }
}
```

**🟢 Implementação Correta**

```typescript
// A classe Database agora se preocupa somente com o que tem relação com o banco de dados em si.
// Isso pode ser a conexão, os encapsulamentos, as transactions, e tudo mais.
class Database {
  constructor() {}

  public connect() {
    console.log("Connected to the database!");
  }
}

// Já a classe UserDatabase tem um viés mais voltado para um domínio de negócio específico.
// Além de estar agnóstica ao provedor do Database, tem um único motivo para ser alterada - o que tem relação ao user.
class UserDatabase {
  constructor(private database: Database) {}

  public getUser(id: string) {
    this.database.connect();
    console.log(`Got user ${id}`);
  }
}
```

---

## Exemplo pessoal

Vamos brincar um pouco com um exemplo mais pessoal. Como comentei na primeira parte, gosto de pensar em alguns _hobbies_ para interpretar alguns princípios. Imaginemos um jogo como **Super Mario 64**, onde eu quero:

- **Coletar moedas.** Salvar a quantidade obtida.
- **Coletar estrelas.** Salvar a quantidade obtida.
- **Movimentar o jogador.** Atualizar conforme necessidade.

**🔴 Implementação Incorreta**

```typescript
// Vamos criar a classe do jogo como um todo.
class SuperMario64 {
  // Neste jogo, controlamos as quantidades de moedas, estrelas e posição do jogador (x, y, z).
  private coinCount: number = 0;
  private starCount: number = 0;
  private playerPosition = "0,0,0";

  constructor() {}

  // E então, podemos jogar o jogo!
  public playGame() {
    console.log(
      `Coins: ${this.coinCount}, Stars: ${this.starCount}, Position: ${this.playerPosition}`
    );
  }

  // VIOLAÇÃO DO PRINCÍPIO: Essa classe deveria se preocupar somente em consumir o status do jogo, e não
  // em atualizar algo específico.
  public getCoin() {
    this.coinCount++;
  }

  // VIOLAÇÃO DO PRINCÍPIO: Mesmo que o de cima.
  public getStar() {
    this.starCount++;
  }

  // VIOLAÇÃO DO PRINCÍPIO: Mesmo que o de cima.
  public updatePlayerPosition(newPosition: string) {
    this.playerPosition = newPosition;
  }
}
```

**🟢 Implementação Correta**

```typescript
// Separamos o jogador em uma classe própria.
class Player {
  constructor(public playerPosition: string) {}

  updatePlayerPosition(newPlayerPosition: string) {
    this.playerPosition = newPlayerPosition;
  }
}

// Identificamos que estrelas e moedas funcionam da mesma forma, e criamos uma classe própria.
class Collectible {
  private count: number;

  constructor() {}

  public add() {
    this.count++;
  }

  public getCurrent() {
    return this.count;
  }
}

// Agora, o jogo consome o que precisa, ao invés de se responsabilizar por isso.
class SuperMario64 {
  constructor(
    public player: Player,
    public coins: Collectible,
    public stars: Collectible
  ) {}

  public playGame() {
    const playerPosition = this.player.playerPosition;
    const coinCount = this.coins.getCurrent();
    const starCount = this.stars.getCurrent();

    console.log(
      `Coins: ${coinCount}, Stars: ${starCount}, Position: ${playerPosition}`
    );
  }
}
```

---

## Exemplo funcional

Também na parte inicial dessa série de artigos, mencionei que os paradigmas de SOLID costumam se aplicar ao padrão de onde nasceram - _Object-Oriented Programming_. Apenas para provar o conceito, gostaria de apresentar uma ideia utilizando a visão de _Functional Programming_.

**🔴 Implementação Incorreta**

```typescript
function readFileAndMap(fileName: string) {
  // Suponha que vamos ler um arquivo de forma síncrona aqui.
  const file = fileSystem.read(fileName);

  // VIOLAÇÃO DO PRINCÍPIO: Estamos tratando o dado lido ao invés de só lê-lo.
  const mappedContent = file.replace(/alguma-coisa/gm, 'outra-coisa');

  return mappedContent;
}

const content = readFileAndMap('file-name');
```

**🟢 Implementação Correta**

```typescript
function readFile(fileName: string) {
  const file = fileSystem.read(fileName);
  return file;
}

function mapContent(fileContent: string) {
  const mappedContent = fileContent.replace(/alguma-coisa/gm, "outra-coisa");

  return mappedContent;
}

// Aqui separamos o momento de leitura do arquivo do momento de mapeamento.
// Assim, para modificar a lógica de mapeamento, não alteraremos a função
// responsável somente por ler arquivos, e poderemos reaproveitá-la para outros
// cenários com outros mapeamentos.
const fileContent = readFile("file-name");
const content = mapContent(fileContent);
```

---

## As aplicabilidades

Com os exemplos descritos, fica a dúvida: **"Como posso aplicar esse conhecimento no meu dia-a-dia?"**

O principal ponto é **analisar as entidades envolvidas e buscar as responsabilidades do processo**. A classe que você está manipulando já possui uma grande complexidade? Ou ela já possui uma responsabilidade muito bem definida? Então, talvez será necessário delegar a sua lógica para uma classe separada.

Comece a observar com mais atenção as entidades com as quais você interage, e busque identificar o propósito de cada uma delas. Você pode até mesmo diagramar, descrever ou estudar as implementações para ter uma visão mais concreta da classe e dos seus relacionamentos.

---

## Reflexões finais

É muito provável que você se depare com **diversos projetos com responsabilidades misturadas** no dia-a-dia, visto que nem todo mundo está acostumado a observar o desenvolvimento com esse viés na prática. Em momentos assim, **tome cuidado para não cair na armadilha do _refactoring_ exagerado**. A vontade de aplicar os princípios pode ser grande, mas você deverá ter um **posicionamento mais analítico e cuidadoso**, pois uma refatoração sem planejamento pode levar a resultados inesperados (um assunto para outro artigo).

Lembre-se que vale muito a **experimentação** para entender o que dá certo e o que não dá, **sem exagerar**. Precisamos tomar muito cuidado com aquilo que chamamos _over-engineering_, a aplicação de conceitos exageradamente e de forma extremamente minuciosa, especialmente em contextos simples.

O Princípio da Responsabilidade Única é um excelente ponto de partida para conhecer o SOLID, e é provavelmente o que a maioria das pessoas estão familiarizadas. Espero que esses descritivos e exemplos tenham ajudado para visualizá-lo de forma mais prática.