DEV Community

Cover image for Reflexões sobre SOLID - A Letra "O"
Mauricio Paulino
Mauricio Paulino

Posted on

Reflexões sobre SOLID - A Letra "O"

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
Exemplo abstrato
Exemplo técnico (Front-End)
Exemplo técnico (Back-End)
Exemplo pessoal
Exemplo funcional
As aplicabilidades
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 ifs 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

// 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
Enter fullscreen mode Exit fullscreen mode

🟢 Implementação Correta

// 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
Enter fullscreen mode Exit fullscreen mode

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

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!");
  }
}
Enter fullscreen mode Exit fullscreen mode

🟢 Implementação Correta

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!");
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

🟢 Implementação Correta

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.
Enter fullscreen mode Exit fullscreen mode

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

// 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

🟢 Implementação Correta

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!");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

// 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;
}
Enter fullscreen mode Exit fullscreen mode

🟢 Implementação Correta

// 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));
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)