Voltar

Introdução à POO - Parte 5

Artigo no estilo: Curso

Do que trata o artigo: Este artigo aborda a orientação a objetos com o Delphi, usando uma metodologia simples, didática, de fácil aprendizado. Veremos na teoria, e também na prática, todos os conceitos, fundamentos e recursos oferecidos pelo Delphi para promover a POO.


Para que serve:
A POO pode e deve ser aplicada de forma inteligente, ela serve para construir sistemas mais robustos, mais confiáveis, de fácil manutenção, que permitam maior reaproveitamento de código.


Em que situação o tema é útil:
A POO é útil em qualquer sistema, seja ele Web, Desktop, Mobile, não importa o tipo de aplicação. Os conceitos aqui apresentados, em um exemplo simples, podem ser utilizados em aplicações reais, como é apresentado em um estudo de caso no final desta série.

Resumo: Pacotes são importantes para organizar a forma como nossos componentes são distribuídos. Neste artigo, veremos como criar pacotes e como trabalhar com as sessões contains, requires e dependências. Vamos também examinar como usar UML com Delphi, para modelar nosso pequeno framework. Veremos como criar mais uma classe no framework: TBike, que por tabela já poderá ser usada por TPessoas, graças a abstração. E finalmente, vamos examinar um exemplo que usa interfaces.

No artigo da edição anterior, vimos como dar um grande passo além das classes. Vimos como usar componentes. Praticamente tudo o que fazemos hoje no Delphi é usando, ligando e configurando componentes. Vimos a importância de TPersistent, o uso de RTTI (Runtime Type Information) e serialização de objetos, e como as classes “core” da hierarquia da VCL do Delphi estão organizadas, como TObject, TPersistent, TComponent, TControl, TGraphicControl e TWinControl. Além disso, vimos como criar pacotes, que ajudam a organizar componentes e units e também ajudam na modularização, seja em design time ou runtime. O mais interessante foi quando usamos abstração e polimorfismo através de componentes, quando uma propriedade Transporte no componente Pessoa abstraiu os vários tipos complexos de meio de transporte (reforçando, isso é programar para interfaces, ou mais exatamente neste caso, uma classe abstrata).

Este será o penúltimo artigo da série sobre orientação a objetos com o Delphi. Vamos estudar mais sobre pacotes (packages) e as importantes sessões contains e requires. Além disso, vamos criar um novo pacote exclusivo para abrigar as classes de meio de transporte e separar da classe que opera os transportes (pessoa). Veremos também o suporte à modelagem UML no Delphi. Vamos ainda estender nosso framework criando mais um meio de transporte e assim comprovar que ele é expansível e plugável, reforçando todos os princípios que já vimos até aqui. E finalmente, vamos estudar um pouco de interfaces.

Criando um novo pacote para meios de transporte

No Delphi 2010, clique em File>New>Package. Para facilitar, salve o novo pacote no mesmo diretório de nossa aplicação criada até aqui. Usando o Project Manager, no editor do pacote, dê um clique de direita no item contains e adicione todas as units relativas aos meios de transporte, ou seja, uMeioTransporte.pas, uAviao.pas e uCarro.pas. Ainda no Project Manager, dê um clique de direita sobre o grupo de projetos e adicione o pacote pkPessoa. Remova deste último pacote as units que contêm as classes relativas à meios de transporte, deixando somente a unit que define a classe (componente) TPessoa. Clique de direita no grupo e dê um Build em ambos os pacotes, confirmando as dependências.

Como agora a nossa classe (componente) TPessoa referencia um TMeioTransporte que está em outra unit em outro pacote, precisamos trabalhar na seção Requires. Dê um clique de direita sobre este item no pacote pkPessoa, escolha Add Reference, clique em browse e localize o pkTransporte.dcp, que deve estar por padrão na pasta C:\Users\Public\Documents\RAD Studio\7.0\Dcp (no caso do Windows Vista e Delphi 2010). Isto também pode ser feito dando-se agora um clique de direita no nome do pacote e escolhendo Dependencies. No grupo, dê um clique de direita e escolha Build All para compilar novamente ambos os pacotes. Instale o pacote de Transportes. Seu grupo de programas e pacotes devem agora estar semelhantes ao mostrado na Figura 1.

Pacotes pkPessoa e pkTransporte

Figura 1. Pacotes pkPessoa e pkTransporte

Como você deve ter notado, a seção contains de um pacote indica quais units estão dentro do pacote e serão compiladas no BPL. A seção requires indica que outros pacotes são necessários para que seja feito com sucesso o projeto de compilação. Requires indica dependência e é muito importante que você tome cuidado com isso. Se você construir um grande framework com várias classes distribuídas em vários pacotes, uma mudança em uma única classe pode fazer com que todo o framework seja recompilado. Além disso, se um pacote depende de outro, isso também afeta o deploy. Em nosso exemplo, você não vai conseguir usar o pacote pkPessoa sem distribuir e instalar o pkTransporte, pois em nossa modelagem, toda pessoa deve ser capaz de operar um meio de transporte. Porém, o inverso não é verdadeiro, podemos distribuir meios de transporte sem pessoas, porque não há uma relação de dependência nesse sentido. É nesse ponto que vemos a vantagem de usar classes abstratas e interfaces para diminuir impacto na mudança de interfaces. Poderíamos dar um passo além, como faz a VCL (com DataSources e DataSets), colocando as classes abstratas em um pacote separado e as implementações em outro. Em nosso exemplo, para fins didáticos, não usamos essa abordagem. A esta altura, você já deve estar familiarizado com pacotes e fica aí uma bela sugestão de exercício.

Um outro cuidado com relação a dependências: o próprio IDE do Delphi faz uso desses BPLs, claro, porque eles são de design time e runtime (o padrão). Com isso, se você instalar pkPessoa, automaticamente o IDE vai instalar pkTransporte. Se retirar pkTransporte, pkPessoa não poderá ficar sozinho no IDE. Faça um teste com a VCL para comprovar, clique em Component>Install Packages e tente desinstalar o pacote Embarcadero Database Components, que contém classes core de acesso a dados da VCL como TDataSource, TDataSet etc. Como todas as implementações de acesso a dados no Delphi (BDE, ADO, DBX, ClientDataSet, controles Dara Aware) dependem dessas classes, todas serão retiradas da memória e não podem ser usadas. Veja o warning mostrado na Figura 2 (não confirme, clique em No, fizemos apenas um teste). Agora aproveitemos o exemplo para pensar na seguinte hipótese: e se por algum acaso a equipe do Delphi resolvesse modificar algo na interface deTDataSet, como incluir mais um método virtual? Todos os pacotes dependentes teriam que ser recompilados, incluindo de terceiros (que teriam que fornecer novas versões de seus pacotes). Ou seja, uma reação em cadeia devastadora. Lembro-me até hoje de uma proeza que a equipe do Delphi fez, lançar o Service Pack 1 do Delphi 7 sem que para isso nenhuma interface fosse modificada, ou seja, nenhum pacote /unit dependente foi afetado. Por isso sempre digo, o que você escreve entre o Begin e o End não importa para efeitos de modelagem, o importante é acertar a interface! Jamais publique uma interface, uma classe abstrata, que possa estar suscetível a mudanças constantes. Existe uma regra na OO que diz: “Interfaces são imutáveis”. Vale lembrar que aqui não estamos usando diretamente interfaces, mas sim classes abstratas (TMeioTransporte), mas seu papel e importância é equivalente.

Tente remover o pacote base de acesso a dados do IDE para ver o que acontece

Figura 2. Tente remover o pacote base de acesso a dados do IDE para ver o que acontece

Nota

A primeira implementação do MIDAS (Multitier Distributed Application Services), lançada originalmente no Delphi 4 (e um protótipo no Delphi 3), tinha um erro grave que feria o princípio que citei, que interface são imutáveis. Quando você colocava um DataSetProvider em um DataModule remoto, um novo método de interface (get e set) era criado para que fosse possível a comunicação entre cliente e servidor e troca de Data Packets. Ou seja, uma nova tabela no banco, novo DataSetProvider. E sabe o que ocorria quando você colocava um novo DataSetProvider? Um novo método era criado, logo, a interface COM era modificada, logo, aplicações clientes que acessam aquele servidor também precisariam ser recompiladas. IAppServer, no Delphi 5, resolveu tudo isso. Não são criados novos métodos para cada tabela, pois tudo pode ser trocado através de apenas 7 métodos básicos da interface, que trabalham com todos os tipos de providers. É por esse motivo que você coloca um DataSetProvider em um DataModule, seja ela remoto ou não, e automaticamente já o enxerga na propriedade ProviderName de um ClientDataSet.

UML

Desde o Delphi 2005, o suporte à modelagem UML foi incluído no IDE. Com isso, podemos criar diagramas de classes e outros diagramas UML dentro do próprio ambiente. Para nosso exemplo, nada melhor do que visualizarmos como ficou nosso framework de transporte e pessoas em um diagrama, fazendo engenharia-reversa. Aliás, poderíamos ter feito tudo isso desde o começo, mas deixei para apresentar a UML justamente agora. Poderíamos ter criado um diagrama de classes novinho, visualmente criado as classes, seus atributos, métodos, e o Delphi geraria o respectivo código-fonte, inclusive com as relação de herança (Transporte<-Carro/Aviao) e associação (Transporte-Pessoa).

No editor do pacote, clique de direita em qualquer um dos pacotes e escolha Modeling Support. Marque os dois pacotes (Figura 3) e clique em Ok. Será criado um diretório especial na pasta do projeto/pacotes para incluir o suporte à modelagem. Agora clique em View>Model View, no editor que aparece clique de direita na parte branca e escolha Add>Class Diagram. Arraste as classes para o diagrama. Veja o resultado na Figura 4.

Adicionando suporte
à modelagem

Figura 3. Adicionando suporte à modelagem

Diagrama de classes -
framework de transportes

Figura 4. Diagrama de classes - framework de transportes

Estendendo o framework

Vamos neste ponto do artigo criar mais uma classe descendente de TMeioTransporte, para comprovar que nosso framework é expansível, assim como a VCL, reforçando a abstração, encapsulamento e polimorfismo.

Com o pacote pkTransporte aberto, crie uma nova unit no Delphi e implemente-a conforme mostrado na Listagem 1. Note que aqui estamos criando uma classe descendente de TMeioTransporte (TBike), logo, devemos implementar seus métodos polimórficos. Além disso, adicionei uma propriedade extra à classe, chamada Suspensao (booleana), para indicar se a bicicleta tem esse acessório. Salve a unit como uBike.pas. Codificada a classe, basta recompilar tudo que o pacote será reinstalado e o novo componente vai aparecer no IDE.

Listagem 1. Nova classe: TBike


        unit uBike;

        interface

        uses
        // coloque essa unit p/ acessar a classe TMeioTransporte
        uMeioTransporte, Classes;

        type
        // observe que TBike herda de TMeioTransporte
        TBike = class(TMeioTransporte)
        private
        FSuspensao : boolean;
        function GetSuspensao: boolean;
        procedure SetSuspensao(const Value: boolean);
        protected
        procedure Ligar(); override;
        public
        procedure Mover(); override;
        published
        property Suspensao: boolean
        read GetSuspensao write SetSuspensao;
        end;

        procedure Register;

        implementation

        uses Dialogs;
        { TCarro }

        procedure Register;
        begin
        RegisterComponents('ClubeDelphi',[TBike]);
        end;


        function TBike.GetSuspensao: boolean;
        begin
        result := FSuspensao;
        end;

        procedure TBike.Ligar();
        begin
        // repare que não vai inherited aqui
        // pois não existe nada na classe base
        ShowMessage('Colocando o pé no pedal...');
        end;

        procedure TBike.Mover();
        begin
        inherited;
        ShowMessage(Descricao + ' entrou em movimento.');
        end;

        procedure TBike.SetSuspensao(const Value: boolean);
        begin
        FSuspensao := Value;
        end;

        end.
        

Aqui finalizamos nosso pequeno framework de transporte. Vamos agora examinar um último e importante recurso da OO, interfaces. Para isso, vamos fazer um novo e simples exemplo. Na próxima e última parte deste curso veremos como usar todos os conhecimentos adquiridos nestas cinco partes do curso para criar uma aplicação realusando programação orientada a objetos com o Delphi.

Interfaces

Para finalizar este etapa do nosso mini-curso, vamos fazer uma introdução ao uso de interfaces. Este assunto já foi bastante abordado aqui mesmo na ClubeDelphi, de forma que vou procurar dar uma visão simples desse recurso que é amplamente utilizado na orientação a objetos.

Eu poderia citar aqui a variedade enorme de formas na qual você poderia utilizar interfaces, entre algumas:

  • Na programação de objetos distribuídos – server e cliente assinam um “contrato” e se comunicam através dele. Neste contrato estão definidos tipos, métodos e protocolos;
  • Aplicações DataSnap com banco de dados (ou sem);
  • Criação de plug-ins para o IDE do Delphi;
  • Criar sistemas mais extensíveis e com arquitetura plugável (como o IDE do Delphi);
  • Abstrair totalmente uma classe de uma implementação, por exemplo, declarando uma propriedade de um tipo interface e não uma classe base ou abstrata;
  • Simular herança múltipla, pois uma classe pode implementar mais de uma interface e ainda delegar a implementação delas a outras classes internas;
  • Usar interfaces com polimorfismo (uma mistura poderosíssima);
  • Criar pequenos elos de ligação entre classes, para torná-las o menos dependente possíveis, e facilmente substituíveis;
  • Comunicar aplicações diferentes usando métodos conhecidos, por exemplo, acessar funcionalidades em uma DLL sem usar rotinas básicas como “external”. O mesmo pode ser feito com pacotes de runtime. Exemplos no Delphi são “Import Type Library” (no caso do COM) ou “Import WSDL” (Web Services), ou ainda “Add (Web) Reference” no Dephi Prism;
  • Podem ser usados para implementar contagem de referência de objetos e evitar memory leaks.

Nota

Uma curiosidade. Um dos frameworks que mais faz uso efetivo de interfaces é o WebSnap (framework lançado no Delphi 6 para desenvolvimento Web, sucessor do WebBroker). Você pode encontrar seus fontes na VCL do Delphi. A propósito, a classe que mais implementa interfaces no Delphi é desse framework, observe sua declaração:


            TCustomAdapter = class(TComponent, IIdentifyAdapter, IWebVariableName,
            IWebVariablesContainer, INotifyList, IWebSnapObjClass,
            IGetAdapterErrors, IGetAdapterErrorsList, INotifyWebActivate, IAdapterEditor,
            IGetAdapterFields, IGetAdapterActions, IIteratorSupport,
            IClearAdapterValues, IEchoAdapterFieldValues, IAdapterAccess,
            IGetAdapterHiddenFields, ICreateActionRequestContext, IWebDataFields,
            IWebActionsList, IAdapterNotifyAdapterChange, IIteratorIndex)
            

Uma interface é semelhante a uma classe que possua somente métodos abstratos, ou seja, sem implementação. Uma interface apenas define métodos que depois devem ser implementados por uma classe. Dessa forma, um objeto pode se comunicar com o outro apenas conhecendo a sua interface, que funciona como uma espécie de contrato (veja a Figura 5).

Classes e Interfaces

Figura 5. Classes e Interfaces

Uma interface é como se fosse um controle remoto. Você consegue interagir com um objeto conhecendo o que ele oferece, tendo a interface que descreve cada função, porém, sem a mínima ideia de como ele implementa essa funcionalidade internamente. Assim como TObject é a classe base para todas as classes do Delphi, a interface base para todas as interfaces é IInterface. Veja na Listagem 2 a declaração de IInterface.

Listagem 2. IInterface – interface base do Delphi


        IInterface = interface
        ['{00000000-0000-0000-C000-000000000046}']
        function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
        function _AddRef: Integer; stdcall;
        function _Release: Integer; stdcall;
        end;
        

Os métodos _AddRef e _Release definem o mecanismo de contagem de referência. Isso significa que você não precisa liberar um objeto que implementa IInterface. QueryInterface faz solicitações dinamicamente a um objeto para obter uma referência para as interfaces que ele suporta.

Exemplo usando Interfaces

Para demonstrar o uso de interfaces vamos criar um pequeno exemplo. Inicie uma nova aplicação no Delphi. Salve o formulário como “uFrmMain.pas” e o projeto “Interfaces.dpr”. Dê o nome de “FrmMain” ao formulário. Abra a unit do formulário e declare a interface da Listagem 3 na seção type. Note que não podemos apertar Shift+Ctrl+C e implementar o método Multiplicar. Esse é um método de interface, como um método abstrato de uma classe. Logo abaixo da interface declare o código da Listagem 4.

Nota:

A declaração TComputador = class (TInterfacedObject,ICalc) significa “TComputador herda de TInterfacedObject e implementa a interface ICalc”. Isso não é herança múltipla.

Listagem 3. Declarando uma interface


        type
        ICalc = interface
        // para gerar o GUID abaixo, aperte SHIFT-CTRL+G
        ['{23326572-461F-4571-925E-63DD15AA51D2}']
        function Multiplicar (const x,y : integer) : integer;
        end;
        

Nota:

Note que toda interface tem um GUID – Globally Unique Identifier, um ID único que identifica uma interface. É impossível gerar dois GUIDs idênticos no mundo.

Listagem 4. TComputador sabe calcular, TCalculadora também


        TComputador = class (TInterfacedObject,ICalc)
        function Multiplicar (const x,y : integer) : integer;
        end;

        TCalculadora = class (TInterfacedObject,ICalc)
        function Multiplicar (const x,y : integer) : integer;
        end;
        

Boa Prática

Em um exemplo real, declare a interface em um arquivo (unit) à parte, as classes que a implementam em outra, e obviamente as classes que as consomem em outra.

Observe que ambas as classes TComputador e TCalculadora implementam a interface ICalc e descendem de TInterfacedObject. TInterfacedObject se encarrega de implementar a interface IInterface, logo, não precisamos implementar os métodos que vimos na Listagem 2. Aperte Shift+Ctrl+C para declarar os cabeçalhos dos métodos e implemente-os conforme a Listagem 5. Observe que ambas as classes implementam o método Multiplicar de ICalc, porém de formas diferentes.

Listagem 5. Implementando métodos


        function TComputador.Multiplicar(const x, y: integer): integer;
        begin
        result:=x * y;
        end;

        function TCalculadora.Multiplicar(const x, y: integer): integer;
        var
        i : integer;
        begin
        result := 0;
        if (x <> 0) and (y <> 0) then
        for I := 1 to abs(y) do
        result := result + x;
        end;
        

Usando Edits, Labels, um RadioGroup e um Button construa o formulário mostrado na Figura 6. Dê um duplo clique no botão e digite o código mostrado na Listagem 6. Observe que interessante é o código. Declaramos um objeto que é de um tipo interface. Depois, o criamos a partir de uma implementação / classe concreta, que pode ser qualquer classe que implemente ICalc, neste caso TComputador e TCalculadora. Chamamos então o método Multiplicar, que como foi declarado na interface, permite ser chamado sem que type-casts sejam feitos (o que faria o uso de interfaces perder o sentido). Rode e teste a aplicação, veja o resultado na Figura 7.

Uma aplicação que usa interfaces

Figura 6. Uma aplicação que usa interfaces

Listagem 6. OnClick do botão do formulário


        procedure TFrmMain.Button1Click(Sender: TObject);
        var
        Obj : ICalc;
        n1,n2,r : integer;
        begin
        if RadioGroup1.ItemIndex = -1 then exit;
        case RadioGroup1.ItemIndex of
        0 : Obj := TCalculadora.create;
        1 : Obj := TComputador.create;
        end;
        n1 := StrToInt(Edit1.Text);
        n2 := StrToInt(Edit2.Text);
        r := Obj.Multiplicar(n1,n2);
        Label2.Caption := IntToStr(r);
        end;
        
Usando interfaces

Figura 7. Usando interfaces

Algumas coisas a mais que você precisa saber sobre a técnica aqui demonstrada:

  • Reforçando, interfaces são imutáveis. Depois que você publica uma interface, ela não pode ser mais modificada. Não que isso não possa ser feito, mas provavelmente é a pior prática da OO. Imagine a equipe do Delphi modificar a interface IAppServer? Imagine alguém modificar o padrão USB para conexão de dispositivos?
  • Poderíamos aqui neste exemplo usar Padrões de Projeto, como o Factory e o Abstract Factory, para delegar a criação da classe concreta a uma “fábrica” que cria classes concretas, tornando nossa arquitetura mais poderosa.

Conclusão

Aqui finalizamos nosso framework de meio de transportes. Comprovamos como ele é expansível, quando adicionamos mais uma classe ao framework. Examinamos como distribuir classes em pacotes, de forma organizada, e a importância da dependência entre eles. Finalmente, um overview simples sobre interfaces. Na próxima e última parte deste curso, vamos criar uma arquitetura real, uma aplicação Delphi que implementa e usa praticamente todos os recursos vistos até aqui. Um grande abraço e até a próxima edição.

Parte VI

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

Introdução à POO - Parte 6

Este artigo aborda a orientação a objetos com o Delphi, usando uma metodologia simples, didática, de fácil aprendizado. Veremos na teoria, e também na prática, todos os conceitos, fundamentos e recursos oferecidos pelo Delphi para promover a POO. Nesta parte final, um exemplo real.

A POO é útil em qualquer sistema, seja ele Web, Desktop, Mobile, não importa o tipo de aplicação. Os conceitos apresentados até aqui foram aplicados em um exemplo simples, agora serão utilizados em uma situação real.

Este artigo mostra como usar a orientação a objetos e todos os fundamentos vistos até aqui em um cenário real e crítico, um sistema de vestibulares e correção de provas.

Chegamos à parte final do nosso curso de Delphi e OO. Aprendemos muito até aqui, os pilares da programação orientada a objetos foram todos estudados: herança, polimorfismo, abstração e encapsulamento. Através da elaboração de um pequeno framework, estudamos como a Delphi Language promove a OO. Criamos classes, propriedades, métodos estáticos, abstratos e virtuais, especificadores de visibilidade / modificadores, estudamos construtores, eventos, relacionamentos entre classes como herança e associação, polimorfismo, interfaces e mais. Estendemos nosso framework comprovando que ele é flexível e expansível.

Muito bem. Vimos que a OO funcionou legal com carrinhos e pessoas, afinal, usamos um exemplo bem simples (até porque estamos na sessão Easy). Lembrando ainda que a maioria dos exemplos em Delphi e OO se baseiam em situações não reais (como o que vimos até aqui). Então, como colocar em uma aplicação real tudo o que aprendemos até aqui? Entender o polimorfismo para quem é novo na OO é difícil, sair da teoria e colocá-lo em prática é muito difícil. Entendê-lo e colocá-lo em prática em um exemplo real, quase impossível. Esse é um grande mito que vamos derrubar hoje.

Cenário, jogo e regras

Há vários anos, mais exatamente em 2000, trabalhei como desenvolvedor em uma grande universidade aqui da minha região. Um dos principais sistemas que desenvolvemos foi o que controlava todos os concursos vestibulares da instituição, incluindo a parte mais crítica: correção e listão. Já adianto que por motivos óbvios não é possível mostrar qualquer código de implementação aqui, apenas vou prototipar uma solução semelhante a que defini na época.

Acredito que todos aqui já prestaram vestibular alguma vez na vida. Todos sabem que não é fácil ter o nome no listão. Isso eu também sabia. O que eu não sabia é que, quando estava do outro lado (como programador), colocar um nome no listão não era nada simples. Se existe um software que não permite qualquer tipo de falha, esse é um sistema de vestibulares. Não seria nada interessante enviar um novo listão para vários jornais e sites dois dias após a divulgação oficial, informando que o mesmo foi anulado devido a uma “falha no sistema”.

Enfim, tínhamos uma missão crítica e de muita responsabilidade a resolver: criar um sistema para controle de processos vestibulares. Não vou me deter aqui em explicar todas as regras de um processo de seleção, acho que a maioria sabe, até porque nosso objetivo aqui não é criar um sistema, mas examinar como a OO resolve problemas de forma muito elegante. Quem me dera que fosse simplesmente calcular uma nota e fazer um select para ordenar pela nota capturando do 1 até n, onde n é o número de vagas do curso.

Mas basicamente podemos considerar o seguinte: existiam três tipos de leituras que eram feitas durante o vestibular, a leitura dos cartões de inscrição (naquela época inscrição pela internet não existia), correção das provas propriamente ditas e correção da nota da redação. Uma máquina se encarregava de ler os cartões respostas que gerava então um arquivo TXT (normal), que devia a seguir ser processado pelo software. Obviamente, cada tipo de leitura tinha suas particularidades. Uma ficha de inscrição contém dados do vestibulando, como nome, data de nascimento, opção de curso, língua estrangeira. A resposta de uma prova contém o número de inscrição e as letras marcadas pelo candidato. Mas as leituras possuem muitas, mas muitas coisas em comum. Sem dúvida estava diante de um belo cenário para colocar em prática os ensinamentos do meu professor Marco Cantu, ensinados em suas “bíblias”.

A solução

A Listagem 1 mostra um protótipo do que seria uma possível solução para o problema. Temos uma classe base chamada TCorrecao, que contém todas as funcionalidades básicas de um processamento de leitura. Por exemplo, todas as correções devem carregar um arquivo texto. Todas as correções têm pré-requisitos: para corrigir uma prova, o candidato deve obviamente estar inscrito. Para corrigir uma redação, ele deve ter sido selecionado no ponto de corte. Todas as leituras têm melhores respostas, isso porque uma mesma ficha de um candidato pode ser passada por uma leitora de cartões inúmeras vezes, com sensibilidades diferentes, até que seja obtida a melhor resposta. Por exemplo, a leitura para um candidato de inscrição 1028 poderia vir “1028AB*D”. Na segunda, “1028ABCD”. Ou seja, na segunda vez temos uma leitura mais consistente e devemos fazer o “merge” de ambas.

Os dados após processados precisam ser enviados ao banco de dados, em uma transação atômica, sem chance a erros nem dados inconsistentes. Se a correção for processada novamente, todos os dados anteriores devem ser apagados. Além disso, uma correção precisa, obviamente, ser corrigida. Nesse caso, respostas são comparadas a gabaritos. Em uma inscrição, as opções de língua estrangeira devem ser carregadas antes. Como você pode notar, existem coisas comuns a todas as classes, ou pelo menos, têm o mesmo nome, mas com implementações diferentes. Polimorfismo. Como você pode ver na mesma listagem, as classes descendentes implementam métodos virtuais definidos na classe base. Alguns métodos são totalmente abstratos, eles só estão lá para poderem ser chamados. Nas implementações, feita por cada uma das classes, esses métodos virtuais chamam outros métodos internos (privados) à classe concreta específica. Por exemplo, o Carregar() definido em TCorrecao e sobrescrito em TCorrecaoInscricao vai ser implementado chamando dois métodos privados para carregar línguas estrangeiras e cursos.

E onde entra o banco de dados? Bom, vale ressaltar que naquela época não estávamos muito bem no que diz respeito a acesso a dados. Tínhamos apenas BDE. O banco de dados era o DB2, da IBM, muito bom por sinal. Mas já tínhamos o ClientDataSet! Lembro-me até hoje que a primeira versão da correção levou 4 horas para processar cerca de 4 mil provas. A versão final, usando ClientDataSet’s, levou 3 minutos. Qual o segredo? Tudo era feito em memória: correção, comparação com gabaritos, extração de respostas, merge, cálculo de notas, classificação etc. Quando todos os dados estavam computados em memória, em uma única transação atômica pegávamos os dados dos ClientDataSet’s e jogávamos no banco.

Costumo dizer que todas as regras básicas da OO e Patterns se resumem a apenas duas: separar e esconder. Então, assim como meu form que fazia a interface com o usuário não sabia nada a respeito de correções, as correções também não sabiam muito sobre o BD. Todo o acesso e SQL’s estavam concentrados em DataModule’s (a SQL do listão tinha mais de 500 linhas). Não existe acesso a dados em uma classe de negócio, isso é papel do DM. Outro detalhe que você deve ter reparado, notou que os métodos praticamente não têm parâmetros em suas assinaturas? Isso é esconder. Lembre-se, mudar uma interface, uma regra, no meio do jogo, pode causar um impacto bem incômodo (interfaces são imutáveis). Então, nada melhor do que deixar tudo escondido entre os begin’s e end’s.

No final da listagem podemos encontrar uma implementação bem simples e rudimentar, mas bastante útil, de um padrão de projeto chamado Factory. Basicamente é uma classe que recebe um tipo de correção (enum) e devolve uma classe concreta a quem pediu, o form.

Listagem 1. Classes “core” do processo de correção


        // Esta unit é um protótipo e não contém implementação
        unit uCorrecao;

        interface

        uses Classes, SysUtils;

        type
        ECorrecaoException = class (Exception);

        TCorrecao = class (TObject)
        private
        FArquivo: TStrings;
        FLog: TStrings;
        procedure VerificarIntegridadeArquivo();
        protected
        procedure CarregarInscricoes(); virtual;
        function Merge(Original,Nova : string) : string;
        procedure LimparAnteriores(); virtual; abstract;
        public
        constructor Create;
        destructor Destroy; override;
        procedure SetArquivo(Arquivo: TStrings);
        procedure CarregarArquivoLeitora();
        procedure VerificarPreRequisitos(); virtual;
        procedure ExtrairMelhoresRespostas();
        procedure Carregar(); virtual; abstract;
        procedure Corrigir(); virtual; abstract;
        procedure EnviarDadosParaBanco(); virtual; abstract;
        procedure Finalizar(); virtual;
        procedure AddLog(Mensagem : string);
        function GetLog: string;
        end;

        TCorrecaoInscricao = class(TCorrecao)
        private
        procedure CarregarLinguas();
        procedure CarregarCursos();
        public
        procedure VerificarPreRequisitos(); override;
        procedure Carregar(); override;
        procedure Corrigir(); override;
        procedure EnviarDadosParaBanco(); override;
        procedure Finalizar(); override;
        end;

        TCorrecaoRedacao = class(TCorrecao)
        private
        procedure CalcularNotas();
        procedure Classificar();
        protected
        procedure CarregarInscricoes(); override;
        public
        procedure Carregar(); override;
        procedure Corrigir(); override;
        procedure EnviarDadosParaBanco(); override;
        procedure Finalizar(); override;
        end;

        TCorrecaoProva = class(TCorrecao)
        private
        procedure CarregarInscricoesLingua();
        procedure CarregarGabaritos();
        procedure CalcularPontoDeCorte();
        procedure CalcularNotas();
        procedure SelecionarRedacoes();
        procedure GerarDesempenhos();
        procedure Classificar();
        protected
        procedure CarregarInscricoes(); override;
        public
        procedure Carregar(); override;
        procedure Corrigir(); override;
        procedure EnviarDadosParaBanco(); override;
        procedure Finalizar(); override;
        end;

        TCorrecaoTipo = (Inscricao, Redacao, Prova);

        TCorrecaoFactory = class(TObject)
        class function CreateCorrecao(Tipo: TCorrecaoTipo): TCorrecao;
        end;

        implementation

        { TCorrecao }
        // Aperte Shift+Ctrl+C para gerar os cabeçalhos dos métodos das classes
        // A implementação das correções não é feita neste exemplo

        { TCorrecaoFactory }

        class function TCorrecaoFactory.CreateCorrecao(Tipo: TCorrecaoTipo): TCorrecao;
        begin
        case Tipo of
        Inscricao : Result := TCorrecaoInscricao.Create();
        Redacao : Result := TCorrecaoRedacao.Create();
        Prova : Result := TCorrecaoProva.Create();
        end;
        end;
        

O Assistente

Confesso, a versão beta do sistema era complexa para o usuário. Ele deveria clicar em várias opções de menu para processar uma correção. Primeiro, deveria carregar um arquivo. Depois extrair as respostas. Depois corrigir. Usuários não sabem nem devem saber essas regras muito menos a ordem em que devem ser feitas. O release final reduziu drasticamente esta complexidade. Tudo o que o usuário deveria fazer, presumindo que todos os pré-requisitos estavam cadastrados (como cursos, línguas etc.) era clicar em um botão em um assistente e aguardar o processo ser feito. A Figura 1 mostra o assistente. A Listagem 2 mostra uma ideia de como seria o código deste form.

Veja algumas coisas muito interessantes aqui. O formulário não tem a menor ideia de como uma correção vai ser feita, apesar do usuário poder escolher o tipo. O que fizemos foi declarar um objeto da classe base, TCorrecao, e dentro dele colocar uma implementação concreta, obtida a partir da Factory. A partir daí, o form começa a disparar os métodos polimórficos e a OO cuida do resto. Note, não existe IF’s. Nem RTTI. É tudo polimorfismo e abstração. Para dar um feedback elegante ao usuário, defini um método que recebe como parâmetro um “procedure of object” (ou delegate, para quem já desenvolve em .NET). Este procedimento chama então esse ponteiro, que nada mais é que o método polimórfico da classe base e destaca uma label que indica em que etapa do processamento estamos. Tudo isso protegido com try finally.

Veja que toda exceção gerada nas classes de negócio (correções) vai parar na UI. Jamais damos um feedback ao usuário a partir de uma classe de negócio. Deixamos ela chegar no form e a tratamos. Nesse caso, colocamos em um LOG, que a propósito, é comum a todas as classes de correção. A correção também define um tipo específico de Exception. Isso é ótimo para sabermos, por exemplo, quando uma exceção acontece na correção, se ela foi gerada devido a uma falha no banco, no arquivo etc. ou se ela foi levantada pela própria classe de correção(ou seja, por mim, com raise). Se ao processar uma inscrição eu verificar que faltou um pré-requisito (as línguas estrangeiras não estão cadastradas), levanto uma ECorrecaoException que vai parar no form e dar esse feedback ao usuário (e mais, já perguntar se ele quer abrir o form para cadastrar as línguas já que não o fez). A Figura 2 mostra o demo em execução, dando um feedback ao usuário. Note que simulei o processamento dando um sleep no código.

Assistente para processar correções

Figura 1. Assistente para processar correções

Listagem 2. Código da unit do assistente


        unit uFrmCorrecao;

        interface

        uses
        Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
        Dialogs, WideStrings, StdCtrls, jpeg, ExtCtrls, uCorrecao;

        type

        TAcao = procedure of object;

        TFrmCorrecao = class(TForm)
        Panel1: TPanel;
        Label1: TLabel;
        Button1: TButton;
        Image1: TImage;
        Edit1: TEdit;
        Label2: TLabel;
        Button2: TButton;
        Label3: TLabel;
        Label4: TLabel;
        Label5: TLabel;
        Label6: TLabel;
        Label7: TLabel;
        Label8: TLabel;
        RadioGroup1: TRadioGroup;
        Label9: TLabel;
        procedure Button1Click(Sender: TObject);
        private
        { Private declarations }
        procedure Execute(ALabel: TLabel; Correcao: TCorrecao; Acao: TAcao);
        public
        { Public declarations }
        end;

        var
        FrmCorrecao: TFrmCorrecao;

        implementation

        {$R *.dfm}

        procedure TFrmCorrecao.Button1Click(Sender: TObject);
        var
        Correcao: TCorrecao; ß único ponto de ligação entre form e negócio
        Tipo: TCorrecaoTipo;
        begin
        Tipo := TCorrecaoTipo(RadioGroup1.ItemIndex);
        Correcao := TCorrecaoFactory.CreateCorrecao(Tipo);
        try
        try
        Execute(Label3,Correcao,Correcao.CarregarArquivoLeitora);
        Execute(Label4,Correcao,Correcao.VerificarPreRequisitos);
        Execute(Label5,Correcao,Correcao.ExtrairMelhoresRespostas);
        Execute(Label6,Correcao,Correcao.Carregar);
        Execute(Label7,Correcao,Correcao.Corrigir);
        Execute(Label8,Correcao,Correcao.EnviarDadosParaBanco);
        Execute(Label9,Correcao,Correcao.Finalizar);
        except
        // Exibe log de forma simples
        // sugestão, colocar no final do assistente
        ShowMessage(Correcao.GetLog());
        end;
        finally
        Correcao.Free();
        end;
        end;

        procedure TFrmCorrecao.Execute(ALabel: TLabel; Correcao: TCorrecao; Acao: TAcao);
        begin
        ALabel.font.style := [fsbold];
        Application.ProcessMessages();
        try
        Acao();
        // Simula processamento ...
        Sleep(500);
        ALabel.caption := ALabel.caption+' (OK!)';
        ALabel.font.color := clSilver;
        except
        // aqui você pode tratar se E é ECorrecaoException
        on E : Exception do
        begin
        Correcao.AddLog('Houve um erro enquanto o sistema estava ' + ALabel.Caption);
        Correcao.AddLog(E.Message);
        ALabel.caption := ALabel.Caption + ' (Erro!)';
        ALabel.font.color := clred;
        end;
        raise; // propaga exceção para ser tratada acima
        end;
        end;

        end.
        
Assistente chamando métodos polimórficos e dando feedback

Figura 2. Assistente chamando métodos polimórficos e dando feedback

Conclusão

Como comprovamos aqui, a OO não precisa ser usada com complexidade. Aliás, ela nos ajuda a resolver problemas críticos e complexos, como um sistema de vestibular. O que vimos aqui hoje é algo extremamente simples, mas mostra que mesmo pequenos sistemas podem usar a POO e desfrutar de todos os seus recursos. Eu certamente não imaginaria outra forma de criar um sistema deste tipo e porte (e com tamanha responsabilidade nas costas do desenvolvedor) sem usar a OO. Sem dúvida, o que mostrei aqui não é a única solução possível para o problema, nem tem uma definição / implementação perfeita, mas comprova que o Delphi permite o uso da OO sem perder seu estilo RAD de ser.

Desejo sucesso a todos em seus novos projetos, agora com muita OO. Grande abraço.