De que se trata o artigo: Neste artigo falaremos sobre a importância da modelagem, as atividades gerais do desenvolvimento de software e como utilizar a linguagem UML em cada uma dessas atividades.


Para que serve:
Visa mostrar ao leitor a importância da utilização da linguagem UML no processo de modelagem, bem como as atividades realizadas durante o desenvolvimento de um software.


Em que situação o tema é útil:
A utilização de modelos UML visa uma melhor estruturação do software, bem como uma padronização do processo de modelagem para facilitar a comunicação com o cliente e também entre os membros da equipe de desenvolvimento. UML tem uma importância crucial no desenvolvimento de software, pois ela aspira, entre muitas características, aumentar a qualidade do mesmo.

A modelagem é uma das principais atividades que levam à implementação de um bom software. Construímos modelos para comunicar a estrutura e o comportamento desejados do sistema, visualizar e controlar a arquitetura do mesmo e compreender melhor o sistema que estamos elaborando.

A modelagem de software utiliza vários modelos para projetar um determinado sistema. Um modelo é uma simplificação da realidade, criado para facilitar o entendimento de sistemas complexos. Estes modelos podem abranger planos detalhados, assim como planos mais gerais com uma visão panorâmica do sistema.

Todos os sistemas podem ser descritos sob diferentes aspectos, com a utilização de modelos distintos, onde cada modelo será, portanto, uma abstração específica do sistema. Os modelos podem ser estruturais, dando ênfase à organização do sistema, ou podem ser comportamentais, dando ênfase à dinâmica do sistema.

De acordo com Booch, Rumbaugh e Jacobson [1], há quatro objetivos principais para se criar modelos:

  1. Eles ajudam a visualizar o sistema como ele é ou como desejamos que ele seja;
  2. Eles permitem especificar a estrutura ou o comportamento de um sistema;
  3. Eles proporcionam um guia para a construção do sistema;
  4. Eles documentam as decisões tomadas no projeto.

Através dos modelos, conseguimos obter múltiplas visões do sistema, particionando a complexidade do sistema para facilitar sua compreensão, e atuando como meio de comunicação entre os participantes do projeto. Portanto, uma linguagem de modelagem padronizada, tal como a UML, é fundamental para a construção e o entendimento de bons modelos.

Se você quiser construir grandes softwares, o problema não se restringirá a uma questão de escrever grandes quantidades de código – de fato, o segredo está em elaborar o modelo correto e pensar em como será possível elaborar menos código, com maior confiabilidade e qualidade. Isso faz com que o desenvolvimento de software de qualidade se torne uma questão de arquitetura, processo e ferramentas, reduzindo um pouco a responsabilidade da implementação.

Princípios de modelagem

Segundo Booch, Rumbaugh e Jacobson [1] há quatro princípios de modelagem, os quais são citados a seguir.

A escolha dos modelos a serem criados tem profunda influência sobre a maneira como um determinado problema é atacado e como uma solução é definida.

Em relação aos softwares, a escolha de modelos poderá ser modificada, de maneira significativa, de acordo com a visão de mundo do projetista. Projetistas distintos podem criar modelos bastante variados entre si, dados os mesmos requisitos do software. Um ponto importante é que cada visão de mundo conduz a um tipo diferente de sistema, com custos e benefícios diversos. A visão de mundo, no caso do desenvolvimento de software, refere-se à experiência dos desenvolvedores e às tecnologias que eles conhecem.

Cada visão de mundo pode gerar modelos de sistema diferentes (as tecnologias e técnicas utilizadas nos sistemas podem ser diferentes). O modelo do sistema deve adequar-se não só aos requisitos do mesmo, como também aos conhecimentos da equipe de desenvolvimento. Criar projetos que utilizem tecnologias desconhecidas pela equipe pode atrapalhar o andamento do projeto, pois os desenvolvedores teriam que aprender essas novas tecnologias e como utilizar seus recursos.

Há várias técnicas de modelagem existentes e cada uma delas é adequada para um determinado problema. Uma técnica pode ser mais eficiente que outra em um caso, o que não significa que ela será a melhor em todos os casos. Dessa forma, saber qual técnica utilizar num determinado momento é um fator crucial para o sucesso do desenvolvimento de software de boa qualidade.

Cada modelo poderá ser expresso em diferentes níveis de precisão.

Os melhores tipos de modelos são aqueles que permitem a escolha do grau de detalhamento das informações, dependendo de quem esteja fazendo a visualização e por que deseja fazê-la. O diagrama de casos de uso, que será discutido nos próximos artigos dessa série, permite escolher o grau de detalhes que será mostrado. Com esse diagrama, podemos ilustrar as funcionalidades do software numa visão de alto nível. E a partir de cada caso de uso elaborado, podemos subdividi-lo em outros casos de uso mais específicos, o que aumenta o nível de precisão do diagrama.

Um usuário final dirigirá a atenção para questões referentes ao que será visualizado pela interface gráfica do sistema e para as funcionalidades do mesmo, enquanto o desenvolvedor moverá o foco para a maneira como o software deve funcionar.

Os melhores modelos estão relacionados à realidade.

Todos os modelos simplificam a realidade. Por isso, certifique-se de que sua simplificação não ocultará detalhes importantes.

De acordo com Booch, Rumbaugh e Jacobson [1], o tendão de Aquiles das técnicas de análise estruturada está no fato de não haver um relacionamento entre o modelo de análise e o modelo de projeto do sistema. Dessa forma, ao longo do tempo, aparecerá uma divergência entre o sistema concebido e o sistema projetado. Nos sistemas orientados a objetos, é possível estabelecer uma ligação entre várias partes do sistema, o que facilita bastante o desenvolvimento de sistemas complexos e o bom entendimento dos mesmos.

Nenhum modelo único é suficiente. Qualquer sistema não trivial será melhor investigado por meio de um pequeno conjunto de modelos quase independentes.

Nesse contexto, a expressão “quase independentes” significa modelos que possam ser criados e estudados separadamente, mas que continuam inter-relacionados. Podemos criar vários modelos distintos a partir de um modelo qualquer. Por exemplo, suponha que tenhamos criado um diagrama de casos de uso. A partir dele, podemos criar diagramas de atividades, de classes, de sequência, etc. Cada diagrama pode ser estudado independentemente, mas eles continuam inter-relacionados.

Segundo Booch, Rumbaugh e Jacobson [1], para compreender a arquitetura de sistemas é necessário recorrer a várias visões complementares e inter-relacionadas, tais como: a visão dos casos de uso (expondo os requisitos do sistema); a visão de projeto (capturando o vocabulário do problema a ser resolvido); a visão do processo (modelando a distribuição dos processos e das threads do sistema); e a visão da implementação (com o foco voltado para questões de engenharia de sistemas). Cada uma dessas visões poderá conter aspectos estruturais como também aspectos comportamentais. Em conjunto, elas representam a base do projeto de software.

Introdução à linguagem UML

A UML (Unified Modeling Language) foi desenvolvida por Grady Booch, James Rumbaugh e Ivar Jacobson. Cada um deles possuía sua própria sistemática de criar modelos e a UML é a junção dos pontos fortes dessas três abordagens, adicionando novos conceitos e visões de linguagem.

Antes da elaboração da Unified Modeling Language, existiam diversos padrões para se criar modelos de software, o que dificultava a elaboração de software por equipes que utilizassem padrões diferentes. Esta linguagem é uma maneira de padronizar a modelagem orientada a objetos de forma que qualquer sistema possa ser modelado da maneira apropriada, simples de ser atualizado e compreendido. UML é uma linguagem muito expressiva, abrangendo todas as visões necessárias ao desenvolvimento e implantação de sistemas de software em geral.

UML é uma linguagem padrão para a elaboração da arquitetura de projetos de software. Ela pode ser empregada para visualização, especificação, construção e documentação de artefatos de software.

Ela aborda o caráter estático e dinâmico do sistema a ser analisado. Além disso, UML leva em consideração, já no período de modelagem, todas as futuras características do sistema. Tais características estão relacionadas a trocas de mensagens entre as diversas partes do sistema, o padrão arquitetural adotado e os padrões de projeto utilizados no mesmo.

A linguagem UML é usada no desenvolvimento dos mais diversos sistemas, variando desde sistemas de pequeno porte, tais como um sistema de comércio eletrônico para uma livraria, a sistemas de grande porte, como um sistema de transações bancárias. Ela pode abranger várias características de um sistema em um de seus diagramas, sendo aplicada nas diferentes atividades do desenvolvimento de software, desde a especificação de requisitos até a implementação e os testes.

Atividades gerais do desenvolvimento de software

Desenvolver software envolve diversas atividades que são realizadas em momentos distintos, de acordo com o processo de desenvolvimento de software utilizado. A seguir, destacaremos seis dessas atividades – a especificação de requisitos, a análise, o projeto, programação, testes e implantação, bem como a importância da UML para as suas realizações.

Especificação de requisitos

A atividade de especificação de requisitos envolve alta comunicação e colaboração com o cliente e todos os outros interessados no sistema, conhecidos como stakeholders, e abrange o levantamento de requisitos e outras atividades relacionadas, como por exemplo, a validação e a gestão de requisitos.

Segundo McLaughlin, Pollice e West [5], um requisito é uma necessidade única que detalha o que um produto ou serviço em particular deve ser ou fazer. É mais comumente utilizado na engenharia de sistemas ou engenharia de software.

Esta atividade captura as intenções e necessidades dos usuários do sistema a ser desenvolvido através do uso de modelos conhecidos como casos de uso. Cada caso de uso modelado é descrito através de um texto e/ou diagrama, os quais especificam os requerimentos do ator externo (usuário ou outro sistema) que o utilizará.

O diagrama de casos de uso mostrará o que os usuários finais deverão esperar do aplicativo, conhecendo toda sua funcionalidade sem se importar como ela será implementada. Os diagramas de casos de uso são uma forma de representar os requisitos funcionais de um sistema em um alto nível de abstração, sem considerar os objetos que fazem parte do sistema, a estrutura de classes e detalhes de implementação.

Eles indicam um caminho para a análise e compreensão do sistema como um todo sem a preocupação com o seu funcionamento interno. Esses diagramas criam uma forma clara e simples de comunicação e especificação entre usuários (pessoas que utilizarão o sistema) e membros da equipe de desenvolvimento.

Segundo Cockburn [6], um caso de uso se refere a um contrato que descreve o comportamento do sistema sob várias condições em que o sistema responde a uma solicitação de um dos seus interessados. Em essência, um caso de uso conta uma história sobre a maneira como um usuário final interage com o sistema sob um conjunto específico de circunstâncias. A história pode ser um texto narrativo, um delineamento das tarefas e/ou interações ou uma representação em diagrama. Independentemente de sua forma, um caso de uso descreve o sistema do ponto de vista do usuário.

Muitos problemas e falhas podem surgir durante a especificação dos requisitos em virtude das dificuldades desta fase inicial do desenvolvimento. Incertezas e erros podem ocorrer por causa da má comunicação ou falta de especialistas do domínio do problema em questão.

Além disso, a linguagem natural não é precisa, o que pode acarretar interpretações conflitantes. Há ainda a própria dificuldade do usuário em explicitar de forma completa, clara e estável seus requisitos. O uso de uma notação precisa que atue como base para evolução, documentação e comunicação, facilita a organização, o entendimento e a visualização padronizada do problema. O uso de modelos consistentes desde esta etapa, portanto, mostra-se determinante para evitar a propagação de erros para as atividades posteriores e assim diminuir os esforços para corrigi-los.

Segundo Booch [4], nossa habilidade em imaginar novas aplicações complexas sempre será superior à nossa habilidade de desenvolvê-las, e a construção de coisas erradas é um dos motivos da maioria das falhas dos projetos de software.

Análise

A atividade de análise, também conhecida como planejamento, estabelece um plano para o trabalho de engenharia de software que se segue. Ela descreve os riscos prováveis, os recursos que serão necessários, os produtos de trabalho a serem produzidos (documentações e diagramas), um cronograma do trabalho e as tarefas a serem conduzidas, tais como o refinamento dos casos de uso.

O objetivo da análise de requisitos é a compreensão e representação da informação sobre o domínio da aplicação, funcionalidades e comportamentos esperados e a construção de modelos que particionem estas informações de forma a constituir uma especificação básica do sistema sendo desenvolvido.

Esta atividade está preocupada com as primeiras abstrações (classes e objetos) e mecanismos que estarão presentes no domínio da aplicação. Tais mecanismos podem ser entendidos como algo intrínseco ao problema. Para facilitar o entendimento, os mecanismos do domínio da aplicação serão explicados tomando como exemplo um sistema de frenagem de um metrô.

Se estivermos desenvolvendo um sistema de frenagem de um metrô, teremos que estudar o domínio do problema para criar uma arquitetura que atenda aos requisitos específicos do metrô. Independente do processo de software que seja adotado, o metrô está suscetível a algumas regras físicas que devem ser entendidas.

Sabendo que o sistema de frenagem deve impor uma aceleração constante ao metrô, o sistema deve ser capaz de medir a velocidade atual do metrô e a distância que falta até o ponto de chegada. Com essas duas informações, ele será capaz de calcular a aceleração necessária para fazer o metrô parar no ponto de chegada. Nesse exemplo, a velocidade, a aceleração e a distância são variáveis presentes no domínio da aplicação, enquanto que o sistema de frenagem utiliza um mecanismo do domínio da aplicação (o cálculo da aceleração de acordo com a velocidade atual e a distância até o ponto de chegada).

Durante a atividade de análise, as classes são modeladas e ligadas através de relacionamentos com outras classes, e são descritas no Diagrama de Classes. Na análise só serão modeladas classes que pertençam ao domínio principal do software, ou seja, classes que gerenciam bancos de dados, interface, comunicação com outros sistemas, concorrência, entre outros, não estarão presentes nesta etapa.

Projeto

A atividade de projeto, também conhecida por atividade de modelagem, inclui a criação de modelos que permitam ao desenvolvedor e ao cliente entender melhor os requisitos do software e o projeto que vai satisfazer a esses requisitos. O objetivo principal da atividade de projeto é a construção de modelos que acomodem especificações sobre a estruturação dos dados e arquitetura do sistema, além de detalhes de implementação e de interface, relacionados diretamente às funcionalidades e comportamentos esperados.

Segundo Pressman [3], análise e projeto representam as atividades mais importantes dos processos de desenvolvimento de software. O entendimento dos requisitos é essencial para o sucesso do desenvolvimento, provendo a especificação e os modelos do sistema de forma clara e precisa, enquanto um bom projeto pode ser uma das bases para a qualidade do software em desenvolvimento.

Nesta atividade, o resultado da análise é expandido em soluções técnicas. Novas classes serão adicionadas para prover uma arquitetura, como interface gráfica de usuários (GUI), gerenciamento de banco de dados e a comunicação com outros sistemas. As classes de domínio do problema, modeladas na atividade de análise, são utilizadas na etapa de projeto para criar uma arquitetura que atenda aos requisitos do sistema.

No exemplo do metrô, vimos algumas variáveis da aplicação, tais como a velocidade e a distância. Com elas é possível montar uma arquitetura para desenvolver o sistema de frenagem do metrô. Vimos um caso simples, mas e se houver atrito entre o metrô e a pista? E se houver resistência do ar? E se a trajetória for uma curva?

Para resolver esses novos problemas, teremos que entender um pouco mais do domínio da aplicação. Se houver resistência do ar ou atrito, a maneira de se calcular a aceleração mudaria, mas a arquitetura do sistema seria a mesma. Nesse caso, apenas a implementação dos métodos mudaria, o que não acarretaria mudança na arquitetura do software.

Caso a trajetória seja curvilínea, seria necessário calcular a velocidade máxima com que o metrô poderia trafegar num determinado trecho para que não haja descarrilamento. Assim, teríamos que alterar a arquitetura do sistema para que ele seja capaz de calcular essa velocidade e manter o metrô com velocidade abaixo dela.

Além de poder calcular essa velocidade, teríamos que garantir que a velocidade do metrô esteja sempre abaixo do valor limite. Em outras palavras, se o maquinista acionar um evento para acelerar o metrô e este possuir uma velocidade muito próxima da velocidade máxima, o evento precisa ser ignorado para garantir que não haja o descarrilamento do metrô. Nesse caso, teríamos uma nova restrição para o sistema, o que acarretaria numa necessidade de mudança da sua arquitetura.

Durante o Projeto, os projetistas devem ser capazes de analisar qual arquitetura, padrões de projeto e metodologias a serem utilizados para se construir um software de qualidade. As respostas a essas variáveis dependem da experiência dos projetistas sobre o domínio da aplicação e do conhecimento da equipe de desenvolvimento sobre as tecnologias e técnicas a serem utilizadas.

Como podemos notar, a atividade de projeto resulta no detalhamento das especificações para a atividade de programação do sistema. Há várias metodologias para se criar projetos consistentes, por exemplo, Scrum[1], Extreme Programming, o modelo cascata, os modelos evolucionários, entre muitas outras.

Um desenvolvimento de software organizado tem como premissa uma metodologia de trabalho. Esta deve ter como base conceitos que visem a construção de um software de forma eficaz e os passos necessários para se chegar ao produto final esperado.

Assim, quando se segue uma metodologia para o desenvolvimento de software, espera-se desenvolver um sistema que satisfaça todos os requisitos dos clientes de forma eficiente. Observando este aspecto, não faz sentido iniciar a construção de um software sem ter uma metodologia de trabalho bem definida e que seja do conhecimento de todos os envolvidos no processo. Porém, além de uma crescente demanda por softwares de qualidade, as empresas de desenvolvimento de software sofrem cada vez mais pressão por parte dos clientes para que o produto seja entregue com prazos menores. Este fato pode fazer com que uma metodologia de trabalho que já foi utilizada com sucesso em diferentes projetos não alcance o resultado esperado.

Programação

Na atividade de programação, as classes provenientes do projeto são implementadas na linguagem escolhida. Dependendo dos recursos providos pela linguagem utilizada, essa implementação pode ser uma tarefa fácil ou bastante complicada.

Nas atividades anteriores, os modelos criados são o significado do entendimento da estrutura do sistema, enquanto que na atividade de programação os modelos criados são convertidos em código.

Linguagens que não utilizam o paradigma orientado a objetos ou orientado a aspectos não podem usufruir de todos os diagramas disponíveis pela UML. Por exemplo, se estivermos utilizando uma linguagem procedural para desenvolvermos um sistema, não poderemos utilizar o diagrama de sequência para modelar seu comportamento. Nesse caso, teríamos que utilizar diagramas de fluxo de dados para modelar o comportamento do sistema.

A linguagem Java implementa diversos recursos da orientação a objetos, os quais serão vistos nos próximos artigos dessa série. Veremos como entender um diagrama UML e, a partir dele, como criar o código Java correspondente.

Testes

Segundo Dijkstra, “testes podem mostrar a presença de defeitos, mas nunca a sua ausência”. De acordo com Pressman [3], aproximadamente 40% dos custos de software são de teste.

Um sistema geralmente é testado a partir dos testes de unidade, de integração, de aceitação e de sistema. Os testes de unidade são para classes individuais ou grupos de classes. Eles verificam se cada módulo funciona apropriadamente. Os testes de integração são aplicados para verificar se as classes estão cooperando umas com as outras como especificado nos modelos. Os testes de aceitação servem para assegurar que o software satisfaz os requisitos do usuário. Os testes de sistema verificam se a integração do software com outros sistemas é feita de forma apropriada.

Há dois tipos de teste principais: funcional ou caixa branca; estrutural ou caixa preta. O teste de caixa branca baseia-se apenas nos requisitos. Ele verifica se o comportamento do sistema é compatível com os requisitos do mesmo. O teste de caixa preta baseia-se no código-fonte, ou seja, os casos de teste são criados a partir da análise do código-fonte.

A importância dos testes para a qualidade do software é tão expressiva que até mesmo uma metodologia de desenvolvimento de software baseada em testes foi criada, TDD ou Test Driven Development.

Implantação

O software (como entidade completa ou incremento parcialmente completo) é entregue ao cliente, que avalia o produto e fornece um feedback com base na avaliação. É nessa atividade que o cliente consegue fazer uso do software propriamente dito e avaliar o nível de qualidade do mesmo.

Neste momento o cliente pode solicitar novas funcionalidades para o sistema, encontrar alguns defeitos no mesmo e informar pontos que podem ser melhorados para aprimorar a usabilidade da aplicação. Por isso, o feedback do cliente é de suma importância para a equipe de desenvolvimento.

Desenvolvimento de software e orientação a objetos

A orientação a objetos está intimamente ligada ao desenvolvimento de software. Segundo McLaughlin, Pollice e West [5], devemos seguir algumas etapas durante o desenvolvimento de software:

  1. Verifique se o seu software faz o que o cliente deseja que ele faça;
  2. Aplique os princípios básicos da orientação a objetos para adicionar flexibilidade;
  3. Empenhe-se para ter um projeto reutilizável e que possa ser mantido.

A análise e projeto orientados a objetos têm como meta identificar o melhor conjunto de objetos para descrever um sistema de software. O funcionamento deste sistema se dá através do relacionamento e troca de mensagens entre estes objetos.

Segundo Pressman [3], uma série de conceitos determina a qualidade de um projeto, como coesão, acoplamento, abstração, particionamento hierárquico, entre outros. Esses conceitos serão vistos nos próximos artigos dessa série. O uso de modelos ruins pode comprometer a codificação, a flexibilidade e a manutenção do sistema.

A orientação a objetos apresenta-se favorável à modularização e reutilização de componentes, facilita a transição entre análise e projeto, exibe melhores resultados em qualidade, produtividade e apresenta-se mais flexível a mudanças e adaptações. A análise e projeto orientados a objetos são voltados para a construção de modelos melhores, mais próximos da realidade através do uso dos principais conceitos da orientação a objetos, como abstração, encapsulamento, herança e polimorfismo, criando um vocabulário e entendimento comuns entre os usuários do sistema e os desenvolvedores.

Conclusões

Vimos a importância da UML, os princípios de modelagem e como a UML pode ser utilizada nas atividades de desenvolvimento de software.

A orientação a objetos está intimamente ligada ao desenvolvimento de software, por isso também vimos alguns dos pontos fortes desse paradigma, enfatizando a flexibilidade e a manutenção do sistema, a modularização e a reutilização de componentes.


Livros

  • [1] BOOCH, Grady; RUMBAUGH, James; JACOBSON, Ivar. UML: guia do usuário. 2ª ed. Rio de Janeiro: Campus, 2005.
  • [2] SIERRA, Kathy; BATES, Bert. Certificação Sun para programador Java 6: guia de estudo. 1ª ed. São Paulo: Alta Books, 2008.
  • [3] PRESSMAN, Roger. Engenharia de Software. 6ª ed. São Paulo: Mc Graw Hill, 2006.
  • [4] BOOCH, Grady. Object Solutions. Managing the Object-Oriented Project. Addison-Wesley, 1996.
  • [5] MCLAUGHLIN, Brett; POLLICE, Gary; WEST, David. Use a cabeça – Análise e projeto orientado ao objeto. Rio de Janeiro: Alta Books, 2007.
  • [6] COCKBURN, Alistair. Writing Effective Use-Cases. Addison-Wesley, 2001.

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


Modelagem de software com UML – Parte 2

Do que se trata o artigo: O artigo é a segunda parte de uma série de artigos sobre modelagem de sistemas orientados a objetos (OO) com a UML (Unified Modeling Language). Nesta parte da série, é abordado o diagrama de classes, um dos diagramas mais utilizados desta linguagem de modelagem.


Para que serve:
O diagrama de classes da UML serve para representar a parte estática de um sistema: as classes existentes e seus atributos, métodos e relacionamentos com outras classes. Tal diagrama pode ser utilizado em deferentes fases do desenvolvimento de software (análise de requisitos, projeto de software, etc.) para representar características do sistema em diferentes níveis de abstração.


Em que situação o tema é útil:
O tema é útil para qualquer desenvolvedor de software que tenha que escrever ou ler especificações de requisitos, projetos de software ou modelos de sistema em geral, desde que a UML – mais especificamente o diagrama de classes – tenha sido utilizado. A grande maioria dos modelos de sistemas construídos no paradigma OO utiliza a UML e o seu diagrama de classes.

Resumo DevMan: A UML especifica diversos diagramas que podem ser utilizados para modelagem de sistemas OO, cada um adequado para uma ou mais situações específicas. O diagrama de classes da UML é um destes, cujo propósito é apresentar as estruturas estáticas de um sistema: classes, seus atributos, métodos e relacionamentos. Neste artigo, apresentamos o diagrama de classes da UML, explicando como modelar as diferentes características de um sistema utilizando sua sintaxe e como interpretá-los posteriormente, implementando em Java as classes que nele forem modeladas. Começamos com os fundamentos básicos (classes, atributos e métodos – Figura 1 e Listagem 1), passando em seguida a falar de associação/delegação entre classes (Figura 2, Listagens 2 a 4) e herança (Figura 3 e Listagem 5), abordando especificamente a sobrescrita de métodos (Listagem 6), a herança múltipla (Figura 4 e Listagem 7) e o polimorfismo (Listagem 8). Apresentados os conceitos de delegação e herança, discutimos quando utilizar um ou outro e finalizamos o artigo apresentando a relação de dependência entre classes (Figura 5).

A UML provê diversos diagramas para modelar sistemas de software. Cada um desses diagramas é adequado para uma determinada situação e visão de software. O livro “UML: guia do usuário” [1], aborda todos os diagramas UML de forma didática e exemplificada. Ele é uma boa referência para o leitor que deseja se aprofundar no estudo de modelagem de software.

Neste artigo o foco será o diagrama de classes da UML, que é um dos diagramas mais importantes da UML. Ele apresenta as estruturas estáticas do sistema, que são as classes, os relacionamentos, atributos e métodos de cada classe. Ele pode ser utilizado tanto na fase de análise dos requisitos (representando conceitos do mundo real relevantes ao sistema) quanto na fase de projeto (modelando artefatos de código que serão implementados em alguma linguagem de programação do modo como estão representados no diagrama).

Nas seções a seguir, apresentaremos suas características, demonstrando como modelar neste diagrama os conceitos relacionados aos sistemas que desenvolvemos e, em seguida, como implementar o que modelamos no diagrama de classes utilizando a linguagem Java.

Fundamentos: Classes, Atributos e Métodos

O elemento fundamental do diagrama de classes é, obviamente, a classe. Uma classe é representada por um retângulo dividido verticalmente em três seções que contêm, respectivamente, o nome, os atributos e os métodos dessa classe. A Figura 1 mostra como representar classes em UML.

Representação de classes e interfaces em UML

Figura 1. Representação de classes e interfaces em UML.

No diagrama da Figura 1 foi modelada a classe Usuario, que possivelmente representa usuários do sistema em questão. Usuario é uma classe concreta, ou seja, pode ter instâncias diretas. Caso se queira representar uma classe abstrata (em Java utiliza-se as palavras-chave abstract class), deve-se colocar o nome da classe em itálico ou associá-la ao estereótipo <<abstract>>, colocando-o acima do nome da mesma. Estereótipos são utilizados também para declarar interfaces, as quais serão abordadas mais adiante.

Estereótipos: Um estereótipo é um dos mecanismos de extensão da UML, que permite que um desenvolvedor crie uma nova categoria de elementos – no caso deste artigo, classes, porém estereótipos podem ser aplicados a vários outros elementos da UML.

A nova categoria pode definir propriedades ou maneiras de utilização específicas, geralmente ligadas a um domínio em particular, que são associadas automaticamente ao elemento que recebe o estereótipo. Por exemplo, as extensões da UML para aplicações Web propostas por Conallen [5] incluem estereótipos relacionados com a plataforma Web, como <<screen>> para telas, <<input form>> para formulários, e assim por diante.

Estereótipos podem ter um ícone ou representação gráfica associada. Nestes casos, é possível representar que um elemento pertence a um determinado estereótipo utilizando tal ícone ou representação gráfica, além da forma padrão que adiciona o nome do estereótipo entre os símbolos “<<” e “>>” acima do nome do elemento.

Métodos e atributos possuem modificadores de acesso que definem a visibilidade destes elementos por parte de outras classes do sistema. Tais modificadores são representados no diagrama pelos símbolos ~, +, , e #, que devem ser colocados antes dos nomes dos métodos/atributos. Estes quatro símbolos representam os seguintes modificadores na linguagem Java:

  • Default ou package-private (privativo ao pacote): representado pelo símbolo ~ (til) e conhecido como package em UML. Não há palavra-chave associada em Java e é utilizado quando o modificador não é especificado. Ex.: o atributo CPF da classe Usuario;
  • private (privado): representado pelo símbolo (menos). Ex.: o atributo nome;
  • protected (protegido): representado pelo símbolo # (cerquilha). Ex.: o atributo idade;
  • public (público): representado pelo símbolo + (mais). Ex.: o atributo RG.

Existem algumas características dos modificadores de acesso que devem estar em mente ao se criar um modelo. Os membros (atributos ou métodos) marcados como privado só podem ser acessados por métodos da mesma classe. Dessa forma, para outra classe acessar um membro privado é necessário invocar um método não privado declarado na mesma classe que chame esse membro internamente. Utiliza-se o modificador private para restringir o acesso desse membro por outras classes.

Confira também

Neste ponto já é possível ver alguns dos benefícios do paradigma OO citados na primeira parte desta série de artigos: se uma classe A só pode manipular os atributos de uma classe B através dos métodos de B, então dizemos que os atributos de B estão encapsulados. E se a classe A só possui conhecimento dos métodos da classe B e não conhece os detalhes de sua implementação, então dizemos que A e B são fracamente acopladas.

Os modificadores de acesso protected e default possuem características semelhantes, mas com uma diferença fundamental. Um membro marcado como default só pode ser acessado por uma classe que pertença ao mesmo pacote, enquanto que um membro protegido, além de possuir essa característica, pode ser acessado (através da herança, que será abordada mais à frente) por uma subclasse, mesmo que essa subclasse pertença a um pacote diferente.

O modificador public não restringe acesso a nenhuma classe e é mais comumente empregado para métodos. O uso de modificadores public em atributos faz com que qualquer classe possa acessar tais atributos diretamente, o que contraria o princípio de encapsulamento trazida pelo paradigma orientado a objetos. Os modificadores de acesso, assim como outros tipos de modificadores, são amplamente explicados por Kathy Sierra e Bert Bates [2].

Além do modificador de acesso, um método pode ainda ter escopo de classe (declarado como static em Java) ou ser abstrato (abstract em Java). No primeiro caso, o método deve ser sublinhado (ex.: o método efetuarLogin() da classe Usuario), enquanto no segundo, assim como em classes abstratas, ele deve ter seu nome escrito em itálico (ex.: metodoAbstrato() de ClasseAbstrata01).

Para exemplificar, a Listagem 1 traz a implementação em Java das classes Usuario e ClasseAbstrata01, demonstrando como implementar os conceitos explicados acima.

Listagem 1. Alguns elementos da Figura 1 implementados em Java.


class Usuario {
   private String nome;
   protected int idade;
   public String RG;
   String CPF;
   
   public void pesquisar() {
    /* Implementação aqui... */
   }
   
   public static void efetuarLogin(String usuario, String senha) {
    /* Implementação aqui... */
   }
  }
   
  abstract class ClasseAbstrata01 {
   public abstract void metodoAbstrato();
  } 

Interfaces

Outra estrutura interessante é a interface (em Java: interface), a qual possui uma representação semelhante a uma classe. A Figura 1 mostra a representação de interfaces, que podem ser modeladas como classes com a adição do estereótipo <<interface>> ou por meio de sua representação gráfica particular.

Segundo Kathy Sierra e Bert Bates [2], há algumas regras para a criação de interfaces em Java. São elas:

  • Todos os métodos de interface são implicitamente public e abstract. O programador não precisa declarar os métodos de interface como public e/ou abstract, pois o compilador faz isso na hora da compilação;
  • Todas as variáveis definidas em uma interface devem ser public, static e final, em outras palavras, as interfaces só podem declarar constantes e não variáveis de instância;
  • Os métodos de interface não podem ser static;
  • Já que os métodos de interface são abstract, não podem ser marcados com final, native, strictfp ou synchronized;
  • Uma interface pode estender uma ou mais interfaces;
  • Uma interface não pode estender nada que não seja outra interface;
  • Uma interface não pode implementar outra interface ou classe;
  • Uma interface deve ser declarada com a palavra-chave interface;
  • As interfaces podem ser usadas polimorficamente. O polimorfismo será explicado mais à frente.

É interessante notar que a “herança” entre uma classe e uma interface (i.e., o fato de uma classe implementar todos os métodos de uma interface) em Java é denominada implementação de interface, enquanto em UML denomina-se realização de interface. O conceito de herança será abordado mais à frente no artigo.

Associação entre Classes

A associação representa um relacionamento entre as classes e/ou interfaces do sistema. Ela é modelada como um segmento ligando dois componentes (classes ou interfaces). Nesse tipo de relacionamento, há objetos e/ou coleções de objetos de uma classe sendo atributo(s) de outra classe. A Figura 2 representa diversas associações entre as classes Equipe, Funcionario, Carro e Motor.

Exemplos de associações entre classes em UML

Figura 2. Exemplos de associações entre classes em UML.

A figura ilustra várias características das associações. Uma equipe é responsável pela construção de um carro, portanto ligamos as classes Equipe e Carro com uma associação. Nas extremidades das associações, podemos determinar a multiplicidade ou cardinalidade das mesmas, indicando o mínimo e o máximo de cada ponta. Quando somente um valor é indicado, ele representa tanto o mínimo quanto o máximo. Além disso, um asterisco (*) ou a letra n representam “muitos” ou “vários” (ou seja, um valor indeterminado) e quando aparecem sozinhos indicam um mínimo de zero (ou seja, * é equivalente a 0..*). No exemplo, uma equipe pode ser responsável por nenhum ou por vários carros, enquanto cada carro deve ter uma e somente uma equipe responsável.

Ainda usando a associação entre Equipe e Carro como exemplo, extremidades de associações podem ter também um nome, acompanhado de um modificador de acesso, o que vai indicar como o atributo que representa a associação deve ser implementado. A Listagem 2 mostra parte da implementação Java das classes modeladas na Figura 2. Podemos ver que as propriedades Equipe.subEquipes e Carro.equipeResponsavel receberam os nomes especificados nas extremidades, enquanto os demais atributos possuem simplesmente o nome da classe associada com a primeira letra em minúsculo (convenção que pode ou não ser adotada pelos desenvolvedores). Além disso, subEquipes recebe o modificador de acesso protected, já que possui o símbolo # no modelo.

Listagem 2. Implementação Java das associações modeladas na Figura 2.


class Equipe {
   private Set<Funcionario> funcionarios;
   private Equipe equipe;
   protected Equipe[] subEquipes;
   private List<Carro> carros;
  }
   
  class Funcionario {
   private Set<Equipe> equipes;
  }
   
  class Carro {
   private Equipe equipeResponsavel;
   private Motor motor;
  }
   
  class Motor {
   private Equipe equipe;
   private Carro carro;
  } 

Podemos ver também na listagem que atributos que representam extremidades de cardinalidade máxima 1 possuem como tipo a classe associada, enquanto os que possuem multiplicidade N devem ser implementados como vetores, conjuntos, listas ou qualquer outro tipo de coleção. Além disso, associações podem ter nomes para facilitar a leitura do modelo (no exemplo, uma equipe é formada por funcionários), porém isso não afeta a forma que implementamos a associação. É possível também especificar associações unidirecionais, indicando que apenas uma das extremidades da mesma é navegável. Na Figura 2, a associação entre Motor e Equipe é navegável apenas no sentido de Equipe e isso afeta diretamente a implementação, já que a classe Motor tem um atributo equipe, porém Equipe não possui um atributo motor. Quando não especificadas as navegabilidades, associações são bidirecionais.

Por fim, a Figura 2 mostra também dois tipos especiais de associação: a agregação (representada por um losango branco) e a composição (losango preto). Tais associações representam relacionamentos todo-parte, sendo que o losango indica a extremidade do todo. No exemplo, funcionários e subequipes fazem parte de uma equipe e um motor é parte de um carro. A diferença entre a agregação e a composição é que esta última representa uma dependência mais forte do ciclo de vida entre a parte e o todo. Em outras palavras, de maneira geral as partes “vivem e morrem” com o todo: se um carro é destruído, seu motor também é, porém se uma equipe é dissolvida, seus funcionários podem ser realocados. Nada impede, porém, que uma parte (ex.: um motor) seja retirada do todo (ex.: carro) e associada a uma outra instância antes que este seja excluído. Resumindo, uma composição modela uma associação todo-parte entre classes que são fortemente acopladas.

Ao implementar classes a partir de um diagrama, é importante prestar bastante atenção nas restrições impostas pelas cardinalidades (mínimas e máximas) das extremidades das associações e pelo fato de serem do tipo agregação ou composição. A Listagem 3 mostra um exemplo de implementação da classe Equipe que garante as seguintes restrições presentes no modelo:

  • Uma equipe deve ter ao menos um funcionário, portanto para criar uma equipe é necessário sempre informar um funcionário inicial. Posteriormente, algum método adicionarFuncionario() (não implementado na listagem) pode ser utilizado para adicionar mais membros na equipe;
  • Pelo mesmo motivo (cardinalidade mínima 1), ao remover um funcionário é preciso verificar se a equipe não ficará vazia. Neste caso, uma exceção (estado ilegal) é lançada;
  • Equipe possui uma associação do tipo composição com si própria, representando subequipes. Um construtor garante que uma subequipe será sempre associada à sua superequipe e, caso esta última seja dissolvida, todas as suas subequipes também são dissolvidas em cascata, como na implementação do método dissolver().

Listagem 3. Garantindo as restrições modeladas no diagrama de classes em Java.


import java.util.*;
   
  class Equipe {
   private Set<Funcionario> funcionarios = new HashSet<Funcionario>();
   private Equipe equipe;
   protected Set<Equipe> subEquipes = new HashSet<Equipe>();
   private List<Carro> carros;
   
   Equipe(Funcionario func) {
    funcionarios.add(func);
   }
   
   Equipe(Equipe superEquipe, Funcionario func) {
    funcionarios.add(func);
    equipe = superEquipe;
   }
   
   void removerFuncionario(Funcionario func) {
    if (funcionarios.contains(func)) {
     if (funcionarios.size() <= 1) 
      throw new IllegalStateException("Uma equipe deve ter ao menos um funcionário!");
    }
    else throw new IllegalArgumentException("O funcionario " + func + " não pertence à equipe!");
   }
   
   void criarSubEquipe(Funcionario func) {
    Equipe subEquipe = new Equipe(this, func);
    subEquipes.add(subEquipe);            
   }
   
   void dissolver() {
    for (Equipe equipe : subEquipes) equipe.dissolver();
    /* Código de negócio referente à dissolução de uma equipe. */
   }
  } 

Há outra maneira de se implementar a composição, que é através do conceito de inner class (classe interna). Esse conceito nos diz que uma classe interna só pode ser acessada através da classe que a contém. A Listagem 4 nos mostra um exemplo de como usar inner classes para implementar o mecanismo de composição.

Listagem 4. Usando inner class para implementar o mecanismo de composição.


public class MinhaClasse{
     private ClasseInterna classeInterna;
     public MinhaClasse(){
        classeInterna = new ClasseInterna();
     }
     public void imprimir(){
        classeInterna.imprimir();
     }
     private class ClasseInterna{
        public void imprimir(){
           System.out.println("Imprimindo em: " + this);
        }
     }
  }
   
  public class TesteMinhaClasse {
     public static void main(String[] args){
        MinhaClasse minhaClasse = new MinhaClasse();
        minhaClasse.imprimir();
     }
  } 

Generalização/Especialização (Herança)

A generalização e a especialização, também conhecidas como herança, são mecanismos muito importantes para a modelagem de software, pois possibilitam a reutilização de código. A generalização/especialização está associada ao relacionamento É-UM, ao contrário da associação, que acaba de ser vista na seção anterior, que representa um relacionamento TEM-UM.

O relacionamento É-UM é baseado na herança de classes ou implementação de interfaces e é expresso em Java por meio das palavras-chave extends (para herança de classes) e implements (para realização/implementação de interfaces). A Figura 3 mostra o mecanismo de herança entre classes no diagrama UML, enquanto a Listagem 5 demonstra como implementá-los em Java.

Representando realização de interface e herança de classes em UML

Figura 3. Representando realização de interface e herança de classes em UML.

Listagem 5. Implementação de herança e realização em Java.


interface Veiculo {
   void locomover();
  }
   
  abstract class VeiculoMotor implements Veiculo {
   protected Motor motor;
  }
   
  class Guidao {}
  class Volante {}
   
  class Motocicleta extends VeiculoMotor {
   private Guidao guidao;
   
   @Override
   public void locomover() {
    System.out.println("Locomovendo uma moto utilizando " + guidao + " e " + motor);
   }
  }
   
  class Carro extends VeiculoMotor {
   private Volante volante;
   
   @Override
   public void locomover() {
    System.out.println("Locomovendo um carro utilizando " + volante + " e " + motor);
   }
   
   public void abrir() { }
  } 

No modelo, é incluída uma classe Veiculo que define um contrato, ou seja, um conjunto de métodos (no caso, apenas um) que todas as classes concretas que realizam a interface deverão implementar. Em outras palavras, qualquer objeto que possua uma instância de Veiculo poderá chamar o método locomover() nesta instância, independentemente de qual é a implementação concreta da interface.

A classe abstrata VeiculoMotor realiza (implementa) a interface Veiculo – em outras palavras, realiza a interface Veiculo a classe abstrata VeiculoMotor (e VeiculoMotor é uma especialização de Veiculo, que por sua vez é uma generalização de VeiculoMotor). Tal classe é considerada abstrata pois é muito genérica: um veículo motor pode ser um carro, uma moto, um caminhão, etc. Porém, todos os veículos deste tipo possuem um motor e ao declará-lo como um atributo da classe abstrata, ele será herdado por todas as classes que a estenderem.

Finalmente, as classes Motocicleta e Carro herdam de VeiculoMotor e especificam, cada uma, suas particularidades: uma moto possui um guidão, enquanto um carro possui um volante e pode ser aberto (i.e. possui o método abrir()). Como são classes concretas, devem obrigatoriamente implementar todos os métodos abstratos herdados, o que significa também implementar os métodos declarados na interface (e que não foram implementados por uma de suas superclasses).

Sobrescrita de métodos

Quando se fala de herança, é interessante lembrar de outro mecanismo importante, conhecido como sobrescrita de métodos. A sobrescrita indica que uma subclasse altera a implementação de um método que ela herdou de outra classe. Esse mecanismo é importante quando precisamos tratar de classes que podem executar um método com a mesma assinatura, mas com comportamentos diferentes.

A Listagem 6 mostra um exemplo de sobrescrita de métodos. Como vimos na Listagem 5 e na Figura 3, a classe Carro possui um método abrir(). Vamos imaginar que este método seja utilizado para abrir a porta do carro. Um carro como o DeLorean[1], que abre as portas verticalmente teria que sobrescrever este método, provendo uma implementação diferente.

Listagem 6. Sobrescrita de método.


class DeLorean extends Carro {
  @Override
  public void abrir() { }
} 

Kathy Sierra e Bert Bates [2] abordam pontos importantes sobre a sobrescrita de métodos:

  • A lista de argumentos deve coincidir exatamente com a do método sobrescrito. Se não coincidirem, você poderá ficar com um método sobrecarregado que não queira usar. Um método sobrecarregado indica que ele possui o mesmo nome de outro método, mas possui assinatura diferente;
  • O tipo de retorno deve ser o mesmo, ou um subtipo do tipo de retorno declarado no método sobrescrito da superclasse. A utilização de um subtipo só foi permitida a partir do Java 5, graças ao mecanismo de covariância;
  • Os métodos de instâncias somente podem ser sobrescritos se forem herdados pela subclasse. Se um método for private, ele não pode ser acessado pela subclasse, então não há a possibilidade de realizar uma sobrescrita nesse método (no entanto, é possível declarar um método com a mesma assinatura na subclasse, ele só não estará sobrescrevendo o método da superclasse);
  • Uma subclasse, dentro do mesmo pacote que sua superclasse, é capaz de sobrescrever qualquer método da superclasse que não seja marcado como private ou final;
  • Uma subclasse de um pacote diferente somente pode sobrescrever os métodos não final marcados com public ou protected.

Covariância (covariant return types): é a capacidade de uma subclasse sobrescrever um método trocando seu tipo de retorno por uma subclasse do tipo de retorno original. Por exemplo, se uma classe A possui um método obterColecao() que retorna um objeto do tipo java.util.Collection, uma classe B pode sobrescrever este mesmo método, declarando como tipo de retorno, por exemplo, java.util.List, que é subclasse de Collection. A covariância foi proposta dentro da JSR-14 (que propôs tipos genéricos) e começou a fazer parte da especificação Java na versão 5 (ou seja, no Java 1.4 e anteriores, a classe B seria obrigada a declarar Collection como retorno).

A Listagem 6 ilustra, também, o primeiro ponto da lista acima (também relacionado ao comentário ao final do terceiro item). A anotação @Override, introduzida no Java 5, foi utilizada para indicar a sobrescrita. Com a anotação acima do método, caso o mesmo seja declarado de forma incorreta (ex.: public void abri() { }, sem o r), @Override fará com que o compilador acuse um erro, dizendo que aquele método não está sobrescrevendo nada, pois não foi encontrado nas superclasses um método equivalente. Sem a anotação o código estaria correto, porém o método abrir() não seria sobrescrito, mas sim seria criado um outro método com nome diferente e abrir() seria herdado da superclasse, com sua implementação inadequada.

Herança Múltipla

Duas outras características da herança em UML são mostradas na Figura 4. Ao contrário da herança de Carro e Motocicleta, as linhas que ligam AnimalAquatico e AnimalTerrestre à sua superclasse estão separadas. Isso indica que Carro e Motocicleta são classes incompatíveis – não é possível que uma instância de Carro seja também uma instância de Motocicleta – enquanto AnimalAquatico e AnimaTerrestre não o são – é possível encontrar um animal que seja tanto aquático quanto terrestre.

Herança múltipla em UML

Figura 4. Herança múltipla em UML.

Porém, dado que um objeto é instância direta de uma e somente uma classe, para que ele seja instância (indireta) de duas classes irmãs, como neste exemplo, é preciso que haja herança múltipla, que é a segunda característica mostrada na Figura 4. A classe AnimalAnfibio herda todas as características tanto de AnimalAquatico quanto de AnimalTerrestre. O leitor poderia pensar: esse diagrama faz uso de herança múltipla, mas Java não a permite. Esse diagrama não estaria incorreto? A resposta é: não. A UML é uma linguagem para modelagem de software e não é voltada para nenhuma linguagem específica. Ela aceita modelagem usando herança múltipla, mas, caso a linguagem de programação em questão não a aceite, deve-se utilizar a realização de interfaces para simular a herança múltipla. Isso é demonstrado na Listagem 7.

Listagem 7. Simulação de herança múltipla em Java.


class Animal { }
   
  class AnimalAquatico {
   public void nadar() { }
  }
   
  interface IAnimalTerrestre {
   void andar();
  }
   
  class AnimalTerrestre implements IAnimalTerrestre {
   public void andar() { }
  }
   
  class AnimalAnfibio extends AnimalAquatico implements IAnimalTerrestre {
   private AnimalTerrestre animalTerrestre = new AnimalTerrestre();
   
   @Override
   public void andar() {
    animalTerrestre.andar();
   }
  } 

Para simular a herança múltipla, a subclasse (AnimalAnfibio) deve escolher uma das superclasses para estender (AnimalAquatico) e implementar uma interface (IAnimalTerrestre) que declare todos os métodos públicos da superclasse não herdada (AnimalTerrestre). Para não ter que repetir a implementação dos métodos da superclasse que não foi herdada, declara-se uma instância desta na subclasse e se repassam a ela todas as chamadas de método a ela relacionadas (no exemplo, o método andar()).

Polimorfismo

O mecanismo de herança permite utilizar uma característica de linguagens de programação chamada polimorfismo. Esta característica possibilita que objetos de diferentes classes sejam tratados de maneira uniforme graças a uma interface em comum. A hierarquia representada na Figura 3 é polimórfica, pois qualquer classe concreta que realiza a interface Veiculo deve ter o método locomover() implementado.

Desta maneira, podemos fazer o que é exemplificado na Listagem 8: no método main() é criado um vetor com diversos tipos de veículos (inclusive duas classes novas, Onibus e Bicicleta, definidas nesta listagem). Em seguida, um loop for obtém cada veículo da lista e chama o método locomover(), tratando todas as instâncias de modo uniforme. Em outras palavras, se queremos locomover um veículo, não importa qual é a classe concreta da instância com a qual estamos lidando, pois sabemos que todas elas implementam a interface Veiculo e, portanto, basta chamar o método e a máquina virtual identifica a implementação correta.

Listagem 8. Polimorfismo em Java.


class Onibus extends VeiculoMotor {
   @Override
   public void locomover() {
    System.out.println("Locomovendo um ônibus");
   }
  }
   
  class Bicicleta implements Veiculo {
   @Override
   public void locomover() {
    System.out.println("Locomovendo uma bicicleta");
   }
  }
   
  public class Listagem08 {
   public static void main(String[] args) {
    Veiculo[] veiculos = new Veiculo[] {new Carro(), new Bicicleta(), 
     new Onibus(), new Motocicleta() };
    for (Veiculo v : veiculos) v.locomover();
   }
  } 

O grande benefício do polimorfismo é definir na classe ou interface base da hierarquia os métodos que devem ser implementados e, assim, se pudermos implementar as funcionalidades do sistema utilizando estes métodos, não só tratamos todas as classes existentes de uma só vez, como o sistema estará já preparado para futuras classes que viermos a inserir nesta mesma hierarquia.

Herança vs. Delegação

As duas maneiras principais de reutilização de código no paradigma OO são herança e delegação. Como visto na seção anterior, quando uma classe especializa (estende) outra, herda todos os atributos e métodos desta última. Com a delegação também é possível reutilizar código, como fizemos ao simular a herança múltipla em Java: define-se como atributo uma instância da classe cujo comportamento deseja-se reutilizar e delega-se a ela a (ou parte da) implementação de um ou mais métodos.

É importante, no entanto, saber quando usar um ou outro mecanismo. A herança, apesar de parecer mais cômoda pois não é preciso implementar o código de delegação, deve ser utilizada com cuidado sempre obedecendo o princípio do É-UM: uma classe deve ser um subtipo de outra para que a herança faça sentido. Do contrário, é melhor utilizar a delegação.

Por exemplo, imagine que seja necessário implementar uma classe Pilha[2] em Java. Se já existe uma classe Lista implementada, qual é a melhor solução: Pilha estender Lista ou delegar a esta o armazenamento dos elementos? Sabendo que Lista provavelmente possui métodos que permitem adicionar e remover elementos em qualquer posição, tais métodos deverão ser sobrescritos pela classe Pilha e lançar exceções, pois nesta última só é permitido adicionar e remover elementos do topo. Conclui-se, portanto, que uma Pilha NÃO É-UMA Lista, e o mecanismo de delegação seria mais apropriado para reutilizar a implementação da lista para construir uma pilha.

Dependência

A dependência indica que o comportamento de uma classe pode ser influenciado pelo comportamento de outra classe, ou seja, a mudança no comportamento de uma classe pode acarretar mudança no comportamento de outra classe.

A Figura 5 nos mostra a dependência entre as classes Cliente e Servidor. O que significa que uma mudança no comportamento desta última poderia acarretar em uma mudança no comportamento da primeira. Isso ocorre porque a classe Servidor é utilizada de alguma forma pela classe Cliente em sua implementação e a inclusão desta informação no modelo serve para alertar aos desenvolvedores que se alguma alteração deve ser feita em uma dependência, deve-se verificar primeiro como isso afeta os seus dependentes.

Dependência entre classes em UML

Figura 5. Dependência entre classes em UML.

Seja s uma instância da classe Servidor e c uma instância da classe Cliente, tal que s possua um método enviarResposta() e c possua um método receberResposta(). Se s envia um conteúdo HTML através do método enviarResposta(), o método receberResposta() de c poderia apenas mostrar o conteúdo no navegador Internet. Supondo que num dado momento o conteúdo a ser enviado pelo servidor deixa de ser HTML e passa a ser um conteúdo de vídeo (MPEG-4, por exemplo), o método receberResposta() teria que abrir o reprodutor de vídeo antes de exibir o conteúdo do mesmo. A partir desse exemplo, percebemos que o comportamento do cliente fica condicionado ao comportamento do servidor. Isso indica que há dependência entre o cliente e o servidor.

Conclusões

A orientação a objetos é um paradigma bastante utilizado atualmente para o desenvolvimento de software. Com ele podemos criar projetos de software com maior flexibilidade, legibilidade e reusabilidade. Alguns conceitos da orientação a objetos vistos neste artigo, tais como herança, polimorfismo e delegação, são cruciais para um bom projeto de software. Vimos estes conceitos em conjunto com o diagrama de classes, que é um dos diagramas mais importantes da UML.

Ao se projetar um software, devemos pensar numa maneira de reduzir a dependência entre as partes do sistema para aumentar a coesão do mesmo. Um alto grau de coesão acarreta uma maior flexibilidade e extensibilidade do sistema, bem como uma redução do acoplamento do mesmo. Além de saber os conceitos da orientação a objetos e as melhores práticas para análise e projeto de sistemas, é preciso dominar também uma ferramenta que permita comunicar estes modelos. O diagrama de classes da UML é uma das ferramentas mais utilizadas hoje em dia para modelagem de sistemas OO e, portanto, é fundamental conhecê-lo.


Livros

  • [1] BOOCH, Grady; RUMBAUGH, James; JACOBSON, Ivar. UML: guia do usuário. 2ª ed. Rio de Janeiro: Campus, 2005.
  • [2] SIERRA, Kathy; BATES, Bert. Certificação Sun para programador Java 6: guia de estudo. 1ª ed. São Paulo: Alta Books, 2008.
  • [3] PRESSMAN, Roger. Engenharia de Software. 6ª ed. São Paulo: Mc Graw Hill, 2006.
  • [4] PAGE-JONES, Meilir. Fundamentals of Object-Oriented Design in UML. 2ª ed. Addison-Wesley Professional, 1999.
  • [5] CONALLEN, Jim. Building Web Applications with UML. 2a ed. Addison-Wesley, 2002.

Confira também