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, não sempre. 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!
Introdução
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.
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:
1 2 3 4 5 6 7 8 9 |
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:
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:
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.
Exemplo prático
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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.
Aplicando o OCP
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
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:
1 |
TDataBaseSystem = (dbFirebird, dbOracle, dbSQLServer, dbPostgreSQL); |
Agora, codificaremos um Factory Method (opa!) para retornar a instância da classe de acordo com o SGBD selecionado.
1 2 3 4 5 6 7 8 9 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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!
Conclusão
Viram que usei um Case para selecionar a classe referente ao banco de dados, certo? Case também é uma estrutura condicional. No entanto, 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.
Vamos fechar com as vantagens:
- 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.
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!
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!
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”!
Muito obrigado, Ilara!
Você verá, nos próximos artigos, que é possÃvel melhorar ainda mais o nosso código! 🙂
Até lá!
Parabéns meu amigo, sensacional. Rumo à letra L. Abraços!
Obrigado, meu caro Pinha!
Letra “L” em breve!
Abração!
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.
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!
Excelente artigo e explicação. Temos que mudar a forma de programar e usar essas técnicas que enriquecem os nossos projetos.
Abraços André!
Olá, Aleandro, tudo certo?
Obrigado pelo feedback, meu caro. Concordo com você sobre essa mudança de mindset.
Amanhã publico o artigo sobre o DIP! 😉
Abraço!
O emprego de Interfaces pode ser aplicado a este contexto?
Olá, Fábio!
O emprego de Interfaces DEVE ser aplicado! 🙂
No artigo, não utilizei Interfaces para não estender o código, mas, para que a solução ficasse mais robusta, o ideal seria criar uma Interface para os tipos de SGBD (por exemplo, IDataBaseLayer), até mesmo para aprimorar o Factory. Além disso, ao utilizar uma Interface, as classes seriam obrigadas a implementar o método
GetFirstRecordsSQL
, evitando erros de abstrações em runtime.Abração!