Reflexões sobre SOLID - A Letra "O"
Tags: solidprinciples, programming, designpatterns, typescript
Published: 2024-05-10 17:41:51

Na terceira parte desta série de reflexões, continuo seguindo a ordem das letras dos princípios SOLID, chegando na letra "O". Trabalharei com TypeScript nos exemplos.

---

## Neste artigo

[Open/Closed Principle](#openclosed-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)

---

## Open/Closed Principle

O Open/Closed Principle (ou _Princípio de Aberto/Fechado_) determina que é preferível que uma classe seja **aberta para extensão, mas fechada para modificação**. Por conta disso, ela é considerada aberta e fechada ao mesmo tempo. De todos os princípios, talvez seja o mais pontual para entendimento.

De forma bem prática, esse princípio propõe que uma classe **não deve ser alterada por algum motivo arbitrário**, principalmente se esse motivo tiver raízes em uma funcionalidade nova e muito específica. Ao invés de modificar uma classe que pode ser consumida por diversas áreas do sistema, **é mais valioso estendê-la**.

Você já deve ter visto o resultado do não cumprimento deste princípio: diversos `if`s para cada cenário específico.

> OBS: Sim, o verbo do substantivo extensão (com X) é estender (com S).

---

## Exemplo abstrato

Vamos continuar com o tema de **biblioteca**, mas dessa vez focando na entidade **livro**. Um livro pode ter um título e um autor, mas também quero identificar cenários completamente diferentes para o caso em que possuir capa dura e se possuir imagens.

**🔴 Implementação Incorreta**

```typescript
// Nossa classe simples para livros.
class Book {
  constructor(
    private title: string,
    private author: string,
    private hasHardCover: boolean,
    private hasImages: boolean
  ) {}

  // Um método para vermos o livro.
  public viewBook() {
    console.log(`Livro "${this.title}", Autor "${this.author}"`);

    // VIOLAÇÃO DO PRINCÍPIO: O cenário de capa dura teria um fluxo muito específico, por exemplo.
    // Isso causaria a classe Book cuidar de especificidades, e não da generalização.
    if (this.hasHardCover) {
      console.log("Este livro possui capa dura");
    }

    // VIOLAÇÃO DO PRINCÍPIO: Mesma visão do item acima.
    if (this.hasImages) {
      console.log("Este livro possui imagens");
    }
  }
}

// Veja na instanciação abaixo que os parâmetros para inicialização também ficam um pouco complexos.
// Se tivermos mais casos, teremos mais parâmetros chaveados sem necessidade.

const viagemAoCentroDaTerra = new Book(
  "Viagem ao Centro da Terra",
  "Julio Verne",
  true, // Tem capa dura.
  false // Não tem imagens.
);

const aMaquinaDoTempo = new Book(
  "A Máquina do Tempo",
  "Herbert George Wells",
  false, // Não tem capa dura.
  true // Não tem imagens.
);

viagemAoCentroDaTerra.viewBook();
// OUTPUT:
// Livro "Viagem ao Centro da Terra", Autor "Julio Verne"
// Este livro possui capa dura

aMaquinaDoTempo.viewBook();
// OUTPUT:
// Livro "A Máquina do Tempo", Autor "Herbert George Wells"
// Este livro possui imagens
```

**🟢 Implementação Correta**

```typescript
// A classe Book passa a ter somente aquilo que existe em comum entre todos os livros.
class Book {
  constructor(private title: string, private author: string) {}

  public viewBook() {
    console.log(`Livro "${this.title}", Autor "${this.author}"`);
  }
}

class HardCoverBook extends Book {
  // OBS: Aqui removemos o shorthand do TypeScript de indicar se é `private` ou `public`,
  // para que ele não crie a propriedade localmente.
  // Se tivéssemos mantido o `private`, ele criaria uma nova propriedade com o nome `title`,
  // por exemplo, dentro de HardCoverBook, que conflitaria com a propriedade de mesmo nome em Book.
  // Esse shorthand pode ser um tema para outro artigo!
  constructor(title: string, author: string) {
    super(title, author);
  }

  public viewBook() {
    super.viewBook();
    console.log("Este livro possui capa dura");
  }
}

class ImageBook extends Book {
  // OBS: Mesma observação do item acima.
  constructor(title: string, author: string) {
    super(title, author);
  }

  public viewBook() {
    super.viewBook();
    console.log("Este livro possui imagens");
  }
}

// Agora, as instanciações ficam muito mais simples e direcionadas,
// assim como as implementações.

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

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

viagemAoCentroDaTerra.viewBook();
// OUTPUT:
// Livro "Viagem ao Centro da Terra", Autor "Julio Verne"
// Este livro possui capa dura

aMaquinaDoTempo.viewBook();
// OUTPUT:
// Livro "A Máquina do Tempo", Autor "Herbert George Wells"
// Este livro possui imagens
```

---

## Exemplo técnico (Front-End)

Imaginemos um **botão** que implemente uma funcionalidade simples de pressionar. Contudo, temos também um cenário de botão que será **arrastável** ao longo da interface. Esse cenário específico não deveria ser implementado junto com a base do botão.

**🔴 Implementação Incorreta**

```typescript
class Button {
  constructor(private label: string, private draggable: boolean) {}

  public onPress() {
    console.log("Você pressionou o botão!", this.label);
  }

  // VIOLAÇÃO DO PRINCÍPIO: A funcionalidade de arrastar quebra o princípio, pois
  // especifica um tipo de botão muito particular.
  public onDrag() {
    // E, além de tudo, é chaveável.
    if (this.draggable) console.log("Você arrastou o botão!");
  }
}
```

**🟢 Implementação Correta**

```typescript
class Button {
  constructor(private label: string, private draggable: boolean) {}

  public onPress() {
    console.log("Você pressionou o botão!", this.label);
  }
}

// Separando o botão arrastável, criando uma implementação mais elegante e escalável.
class DraggableButton extends Button {
  public onDrag() {
    console.log("Você arrastou o botão!");
  }
}
```

---

## Exemplo técnico (Back-End)

Trazendo uma visão de **Banco de Dados**, imagine que temos uma classe genérica para conexão com o banco, mas que podemos ter mais de uma instância, por conta de cada domínio ter sua própria persistência. Nesse caso, não devemos alterar uma única classe.

**🔴 Implementação Incorreta**

```typescript
class Database {
  constructor() {}

  // Uma função reaproveitável, interna, para conexão com qualquer banco de dados.
  private connect(dbName: string) {
    console.log(`Conectou ao banco ${dbName}!`);
  }

  // VIOLAÇÃO DO PRINCÍPIO: A classe genérica Database agora está com a especificidade
  // do banco de dados de usuário.
  public connectToUserDb() {
    this.connect("user");
  }

  // VIOLAÇÃO DO PRINCÍPIO: Mesmo comentário do anterior.
  public connectToEventsDb() {
    this.connect("events");
  }
}
```

**🟢 Implementação Correta**

```typescript
class Database {
  constructor() {}

  // A função passa a ser pública, para que as subclasses possam acessá-la.
  public connect(dbName: string) {
    console.log(`Conectou ao banco ${dbName}!`);
  }
}

// Com essa estrutura, separamos as responsabilidades, e garantimos a generalização de Database.
class UserDatabase extends Database {
  public connect() {
    super.connect("user");
  }
}

// Mesma coisa para o banco de dados de eventos.
class EventsDatabase extends Database {
  public connect() {
    super.connect("events");
  }
}

// Vale notar que existem outras formas de fazer, a ideia aqui é ser didático.
```

---

## Exemplo pessoal

Em **Banjo-Kazooie**, você precisa abrir algumas portas para acessar novas áreas do _hub world_ utilizando notas coletadas nas fases, assim como peças de quebra-cabeça (chamadas _Jiggies_) para destravar novos mundos.

Ou seja, nesse cenário, temos dois tipos de _roadblocks_. O ideal é que cada um contenha sua própria implementação específica, mas compartilham alguns ideais.

**🔴 Implementação Incorreta**

```typescript
// Imaginemos uma classe que controle os possíveis bloqueios.
class Roadblock {
  constructor(private minimumNotes: number, private minimumJiggies: number) {}

  // VIOLAÇÃO DO PRINCÍPIO: O método `allow` está cuidando tanto das notas quanto
  // dos Jiggies, criando uma ausência de generalização. Ambos são roadblocks, mas
  // com comportamentos específicos.
  public allow(type: string, total: number) {
    if (type === "notes") {
      if (total >= this.minimumNotes) {
        console.log("Você pode passar!");
        return true;
      }

      console.log("Ops, ainda falta coletar mais notas...");
      return false;
    }

    if (type === "jiggies") {
      if (total >= this.minimumJiggies) {
        console.log("Você pode entrar!");
        return true;
      }

      console.log("Ops, ainda falta coletar mais Jiggies...");
      return false;
    }

    return false;
  }
}
```

**🟢 Implementação Correta**

```typescript
class Roadblock {
  constructor(private minimum: number) {}

  // Aqui, simplificamos o método e unificamos o que é genérico.
  public allow(total: number) {
    return total >= this.minimum;
  }
}

// Abaixo, estendemos o Roadblock para implementar as especificidades.
// Novamente, importante entender que as implementações são didáticas.

class NoteRoadblock extends Roadblock {
  public openDoor(total: number) {
    const isAllowed = this.allow(total);
    if (isAllowed) {
      console.log("Você pode passar!");
    }
  }
}

class JiggiesRoadblock extends Roadblock {
  public openWorld(total: number) {
    const isAllowed = this.allow(total);
    if (isAllowed) {
      console.log("Você pode entrar!");
    }
  }
}
```

---

## Exemplo funcional

Como podemos observar este princípio com _Functional Programming_ se não temos, explicitamente, extensão de classes? Bom, trago um exemplo simples envolvendo, novamente, leitura de arquivos, e o conceito básico de _Currying_.

**🔴 Implementação Incorreta**

```typescript
// Aqui, criamos uma função pouco escalável, com tratativas manuais.
function readFile(fileName: string, isCSV: boolean, isPNG: boolean) {
  const file = fileSystem.read(fileName);

  // Essa coleção de `if`s abaixo vai crescer cada vez mais.

  if (isCSV) {
    return mapCSV(file);
  }

  if (isPNG) {
    return mapPNG(file);
  }

  return file;
}
```

**🟢 Implementação Correta**

```typescript
// Aqui, utilizando a técnica de Currying, fizemos um criador de funções.
// Cada criador recebe um `mapper` próprio, criando uma função extensível.
function createFileReader(mapper: (file: string) => string) {
  // Função reaproveitável.
  const readFile = (fileName: string) => fileSystem.read(fileName);

  // O retorno é uma nova função que se encarrega de executar o `mapper`.
  return (fileName: string) => {
    const file = readFile(fileName);

    return mapper(file);
  };
}

const imgReader = createFileReader((file) => imgMapper(file));
const csvReader = createFileReader((file) => csvMapper(file));
```

---

## As aplicabilidades

Na minha opinião, esse é um dos princípios **mais facilmente identificados** em tempo de desenvolvimento. Enquanto outros podem exigir mais análise, é um pouco mais prático percebermos quando uma classe ou entidade está sofrendo modificações em excesso, executando diversos desvios para cenários específicos.

A principal sugestão é olhar de forma crítica para a **generalização e especialização** dessas entidades. Por mais que eu possua entidades similares, que parecem fazer parte de um mesmo escopo, pode ser que faça mais sentido dedicarmos um tempo para abstrair uma nova subclasse com as especificidades.

---

## Reflexões finais

Reitero novamente o cuidado com o _over-engineering_. Nem tudo precisa se tornar uma subclasse, nem tudo precisa ser genérico, e nem tudo irá se resolver simplesmente com essas segregações. Contudo, é importante manter no radar essa criticidade, e passar a observar os possíveis cenários de implementação com esses contextos gerais.

O Open/Closed Principle pode ser uma ferramenta poderosa para aumentar a facilidade de manutenção de um sistema, visto que tudo que é concreto ficará segregado em sua própria classe.