Do que trata o artigo

Fundamentos de OO: abstração, encapsulamento, polimorfismo e herança. Mostra como criar e utilizar interfaces, separar responsabilidades e aplicar padrões de projeto (Design Patterns).


Para que serve

Aumentar o conhecimento sobre POO para desenvolver sistemas mais flexíveis com baixo custo de manutenção, tornando a arquitetura de sistemas mais expansível.


Em que situação o tema é útil

As boas práticas apresentadas neste artigo são úteis ao desenvolver aplicações Delphi de qualquer tipo, sejam aplicações comerciais, desktop, web, pois a OO pode ser empregada com sabedoria em qualquer situação, tornando a arquitetura mais robusta.

Resumo do DevMan

Este artigo mostra para você o caminho a ser seguido para criar sistemas flexíveis que tenham baixo custo de manutenção. Aqui não será discutido, o que é uma abstração ou uma interface, mais sim como, quando, porque e qual a melhor maneira de usar este e outros recursos.

Programar orientado a objeto não é difícil. É tão fácil que até os programadores mais inexperientes programam utilizando a orientação a objetos, antes mesmo de saber que existe esta forma de programar. Isto pode ser notado, por exemplo, ao criar um novo formulário. Quando você pede um novo formulário (File + New + Form), o que acontece de verdade é que o IDE cria automaticamente para você, uma classe que herda as características necessárias para um formulário. É esta facilidade que gera alguns problemas.

Devido à grande facilidade, que permite ao programador codificar sem a necessidade de seguir um padrão específico, os fundamentos da orientação a objetos muitas vezes são ignorados e quando são utilizados, acabam por ser utilizados de maneiras pouco eficientes, levando o programador a pensar que: ou ele não sabe programar orientado a objeto, ou programar orientados a objeto é muito difícil e não vale a pena o tempo gasto em relação aos benefícios que traz. Não importa o que você pensa, o fato é que querendo ou não, sabendo ou não, em Object Pascal você sempre está programando OO. O “segredo” é saber usar os fundamentos da OO da melhor maneira possível para o seu caso.

O foco deste artigo não é apresentar os fundamentos da OO, mas sim abordar a melhor maneira de usá-los, aplicando alguns dos principais princípios da programação OO para que o resultado seja um programa flexível, como a nossa realidade exige.

O Jogo

Para entender quais as vantagens e desvantagens da OO, imagine que você criou um jogo que simula o desempenho de diferentes tipos bolas em ambientes diferentes. Todas as bolas do jogo podem rolar ou quicar. Neste jogo, você usou a herança para reduzir ao máximo a duplicação de código. Você criou uma superclasse TBola herdada por todos os outros tipos de bolas (Figura 1).

Figura 1. Primeiro design para o simulador (framework)

Na Figura 1 podemos observar que:

• Além de TBolaDeCouro e TBolaDePlastico podem existir outros tipos de bolas que herdam de TBola;

• Como todas as bolas podem quicar e rolar da mesma maneira, a superclasse TBola cuida da implementação para os métodos Quicar() e Rolar(). Fazendo isto, você aproveita uma das vantagens da herança que é o reaproveitamento de código. Implementando os métodos Quicar() e Rolar(), apenas em uma classe é o suficiente para que todas as suas subclasses tenham o mesmo comportamento;

• Como cada bola tem um desenho e formato diferente – umas com quadradinhos desenhados, outras sem nada desenhado e assim por diante – o método Show() é abstrato e cada subclasse deve implementar a maneira como é exibida.

As alterações começam a chegar

O simulador está estável e funciona perfeitamente, porém os concorrentes sempre estão inovando e agora o simulador precisa de novas funções para continuar líder no mercado. Após alguma análise, é decidido que o simulador deve esvaziar as bolas e essa nova função precisa estar pronta em uma semana. Parece que uma semana é tempo de sobra já que esta é uma alteração simples. Basta aproveitarmos mais uma vez da herança criada e adicionar um novo método Murchar() na superclasse TBola e todas as suas descendentes poderão murchar também (Figura 2).

Figura 2. Método Murchar() na classe base

Implementando o método murchar em TBola, você evita reescrever o mesmo código em cada classe de bola e consegue terminar a alteração e menos de uma semana, como havia prometido. Se futuramente a forma como as bolas murcham precisar ser alterado, basta fazer uma única alteração.

Usar a herança e implementar o método Murchar() na superclasse TBola economizou tempo, mas por outro lado criou um comportamento estranho no simulador, pois algumas bolas de boliche e de sinuca começaram a esvaziar durante o jogo. E agora? Como você resolve este problema? Sobrescrever o método Murchar() em cada subclasse de TBola parece uma boa solução, mas desta maneira você terá espalhado por todo o programa o código que faz uma bola murchar e com certeza o tempo gasto em futuras alterações seria monstruoso. Sem contar a grande porta que se abre para os erros, já que quando você precisar alterar o método Murchar(), poderá acabar se esquecendo de mudá-lo em uma classe ou outra.

Você sabe que com o passar do tempo muitos outros tipos de bolas irão surgir e você não pode prever o material e nem o formato que elas terão. É possível que algumas delas tenham um formato diferente de redondo e por isto, rolam de maneira diferente e dependendo do material que é feita, também não deveria murchar. Você precisa de uma solução mais simples, para que somente as bolas que podem esvaziar implementem o método Murchar() e ainda mais, que este método de murchar seja escrito somente uma vez. Será que existe um padrão de projeto para isto?

Não misture as bolas

Até agora vimos que herança tem suas vantagens, como evitar a duplicação de código, mas por outro lado, fazer com que todas as subclasses de uma superclasse herdem todos os seus comportamentos não parece muito bom, pois uma simples alteração na superclasse pode fazer com que as subclasses tenham comportamentos estranhos como foi o caso das bolas de sinuca e boliche. Isto torna a manutenção do software um verdadeiro pesadelo.

Para solucionar este problema, primeiro vamos identificar o que muda de uma bola para outra. No nosso simulador o que muda são os comportamentos de esvaziar e rolar, já que nem todas as bolas esvaziam ou rolam e até mesmo entre as que rolam existem diferença, pois uma bola de futebol americano não roda da mesma maneira que uma bola de basquete, por exemplo. Portanto, o que muda em nosso código são os comportamentos de rolar e esvaziar e a classe TBola não deve implementá-los.

Sabendo disto, podemos separar as bolas do simulador em dois grupos: bolas que rolam e bolas que murcham. Assim você está aplicando seu primeiro princípio de orientação a objetos.

Princípio de orientação a objetos

Identifique os aspectos que mudam em seu aplicativo e separe-os do que permanecem iguais.

Pelo que vimos até agora, só temos problemas com os métodos Murchar() e Rolar(), os demais métodos não variam entre as subclasses de TBola e estão funcionando muito bem. Por tanto vamos deixar um pouco a classe TBola de lado e nos concentrar nos grupos de classes que criamos.

Para separar os comportamentos que mudam entre uma bola e outra, vamos retirar os métodos Murchar() e Rolar() de TBola e colocá-los em grupos específicos de classes que representam estes comportamentos (Figura 3).

Figura 3. Separando o que muda

Nota: Não se preocupe como são feitos este grupo de classes. Você verá isto em outro momento.

Definido os grupos de comportamentos, queremos agora que as subclasses de TBola possam rolar e esvaziar sem perder a flexibilidade do projeto. Por exemplo, queremos fazer uma bola de basquete rolar e após isto acontecer, gostaríamos de trocar a maneira como ela rola ou até mesmo fazer com que ela não possa rolar mais, tudo isso em tempo de execução. Em outras palavras, queremos alterar a maneira como uma bola rola ou murcha sem precisar re-compilar o simulador.

Princípio de orientação a objetos

Programe para uma interface, não para uma implementação.

Vamos aplicar o princípio da orientação a objetos que nos diz para SEMPRE programar para interface (contrato), ou seja, devemos sempre identificar os grupos de classes com comportamentos em comum e criar interfaces que identifiquem o que elas têm em comum. No nosso simulador, vamos criar duas interfaces: IComportamentoDeRolar e IComportamentoDeMurchar e assim qualquer classe que queira rolar ou murchar basta implementar a interface que identifica o comportamento desejado. Desta maneira, ao invés das subclasses de TBola implementar os métodos Rolar() e Murchar(), existirá um grupo de classes cuja única razão de existir é o de representar um destes comportamentos (Figura 4).

Figura 4. Criando as classes de comportamentos

Na Figura 4 podemos ver as classes de comportamentos, cujo o único objetivo de existir é implementar o método que a interface ao qual ela implementa declara. Algumas considerações:

• Cada classe que implementa a interface IComportamentoDeRolar deve escrever o código necessário para fazer uma bola rolar;

• A classe TRolarComoBolaDeFutebolAmericano faz com que uma bola role como se fosse uma bola de futebol americano;

• A classe TNaoPodeRolar deixa o corpo do método Rolar() vazio, visto que as bolas com este comportamento não devem se movimentar.

Veja nas Listagens 1, 2 e 3 como ficam os códigos destas classes.

Listagem 1. Declarando a interface para classes que sabem rolar uma bola


  unit ComportamentoDeRolar;
   
  interface
   
  type
    IComportamentoDeRolar = interface
      ['{83D1C793-73D3-4F22-83DF-FCF51905B815}']
      procedure Rolar;
    end;
   
  implementation
   
  end. 

Na Listagem 1 temos a declaração da interface (contrato) que as classes devem implementar (assinar), para poder fazer parte do grupo de classes que sabem rolar uma bola.

Nota do DevMan

Se você não tem muita experiência com as interfaces do Object Pascal, deve ter achado estranho o código ['{83D1C793-73D3-4F22-83DF-FCF51905B815}'], que você pode ver na Listagem 1. Este código é chamado de GUID - Globally Unique Identifier e serve para tornar sua interface “única no mundo”. Tudo o que você precisa saber sobre ele é que não é obrigatória sua declaração para criar uma interface, porém, você ganha uma série de facilidades ao declará-lo. Fora as facilidades com type cast, você também ganha a capacidade de testar se uma classe implementa ou não uma interface, como no código a seguir: if Supports(Obj, IComportamentoDeRolar) then. Este código testa se objeto Obj implementa a interface IComportamentoDeRolar. Se você não declarar o GUID na interface IComportamentoDeRolar, este teste não funcionara. O IDE pode gerar um GUID aleatório para você, basta pressionar Ctrl + Shift + G.

Listagem 2. Criando a classe TRolarComoBolaDeFutebolAmericano


  unit RolarComoBolaDeFutebolAmericano;
   
  interface
   
  uses ComportamentoDeRolar;
   
  type
    TRolarComoBolaDeFutebolAmericano = class(TInterfacedObject, IComportamentoDeRolar)
    public
      procedure Rolar;
    end;
   
  implementation
   
  uses Dialogs;
   
  procedure TRolarComoBolaDeFutebolAmericano.Rolar;
  begin
    ShowMessage('Estou rolando como uma bola de futebol americano');
    {aqui vai o código necessário para fazer uma bola rolar
     como se fosse uma bola de futebol americano}
  end;
   
  end. 

Na listagem 2 temos a classe que sabe fazer uma bola rolar como se fosse uma bola de futebol americano.

Listagem 3. Criando a classe TNaoPodeRolar


  unit NaoPodeRolar;
   
  interface
   
  uses ComportamentoDeRolar;
   
  type
    TNaoPodeRolar = class(TInterfacedObject, IComportamentoDeRolar)
    public
      procedure Rolar;
    end;
   
  implementation
   
  uses Dialogs;
   
  procedure TNaoPodeRolar.Rolar;
  begin
    //Sou uma bola que não pode rolar
  end;
   
  end. 

Nota: estes arquivos de código-fonte serão criados adiante neste exemplo.

Na Listagem 3 vemos a criação da classe TNaoPodeRolar. Usaremos uma instância desta classe para fazer com que um objeto TBola pare de rolar. A classe TNaoPodeRolar não implementa código algum no método Rolar() pois ela não deve mover as bolas em nosso simulador.

Do jeito que o simulador foi feito no início, estávamos contando com uma implementação feita na superclasse e por isso ficávamos presos a uma única definição de comportamento e se quiséssemos fazer alterações neste comportamento deveríamos escrever mais e mais código a cada novo tipo de bola que fosse inventada. Agora estamos usando um contrato (as interfaces) que definem regras para qualquer classe que deseje assiná-lo. Isto pode parecer um pouco radical a princípio.

Programar para uma interface significa na verdade, programar para um contrato. A vantagem está na arquitetura, pois através das interfaces, podemos referenciar um objeto sem saber realmente qual o seu tipo, pois o que importa é se ele implementa aquela interface ou não.

Mudando o comportamento em tempo de execução

Agora que você entendeu as vantagem e desvantagens da herança e mudou a maneira de pensar quando se deseja obter mais flexibilidade no sistema, vamos fazer o simulador alterar o comportamento das bolas em tempo de execução. Veja na Figura 5 o que muda na classe TBola após aplicados os princípios aprendidos no tópico anterior.

Figura 5. Novo design de TBola

Princípio de orientação a objetos

Dê prioridade à composição.

A Figura 5 mostra uma mudança sutil, porém, reveladora em TBola. Agora, cada bola TEM UM IComportamentoDeRolar e um IComportamentoDeMurchar. Relacionamentos deste tipo entre os objetos recebem o nome de composição. No decorrer do artigo você entenderá as vantagens de TER UM (composição), sobre SER UM (herança).

Foram feita três mudanças na classe TBola. Primeiro foram adicionadas duas novas variáveis que definem o comportamento que uma bola terá em tempo de execução. Repare que estas variáveis são do tipo das interfaces criadas para definir os grupos de comportamentos. Desta maneira as variáveis não ficam presas a uma implementação e assim elas poderão referenciar qualquer tipo de objeto existente, desde que estes objetos implementem a interface pedida. Se você definisse a variável FComportamentoDeRolar do tipo de uma classe que faz rolar, como por exemplo TRolarComoBolaDeBasquete, esta bola só poderia rolar como uma bola de basquete, pois a variável só aceitaria instâncias de objetos da classe TRolarComoBolaDeBasquete.

Então você deve estar se perguntando: “Por que eu não posso criar uma superclasse para os grupos de classes de comportamentos ao invés de criar uma interface?”. A resposta é: você pode, mas isto seria ruim. A grande vantagem da interface em relação à herança é que uma classe pode implementar quantas interfaces você quiser, enquanto a herança só pode ser feita de uma única classe. Você estaria programando para implementações novamente e se no futuro você precisar fazer as bolas rolarem em quanto estiverem quicando, teria que criar uma nova superclasse para este novo comportamento de rolar. Veja como ficaria na Figura 6.

Figura 6. Programando para uma implementação ao invés de interface

Na Figura 6 a herança mostra mais uma vez que sozinha não consegue resolver todos os problemas, deixando o programa inflexível. Além dos problemas já vistos nos tópicos anteriores, temos o grande número de classes novas criadas em duplicidade apenas para o novo comportamento de rolar no ar, isto porque uma classe não pode herdar mais de uma classe ao mesmo tempo (o Delphi não suporta herança múltipla). Programando para interfaces, você não tem este problema, pois uma mesma classe pode implementar quantas interfaces for necessário, em outras palavras, uma mesma classe pode assinar quantos contratos você quiser (isso simula herança múltipla).

Figura 7. Programando para interface

A Figura 7 mostra que não precisamos criar mais classes para o caso de implementar um novo comportamento de rolar. Neste caso, seria necessário apenas criar mais uma interface para este comportamento e implementá-la nas classes já existentes.

Voltando para a classe TBola, vamos definir o comportamento que queremos para a bola no momento em que criarmos ela, por isto foram adicionados dois parâmetros ao construtor da classe. Por último, foi alterado o nome dos métodos Rolar() e Murchar() para FazerRolar() e FazerMurchar(), respectivamente. Os nomes foram alterados porque agora os métodos que fazem uma bola rolar e murchar pertencem às classes de comportamentos – aquelas que implementam as classes IComportamentoDeMurchar ou IComportamentoDeRolar. Como estes métodos não pertencem mais a classe TBola, então faz mais sentido chamar o método de FazerRolar() do que apenas Rolar(). Nada impede que você use os nomes antigos. Nenhum problema será criado por conta disto, apenas fica mais fácil de ler e entender o código com os novos nomes.

Da teoria à prática

Feitas todas as alterações necessárias no design inicial do simulador, chegou a hora de colocar tudo isto para funcionar. Neste tópico, você tem a oportunidade de praticar os conceitos abordados neste artigo.

Não existe muita diferença entre a manipulação das classes que fazem uma bola rolar e manipulação das classes que fazem murchar, por isto, neste exemplo vamos trabalhar apenas com as classes que definem o comportamento de rolar, porém, no exemplo disponível para download, todos os comportamentos são manipulados. Recomendo, de verdade, que você tente fazer sozinho toda a parte referente ao comportamento de murchar e só depois de ter conseguido, use o exemplo para conferir se você fez algo diferente. Mas primeiro, será feita a manipulação do comportamento de rolar.

Crie um novo projeto (File + New + Application), deixe o formulário principal como na Figura 8. Após isto, configure os componentes do formulário principal de acordo com a Tabela 1.

Figura 8. Formulário principal do exemplo

Objeto

Propriedade

Valor

TForm

Name

FFormularioPrincipal

TRadioGroup

Name

RGRolarComo

Caption

Rolar como...

Columns

2

ItemIndex

0

Items

Bola de futebol americano

Não pode rolar

TButton

Name

BCriar

Caption

Criar bola

TButton

Name

BFazerRolar

Caption

Fazer rolar

Enabled

False

TButton

Name

BFazerMurchar

Caption

Fazer murchar

Enabled

False

TButton

Name

BDestruirBola

Caption

Destruir bola

Enabled

False

Tabela 1. Componentes do formulário principal

Listagem 4. Evento OnClick do botão BCriar


  1    procedure TFFormularioPrincipal.BCriarClick(Sender: TObject);
  2    Var
  3      CompDeRolar: IComportamentoDeRolar;
  4    Begin
  5      case RGRolarComo.ItemIndex of
  6        0: CompDeRolar := TRolarComoBolaDeFutebolAmericano.Create;
  7        1: CompDeRolar := TnaoPodeRolar.Create;
  8      End;
  9    
  10     FBola := TBola.Create(CompDeRolar, nil);
  11   
  12     BCriar.Enabled := False;
  13     BFazerRolar.Enabled := True;
  14     BFazerMurchar.Enabled := True;
  15     BDestruirBola.Enabled := True;
  16     RGRolarComo.Enabled := False;  
  17   end; 

Na Listagem 4 você pode ver como a classe TBola é criada dentro do simulador.

• Na linha 3, declaramos a variável que vai receber uma referência a uma classe que implemente a interface IComportamentoDeRolar. Não esqueça de declarar a unit ComportamentoDeRolar na sessão uses do formulário principal;

• Entre as linhas 5 e 8, é feito um teste para saber qual comportamento de rolar deve ser usado. Observe que, para os dois comportamentos é usado a mesma variável, embora as classes que estão sendo criadas sejam diferentes. Isto porque o sistema foi programado para interface e a variável da linha 3 é do tipo de uma interface e não um tipo de uma classe (implementação). Não esqueça de declarar as units RolarComoBolaDeFutebolAmericano e NaoPodeRolar na sessão uses do formulário principal;

• Na linha 10, a TBola é criada definitivamente e no seu construtor passamos a variável que possui uma referência para o comportamento que deve ser usado. É passado nil para o parâmetro que define o comportamento de murchar, porque não vamos manipular este comportamento neste exemplo;

• A variável FBola usada na linha 10 foi declarada (FBola: TBola), na sessão private do formulário. Não esqueça de declarar a unit Bola na sessão uses do formulário principal;

• Com a bola criada, o botão de criar bolas é desabilitado e os demais botões que manipulam uma bola são habilitados, como pode ser visto entre as linhas 12 e 16.

Listagem 5. Evento OnClick dos botões BFazerRolar, BFazerMurchar e BDestruirBola


  1    procedure TFFormularioPrincipal.BFazerRolarClick(Sender: TObject);
  2    Begin
  3      FBola.FazerRolar;
  4    end;
  5    
  6    procedure TFFormularioPrincipal.BFazerMurcharClick(Sender: TObject);
  7    Begin
  8      FBola.FazerMurchar;
  9    end;
  10   
  11   procedure TFFormularioPrincipal.BDestruirBolaClick(Sender: TObject);
  12   Begin
  13     FreeAndNil(FBola);
  14   
  15     BCriar.Enabled := True;
  16     BFazerRolar.Enabled := False;
  17     BFazerMurchar.Enabled := False;
  18     BDestruirBola.Enabled := False;
  19     RGRolarComo.Enabled := True;
  20   end; 

Não há segredos na Listagem 5.

• Da linha 1 a 4 é definido o evento OnClick do botão BFazerRolar. Quando o usuário clicar neste botão a bola deve rolar de acordo com o comportamento que ele escolheu antes de criar a bola;

• Da linha 6 a 9 é definido o evento OnClick do botão BFazerMurchar. Embora não tenha sido definido um comportamento de murchar quando a classe TBola foi criada, nada impede o formulário principal de chamar o método FazerMurchar(), isto porque quem deve se preocupar se o comportamento foi definido ou não é a classe TBola e não o formulário principal;

• Entre as linhas 11 e 20 você pode ver o evento OnClick do botão BDestruirBola. Quando o usuário clicar neste botão a instância da bola que foi criada é destruída e os objetos da tela são atualizados para que o usuário possa criar uma outra bola.

Listagem 6. A classe TBola


  1   unit Bola;
  2   
  3   Interface
  4   
  5   uses ComportamentoDeRolar, ComportamentoDeMurchar;
  6   
  7   Type
  8     TBola = class
  9     Private
  10      FComportamentoDeRolar: IComportamentoDeRolar;
  11      FComportamentoDeMurchar: IComportamentoDeMurchar;
  12    Public
  13      constructor Create(CompRolar: IComportamentoDeRolar; CompMurchar: IComportamentoDeMurchar);
  14      procedure Show;
  15      procedure Quicar;
  16      procedure FazerRolar;
  17      procedure FazerMurchar;
  18    End;
  19  
  20  Implementation
  21  
  22  { TBola }
  23  
  24  constructor TBola.Create(CompRolar: IComportamentoDeRolar; CompMurchar: IComportamentoDeMurchar);
  25  Begin
  26    FComportamentoDeRolar := CompRolar;
  27    FComportamentoDeMurchar := CompMurchar;
  28  
  29    Show;
  30  End;
  31  
  32  procedure TBola.FazerMurchar;
  33  Begin
  34    if Assigned(FComportamentoDeMurchar) then
  35      FComportamentoDeMurchar.Murchar;    
  36  end;
  37  
  38  procedure TBola.FazerRolar;
  39  Begin
  40    If Assigned(FComportamentoDeRolar) then
  41      FComportamentoDeRolar.Rolar;
  42  end;
  43  
  44  procedure TBola.Quicar;
  45  Begin
  46    //código para fazer uma bola quicar
  47  end;
  48  
  49  procedure TBola.Show;
  50  Begin
  51    //desenha uma bola na tela
  52  end;
  53  
  54  end. 

Na Listagem 6 você pode ver como a classe TBola é por dentro. Para criar a classe TBola, peça uma nova unit (File + New + Unit), salve-a com o nome de Bola.pas e digite o código apresentado. Explicando:

• Nas linhas 10 e 11 são declaradas as variáveis que receberão o respectivo comportamento escolhido quando a classe é criada;

• Nas linhas 26 e 27 é atribuído o valor para as variáveis que definem o comportamento que a bola deve ter;

• Entre as linhas 38 e 42 é feita a implementação do método FazerRolar(). Este método apenas chama o método Rolar() da variável FComportamentoDeRolar definida na linha 10. Repare na linha 40 que existe um teste para garantir que o método Rolar() só será invocado quando tiver um comportamento definido. Isto vai evitar os erros de acesso à memória (Access Violation), quando nenhum comportamento de rolar for definido;

• Entre as linhas 32 e 36 está a chamada do método Murchar() da variável que define o comportamento de esvaziar uma bola. Lembre-se que as classes que definem este comportamento devem ser criadas por você. Após criá-las, você pode baixar o arquivo de exemplo para conferir se você fez algo diferente.

Para criar a interface IComportamentoDeRolar, peça uma nova unit (File + New + Unit), salve-a com o nome de ComportamentoDeRolar.pas e digite o código da Listagem 1. Para criar a classe TRolarComoBolaDeFutebolAmericano, peça uma nova unit (File + New + Unit), salve-a com o nome de RolarComoBolaDeFutebolAmericano.pas e digite o código da Listagem 2. Para criar a classe TNaoPodeRolar, peça uma nova unit (File + New + Unit), salve-a com o nome de NaoPodeRolar.pas e digite o código da Listagem 3. Após seguir estes passos, ainda estará faltando criar a interface IComportamentoDeMurchar. Crie uma nova unit (File + New + Unit), salve-a com o nome ComportamentoDeMurchar.pas e digite o código Listagem 7.

Listagem 7. Declarando a interface IComportamentoDeMurchar


  unit ComportamentoDeMurchar;
   
  interface
   
  type
    IComportamentoDeMurchar = interface
      ['{9A434BD8-7F13-43EF-BB28-9760137DD94B}']
      procedure Murchar;
    end;
   
  implementation
   
  end. 

Veja que o código da Listagem 1 e da Listagem 7 são muito parecidos, isto porque ambas as listagens estão definindo as interfaces para os grupos de classes de comportamentos. Após criar todos os arquivos, você pode compilar e executar a aplicação para ver o resultado.

Lembrete!

Depois que você ver o resultado, não se esqueça de criar as classes que definem o comportamento de esvaziar e usá-las assim como já esta feito para o comportamento de rolar. Uma dica: Estas classes devem implementar a interface IComportamentoDeMurchar.

Nota do DevMan

A função Assigned(const P) usada na Listagem 6 é uma função nativa do Delphi. Está função recebe uma variável que aponta para um objeto ou método e testa se o local para onde a variável aponta é válido. Se o ponteiro passado no parâmetro P estiver apontando para um local válido é retornado True, caso o ponteiro esteja apontando para um endereço inválido é retornado False. Quando Obj estiver apontando para um objeto, usar if Assigned(Obj) then é o mesmo efeito que usar if Obj <> nil then. Quando Obj estiver apontando para um método, usar if Assigned(Obj) then será o mesmo efeito que usar if @Obj <> nil then.

Ainda mais flexível

Já criou as classes que definem o comportamento de murchar? Então vamos continuar. Aplicando alguns princípios de programação orientada a objetos, foi possível tornar o simulador muito mais flexível. No primeiro design do simulador não existia espaço para alterações e mudar o comportamento de uma bola era uma tarefa complicada, pois para cada novo comportamento, era necessário criar um novo método na superclasse TBola, fazendo com que esta alteração se propagasse para todas as subclasses causando erros e comportamentos estranhos, como por exemplo, fazer uma bola de boliche esvaziar. Com o novo design do simulador podemos definir o comportamento de qualquer objeto da classe TBola em tempo de execução quando criamos uma nova bola. Veja na Figura 9 como ficou o novo design após aplicar os princípios aprendidos até agora.

Figura 9. Novo design para o simulador

Na Figura 9 é possível ver que a classe TBola não sabe mais como fazer para esvaziar ou rolar. Ao invés disto, ela invoca os métodos das variáveis FComportamentoDeRolar e FComportamentoDeMurchar, assim uma bola pode assumir qualquer comportamento em tempo de execução. Para isto, a classe TBola depende das interfaces que definem os grupos de comportamentos IComportamentoDeRolar e IComportamentoDeMurchar, assim se no futuro for preciso adicionar ou remover uma nova maneira de murchar ou rolar, basta criar a classe que saberá como este novo comportamento deve ser executado e implementar a interface correta. É importante frisar que a classe TBola não depende das classes de comportamento, mas sim das interfaces. Isto significa que se você excluir ou adicionar algumas das classes de comportamento, a classe TBola continuará compilando como se nada tivesse acontecido, pois ela depende apenas das interfaces.

Graças às interfaces (e porque não dizer polimorfismo), o simulador alcançou um bom nível de abstração, pois tudo aquilo que muda (comportamento de rolar e murchar), foi separado do que não muda na família de TBola. Embora seja possível definir o comportamento de uma bola em tempo de execução, a definição do comportamento fica restrita ao momento de criação da bola. Após criar uma bola, se torna impossível alterar o comportamento da mesma sem destruí-la.

Para conseguir alterar o comportamento de uma bola mesmo depois de criá-la, se faz necessário criar mais dois métodos: um para alterar o comportamento de murchar, e outro para alterar o comportamento de rolar.

Listagem 8. Métodos para alterar o comportamento de TBola após sua criação


  procedure TBola.SetComportamentoDeMurchar(
    NovoComportamento: IComportamentoDeMurchar);
  begin
    FComportamentoDeMurchar := NovoComportamento;
  end;
   
  procedure TBola.SetComportamentoDeRolar(
    NovoComportamento: IComportamentoDeRolar);
  begin
    FComportamentoDeRolar := NovoComportamento;
  end; 

A Listagem 8 mostra os métodos SetComportamentoDeMurchar(IComportamentoDeMurchar) e SetComportamentoDeRolar(IComportamentoDeRolar) que servem respectivamente para alterar o comportamento de uma bola ao executar a ação de esvaziar e rolar. Não á truques nestes métodos. Eles apenas alteram a referência das variáveis de comportamentos que são iniciadas quando TBola é criada. Através destes métodos uma bola pode alterar seu comportamento sem precisar ser destruída. Esta facilidade é possível graças ao uso da composição (TEM UM).

Fim do jogo

No primeiro design do simulador (Figura 1), a classe TBola “era tudo ao mesmo tempo” e não havia espaço para alterações. Ela (a classe TBola) era responsável por tudo que todas as bolas deveriam fazer. É claro que isto gerou muitos problemas, pois uma bola pode assumir vários formatos, tamanhos e ainda por cima ser feita de materiais diferentes.

Para melhorar o código do sistema e deixá-lo aberto para alterações foi necessário seguir três princípios de programação orientada a objetos. São eles:

1. Identifique os aspectos que mudam em seu aplicativo e separe-os do que permanecem iguais.

Quando este princípio foi usado, a classe TBola passou a ter menos responsabilidades, pois tudo aquilo que podia ser diferente entre uma bola e outra, como a forma de rolar, por exemplo, foi retirado da superclasse e colocado em outras classes criadas com um único objetivo: fazer uma bola assumir um determinado comportamento. Com esta solução, apareceu outro problema: como saber que tipo de comportamento uma classe representa?

2. Programe para uma interface, não para uma implementação.

Este princípio resolveu dois problemas de uma única vez. Programando para interfaces foi possível definir uma maneira de saber qual comportamento de TBola uma classe representava, alem de permitir que uma mesma bola pudesse assumir vários tipos de comportamentos diferentes em tempo de execução. Este resultado foi obtido ao usar as interfaces para criar regras (ou contratos), que definiam o comportamento de uma bola. Assim qualquer classe que assinasse este contrato seria considerada uma classe de comportamento de TBola;

3. Dê prioridade à composição.

Usando composição (TEM UM), uma bola pode mudar o seu comportamento mesmo depois de já estar criada. Desta maneira não é necessário destruir uma bola para alterar o seu comportamento.

Veja que sozinhos estes princípios não trariam um resultado desejável.

Nota do DevMan

Notou que este artigo não faz citações sobre padrões de projeto no texto? Aqui foi discutido apenas alguns dos fundamentos de programação orientado a objetos. Porém mesmo assim, sem perceber, você aprendeu o padrão strategy. Este padrão define uma família de algoritmos, encapsula cada um deles e os torna intercambiáveis. O strategy permite que o algoritmo varie independentemente dos clientes que o utilizam.

Em outras palavras: este padrão separa os algoritmos que fazem a mesma coisa de maneira diferente (como rolar uma bola), e encapsula em classes que serão usadas por outra classe denominada Classe Cliente (a classe TBola).

Conclusão

Programar orientado a objetos todos os programadores Object Pascal já fazem: o que muda de um para o outro é o conhecimento sobre orientação a objetos que cada um tem. Para simplificar o aprendizado, este artigo usou um exemplo de algo incomum para a maioria de nós, mas que pode facilmente ser aplicado em situações mais comuns. Você poderia usar os princípios e fundamentos que aprendeu aqui para abstrair as funções do seu sistema ao emitir um documento fiscal, por exemplo. Todos sabem que existem diversos tipos de documentos fiscais: notas, cupons, notas eletrônicas e assim por diante. Fazendo uma analogia ao exemplo do simulador, a maneira como cada documento fiscal é emitida e armazenada no sistema é a parte que muda. Sabendo disto, é possível criar uma interface que identifique este comportamento e fazer com que a classe TEmiteDocumentoFiscal (por exemplo), assumisse o comportamento desejado de acordo com o tipo de nota configurado no sistema.