Artigo no estilo: Curso

Por que eu devo ler este artigo:Neste artigo veremos os princípios SOLID da orientação a objetos que permitem que nossos softwares sejam flexíveis, de fácil manutenção e entendimento do código. Na primeira parte do artigo aprenderemos a aplicar os três dos cinco princípios SOLID (Responsabilidade Única, Aberto/Fechado, Substituição de Liskov com o Delphi em situações bem comuns do dia-a-dia de todo desenvolvedor.

Veremos as vantagens e as maneiras corretas de aplicar este princípio passo-a-passo. Ainda veremos como unir o desenvolvimento RAD do Delphi com boas práticas de orientação a objetos.

Muitos desenvolvedores Delphi não seguem bons princípios de orientação a objetos com a justificativa ou desculpa como eu prefiro dizer que a Linguagem/IDE/Compilador não oferece bons frameworks para trabalhar desta maneira.

Na verdade o que falta para esses desenvolvedores é abrir a mente para um pensamento orientado a objetos como fazem desenvolvedores Java e C#, pois o Delphi é uma linguagem multiparadigma, suporta tanto a programação procedural quanto a orientada a objetos, cabe a nós desenvolvedores escolhermos de que maneira preferimos trabalhar.

É claro que a adição de frameworks de mapeamento objeto-relacional ou injeção de dependências facilitaria a vida dos desenvolvedores e a Embarcadero poderia focar um pouco mais neste quesito.

É perfeitamente possível unir toda a produtividade que o Delphi nos oferece por ser uma ferramenta RAD com bons princípios de orientação a objetos e isto ficará muito claro no decorrer do artigo.

Alguns desenvolvedores acreditam que mesmo quando estamos somente adicionando e manipulando componentes em um formulário, estamos trabalhando orientado a objetos, pois cada novo formulário que criamos no Delphi gera uma nova classe que herda de TForm e ao adicionar componentes no formulário estamos adicionando objetos no formulário, pois cada componente representa uma classe que herda em última instância da classe TComponent.

SOLID

No desenvolvimento orientado a objetos existem alguns princípios que todo o bom desenvolvedor de sistemas deve adotar para ter um sistema com um código mais limpo e de fácil entendimento e manutenção, e quando exista a necessidade de manutenção ela seja rápida e concisa, de forma que não cause nenhum impacto em outros módulos, sendo mais evolutivas e flexíveis.

SOLID são cinco princípios básicos da orientação a objetos que trazem inúmeros benefícios em uma modelagem de classes. Estes princípios foram base para muitos dos padrões de projeto existentes nos dias de hoje. Cada letra representa um princípio, são eles:

· S – Single Responsibility Principle (SRP): princípio da responsabilidade única;

· O – Open Closed Principle (OCP): princípio do aberto/fechado;

· L – Liskov Substituition Principle (LSP): princípio da substituição de Liskov;

· I – Inteface Segregation Principle (ISP): princípio da segregação de interfaces;

· D – Dependency Inversion Principle (DIP): princípio da inversão de dependência;

Serão tratados cada um dos princípios de forma individual, os três primeiros nesta primeira parte do artigo e os dois últimos na segunda parte do artigo, na próxima edição da revista ClubeDelphi.

Quando seguimos estes princípios da orientação a objetos conseguimos ter uma modelagem de classes bastante correta, de forma que os sistemas não tenham que sofrer muitas alterações nas classes já existentes no futuro.

É indiscutível que os princípios SOLID são base para o desenvolvimento de qualquer aplicação que deseje seguir o paradigma orientado a objetos.

SRP (Sigle Responsibility Principle)

Este princípio de responsabilidade única da orientação a objetos nos ensina que nossas classes devem ter somente uma responsabilidade dentro do modelo e deve estar completamente encapsulada em seus métodos. Todos os métodos contidos nas classes, tanto os privados, protegidos ou públicos devem agir de modo a se completarem e atingir o objetivo que é único desta classe.

Quando seguimos este princípio da maneira descrita acima, esta classe terá somente uma razão para ser alterada. Esta classe poderá ainda conter vários métodos sendo que os mesmos contribuam para somente aquela única tarefa proposta para a classe, também não teremos muitas dependências de outros módulos, de maneira que ao alterarmos esta classe não será necessário recompilar outras classes do sistema.

Quando construímos classes que seguem o princípio de responsabilidade única podemos ter a certeza que estamos criando um sistema que tem um baixo acoplamento (BOX 1) entre os módulos.

BOX 1. Acoplamento

Alto acoplamento é nome dado à ocorrência de alto grau de dependência entre partes de um software, sendo que estas partes deveriam funcionar de forma independente. Numa situação real, do dia a dia poderíamos pensar da seguinte maneira: para se conectar a um banco de dados para realizar uma atividade possuímos um método que realiza a conexão com o banco.

No interior de seu método de conexão, existe a chamada para outro método, sendo que este último apenas identifica se ocorreu algum erro e registra em um arquivo ou tabela de logs do sistema.

Assim, sabemos que existe uma dependência explícita do método de conexão para com o método de registro de log. Um só pode funcionar se o outro funcionar. Tal dependência é chamada de acoplamento. Se este fato ocorre de forma sistemática no projeto de software, dizemos que a aplicação é altamente acoplada.

Este princípio nos ensina que nossas classes devem ter uma única tarefa em nossa aplicação, do contrário significa que ao certo devíamos dividir esta classe em duas ou mais classes para seguir o princípio, porque cada tarefa é uma fonte de mudanças no sistema e estas tarefas por isso devem estar bem isoladas de forma que uma tarefa não influa em outras.

É um princípio bastante simples e que devemos adotar para termos um sistema que demande facilidade de manutenção no futuro.

OCP – Open Closed Principle

Este princípio do Aberto/Fechado da orientação a objetos é um dos que mais chama a atenção dos estudiosos de arquitetura de sistemas, ele nos traz uma definição muito interessante que devemos tomar cuidado em nossas classes, no diz que elas devem estar abertas para extensão e fechadas para modificação, ou seja, quando criamos uma classe devemos levar em consideração uma codificação que muito dificilmente precise ser alterada no futuro, mas deve ter métodos que facilite sua extensão para a modificação de seu comportamento e sem que seja necessário a modificação da classe base.

O princípio do Aberto/Fechado é um grande aliado dos desenvolvedores, principalmente na hora da manutenção dos sistemas, pois se temos um sistema em pleno funcionamento, nada mais correto de bloquearmos a alteração das classes já existentes e que estão cumprindo corretamente suas tarefas, se precisamos da alteração de um comportamento destas classes podemos apenas estende-la com o mecanismo de herança por exemplo.

Outra grande vantagem da adoção deste princípio é que não serão mais necessários fazer testes unitários (BOX 2) novamente nas classes já existentes, somente na classe que está sendo adicionada naquele momento.

BOX 2. Testes Unitários

Os Testes Unitários são um assunto vital para o bom desenvolvimento de qualquer sistema. Eles consistem em testes em cada unidade do sistema (classes), de modo que as mesmas estejam funcionando da melhor maneira.

Estes testes unitários são bastante presente nas principais linguagens orientadas a objetos, porém quem programa em Delphi não costuma fazer estes testes, pois trabalham somente de forma procedural e não conseguem tirar proveito deste excelente princípio que previne a ocorrência de erros nas classes e tornam o código muito mais confiável.

LSP - Liskov Substituition Principle

O princípio da Substituição de Liskov leva este nome pois o seu conceito foi introduzido por Bárbara Liskov e Jeannette Wing em 1993 através do artigo Family Values: A Behavioral Notion of Subtyping.

Este princípio nos ensina que um objeto de uma classe qualquer deve permitir a substituição em um sistema por qualquer instância de suas classes filhas e classes derivadas também devem possibilitar a substituição por sua classe base.

Este princípio é vital para a boa aplicação da orientação a objetos, pois ele facilita e muito o uso do polimorfismo (BOX 3), pois desta forma todo e qualquer método pode ser utilizado tanto na classe base quanto em todas as especializações dela sem nenhum problema.

BOX 3. Polimorfismo

Em Orientação a Objetos o Polimorfismo permite que usemos referências de tipos de classes mais abstratas para representar o comportamento das classes concretas que referenciam. Assim é possível tratar várias classe de maneira homogênea através da interface da classe mais abstrata.

Se temos uma classe filha que não implementa todos os métodos da classe pai então há de se considerar que talvez estejamos fazendo um mal uso do mecanismo de herança em nossa modelagem de classes e que esta herança na realidade nem deveria existir.

Devemos ter muito cuidado com o uso da herança em nossos sistemas, pois a herança muitas vezes acaba quebrado o encapsulamento de nossas classes ou expondo para as classes derivadas métodos e propriedades que nem fazem sentido para ela.

SRP (Sigle Responsibility Principle) na prática

Existem vários exemplos de implementação deste princípio na web, porém na sua grande maioria trazem abordagens muito abstratas sobre o assunto, que foge do dia a dia do desenvolvedor. Nós, porém iremos trazer um exemplo de aplicativo comum a todos os programadores Delphi, que é o uso de um cadastro de clientes, com persistência e log de erros.

Consideramos uma aplicação VCL Win32, com um cadastro de clientes simples, onde temos um formulário com botões para criarmos um novo cliente e outro para persistir este cliente na base de dados, neste caso um arquivo XML simples vinculado a um ClientDatSet, além do componente ApplicationEvents que nos proporciona a codificação de alguns eventos importantes da aplicação. Veja a interface com os componentes visuais e não visuais na Figura 1.

Na Listagem 1 a interface da classe do formulário, juntamente com a implementação dos eventos dos botões e do evento OnException do componente ApplicationEvents.

Figura 1. Interface do Cadastro de Clientes

Listagem 1. Interface e Implementação do código do formulário de clientes


  01 procedure TfrmCliente.ApplicationEventsException
     (Sender: TObject; E: Exception);
  02 const ARQ_ERRO = 'C:\LogErros.txt';
  03 var Arquivo: TextFile;
  04 begin
  05   AssignFile(Arquivo, ARQ_ERRO);
  06   try
  07     if FileExists(ARQ_ERRO) then
  08       Append(Arquivo)
  09     else Rewrite(Arquivo);
  10     Writeln(Arquivo, E.Message);
  11   finally
  12     CloseFile(Arquivo);
  13   end;
  14 end;
  15 
  16 procedure TfrmCliente.btnNovoClick(Sender: TObject);
  17 begin
  18   cdsCliente.Append;
  19 end;
  20 
  21 procedure TfrmCliente.btnSalvarClick(Sender: TObject);
  22 begin
  23   cdsCliente.Post;
  24 end;

Vamos pensar neste formulário e código de acordo com o princípio de responsabilidade única, vemos que são muitas as responsabilidades presentes neles. Vemos que além de fazer a interface com o usuário que é a única responsabilidade que ele deveria ter, ele faz a persistência dos dados através do ClientDataSet (linhas 18 e 23).

Existe ainda uma terceira responsabilidade atribuída a esta classe de formulário que é a realização de log de erros da aplicação em um arquivo de texto no disco.

A implementação deste log se dá através da codificação do evento OnException do componente TApplicationEvents que nos passa por parâmetro qual a exceção levantada pela aplicação. Definimos uma constante do local de persistência do arquivo de erros (linha 2) e usamos uma variável do tipo TextFile para trabalharmos com esse arquivo (linha 3).

No corpo do método, fazemos a referência ao local do arquivo na variável criada (linha 5), e dentro de um bloco try/finally verificamos se o arquivo já existe no disco (linha 7), se sim apenas adicionamos uma linha no final do arquivo, senão criamos e adicionamos uma linha no final. Depois basta escrever o texto da mensagem de exceção no arquivo através do comando writeln.

Veja então que existem vários motivos para que modifiquemos esta classe, mudança do visual, mudança na persistência e mudança no log de erros, portanto esta classe fere completamente o princípio da responsabilidade única.

SRP (Sigle Responsibility Principle) refatorado

Agora vamos considerar novamente o exemplo do cadastro de clientes e vamos refatorar a aplicação de forma que ela possa seguir o princípio de responsabilidade única pregado nos princípios SOLID.

O primeiro passo que devemos tomar para refatorar a aplicação é o de mover o ClientDataSet responsável pela persistência dos dados para um Data Module chamado dmCliente.

Esta prática já é bastante praticada por desenvolvedores Delphi, porém somente isso não é suficiente para termos um design de aplicação, se continuarmos acessando diretamente o ClientDataSet no nosso formulário (dmCliente.cdsCliente.Post) estaremos ferindo outro princípio dos mais básicos da orientação a objetos, que é o de encapsulamento, ou seja, uma classe ser a única a poder acessar e modificar seus atributos.

Neste caso o ClientDataSet presente no DataModule é um atributo do mesmo, sendo que o formulário não deve acessar diretamente este objeto, por isso necessitamos criar dois métodos públicos no DataModule chamados Novo e Salvar que sejam os responsáveis por chamar os métodos Append e Post do ClientDataSet, de maneira que agora estamos respeitando este princípio. Podemos ver esses novos métodos na Listagem 2.

Conforme visto, criamos os métodos necessários para o Append e o Post do ClientDataSet, porém quem nos garante que eles serão chamados no formulário? Quando adicionamos um componente, seja ele num formulário ou data module, o Delphi cria um objeto dentro desta classe na seção Published da mesma.

Outra boa prática de programação é a que diz que um atributo deve estar na seção private de uma classe, para que estes objetos não sejam acessados indevidamente por outros módulos do sistema. O ClientDataSet, ao criarmos suas colunas, cria objetos do tipo TField para um dos seus campos, portanto devemos mover tanto o ClientDataSet quanto os campos TField para a seção private do formulário (linhas 4 a 6), estando desta forma a respeitar o princípio.

Porém simplesmente movendo os objetos da seção published para a private não é o suficiente, a aplicação irá compilar normalmente, porém em tempo de execução termos um erro conforme Figura 2.

Figura 2. Erro ocorrido ao movermos componentes para a seção private

Este erro ocorre porque o sistema de streaming do Delphi precisa conhecer os nomes de todas as classes, afim de localizar todas as referências de classes necessárias para construir os componentes ao carregar o DFM, para contornar este problema, basta fazermos o registro das classes na seção initialization da unidade (linha 28).

Se executarmos o aplicativo, os eventos referentes ao componente cdsCliente não irão funcionar, devido ao ClientDataSet não ser inicializado e tais eventos estarem referenciando objetos nulos. O Delphi inicializa automaticamente somente os componentes presentes na seção published, para corrigirmos este problema, podemos no evento OnCreate do formulário fazer esta inicialização manualmente (linha 14).

Como último ajuste, o Delphi cria uma variável global para cada formulário e data module criados na própria unidade. Sabemos que variáveis globais são uma péssima prática de programação, por esse motivo removemos a declaração da variável global criada no dmCliente, agora ele não será automaticamente criado pelo Delphi e sim iremos instanciar e liberar o data module da memória quando necessário.

Agora sim temos um DataModule com seus objetos encapsulados e somente com métodos públicos para serem consumidos pelo formulário. Foram necessários alguns ajustes e algumas codificações extras que não são necessárias ao trabalharmos de forma procedural, porém vale o esforço para termos um desenvolvimento RAD e orientado a objetos ao mesmo tempo.

Listagem 2. Interface e Implementação do DataModule de persistência


  01 TdmCliente = class(TDataModule)
  02   procedure DataModuleCreate(Sender: TObject);
  03 private
  04   cdsCliente: TClientDataSet;
  05   cdsClienteID: TAutoIncField;
  06   cdsClienteNOME: TStringField;
  07 public
  08   procedure Novo;
  09   procedure Salvar;
  10 end;
  11 
  12 procedure TdmCliente.DataModuleCreate(Sender: TObject);
  13 begin
  14   cdsCliente := FindComponent('cdsCliente') as TClientDataSet;
  15 end;
  16 
  17 procedure TdmCliente.Novo;
  18 begin
  19   cdsCliente.Append;
  20 end;
  21 
  22 procedure TdmCliente.Salvar;
  23 begin
  24   cdsCliente.Post;
  25 end;
  26 
  27 initialization
  28   RegisterClasses([TClientDataSet, TAutoIncField, TStringField]);

A próxima responsabilidade a ser extraída do formulário é a de Log de Erros. Para isso desenvolvemos uma nova classe chamada TLogErro, conforme Listagem 3.

Esta classe terá um único método público chamado Gravar, que irá receber por parâmetro a exceção gerada pela aplicação e fazer a operação de gravação no arquivo de log de erros do disco.

Atente pelo fato da constante ARQ_ERRO estar na seção private da classe, esta é outra boa prática que podemos adotar, pois somente faz sentido esta constante para essa classe, de modo que esta constante não precisa estar acessível a outras unidades.

Iremos receber do formulário principal qual a exceção levantada pela aplicação para coletarmos posteriormente a mensagem de erro. A implementação do método Gravar segue a mesma ideia da codificação na listagem onde usamos no evento OnException do componente ApplicationEvents.

Listagem 3. Interface e Implementação da classe TLogErro


  01 TLogErro = class
  02 private
  03   const ARQ_ERRO = 'C:\LogErros.txt';
  04 public
  05   procedure Gravar(E: Exception);
  06 end;
  07 
  08 procedure TLogErro.Gravar(E: Exception);
  09 var
  10   Arquivo: TextFile;
  11 begin
  12   AssignFile(Arquivo, ARQ_ERRO);
  13   try
  14     if FileExists(ARQ_ERRO) then
  15       Append(Arquivo)
  16     else Rewrite(Arquivo);
  17     Writeln(Arquivo, E.Message);
  18   finally
  19     CloseFile(Arquivo);
  20   end;
  21 end;

No formulário, não teremos mudanças na interface em relação aos componentes, somente o componente ClientDataSet foi movido para o DataModule.

Agora o formulário irá consumir os recursos das duas novas classes adicionada no projeto, a TdmCliente e TLogErro, para isso declaramos na seção private dois objetos destas classes e nos eventos OnCreate e OnDestroy fizemos a criação e destruição destes objetos respectivamente, conforme pode ser visto na Listagem 4.

Agora na implementação dos cliques dos botões Novo e Salvar, chamamos os métodos do criados no data module para a execução das funcionalidades.

Já no evento OnException do componente ApplicationEvents, chamamos o método gravar da classe TLogErro passando o objeto que representa a exceção levantada pelo sistema.

Listagem 4. Declaração objetos e implementação de eventos


  01 private
  02   LogErro: TLogErro;
  03   dmCliente: TdmCliente;
  04 
  05 procedure TfrmCliente.ApplicationEventsException
     (Sender: TObject; E: Exception);
  06 begin
  07   LogErro.Gravar(E);
  08 end;
  09 
  10 procedure TfrmCliente.btnNovoClick(Sender: TObject);
  11 begin
  12   dmCliente.Novo;
  13 end;
  14 
  15 procedure TfrmCliente.btnSalvarClick(Sender: TObject);
  16 begin
  17   dmCliente.Salvar;
  18 end;
  19 
  20 procedure TfrmCliente.FormCreate(Sender: TObject);
  21 begin
  22   LogErro := TLogErro.Create;
  23   dmCliente := TdmCliente.Create(nil);
  24 end;
  25 
  26 procedure TfrmCliente.FormDestroy(Sender: TObject);
  27 begin
  28   LogErro.Free;
  29   dmCliente.Free;
  30 end;

Agora temos as responsabilidades divididas em três classes diferentes, de maneira que cada uma destas responsabilidades podem ser alteradas sem que haja impacto nas outras.

OCP – Open Closed Principle na Prática

Este é um dos princípios de modelagem de classes mais polêmicos, desconhecido e pouco utilizado, que nos diz que uma classe deve estar aberta para extensão e fechada para alteração, mas o que isso significa na prática?

Os softwares são evolutivos, raramente um software é desenvolvido uma única vez e nunca mais será modificado, mas onde este princípio tenta nos ajudas nesta evolução?

Para exemplificarmos o princípio do aberto/fechado na prática, agora vamos considerar uma classe de abstração de clientes de uma determinada empresa. Esta empresa concede descontos para compras à vista, porém estes descontos são diferenciados de acordo com a categoria do cliente.

Esses clientes com o passar do tempo e de acordo com suas quantidades de compras e pagamentos adequados podem ser classificados como clientes ouro ou prata, de forma que possuem descontos especiais. Novos clientes ou clientes recentes são categorizados como normais e possuem um desconto padrão.

De acordo com o negócio, clientes normais possuem desconto de 5% nas compras à vista, já clientes prata possuem 10%, enquanto clientes ouro tem 20% de desconto.

A primeira codificação desta solução pode ser vista na Listagem 5, onde num primeiro momento somos levados a criar uma enumeração para as categorias de clientes (linha 2), onde temos se o cliente será ouro, prata ou normal, para posteriormente criarmos uma propriedade na classe cliente que identifique cada cliente em determinada categoria (linha 8).

Já para a concessão do desconto criamos um método público chamado GetDescontoAVista que recebe por parâmetro o total da venda feita para o cliente e devolvemos o valor atualizado com o desconto já calculado.

Para calcularmos o desconto fazemos uma série de testes condicionais onde verificamos qual a categoria do cliente e concedemos o desconto de acordo com o tipo de cliente.

Listagem 5. Primeira implementação da solução da categoria dos clientes e descontos


  01 type
  02   TCategoria = (cOuro, cPrata, cNormal);
  03 
  04   TCliente = class
  05   private
  06     FCategoria: TCategoria;
  07   public
  08     property Categoria: TCategoria read FCategoria write FCategoria;
  09     function GetDescontoAVista(ATotalVenda: Double): Double;
  10   end;
  11 
  12 implementation
  13 
  14 { TCliente }
  15 
  16 function TCliente.GetDescontoAVista(ATotalVenda: Double): Double;
  17 begin
  18   if FCategoria = cOuro then
  19     Result := ATotalVenda * 0.2
  20   else if FCategoria = cPrata then
  21     Result := ATotalVenda * 0.1
  22   else if FCategoria = cNormal then
  23     Result := ATotalVenda * 0.05
  24 end;

A princípio o código parece simples e conciso, mas o que acontece se adicionarmos mais uma categoria chamada Bronze, por exemplo? Bem, a solução seria adicionar mais um If na implementação do método GetDescontoAVista e estaria pronto.

Mas qual o problema de mais um If? Bem, a adição de mais um If indica que devemos substituir esta classe que já estava funcionando perfeitamente e testarmos novamente os descontos para cada tipo de cliente para vermos se esta adição não ocasionou nenhum bug na aplicação.

Nota: Em alguns sistemas encontramos uma enorme quantidade de IFs, o que torna o sistema de difícil entendimento e algumas vezes bem confusos. Muitos desses IFs podem ser substituídos por soluções muito mais elegantes como o uso de herança e polimorfismo para alterar o comportamento de um método.

Excessos de IFs são sintomas de classes mal estruturadas e que não seguem as boas práticas de orientação a objetos.

OCP – Open Closed Principle Refatorado

O princípio nos indica que as classes devem estar fechadas para modificação, portanto uma nova categoria não deveria ocasionar uma modificação na classe cliente. O princípio também diz que as classes devem estar abertas para extensão, para que possamos herdar da mesma e apenas modificar o comportamento dela, sem que precisamos recompilar e substituir a classe base no sistema.

Para uma categoria não afetar de nenhuma maneira a classe atual que se encontra em perfeito funcionamento em ambiente de produção, devemos criar uma especialização da mesma e sobrescrever o método GetDescontoAVista.

Podemos verifica na Listagem 6 a classe cliente com o método GetDescontoAVista com a diretiva virtual no final de sua declaração (linha 3), possibilitando assim que possamos criar classes descendentes e estender este método modificando seu comportamento ou apenas adicionando mais algum recurso.

Veja que agora temos mais duas classes criadas, TClienteOuro e TClientePrata, e cada uma delas sobrescreve o método GetDescontoAVista da classe TCliente através da diretiva override, e implantando o desconto de acordo com a categoria do clientes.

Listagem 6. Classe Cliente refatorada para seguir o princípio do aberto/fechado


  01 type
  02   TCliente = class
  03     function GetDescontoAVista(ATotalVenda: Double): Double; virtual;
  04   end;
  05   TClienteOuro = class(TCliente)
  06     function GetDescontoAVista(ATotalVenda: Double): Double; override;
  07   end;
  08   TClientePrata = class(TCliente)
  09     function GetDescontoAVista(ATotalVenda: Double): Double; override;
  10   end;
  11 
  12 implementation
  13 
  14 { TCliente }
  15 function TCliente.GetDescontoAVista(ATotalVenda: Double): Double;
  16 begin
  17   Result := ATotalVenda * 0.05;
  18 end;
  19 
  20 { TClienteOuro }
  21 function TClienteOuro.GetDescontoAVista(ATotalVenda: Double): Double;
  22 begin
  23   Result := ATotalVenda * 0.2;
  24 end;
  25 
  26 { TClientePrata }
  27 function TClientePrata.GetDescontoAVista(ATotalVenda: Double): Double;
  28 begin
  29   Result := ATotalVenda * 0.1;
  30 end;

Agora se tivéssemos mais uma categoria de cliente bronze, por exemplo, bastaria criarmos mais uma classe descendente de TCliente sobrescrevendo novamente o método GetDescontoAVista e aplicando o novo desconto.

Observe que a criação de uma nova categoria não iria impactar em nada as categorias anteriores e nenhum teste seria necessário nas classes TCliente, TClienteOuro e TClientePrata, somente a TClienteBronze precisaria ser testada e teríamos a garantia de que nenhum bug seria adicionado as categorias já existentes, na prática é isto que prega o princípio do aberto/fechado.

Desenvolver classes propensas a extensão é uma das chaves da orientação a objetos, quando uma nova funcionalidade ou comportamento precisa ser adicionado no sistema, é esperado que estas classes existentes sejam estendidas e não alteradas, de modo que o código original delas permaneça intacto e confiável, com as novas funcionalidades são implementadas através da extensibilidade.

Quando estamos aprendendo a orientação a objetos ouvimos muito falar sobre abstração, e ela permite que este princípio funcione adequadamente, se um software possui abstrações bem definidas, logo ele estará aberto para extensão.

Criar códigos extensíveis é uma tarefa desempenhada principalmente por desenvolvedores maduros, que conseguem antever a evolução do sistema e criar suas classes habilitadas para este fim, com uma arquitetura e design duradouros, tornando os sistemas de ótima qualidade e manutenibilidade.

Para o uso desde princípio, muitos desenvolvedores usam o padrão de projeto Strategy (BOX 4).

BOX 4. Strategy

A lógica condicional é uma das estruturas mais complexas e utilizadas no desenvolvimento de softwares corporativos. Lógicas condicionais tendem a crescer e tornar-se cada vez mais sofisticadas, maiores e mais difíceis de manter com o passar do tempo. O padrão Strategy ajuda a gerenciar toda essa complexidade imposta pelas lógicas condicionais.

O Padrão Strategy sugere que se produza uma família de classes para cada variação do algoritmo e que se forneça para a classe hospedeira uma instância de Strategy para a qual ela delegará em tempo de execução. Um dos pré-requisitos para o Strategy é uma estrutura de herança onde cada subclasse específica contém uma variação do algoritmo.

Assim, o padrão Strategy possui diversos benefícios como clarificar algoritmos ao diminuir ou remover lógica condicional, simplificar uma classe ao mover variações de um algoritmo para uma hierarquia, e habilitar um algoritmo para ser substituído por outro em tempo de execução.

Em resumo o padrão Strategy pode ser utilizado quando se tem as seguintes situações:

Quando muitas classes relacionadas diferem apenas no seu comportamento;

Quando necessita-se de variantes de um algoritmo;

Quando se precisa ocultar do usuário a exposição das estruturas de dados complexas, específicas do algoritmo;

Quando uma classe define muitos comportamentos e por sua vez eles aparecem como diversos “IFs”. Com isso esses comandos condicionais são movidos para sua própria classe Strategy.

LSP - Liskov Substituition Principle na Prática

Este princípio em resumo diz que: se uma classe B é um subtipo de uma classe A, então objetos da classe A podem ser substituídos por objetos da classe B.

Imaginemos primeiramente um exemplo bem abstrato deste princípio. Temos uma classe TVoyage que herda da classe TCarro. A classe TVoyage possui um método chamado Andar, porém este método deve também existir na classe TCarro, para que possamos a qualquer momento em outro módulo do sistema, fazermos referência a classe TVoyage como sendo uma TCarro, ou seja, devemos ter um método abstrato chamado Andar na classe TCarro, sendo que a classe TVoyage o implementa. Desta forma podemos acessar um objeto de TVoyage através de uma referência TCarro.

Para exemplificarmos o princípio vamos considerar um problema bastante comum no dia-a-dia do desenvolvedor, que é a geração de arquivos nos mais diversos formatos. Frequentemente somos requisitados para geração de vários tipos de arquivo como Texto, Excel, Pdf, etc.

O que ocorre geralmente é a criação de uma classe base com o nome do arquivo e o texto a ser gerado e uma classe filha com as implementações de cada tipo de arquivo, conforme pode ser visto na Listagem 7.

Normalmente também se cria uma classe apenas para fazer todas as gerações e facilitar o uso por parte dos clientes. Criamos portanto a classe TGeradorArquivo que possui um único método chamado GerarArquivo que recebe uma classe Arquivo por parâmetro, que deverá ser feita a geração de acordo com o tipo de classe passada.

No corpo do método temos testes condicionais onde verificamos o tipo da classe passada através do operador IS e chamamos o método apropriado para cada tipo de classe, através do uso de Typecast com o operador AS.

Listagem 7. Implementação defeituosa da geração de arquivos ferindo o princípio LSP


  01 type
  02   TArquivo = class
  03   protected
  04     FNome: string;
  05     FTexto: string;
  06   public
  07     property Nome: string read FNome write FNome;
  08     property Texto: string read FTexto write FTexto;
  09   end;
  10 
  11   TArquivoTexto = class(TArquivo)
  12     procedure GerarArquivoTexto;
  13   end;
  14 
  15   TArquivoPdf = class(TArquivo)
  16     procedure GerarArquivoPdf;
  17   end;
  18 
  19   TGeradorArquivo = class
  20     procedure GerarArquivo(AArquivo: TArquivo);
  21   end;
  22 
  23 procedure TGeradorArquivo.GerarArquivo(AArquivo: TArquivo);
  24 begin
  25   if AArquivo is TArquivoTexto then
  26     (AArquivo as TArquivoTexto).GerarArquivoTexto()
  27   else if AArquivo is TArquivoPdf then
  28     (AArquivo as TArquivoPdf).GerarArquivoPdf();
  29 end;

Existe grandes problemas na modelagem destas classes, esses métodos de geração não deveriam ser específicos de cada classe e sim estar na classe base pré-definido para usufruirmos do poder do polimorfismo na classe de geração de arquivos.

Você deve pensar neste momento, mas as formas de geração de arquivos Texto e Pdf são completamente diferentes. Está correto, elas são mesmo completamente diferentes, mas sua implementação que é diferente, a interface de geração é a mesma, isso que importa no momento da definição.

Uma das principais regras da orientação a objetos é que devemos programar sempre voltados a interface, não para a implementação. É muito mais importante definir o que uma classe vai fazer do como ela irá fazer.

Com esta abordagem, no momento que tivermos a geração de um novo tipo de arquivo pelo sistema, teremos que adicionar mais um else if, o que já vimos não ser uma boa prática de orientação a objetos.

Assim podemos afirmar que as classes TArquivoTexto e TArquivoPdf não estão em conformidade com o princípio de Substituição de Liskov, uma vez que não podemos substituí-la pela classe Arquivo na classe TGeracaoArquivo, porque os métodos de geração são específicos de cada classe derivada. Sendo assim somos obrigados a verificar o tipo e fazer typecast para fazer a chamada do método apropriado.

LSP - Liskov Substitution Principle Refatorado

Uma regra geral para atingirmos o objetivo de respeitar este princípio da orientação a objetos é o de nunca termos métodos específicos em nossas classes descendentes. Todo método necessário numa classe filha da classe pai, deve obrigatoriamente ser previsto na classe pai, mesmo que seja apenas um protótipo, com o uso da diretiva abstract, onde não precisamos fazer nenhuma implementação do método, simplesmente indicamos que este método deve ser implementado pelas classes descendentes.

Esta medida é muito inteligente e nos possibilita fazer uso do polimorfismo para fazermos referência a qualquer classe de uma hierarquia de classes através da classe base da mesma.

Na Listagem 8 temos a definição das classes TArquivo, TArquivoTxt e TArquivoPdf, seguindo o princípio de substituição de Liskov. Veja que não será mais necessário o uso do typecast para fazer a geração correta dos arquivos.

Listagem 8. Geração de arquivos obedecendo o princípio LSP


  01 type
  02   TArquivo = class abstract
  03   protected
  04     FNome: string;
  05     FTexto: string;
  06   public
  07     property Nome: string read FNome write FNome;
  08     property Texto: string read FTexto write FTexto;
  09     procedure Gerar; virtual; abstract;
  10   end;
  11 
  12   TArquivoTexto = class(TArquivo)
  13     procedure Gerar; override;
  14   end;
  15 
  16   TArquivoPdf = class(TArquivo)
  17     procedure Gerar; override;
  18   end;
  19 
  20   TGeradorArquivo = class
  21     procedure GerarArquivo(AArquivo: TArquivo);
  22   end;
  23 
  24 implementation
  25 
  26 { TGeradorArquivo }
  27 
  28 procedure TGeradorArquivo.GerarArquivo(AArquivo: TArquivo);
  29 begin
  30   AArquivo.Gerar();
  31 end;

O primeiro passo feito na refatoração destas classes é o de criarmos um protótipo do método de geração de arquivos na classe base (linha 9), conseguimos isto através das diretivas virtual e abstract, fazendo com que não tenhamos que implementar este método na classe pai, apenas estamos indicando para todas suas descendentes que elas devem obrigatoriamente implementar este método de acordo com a sua forma de geração.

Nas classes descendentes, agora não temos mais um método com cada nome e sim fazemos a sobrescrita do método Gerar da classe base com a codificação específica de cada tipo de geração.

Toda esta refatoração nos remete a um método GerarArquivo da classe TGeradorArquivo muito mais simples e sem o uso de typecast (linha 30) que é propício a erros em tempo de execução.

Esta maneira de resolver este problema é muito mais elegante e aproveita todo o poder do polimorfismo para a geração de arquivos.

Este princípio nos alerta para o cuidado que devemos ter com nossas hierarquias de classes e é muito importante, pois nos sem a aplicação do mesmo a hierarquia de classe fica muito bagunçada e os testes unitários para uma classe base nunca teriam sucesso para a subclasse.

Para evitarmos a violação deste princípio pode-se também aplicar o uso de padrões de projeto, como o composite (BOX 5), onde evitamos o uso da herança em decorrência da composição de classes.

BOX 5. Composite

O padrão Composite é utilizado quando queremos reutilizar método e funções de algum objeto e colocar em qualquer outro, sendo vários objetos ligados a um único. Em certas estruturas necessitamos por muitas vezes de métodos e funções que já estão especificadas em um determinado objeto, sendo necessária apenas a ligação entre os dois para assim poder ser reutilizado o código. Este padrão pode ser usado sempre que desejarmos construir objetos, existindo objetos do mesmo tipo.

Participantes:

Componente:

* Declara a interface para objetos na composição;

* Implementa comportamento default para interface comum a todas as classes, como apropriado;

* Declara uma interface para acessar ou gerenciar seus componentes filhos;

Folha:

*Representa objetos folhas na composição. Uma folha não tem filhos;

*Define comportamento para objetos primitivos na composição.

Composição:

* Define comportamento para Componentes que têm filhos;

* Armazena Componentes filhos;

* Implementa operações relacionadas com filhos na interface do Componente.

Cliente:

*Manipula objetos na composição através da interface Componente.

Com o uso do padrão Composite podemos criar objetos com uma grande complexidade e eles serem compostos por outros objetos menores, além de deixar o código bem estruturado e de fácil entendimento, sendo rápida a forma de adicionar novos componentes, métodos e funções.

Se conseguirmos seguir todos estes princípios apresentados neste artigo, teremos um código limpo, de fácil manutenção e muito mais propício à evolução. Mudanças no sistema serão de fácil acesso e suas correções serão em lugares bem específicos, e problemas não serão propagados para o restante do sistema.

O problema é que não é fácil escrever um código sólido e que siga tais técnicas, isto vai se conseguindo com experiência no desenvolvimento de software e estudo. O interessante é termos sempre classes pequenas com métodos enxutos, o que nos traz muita modularidade.

O Delphi é uma excelente ferramenta RAD onde é muito fácil sair arrastando vários componentes, fazendo várias referências muito rapidamente.

Essas ligações e vínculos criados entre os componentes e os formulários e DataModules vão criando um grande acoplamento entre os módulos do sistema, de forma que sua expansão e manutenção propagam mudanças em cascata para todo o sistema, ou pior, causando bugs em partes do sistema já testadas e em funcionamento.

Muitos hão de perguntar, mas desenvolver orientado a objetos em Delphi é muito mais demorado, vale a pena? A resposta é sim! Vale muito mais a pena perder um tempo a mais num bom design de software na hora de sua criação, do que perder muito mais tempo depois em cada nova manutenção que se faz necessário.

Como já mencionado um software não se desenvolve do dia pra noite e acabou, não precisamos mais botar a mão. Já foi provado através das pesquisas que a grande maioria do tempo dos desenvolvedores é gasto em cima de manutenções e não em desenvolvimento de novos recursos, por isso todo o tempo que for necessário para desenvolver um software com boas práticas de programação e orientação a objetos é muito bem gasto e nos trará um grande benefício no futuro.

Parte II
Veja abaixo a segunda parte do artigo - Agora as partes I e II foram compiladas em um único artigo. Bons estudos :)

Princípios SOLID com Delphi – Parte 2

Por que eu devo ler este artigo:Neste artigo temos a segunda parte da série princípios SOLID da orientação a objetos. Nesta segunda parte faremos um estudo dos dois últimos princípios que são o princípio da segregação de interfaces e o princípio da inversão de dependência.

Além disso, veremos algumas métricas de análise de softwares com baixa qualidade e que precisam de refatoração para melhorar seu design.

Como sabemos, a mudança de requisitos é considerado por muita gente como o principal fator de degradação de um software, mas na realidade não são, porque softwares baseados em princípios e padrões de desenvolvimento devem ser propensos a estas mudanças, sem que as mesmas afetem a arquitetura do sistema.

A realidade é que a mudança de requisitos é uma das grandes “desculpas” dadas pelos desenvolvedores, principalmente aqueles iniciantes que ainda não tem uma formação sólida e conhecimento aprofundado de arquitetura de sistemas.

Este tipo de pensamento também vem de uma cultura existente no desenvolvimento de sistemas, onde se tenta de todas as maneiras evitar a alteração em um software, na maioria das vezes em decorrência e conhecimento da fragilidade de determinado sistema, pois se sabe que ele não está preparado para mudanças.

As metodologias ágeis surgem como a solução para muitos destes problemas, visto que um dos princípios dela é a de que um software obrigatoriamente terá mudanças, ficando a responsabilidade para o desenvolvedor adequar o software já no início do desenvolvimento para que o mesmo permaneça com um bom design mesmo depois de seguidas modificações.

Outra característica presente nas metodologias ágeis é que o desenvolvimento de um software deve ser feito de forma incremental, conforme os desenvolvedores vão entendendo as regras de negócio, sendo que a cada nova iteração, o design do sistema deve ser mantido de acordo com os requisitos e sempre com um código claro e limpo.

Quando surge a necessidade de uma modificação num sistema existente, como a correção de algum erro ou a adição de uma nova funcionalidade, não devemos encarar isso como um problema, e sim como uma oportunidade de qualificarmos o software e disponibilizar para o usuário uma ferramenta eficiente.

Indícios de software de baixa qualidade

Existem diversas formas de diagnosticarmos softwares com baixa qualidade, seguem algumas delas:

· Rigidez: Um software dito rígido é um software muito difícil de ser modificado/alterado. Uma simples mudança ocasiona uma série de mudanças em cascata em várias outras partes do sistema, de forma que além do trabalho de darmos manutenção nestas outras partes do sistema, teremos também que realizar todos os testes nestas unidades que foram alteradas.

Veja que uma alteração em uma parte do sistema fez com que precisássemos alterar outras que estavam funcionando perfeitamente, de maneira que podemos ter adicionado bugs nestas outras partes do sistema, sendo que a mesma não tem nada a ver com a alteração original. Isso é um sinal de mal design de software.

· Fragilidade: Temos um software frágil quando as mais simples mudanças acabam gerando diversos erros em vários outros módulos do software. Na maioria das vezes os erros ocorrem em módulos que não tem ou não deveria ter nenhuma relação com o módulo original.

Um indício de fragilidade do software é quando os desenvolvedores tem muito receio em alterar determinada parte daquele software, com medo de esta alteração “estourar” em outras partes do sistema.

Na maioria das vezes os desenvolvedores, as vezes por falta de conhecimento ou até por pressão de superiores para a rápida manutenção acabam não fazendo todos os testes necessários no módulo alterado e todos os seus dependentes, sendo isso um grave risco de apenas o usuário final se deparar com os erros em ambiente de produção.

· Imobilidade: A imobilidade significa a dificuldade de reaproveitamento de determinados módulos do sistema em outros sistemas, ou seja, a dificuldade em utilizarmos classes que deveriam ser iguais nos dois sistemas, mas devido a falhas no desenvolvimento, essas classes foram aumentando e assumindo responsabilidades extras que não cabiam as mesmas, impedindo que outro software pudesses a utilizar, fazendo com que diminuísse também o tempo de desenvolvimento do software final.

Geralmente há um grande medo por parte dos desenvolvedores na possibilidade de reaproveita aquele módulo, pois ao olhar para o código, as vezes já percebemos que as classes estão mal projetadas.

· Viscosidade: A viscosidade de um software está relacionada com a melhor forma de fazer manutenção do mesmo, ou seja, para realizarmos uma mudança no software é muito mais fácil fazermos uma “gambiarra” para esta modificação do que desenvolvermos um código mais conciso e mantermos o bom design, ou seja, é muito mais fácil alterar um software da maneira errada do que da maneira certa.

Isso ocorre muito também com desenvolvedores menos experientes, que desconhecem boas práticas de programação e padrões de projeto, de forma que acabam prejudicando o design da classe e fazendo com que o software perca qualidade.

· Alta Complexidade: Um software que possui muitas unidades desnecessárias, chamamos isso também de excesso de engenharia, quando o desenvolvedor prevê muitas mudanças futuras e aplica as vezes muitos padrões de projetos, aumentando consideravelmente a complexidade do software de forma desnecessária, na expectativa de atender demandas futuras, prevendo diversas situações que podem ocorrer no futuro, situações estas que na maioria das vezes nem vão ocorrer.

Isso torna o código complexo demais dificultando muito o entendimento, principalmente se alguma manutenção for dada por outro desenvolvedor que não seja o criador original desta classe.

Existem dois princípios menos conhecidos que servem para evitar esta alta complexidade criada de forma errônea, são eles o KISS e o YAGNI (BOX 1).

· Redundância: Com certeza essa é a mais famosa, a duplicidade de código em várias partes da aplicação. Isso acontece porque os desenvolvedores ao invés de encapsular uma determinada ação em um método ou classe, preferem simplesmente copiar e colar o código já existente, causando redundância.

Um exemplo clássico que poderíamos citar é o da validação de CPF, já existe um método em uma classe chamado ValidarCpf, porém este método agora precisa ser utilizado em outra classe que não tem relação nenhuma com esta, então ao invés do desenvolvedor refatorar esta classe criando uma nova classe chamada TValidaCpf com um método ValidarCpf para que ambas possam utilizá-la, mas ele simplesmente copia o método e cola na nova classe causando a repetição deste código, e pior, se um dia precisarmos ajustar esta validação precisaremos lembrar de modificar a implementação em ambas as classes.

· Código de Difícil Leitura: Esta é uma das maiores dificuldades encontradas para desenvolvedores que devem dar manutenção em softwares, pela dificuldade de entendimento do código escrito, por ser desorganizado ou por nomes de métodos e variáveis sem sentido, ou até nomes de classes e interfaces que não expressam no seu nome a sua clara responsabilidade.

BOX 1. KISS e YAGNI

KISS (Keep it Simple) ou ainda Mantenha Simples, é um princípio de desenvolvimento de softwares que valoriza a simplicidade do projeto e defende que toda a complexidade desnecessária seja descartada.

YAGNI (You aren't gonna need it) ou ainda você ainda não vai precisar dele, é um princípio que sugere que os desenvolvedores não adicionem funcionalidades ao software sem que estas necessidades sejam realmente necessárias, ou seja, não se deve prever o que será preciso antes de ser realmente necessário.

KISS e YAGNI são dois princípios que demonstram uma prática de XP – Extreme Programming, que pregam o desenvolvimento ágil baseado em refatorações. Não se devem aplicar padrões de projeto de software antecipadamente, pois causa complexidade desnecessária.

Quando se inicia um projeto de software não conseguimos ter a visão de como a arquitetura irá evoluir e quais padrões de projeto serão necessários, se é que um dia precisarão ser aplicados.

SOLID

Os princípios SOLID são um conjunto de cinco princípios básicos de boas práticas de programação para o desenvolvimento de aplicações orientadas a objetos e de essencial conhecimento para qualquer desenvolvedor que deseje desenvolver sistemas de fácil manutenção, com alta escalabilidade e com códigos limpos e elegantes. Estes princípios farão com que nossas classes possuam um melhor design. São eles:

· SRP: Single Responsibility Principle: princípio da responsabilidade única;

· OCP: Open Closed Principle: princípio do aberto/fechado;

· LSP: Liskov Substituition Principle: princípio da substituição de Liskov;

· ISP: Inteface Segregation Principle: princípio da segregação de interfaces;

· DIP – Dependency Inversion Principle: princípio da inversão de dependência.

ISP - Inteface Segregation Principle

O Princípio da Segregação de Interfaces (ISP - Interface Segregation Principle) nos ensina que numa aplicação que segue o paradigma orientado a objetos, é muito melhor termos várias interfaces específicas do que uma interface de uso geral. Uma classe cliente nunca ser dependente de métodos que não utiliza.

Segregar significa separar, ou seja, devemos pegar nossa interface e separá-la em partes mais lógicas. Então devemos dividir nossas interfaces grandes, com muitos métodos e muitas responsabilidades em interfaces menores, para que as classes clientes dependam apenas de métodos que realmente irão utilizar e não de métodos que não farão sentido para ela.

Quando desenvolvemos interfaces genéricas demais, acabamos fazendo com que uma implementação (classes concretas), não utilizem alguns métodos da interface.

Quando isso ocorre simplesmente precisamos implementar métodos que não fazem nada, somente estão ali para cumprir a regra de implementação de interfaces, que diz que quando implementamos uma interface todos os métodos presentes nela devem ser obrigatoriamente implementados.

Se seguirmos fielmente este princípio da orientação a objetos nosso software será mais escalável, terá uma manutenção facilitada. Como regra geral, sempre que precisarmos de métodos novos em uma interface, veja se este método realmente faz sentido nesta interface, se não fizer, deve-se criar uma nova interface para ele.

Temos o princípio da responsabilidade única relacionado também a este princípio, porque se temos uma interface com muitos métodos, há de ser supor que esta interface não está seguindo o princípio da responsabilidade única e provavelmente deve estar realizando mais de uma tarefa, ferindo o princípio.

Por fim, uma dica é que sempre devemos ter nossas interfaces enxutas, com poucas ações ou métodos. Interfaces com muitos métodos prejudicam muito a manutenção, visto que ao modificarmos a interface, devemos percorrer por todos os pontos do sistema onde ela é utilizada e fazer as adequações necessárias.

Estes cuidados melhoram o design das classes, ficando cada uma com apenas as suas responsabilidades, atendendo também o primeiro princípio SOLID que é o da responsabilidade única, aumentando bastante o nível de coesão (BOX 2) delas.

BOX 2. Coesão

A medida da Coesão de uma classe segundo a Orientação a Objetos, indica de que forma foi projetada uma determinada classes. Nos mostra como a classe possui uma funcionalidade única e clara. Desta forma quanto mais simples e focada for a classe para resolver um único problema, maior a sua coesão, algo desejado no desenvolvimento orientado a objetos.

Classes com alta coesão trazem benefícios como a facilidade de manutenção e evolução, sendo também possível e com facilidade serem reutilizadas posteriormente em outros projetos.

DIP – Dependency Inversion Principle

Este princípio de Inversão de Dependência sem dúvida é um dos mais importantes da orientação a objetos, sendo que o mesmo é bastante estudado e existe uma série de frameworks nas mais variadas linguagens de programação para atendê-lo.

O princípio nos ensina que módulos de níveis mais alto do software nunca devem depender de módulos mais baixos, sempre deve depender de abstrações, estas abstrações também não devem depender de detalhes e sim os detalhes das abstrações.

Ao conseguirmos inverter a dependência, fazemos com que as classes clientes não fiquem frágeis em relação detalhes de implementação, ou seja, se fizermos alguma mudança na implementação nas classes que a classe cliente é dependente, não se faz necessário alterações na classe cliente.

Existem diversos padrões de projetos (BOX 3) relacionados com este princípio, pois na maioria das vezes desejamos criar interfaces que não dependam de implementações.

BOX 3. Padrões de Projeto GoF

Os padrões de projeto do GoF surgiram com o objetivo de padronizar a solução de problemas frequentes no desenvolvimento de um software orientado a objetos, para que estes padrões sejam utilizados na sua resolução, tornando-se estas resoluções um padrão.

Receberam este nome GoF (Gang of Four) devido a publicação de um livro no ano de 1995 chamado Design Patterns: Elements of Reusable Object-Oriented Software por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, a chamada Gangue of Four.

Padrões de criação: relacionados à criação de objetos (Abstract Factory, Builder, Factory Method, Prototype, Singleton);

Padrões estruturais: tratam das associações entre classes e objetos (Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy);

Padrões comportamentais: tratam das interações e divisões de responsabilidades entre as classes ou objetos (Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor).

Outro padrão que anda junto com a inversão de controle é o da injeção de dependências, pois na realidade a injeção de dependências é uma das maneiras de conseguirmos implementar a inversão de controle. O padrão de injeção de dependências se baseia em um padrão de projeto chamado Builder (BOX 4).

BOX 4. Builder

O padrão de projeto de software Builder permite a separação da construção de um objeto complexo da sua representação, de forma que o mesmo processo de construção possa criar diferentes representações. Segundo o livro Design Patterns: Elements of Reusable Object-Oriented Software, este padrão contém os seguintes elementos:

director — constrói um objeto utilizando a interface do builder;

builder — especifica uma interface para um construtor de partes do objeto-produto;

concrete builder — define uma implementação da interface builder, mantém a representação que cria e fornece interface para recuperação do produto;

product — o objeto complexo acabado de construir. Inclui classes que definem as partes constituintes.

Temos hoje no mercado diversos frameworks de injeção de dependências, principalmente para as linguagens Java e C#, onde o desenvolvimento orientado a objetos é obrigatório. Já no Delphi existem algumas iniciativas como o delphi-spring-framework e o delphiDIcontainer. Ao utilizarmos esses frameworks, as dependências entre as classes não são feitas explicitas programaticamente, e sim na infraestrutura do framework, também chamado de container, que fica sendo responsável por injetar as dependências da classe no momento apropriado. Neste sentido, as dependências são configuradas através do uso de XML ou através de reflexão computacional com o uso de atributos e anotações.

Este padrão prega que as dependências de nossas classes não devem ser criadas de maneira explicita e sim ser injetada de forma programática ou através de um framework. Podemos exemplificar, por exemplo, uma classe DAO (TClienteDAO), ela depende de uma classe de conexão com a base de dados para efetuar operações de consulta e atualização na base de dados.

Porém essa dependência com a classe de conexão com o banco de dados (TConexao) será injetada na classe TClienteDAO, de maneira que a classe DAO não precisa preocupar-se com o ciclo de vida do objeto de conexão (criar, destruir, etc.), ela apenas recebe esta instância e a usa.

Existem basicamente quatro maneiras de realizarmos a injeção de dependências em uma classe, são elas:

· Construtor: todas as dependências de uma classe são injetadas via método construtor da classe;

· Propriedade: as dependências da classe são injetadas através de propriedades (setter);

· Interface: a dependência que será injetada é uma abstração (interface ou classe abstrata) da classe concreta;

· Framework: frameworks facilitam a injeção de dependências, utilizando recursos extras como XML e reflexão computacional e armazenando as dependências em um container.

Por fim, podemos concluir que este princípio principalmente nos ensina que nossas classes devem depender sempre de abstrações, nunca de classes concretas, pois as abstrações (interfaces e classes abstratas) mudam com bem menos frequência, facilitando bastante à manutenção e evolução do software.

Isso vai de encontro com um dos maiores lemas da orientação a objetos que diz: programe sempre voltado para a interface e nunca para a implementação. Com isso conseguimos diminuir o acoplamento entre as classes do sistema.

Princípio da Segregação de Interfaces na Prática

Neste exemplo faremos uso de um domínio bastante presente na vida de qualquer desenvolvedor, que é o da persistência. Na Listagem 1 temos a declaração de uma classe TDAO que servirá de classe base para todas as classes de persistência, que deve herdá-la quando assim desejar.

Observe que além dos métodos de inserção/atualização (Salvar), temos alguns métodos de busca que deverão ser implementados por todas as classes filhas. Além desses, temos uma função de efetuar log das operações realizadas.

Veja a classe TFuncionarioDAO, ela herda de TDAO (linha 14) e implementa dos os métodos. No caso desta classe faz sentido a implementação de um log, já que geralmente esta classe possui atributos como salário, que precisam ter maior nível de segurança ao efetuar modificações. As buscas também fazem sentido, já que numa busca por funcionário podemos ter a busca por sua matrícula na instituição e pelo nome do mesmo.

Porém, a implementação do método BuscarTodos na classe TFuncionario não faz sentido, geralmente esse tipo de busca sobrecarrega o servidor, já que muitos dados devem ser trafegados na rede.

Neste sentido, a busca por todos os funcionários não faz sentido e mesmo assim devemos implementá-la, mesmo que não venhamos a utilizar este método.

Agora veja uma classe de persistência ainda mais simples, a TUnidadeMedidaDAO (linha 25), esta classe apenas armazena um identificador e a descrição da sigla, de maneira que buscar por código e nome são completamente desnecessárias, visto que numa aplicação real, geralmente listamos todas as unidades de medida através do BuscarTodos. Nesse sentido os métodos BuscaPorCodigo e BuscaPorNome ficam sobrando nesta classe.

A funcionalidade de EfetuarLog também é totalmente desnecessária no contexto da tabela de Unidades de Medidas, visto que é uma tabela auxiliar e não demanda tanta segurança nas informações. Mesmo assim a classe TUnidadeMedida herda o atributo FArquivoLog e também é obrigada a implementar método EfetuarLog, do contrário não conseguimos compilar o programa.

Listagem 1. Classes ferindo o princípio da Segregação de Interfaces

 
  1.  TDAO = class abstract
  2.  protected
  3.    FArquivoLog: string;
  4.  public
  5.    procedure Salvar(AObject: TObject); virtual; abstract;
  6.    procedure Excluir(AId: Integer); virtual; abstract;
  7.    function BuscarPorId(AId: Integer): TObject; virtual; abstract;
  8.    function BuscarTodos: TObjectList<TObject>; virtual; abstract;
  9.    function BuscarPorCodigo(ACodigo: Integer): TObject; 
          virtual; abstract;
  10.   function BuscarPorNome(ANome: string): TObjectList<TObject>; 
          virtual; abstract;
  11.   procedure EfetuarLog; virtual; abstract;
  12. end;
  13. 
  14. TFuncionarioDAO = class(TDAO)
  15. public
  16.   procedure Salvar(AObject: TObject); override;
  17.   procedure Excluir(AId: Integer); override;
  18.   function BuscarPorId(AId: Integer): TObject; override;
  19.   function BuscarPorCodigo(ACodigo: Integer): TObject; override;
  20.   function BuscarPorNome(ANome: string): TObjectList<TObject>; override;
  21.   procedure EfetuarLog; override;
  22.   function BuscarTodos: TObjectList<TObject>; override;
  23. end;
  24. 
  25. TUnidadeMedidaDAO = class(TDAO)
  26. public
  27.   procedure Salvar(AObject: TObject); override;
  28.   procedure Excluir(AId: Integer); override;
  29.   function BuscarPorId(AId: Integer): TObject; override;
  30.   function BuscarPorCodigo(ACodigo: Integer): TObject; override;
  31.   function BuscarPorNome(ANome: string): TObjectList<TObject>; override;
  32.   procedure EfetuarLog; override;
  33.   function BuscarTodos: TObjectList<TObject>; override;
  34. end;

Como pudemos observar na Listagem 2, temos um design que fere gravemente o princípio de segregação de interfaces, pois tanto na classe TFuncionarioDAO quanto na TUnidadeMedidaDAO são obrigados a implementar métodos que não irão fazer uso. Temos uma interface muito genérica, que foi criada para atender a vários requisitos. Devemos segregá-la (separá-la) em duas ou mais interfaces de modo que viemos a atender o princípio.

Listagem 2. Classes seguindo o princípio da Segregação de Interfaces

 
  1.  IDAO = interface
  2.    ['{8EE7C66D-7AAB-474C-8251-B7225B295A28}']
  3.    procedure Salvar(AObject: TObject);
  4.    procedure Excluir(AId: Integer);
  5.    function BuscarPorId(AId: Integer): TObject;
  6.  end;
  7.  
  8.  IBuscaTodos = interface
  9.    ['{1038D656-5A53-4E7A-8B06-75E9943E0E1B}']
  10.   function BuscarTodos: TObjectList<TObject>;
  11. end;
  12. 
  13. IBuscaPorNome = interface
  14.   ['{BDA23E97-5421-49F7-9622-44115AE7210B}']
  15.   function BuscarPorNome(ANome: string): TObjectList<TObject>;
  16. end;
  17. 
  18. IBuscaPorCodigo = interface
  19.   ['{44F2A9FE-7C68-47A0-A28B-3B64EBD69D91}']
  20.   function BuscarPorCodigo(ACodigo: Integer): TObject;
  21. end;
  22. 
  23. ILog = interface
  24.   ['{066D3F1A-72BA-4839-AC28-780832AC89BC}']
  25.   function GetArquivoLog: string;
  26.   procedure SetArquivoLog(const Value: string);
  27.   property ArquivoLog: string read GetArquivoLog write SetArquivoLog;
  28.   procedure EfetuarLog;
  29. end;
  30. 
  31. TFuncionarioDAO = class(TInterfacedObject, IDAO, ILog, 
       IBuscaPorNome, IBuscaPorCodigo)
  32. public
  33.   procedure Salvar(AObject: TObject);
  34.   procedure Excluir(AId: Integer);
  35.   function BuscarPorId(AId: Integer): TObject;
  36.   function BuscarPorNome(ANome: string): TObjectList<TObject>;
  37.   function BuscarPorCodigo(ACodigo: Integer): TObject;
  38.   procedure EfetuarLog;
  39.   function GetArquivoLog: string;
  40.   procedure SetArquivoLog(const Value: string);
  41. end;
  42. 
  43. TUnidadeMedidaDAO = class(TInterfacedObject, IDAO, IBuscaTodos)
  44. public
  45.   procedure Salvar(AObject: TObject);
  46.   procedure Excluir(AId: Integer);
  47.   function BuscarPorId(AId: Integer): TObject;
  48.   function BuscarTodos: TObjectList<TObject>;
  49. end;

Se observarmos a nova implementação deste modelo, conforme a Listagem 2, veremos que foram feitas diversas refatorações no design das classes.

O primeiro detalhe a se observar é a transformação da classe abstract TDAO em uma interface IDAO (a partir da linha 1), já que esta classe não possuía implementação, sendo uma grande candidata a esta transformação em interface.

Agora nossos métodos não precisam mais das diretivas virtual e abstract, porque todos os métodos de uma interface são assim sem a necessidade de diretivas.

Além disso observe que somente ficaram os métodos que serão de uso de todas as possíveis classes filhas de IDAO, como o Salvar, Excluir e BuscarPorId. Os demais métodos foram extraídos da classe e deram origem a outras interfaces.

Os métodos de busca que diferem de uma classe DAO para outra foram separados em três novas interfaces IBuscaTodos (linha 8), IBuscaPorCodigo (linha 13) e IBuscaPorNome (linha 18), cada uma com um método a ser implementado e que será responsável pela busca.

Como não serão todas as classes que farão uso do log de alterações no banco de dados, faz sentido que também o atributo e o método que existiam anteriormente em uma nova interface chamada ILog (linha 23). Veja que não podemos declarar atributos numa interface, mas propriedades sim, de modo que precisamos obrigatoriamente definir métodos Get e Set para modifica-la (linhas 25 a 28).

Veja agora como ficou a classe TFuncionarioDAO, fazemos com que a mesma herde de TInterfacedObject que é uma das classes responsáveis por implementar a interface IInterface, que é a interface base para todas as demais interfaces do delphi, com os métodos QueryInterface, _AddRef, _Release, que servem para realizar a contagem de referências das interfaces para posteriormente conseguir liberar as instâncias de forma automática quando não existem mais referências pra ela.

Além da herança de TInterfacedObject, a classe TFuncionarioDAO implementa outras interfaces que fazem sentido para ela, a IDAO para a persistência básica, ILog para fazer log de alterações na base de dados e outras duas interfaces de busca específica (IBuscaPorNome e IBuscaPorCodigo).

Desta maneira, veja que agora a classe TFuncionarioDAO implementa apenas os métodos que fazem sentido em seu escopo, o método BuscarTodos agora não é mais obrigatório e a classe consegue atender o princípio.

De maneira análoga temos a implementação de TUnidadeMedidaDAO, que agora não mais terá vários métodos que não fazem absolutamente nada, somente irá implementar os métodos da interface IDAO e implementar a interface de busca de todos os itens (IBuscaTodos), com o método BuscarTodos, atendendo também os bons princípios da orientação a objetos.

Nota: O GUID Globally Unique Identifier utilizado pelas interfaces em Delphi para garantir que exista apenas um número de referência em toda a aplicação nunca se repetindo, a probabilidade desta GUID ser gerada uma outra vez é quase impossível, tornando assim um ótimo identificador para objetos em uma aplicação.

Na Figura 1 temos o diagrama de classes gerado a partir da codificação de nossas classes. Note que é um design muito mais organizado, onde as responsabilidades estão bem divididas.

Deixamos agora de ter uma interface muito genérica e com muitas ações, para termos mais interfaces de tamanhos menores, onde cada uma trabalha com uma única e bem definida responsabilidade, o que acaba atendendo também o princípio da responsabilidade única estudado na parte 1 do artigo.

abrir imagem em nova janela

Figura 1. Diagrama de Classes da solução final

Inversão de controle e injeção de dependências na prática

Vamos exemplificar a inversão de controle e a injeção de dependências de maneira bastante simples para o leitor, e com exemplos corriqueiros do dia a dia.

Neste exemplo temos duas classes, TCliente e TEndereco, sendo que a primeira possui uma referencia para a segunda, caracterizando um relacionamento de composição entre as duas classes, pois um cliente tem um endereço, conforme pode ser visto na Listagem 3.

A classe TObject que é a base de todas as classes do Delphi, possui um método chamado ToString, que para representar as informações daquela classe de forma textual, por esse motivo fazemos a sobrescrita deste método (linha 10) e o implementamos da maneira desejada.

Na implementação do método ToString (linhas 35 a 47) fazemos uso da classe TStringBuilder, presente nas últimas versões do Delphi para fazermos a manipulação de strings, visto que os tipos string são imutáveis e quando fazemos uma concatenação de strings o compilador precisa realocar todo os espaços em memória daquela variável.

A classe TStringBuilder utiliza um mecanismo mais eficiente na manipulação de strings e é muito recomendável nesses casos.

Seu uso é simples, utilizamos o método Append para adicionarmos informações e o método ToString para pegar o resultado final das concatenações. Observe também que utilizamos o método Exit para devolver o resultado da função, este é um recurso também existente somente nas últimas versões do Delphi.

Listagem 3. Classes TCliente e TEndereco


  1.  TEndereco = class;
  2.  
  3.  TCliente = class
  4.  strict private
  5.    FNome: string;
  6.    FEndereco: TEndereco;
  7.  public
  8.    constructor Create;
  9.    destructor Destroy; override;
  10.   function ToString: string; override;
  11.   property Nome: string read FNome write FNome;
  12.   property Endereco: TEndereco read FEndereco write FEndereco;
  13. end;
  14. 
  15. TEndereco = class
  16. strict private
  17.   FLogradouro: string;
  18.   FNumero: Integer;
  19. public
  20.   property Logradouro: string read FLogradouro write FLogradouro;
  21.   property Numero: Integer read FNumero write FNumero;
  22. end;
  23. 
  24. constructor TCliente.Create;
  25. begin
  26.   FEndereco := TEndereco.Create;
  27. end;
  28. 
  29. destructor TCliente.Destroy;
  30. begin
  31.   FEndereco.Free;
  32.   inherited;
  33. end;
  34. 
  35. function TCliente.ToString: string;
  36. var
  37.   StrBuilder: TStringBuilder;
  38. begin
  39.   StrBuilder := TStringBuilder.Create();
  40.   try
  41.     StrBuilder.Append('Cliente: ').Append(FNome).Append(sLineBreak);
  42.     StrBuilder.Append('Endereco: ').Append(FEndereco.Logradouro).Append(', ').Append(FEndereco.Numero);
  43.     Exit(StrBuilder.ToString);
  44.   finally
  45.     StrBuilder.Free;
  46.   end;
  47. end;

Veja que na linha 12 temos uma propriedade do tipo Endereco. Neste modelo, o primeiro item a ser observado é que foi utilizada uma classe concreta para apontar qual o tipo da propriedade. Este mecanismo caracteriza um alto acoplamento entre as partes, pois estamos criando uma interdependência entre as classes por estarmos criando esta referência dentro da classe TCliente.

Outro problema pode ser visto no método construtor e destrutor, nas linhas 27 e 32, onde vemos que quem está gerenciando o ciclo de vida do objeto Endereco é a própria classe cliente. Desta forma se por alguma razão tivermos que mudar a implementação da classe Endereço, a classe Cliente também deverá ser adaptada e recompilada, algo indesejado em um bom projeto orientado a objetos.

Nota: Você pode ter observado linha 1 da Listagem 3 que temos uma pré-declaração da classe TEndereco, isso se faz necessário pela razão do compilador presente no Delphi ser top-down, ou seja de cima para baixo.

Desta maneira para declararmos referências para a classe TEndereco, como nas linhas 6 e 11, precisamos informar ao compilador que esta classe será implementada abaixo.

O segundo detalhe a ser notado é o uso da diretiva strict private para os atributos das classes, isso é feito para evitar o problema existente das “classes amigas”, onde classes presentes numa mesma unidade tem acesso aos atributos privados de todas as classes, quebrando o encapsulamento.

Listagem 4. Teste da classe TCliente do método normal


  1.  class procedure TTeste1.Teste;
  2.  var
  3.    Cliente: TCliente;
  4.  begin
  5.    Cliente := TCliente.Create;
  6.    try
  7.      Cliente.Nome := 'Maria da Silva';
  8.      Cliente.Endereco.Logradouro := 'Rua Tiradentes';
  9.      Cliente.Endereco.Numero := 60;
  10.     Writeln(Cliente.ToString());
  11.   finally
  12.     Cliente.Free;
  13.   end;
  14. end;

Na Listagem 4 temos uma classe e método de teste para nosso modelo, observe que apenas um objeto de TCliente, isto porque a classe TCliente internamente cria e destrói a classe TEndereco, desta forma podemos acessar as propriedades de endereço do cliente. No final mandamos uma mensagem para o console através do método ToString que foi sobrescrito na classe TCliente.

Injeção de Dependências via construtor

A primeira forma de injeção de dependência que será mostrada é a via construtor da classe, onde a classe TCliente irá receber uma instância da classe TEndereco no construtor, conforme Listagem 5.

Nessa implementação já temos a evolução do nosso modelo, de maneira que agora a classe TCliente não precisa mais cuidar do ciclo de vida da classe TEndereco, já que ela recebe uma instância da classe TEndereco via método construtor.

Veja que na linha 15, precisamos criar uma instância de TEndereco antes de chamarmos o construtor da classe TCliente (linha 17) e passarmos a referência. É neste método também que cuidamos da criação e destrução do objeto TEndereco, seu ciclo de vida agora está fora da classe TCliente.

Listagem 5. Injeção de Dependência via método Construtor


  1.  TCliente = class
  2.  private
  3.  ...
  4.    FEndereco: TEndereco;
  5.  public
  6.    constructor Create(AEndereco: TEndereco);
  7.  ...
  8.  end;
  9.  
  10. class procedure TTeste2.Teste;
  11. var
  12.   Cliente: TCliente;
  13.   Endereco: TEndereco;
  14. begin
  15.   Endereco := TEndereco.Create;
  16.   try
  17.     Cliente := TCliente.Create(Endereco);
  18.     try
  19.       ...
  20.     finally
  21.       Cliente.Free;
  22.     end;
  23.   finally
  24.     Endereco.Free;
  25.   end;
  26. end;

Esta já é uma evolução, já que conseguimos fazer a injeção de dependências, porém a inversão de controle ainda não foi atingida, pois existe uma dependência de TCliente para TEndereco, conforme a linha 4. Observe que estamos apontando este atributo para uma classe concreta, não para uma abstração, caracterizando assim um alto acoplamento entre elas.

Para resolvermos este problema precisamos evoluir um pouco o modelo, abstraindo a interface de TEndereco para uma interface chamada IEndereco, conforme Listagem 6.

Listagem 6. Novo design de classes após a refatoração


  1.  IEndereco = interface
  2.    ['{5DD17698-E2B0-4488-AF2B-02695F7B632C}']
  3.    function GetLogradouro: string;
  4.    procedure SetLogradouro(const Value: string);
  5.    function GetNumero: Integer;
  6.    procedure SetNumero(const Value: Integer);
  7.    property Logradouro: string read GetLogradouro write SetLogradouro;
  8.    property Numero: Integer read GetNumero write SetNumero;
  9.  end;
  10. 
  11. TEndereco = class(TInterfacedObject, IEndereco)
  12. strict private
  13.   FLogradouro: string;
  14.   FNumero: Integer;
  15. protected
  16.   function GetLogradouro: string;
  17.   procedure SetLogradouro(const Value: string);
  18.   function GetNumero: Integer;
  19.   procedure SetNumero(const Value: Integer);
  20. end;
  21. 
  22. TCliente = class
  23. strict private
  24.   FNome: string;
  25.   FEndereco: IEndereco;
  26. public
  27.   constructor Create(AEndereco: IEndereco);
  28.   function ToString: string; override;
  29.   property Nome: string read FNome write FNome;
  30. end;

Ao criarmos uma interface para a classe de endereço, criamos duas propriedades na interface IEndereco (linhas 7 e 8) e os métodos getters e setters (linhas 3 a 6) que serão responsáveis pela leitura e escritas nos atributos implementados pela classe TEndereco posteriormente.

A partir da linha 11 temos a implementação da classe TEndereco, onde ela implementa a interface IEndereco, atribuindo o valor recebido para os atributos nos métodos setters e devolvendo o valor do atributo nos métodos getters.

Estas implementações ficaram fora do artigo para não o tornar muito extenso, mas as mesmas podem ser visualizadas ao efetuar o download do código fonte dos exemplos da revista na DevMedia.

A transformação da classe TEndereco para a interface IEndereco traz outro benefício que é a de o desenvolvedor não precisar mais se preocupar com a liberação da instância criada para esta interface, pois o delphi libera os recursos alocados de forma automática.

Veja como fica o uso destas últimas interfaces e classes na Listagem 7. Note que agora não foi mais necessário codificarmos de forma explícita a liberação da instância de endereço da memória, visto que o Delphi faz isso pra nós.

Listagem 7. Uso do modelo com injeção de dependência com construtor e interface


  1.  class procedure TTeste3.Teste;
  2.  var
  3.    Cliente: TCliente;
  4.    Endereco: IEndereco;
  5.  begin
  6.    Endereco := TEndereco.Create;
  7.    Cliente := TCliente.Create(Endereco);
  8.    try
  9.      ...
  10.   finally
  11.     Cliente.Free;
  12.   end;
  13. end;

Injeção de dependências via propriedade

Esta é uma das formas mais simples de injeção de dependência e uma das preferidas pela maioria dos frameworks de injeção de dependências. Basta que adicionemos uma propriedade do tipo da interface desejada e popularmos a propriedade quando desejarmos, conforme Listagem 8.

Listagem 8. Injeção de Dependências via Propriedade


  1.  TCliente = class
  2.  strict private
  3.    FNome: string;
  4.    FEndereco: IEndereco;
  5.  public
  6.    property Nome: string read FNome write FNome;
  7.    property Endereco: IEndereco read FEndereco write FEndereco;
  8.  end;
  9. 
  10. class procedure TTeste4.Teste;
  11. var
  12.   Cliente: TCliente;
  13.   Endereco: IEndereco;
  14. begin
  15.   Endereco := TEndereco.Create;
  16.   Cliente := TCliente.Create;
  17.   try
  18.     Cliente.Endereco := Endereco;
  19.     ...
  20.   finally
  21.     Cliente.Free;
  22.   end;
  23. end;

O restante da codificação fica semelhante ao da injeção de dependências via método construtor visto anteriormente. Quem desejar mais detalhes pode visualizar no código fonte junto do artigo no site da DevMedia.

No método de teste, precisamos novamente da criação da instância de Endereço, só que desta vez atribuímos a referência através da propriedade Endereco (linha 18).

Injeção de dependências via interface

A codificação da injeção de dependências através da criação de uma interface é um pouco diferente, e exige a criação de mais uma interface, conforme pode ser visto na Listagem 9.

Declaramos uma interface IEnderecoDI que irá servir basicamente para ser implementada quando uma classe necessita de um objeto Endereço no seu interior. Também se faz necessária a criação de um método SetEndereco que irá receber essa interface por parâmetro e armazenar na variável de referência.

Listagem 9. Injeção de Dependências via Interface


  1.  IEnderecoDI = interface
  2.    ['{D545EEAC-15CA-4055-8745-B1F96BA8DBF3}']
  3.    procedure SetEndereco(AEndereco: IEndereco);
  4.  end;
  5. 
  6.  TCliente = class(TInterfacedObject, IEnderecoDI)
  7.  strict private
  8.    FEndereco: IEndereco;
  9.  public
  10.   procedure SetEndereco(AEndereco: IEndereco);
  11. end;
  12. 
  13. procedure TCliente.SetEndereco(AEndereco: IEndereco);
  14. begin
  15.   FEndereco := AEndereco;
  16. end;
  17. 
  18. class procedure TTeste5.Teste;
  19. var
  20.   Cliente: TCliente;
  21.   Endereco: IEndereco;
  22. begin
  23.   Endereco := TEndereco.Create;
  24.   Cliente := TCliente.Create;
  25.   try
  26.     Cliente.SetEndereco(Endereco);
  27.     ...
  28.   finally
  29.     Cliente.Free;
  30.   end;
  31. end;

Veja que agora a classe TCliente implementa a interface IEnderecoDI (linha 6) e é obrigada a implementar o método SetEndereco que recebe uma referência e guarda na variável interna FEndereco.

Agora quando desejamos fazer uso destas classes, faz-se necessário fazer chamada ao método SetEndereco da classe TCliente (linha 26) e passarmos a referência de Endereco que foi instanciada anteriormente, para que a mesma possa ser utilizada sem problemas.

Note que existem várias formas de aplicar o conceito de injeção de dependências em nossas modelagens de classe. Cabe a nós analistas de sistemas e desenvolvedores fazer uso daquele que mais admiramos ou aquele que consideremos o mais simples.

Veja que a última maneira apresentada adiciona mais uma camada de abstração, mais uma interface no modelo, de certa forma aumentando um pouco a complexidade do modelo.

Este tipo de análise que deve ser feita principalmente por Analistas Desenvolvedores de Sistemas que já possuem formação e experiência no desenvolvimento de softwares, pois estão mais capacitados a fazer a identificação dos padrões de projeto e princípios da orientação a objetos que se aplicam a determinada situação.

Cada princípio SOLID tem várias formas de análise e implementação, como os domínios dos problemas que temos no dia a dia são bastante variados, indo de softwares simples como de controles de estoque até sistemas mais complexos e extensos como uma folha de pagamento ou um sistema bancário, devemos ter cuidado ao aplicar corretamente os princípios, de maneira que eles venham a ajudar e não apenas adicionar mais complexidade aos softwares.

Como pudemos observar no decorrer do artigo, uma mudança de requisito não deve ser encarada como um problema e sim uma oportunidade de melhora do software. Vimos que através de metodologias ágeis podemos atingir este objetivo.

Também fica bastante claro o papel fundamental dos princípios SOLID no bom design de um software, fazendo com que o mesmo seja mais escalável, de fácil manutenção e de código com fácil entendimento.

Ao aliarmos as metodologias ágeis aos princípios SOLID e os padrões de projeto, termos a certeza de um software eficiente. É claro que estes princípios e padrões precisam ser estudados e entendidos de forma clara, de maneira que no dia a dia o desenvolvedor saiba como e onde aplica-lo.

Notamos também que todos os requisitos estão relacionados e no momento que ferimos um deles, acabamos ferindo outros. Portanto é necessário que sigamos a todos os princípios, para que venhamos obter sucesso no desenvolvimento de nossos aplicativos e que o cliente fique satisfeito com o resultado final, pois na prática é isto que mais importa, a satisfação de nossos clientes.