S[O]LID – Open/Closed Principle (OCP)

SOLID - Open/Closed Principle -OCP

Olá, pessoal!
Algumas vezes, a solução mais rápida para codificar uma funcionalidade ou corrigir um erro é adicionar mais um Else-If em uma estrutura já existente, não é?
Bom, eu não concordo. Apesar de funcional, esse tipo de prática viola o segundo princípio do SOLID, chamado de Open/Closed Principle.
Continue lendo o artigo para entender essa violação e como eliminá-la!

 

O Open/Closed Principle – ou OCP – inicialmente pode parecer um pouco contraditório, mas é bastante simples de compreender.
O princípio define que “entidades de software devem estar abertas para extensão, mas fechadas para modificação”, com o propósito de reduzir estruturas condicionais e, consequentemente, a complexidade ciclomática. A finalidade, na verdade, é permitir que entidades possam receber novos comportamentos sem necessariamente sofrerem alterações excessivas no código.

 

O que são essas “entidades”?
Neste contexto, entidades se referem a classes, módulos, funções, componentes, bibliotecas ou qualquer outra unidade sujeita a alterações no software. Na prática, porém, o OCP geralmente é aplicado na modelagem de classes do projeto para aprimorar a arquitetura.
Um dos objetivos ao utilizar o OCP é combater o crescimento de estruturas If no código-fonte, como no exemplo abaixo:

if A then
  ProcessarA
else if B then
  ProcessarB
else if C then
  ProcessarC
else if D then
  ProcessarD
...

 

Considere que apenas a condição “D” é verdadeira. Para chegar até essa avaliação, o fluxo de processamento precisa testar as condições “A”, “B” e “C”. No melhor cenário (que eu chamaria de “cenário de sorte”), a avaliação dessas condições testa apenas variáveis locais. No entanto, na pior das hipóteses, os testes podem acessar o banco de dados ou serviços externos, comprometendo o desempenho da aplicação. Pode-se afirmar, então, que estes fluxos de dados sucessivos aumentam a complexidade do código, além, claro, de deixar o código feio! 😀

Há alguns anos, tomei conhecimento de uma técnica de arquitetura que jamais esqueci. Durante o treinamento com um arquiteto de software, os Ifs sucessivos foram ilustrados como flechas apontando para a direita, descrevendo o fluxo de um código:

Estruturas condicionais representadas por flechas

Segundo ele, quando o código chega à esse nível, devemos girar as flechas para a direita, de modo que elas fiquem verticais, para representar classes:

Subclasses representadas por flechas

Isso significa que cada condição deve ser “transformada” em uma classe por meio de herança. Como resultado, as condições são eliminadas do código e cada comportamento é movido para uma classe exclusiva, satisfazendo não só o OCP, mas também o Single Responsibility Principle apresentando no artigo anterior.

 

Estou quase entendendo, André. É possível apresentar um exemplo prático?
Claro que sim!
O exemplo do OCP envolve um cenário relativamente comum. Considere uma classe que realiza algumas operações em um banco de dados Firebird, como selecionar os primeiros 100 registros de uma tabela e retorná-los em JSON:

type
  TDataBaseLayer = class
  public
    function SelectFirstRecords: TJSONObject;
  end;
 
implementation
 
{ TDataBaseLayer }
 
function TDataBaseLayer.SelectFirstRecords: TJSONObject;
var
  Query: TFDQuery;
begin
  Query := TFDQuery.Create(nil);
  try
    Query.Connection := FDConnection;
    Query.Open('SELECT FIRST 100 * FROM CLIENTES');
 
    result := Query.AsJSONObject;
  finally
    Query.Free;
  end;
end;

 

O uso da classe é simples. Nada de especial.

var
  DataBaseLayer: TDataBaseLayer;
  JSONObject: TJSONObject;
begin
  DataBaseLayer := TDataBaseLayer.Create;
  try
    JSONObject := DataBaseLayer.SelectFirstRecords;
 
    // Operações com o objeto JSON...
  finally
    DataBaseLayer.Free;
  end;
end;

 

Sabemos que a cláusula First para selecionar as primeiras ocorrências é um comando particular do Firebird, certo? O que aconteceria, então, se um novo cliente solicitasse que a aplicação trabalhasse com Oracle? O comando SQL retornaria um erro, informando que o comando First não existe.
Bom, a classe terá que ser modificada para que a rotina funcione. Basta apenas parametrizar o método, incluindo uma condição para executar o SQL conforme o SGBD selecionado:

function TDataBaseLayer.SelectFirstRecords(const EhOracle: boolean): TJSONObject;
var
  Query: TFDQuery;
begin
  Query := TFDQuery.Create(nil);
  try
    Query.Connection := FDConnection;
 
    if EhOracle then
      Query.Open('SELECT * FROM Clientes WHERE rownum <= 100')
    else
      Query.Open('SELECT FIRST 100 * FROM CLIENTES');
 
    result := Query.AsJSONObject;
  finally
    Query.Free;
  end;
end;

 

Ficou feio, não é? Mas vai piorar um pouco mais…
Por questões comerciais, nas próximas versões a aplicação também deverá trabalhar com o Microsoft SQL Server e PostgreSQL. Neste caso, um parâmetro boolean já não é mais o suficiente. A classe deverá ser modificada para trabalhar com os quatro SGBDs. Para isso, imagine que a tipo do parâmetro foi substituído por string, recebendo o nome do SGBD selecionado:

function TDataBaseLayer.SelectFirstRecords(const DataBaseSystem: string): TJSONObject;
var
  Query: TFDQuery;
begin
  Query := TFDQuery.Create(nil);
  try
    Query.Connection := FDConnection;
 
    if DataBaseSystem = 'Oracle' then
      Query.Open('SELECT * FROM Clientes WHERE rownum <= 100')
    else if DataBaseSystem = 'Firebird' then
      Query.Open('SELECT FIRST 100 * FROM CLIENTES')
    else if DataBaseSystem = 'SQL Server' then
      Query.Open('SELECT TOP 100 * FROM CLIENTES')
    else if DataBaseSystem = 'PostgreSQL' then
      Query.Open('SELECT * FROM CLIENTES LIMIT 100');
 
    result := Query.AsJSONObject;
  finally
    Query.Free;
  end;
end;

String Literals, várias condições If… há muita coisa errada aí. Vocês notaram também que destaquei a palavra “modificada” duas vezes nos parágrafos anteriores? O objetivo é enfatizar que a classe sofreu duas modificações conforme novos requisitos foram solicitados. Mas, espere aí… como é mesmo a definição do OCP?

“Software entities should be open for extension, but closed for modification”
(Entidades de software devem estar abertas para extensão, mas fechadas para modificação)

A classe acima não está fechada para modificação, já que foi necessário alterar o método para cada novo SGBD. Logo, ela quebra o Open/Closed Principle.

 

Como podemos corrigir este cenário?
Lembram-se da técnica de converter condições If em classes? É isso que faremos!
Em primeiro lugar, a classe TDataBaseLayer será transformada em uma classe base, declarando um método chamado GetFirstRecordsSQL como abstrato:

type
  TDataBaseLayer = class
  protected
    function GetFirstRecordsSQL: string; virtual; abstract;
  public
    function SelectFirstRecords: TJSONObject;
  end;
 
implementation
 
{ TDataBaseLayer }
 
function TDataBaseLayer.SelectFirstRecords: TJSONObject;
var
  Query: TFDQuery;
begin
  Query := TFDQuery.Create(nil);
  try
    Query.Connection := FDConnection;
    Query.Open(GetFirstRecordsSQL);
    result := Query.AsJSONObject;
  finally
    Query.Free;
  end;
end;

 

Em seguida, para cada condição existente, será declarada uma classe herdada de TDataBaseLayer para implementar o método GetFirstRecordsSQL:

type
  TFirebird = class(TDataBaseLayer)
  protected
    function GetFirstRecordsSQL: string; override;
  end;
 
  TOracle = class(TDataBaseLayer)
  protected
    function GetFirstRecordsSQL: string; override;
  end;
 
  TSQLServer = class(TDataBaseLayer)
  protected
    function GetFirstRecordsSQL: string; override;
  end;
 
  TPostgreSQL = class(TDataBaseLayer)
  protected
    function GetFirstRecordsSQL: string; override;
  end;
 
implementation
 
{ TFirebird }
 
function TFirebird.GetFirstRecordsSQL: string;
begin
  result := 'SELECT FIRST 100 * FROM CLIENTES';
end;
 
{ TOracle }
 
function TOracle.GetFirstRecordsSQL: string;
begin
  result := 'SELECT * FROM Clientes WHERE rownum <= 100';
end;
 
{ TSQLServer }
 
function TSQLServer.GetFirstRecordsSQL: string;
begin
  result := 'SELECT TOP 100 * FROM CLIENTES';
end;
 
{ TPostgreSQL }
 
function TPostgreSQL.GetFirstRecordsSQL: string;
begin
  result := 'SELECT * FROM CLIENTES LIMIT 100';
end;

Com essa pequena reestruturação de classes, o OCP já deixa de ser violado. A classe TDataBaseLayer está aberta para extensão (cada tipo de SGBD é uma herança) e fechada para modificação (novos SGBDs não exigem alterações na classe base). Dessa forma, caso seja necessário trabalhar também com MySQL, por exemplo, a classe TDataBaseLayer não seria modificada. Ao invés disso, criaríamos uma nova extensão da classe! 🙂

Ainda não terminamos. Precisamos ainda ajustar o consumidor dessa funcionalidade.
O próximo passo é eliminar as String Literals, declarando os SGBDs como um tipo enumerado:

TDataBaseSystem = (dbFirebird, dbOracle, dbSQLServer, dbPostgreSQL);

 

Agora, codificaremos um Factory Method (opa!) para retornar a instância da classe de acordo com o SGBD selecionado.

function DataBaseFactory(DataBaseSystem: TDataBaseSystem): TDataBaseLayer;
begin
  case DataBaseSystem of
    dbFirebird:   result := TFirebird.Create;
    dbOracle:     result := TOracle.Create;
    dbSQLServer:  result := TSQLServer.Create;
    dbPostgreSQL: result := TPostgreSQL.Create;
  end;
end;

Observe o nível de abstração ao definir o retorno do método como o tipo da classe base, tornando-o “genérico” para todos os SGBDs. Em tempo de execução, uma das classes filhas será instanciada de acordo com o parâmetro informado.

Por fim, na classe cliente, não há muita alteração. Apenas substituímos a criação do objeto pelo Factory:

var
  DataBaseLayer: TDataBaseLayer;
  JSONObject: TJSONObject;
begin
  // Usando o Firebird como exemplo
  DataBaseLayer := DataBaseFactory(dbFirebird);
  try
    JSONObject := DataBaseLayer.SelectFirstRecords;
 
    // Operações com o objeto JSON...
  finally
    DataBaseLayer.Create;
  end;
end;

 

Bem melhor! Além de satisfazer o Open/Closed Principle, o código fica mais profissional, não acham? 🙂
Pessoal, vale ressaltar que o exemplo desse artigo é bastante simples, desenvolvido apenas para demonstrar a aplicação do OCP. Em ambientes reais, com classes extensas e regras de negócio complexas, o OCP traz grandes benefícios! Garanto!

 

André, mas o Case é uma estrutura condicional também, não é?
Sim, mas há uma diferença. No código anterior, era necessário replicar as instruções If para cada vez que precisássemos verificar o tipo do SGBD. Por exemplo, se o método “SelectCurrentDate” fosse criado para retornar data atual do servidor do banco de dados, as quatro instruções If seriam necessárias para executar a SQL correta. Com o OCP, a única estrutura condicional estará no método DataBaseFactory. Uma vez retornada a instância da classe do SGBD desejada, não será necessário, em momento algum, utilizar instruções If para verificar o tipo do banco de dados novamente.

 

Quais são as principais vantagens?
Vamos lá:

  • Reduz a complexidade ciclomática da arquitetura, eliminando condições If;
  • Facilita a manutenção, já que cada classe possui uma responsabilidade única;
  • A adição de novas condições (neste caso, um novo SGBD) não exige a modificação da classe base. Basta somente criar uma nova herança;
  • Contribui para a arquitetura sustentável do projeto, possibilitando evoluções sem comprometer outras funcionalidades.

 

Uma última dúvida: como você usou o “AsJSONObject”?
Este método é um Class Helper de um framework muito útil desenvolvido pelo Ezequiel Juliano para converter DataSets em JSON e vice-versa. Para utilizá-lo, acesse o link abaixo do GitHub:

https://github.com/ezequieljuliano/DataSetConverter4Delphi

 

Obrigado pela atenção, leitores!
Vejo vocês na letra “L” do SOLID.


 

8 comentários

  1. Excelente texto, meu amigo André. Os princípios SOLID são independentes de linguagem de programação e ajudam a simplificar trechos complexos. Orientação a Objetos é Vida! Parabéns mais uma vez!

    1. Opa, Jorge! Muito obrigado, meu caro!
      O que você comentou é justamente o que me fascina na Engenharia de Software: os conceitos são agnósticos à linguagem de programação! 🙂
      Grande abraço!

  2. Nossa, André! Parabéns! Mais um artigo maravilhoso com dicas cruciais para melhorar nossa forma de programar! Muuuuitíssimo obrigada! Ansiosa com a letra “L”!

  3. Boa noite, André,

    Parabéns… Dica simples e poderosa, procedimentos como esses, são de grande valia, código limpo, organizado e melhora no desempenho da aplicação.

    Grande abraço.

    1. Fala, Daniel!
      Concordo plenamente, meu caro! Espero que essas técnicas sejam cada vez mais disseminadas na comunidade de desenvolvimento, principalmente Delphi.
      Obrigado por sempre acompanhar o blog!
      Abração!

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.