Resumo da aula 10

[Incompleto, ver páginas mais recentes.]

Sumário

Introdução

Um arquitecto, quando lhe é encomendado um trabalho, começa por discutir com o cliente quais são as suas necessidades.  Depois estuda o problema na sua generalidade e desenha alguns esquissos.  Estes esquissos, para além de permitirem facilmente ao arquitecto experimentar rapidamente várias possíveis soluções, são também uma boa forma de comunicação com o cliente e com outros membros da sua equipa.  Uma vez decidido o aspecto geral da solução, a equipa do arquitecto começa a produzir sucessivos desenhos com um grau de pormenor crescente.  O projecto final não deixa nada ao acaso, contendo especificações de tudo o que um edifício deve conter, desde a parte estrutural, até aos sistemas eléctricos e de climatização.  Por vezes são feitas maquetas para acertar melhor o funcionamento de determinadas partes do projecto antes de passar à construção.  As maquetas depois de usadas são deitadas fora.

A fase seguinte é a implementação prática do projecto: a construção.  Durante a construção surgem problemas imprevistos que têm de ser resolvidos, o que pode implicar refazer partes do projecto (de preferência pequenas partes).  Por outro lado, há pormenores de construção que têm de ser decididos em obra e que não necessitam da intervenção directa do atelier de arquitectura.  Note-se que a construção está tipicamente a cargo de um construtor civil, e não do gabinete de arquitectura.

No desenvolvimento disciplinado de software também pode ser assim.  Tal como não faz sentido que o arquitecto comece o seu trabalho assentando tijolos no edifício final sem antes o ter projectado, também não faz qualquer sentido uma empresa de desenvolvimento de software começar a resolver um problema escrevendo linhas de código em C++ ou em qualquer outra linguagem.

Mas a tendência do programador noviço é começar por se sentar ao computador, executar o editor, criar um novo ficheiro C++ e escrever:

#include <iostream>

using namespace std;

...

É fundamental resistir a essa tentação!  Os problemas reais são complexos e é necessário usar técnicas de abordagem semelhantes à do arquitecto que permitam lidar com essa complexidade.  Ou seja:

Uma empresa de desenvolvimento de software, quando lhe é encomendado um trabalho, começa por enviar um analista discutir com o cliente quais são as suas reais necessidades.  Depois a equipa a cargo do trabalho estuda o problema na sua generalidade e desenha alguns diagramas representativos do problema a resolver.  Estes diagramas permitem à equipa voltar a discutir com o cliente e chegar finalmente a um conjunto mais ou menos definitivo de requisitos para o trabalho.  A esta primeira fase chama-se análise.  

A seguir, a equipa começa a desenhar diagramas com possíveis soluções para o problema.  Estes diagramas, para além de permitirem à equipa experimentar rapidamente várias possíveis soluções, são também uma boa forma de comunicação com o cliente e entre os membros da equipa.  Uma vez decidida arquitectura geral da solução, a equipa começa a produzir diagramas com um grau de pormenor crescente.  Por vezes são desenvolvidos protótipos (pequenos programas) para acertar melhor o funcionamento de determinadas partes da solução ou para experimentar melhor possíveis soluções e interacções com o utilizador do programa.  Os protótipos depois de usados são deitados fora.  A esta segunda fase chama-se desenho ou concepção.

A fase seguinte é a implementação prática da solução entretanto desenhada.  Durante a implementação surgem problemas imprevistos que têm de ser resolvidos, o que pode implicar refazer partes do desenho original (de preferência pequenas partes).  Por outro lado, há pormenores de implementação que têm de ser decididos durante a implementação e que não necessitam da intervenção directa dos elementos da equipa que analisaram o problema e desenharam a sua solução.  Note-se que a análise do problema a resolver, o desenho da solução para o problema e a implementação são realizadas normalmente por diferentes colaboradores da empresa de desenvolvimento de software, embora trabalhando em equipa.

Resumindo:

Na prática também é comum, e muitas vezes desejável, que as várias fases de resolução do problema (análise, desenho e implementação) se sobreponham parcialmente ou que sejam iteradas várias vezes.  Isto quer dizer que não se deve ter a veleidade de pretender que o analista em conjunto com o cliente conseguem estabelecer todos os requisitos do sistema a desenvolver de uma só penada, e sem erros.  O desenvolvimento de software é um processo iterativo, onde os sucessivos refinamentos devem poder ter lugar.  Também é verdade que nem sempre é possível, ou mesmo desejável, que haja uma tão clara especialização dos elementos da equipa, sendo comum que, ao contrário do que acontece na construção de edifícios, as mesmas pessoas façam simultânea ou alternadamente desenho, implementação e mesmo análise.

Em qualquer dos casos, é muito importante que haja uma comunicação fácil entre todos as pessoas envolvidas no projecto.  Isso implica que haja uma linguagem comum a clientes, analistas, arquitectos (desenhadores) e implementadores.  Naturalmente que nessa comunicação se pode usar um língua natural, como o português, mas as linguagens naturais têm o problema de poderem ser ambíguas e imprecisas e ainda de não serem práticas para representar relações complexas entre partes de um todo (imagine-se um projecto de arquitectura expresso apenas através de palavras: possível, mas basicamente ilegível).  O ideal passa, pois, por usar diagramas, mas diagramas expressos numa linguagem gráfica que seja universal e que precisa.

Diz-se que uma imagem muitas vezes vale mil palavras.  Na realidade pode valer muito mais do que isso.  É comum ver um programador em concentração profunda (ou completamente perdido) tentando perceber um troço de código escrito por outrem.  O mesmo acontece por vezes quando um programador olha para código escrito por ele próprio há uns meses ou anos atrás.  As linguagens de programação de alto nível, apesar de estarem bastante mais próximas da linguagem natural do que a linguagem máquina ou o assembly, são desenhadas para comunicar com um compilador e não com humanos.  Apesar de ser expectável que os programadores documentem e comentem apropriadamente o seu código e que, sobretudo, escrevam de uma forma tão clara e transparente quanto possível, o código pode ser surpreendentemente difícil de perceber.  Todos os aprendizes de programação passam pela experiência de desenvolver o algoritmo de remoção de um nó de uma árvore binária ordenada.  Ou pelo menos pela provação de perceberem um algoritmo fornecido por terceiros.  Ambas são experiências traumatizantes se não se fizer uso de um diagrama para representar os nós da árvore e respectivos ramos.

Significa isto naturalmente que qualquer programador deve recorrer frequentemente ao papel ou ao quadro branco para desenhar diagramas que lhe permitam perceber melhor um problema ou comunicar mais eficientemente as suas ideias.

A criatividade é uma qualidade importante no desenvolvimento de software.  Mas é conveniente reservá-la para o que é fundamental (a solução em si) e não para a forma como ela é escrita ou apresentada.  Assim, é importante que o código seja escrito usando convenções comummente aceites, tais como deixar espaços entre operadores e operandos (e.g., x * y), escrever uma instrução por linha, usar substantivos (possivelmente adjectivados) para o nome das classes, começar o nomes das classes por maiúsculas, etc.

Da mesma forma a criatividade é inútil quando aplicada ao aspecto gráfico dos diagramas.  Por isso é importante que exista um convenção ou, de preferência, uma norma, que estabeleça os símbolos gráficos a usar para desenhar os diagramas representativos das entidades e respectivas relações de um pedaço de software implementado ou a implementar.  É necessária uma linguagem gráfica de modelação.  Foi reconhecendo este facto que os "três amigos" (Grady Booch, Ivar Jacobson e James Rumbaugh) desenvolveram a chamada UML (Unified Modelling Language) [2, 4], normalizada posteriormente pelo OMG (Object Management Group) [1].

1.1  UML

Um programa é muitas vezes um modelo simplificado de uma parte do mundo, sobretudo em programação orientada para objectos.  As palavras chave aqui são "modelo" e "simplificado".  É absolutamente fundamental que se lute por programas simples e claros.  O mundo real é complexo, e isso pode levar a soluções que são inevitavelmente complexas.  Mas uma solução só deve ser complexa se não existir uma solução mais simples.

A UML especifica um conjunto de diagramas, e as respectivas simbologia e semântica que servem para representar graficamente o modelo simplificado que, quando implementado numa linguagem de programação, resolverá o problema em causa.  Está implícita nesta observação a noção de que o resultado do desenho devem ser um conjunto de diagramas que, depois de implementados numa linguagem de programação arbitrária (ou em várias), resultarão num pacote de software que resolve o problema em causa.  Esta é uma visão simplificada da realidade por duas razões: (a) o desenvolvimento é iterativo e existe realimentação entre as sua várias fases (e.g., a implementação pode identificar problemas de desenho que têm de ser resolvidos) e (b) raramente é possível que um desenho possa ser feito independentemente das ferramentas usadas na implementação.  Aliás, é muitas vezes necessário levar em conta por exemplo as linguagens de programação a utilizar durante o desenho da solução, sob risco de se proporem desenhos que ou são inexequíveis ou pelo menos extremamente difíceis de implementar.  Apesar destas ressalvas, a UML é uma ferramenta importante de comunicação e não pode ser desprezada.

Existe um conjunto vasto de diagramas em UML:

  1. Diagramas estáticos:
  2. Diagramas de casos de utilização
  3. Diagramas de sequência
  4. Diagramas de colaboração
  5. Diagramas de estado
  6. Diagramas de actividade
  7. Diagramas de implementação:
O desenvolvimento de um programa passa pelo desenho de variados destes diagramas.  Esse desenho é muitas vezes informal, mas existem ferramentas de modelação que permitem o desenho de diagramas UML, verificação da consistência entre eles, geração automática de código fonte (e.g., C++) a partir dos diagramas ou extracção de diagramas ou pelo menos da informação necessária para os desenhar facilmente, a partir de código fonte (e.g., C++).  Neste âmbito, no entanto, a UML será usada informalmente.

Nesta disciplina abordar-se-ão apenas os diagramas estáticos e os diagramas de sequência e de actividade.  Os diagramas estáticos dividem-se em diagramas de classes e diagramas de objectos.  Os diagramas de classes representam as classes existentes nos modelos e as relações entre elas.  Estes diagramas são estáticos porque as relações entre as classes não variam durante a execução do programa.  Os diagramas de objectos, por outro lado, representam os objectos existentes e as ligações entre eles.  Estes diagramas são estáticos porque representam o estado do sistema num dado instante de tempo.

* Procura-se melhor tradução...

Diagramas de classes

Um diagrama de classes serve para representar as classes existentes no modelo e respectivas relações.  Um diagrama de classes não tem de mostrar todas as classes do modelo.  Aliás, a maior parte das vezes não o deve fazer, pois os diagramas existem com o objectivo de clarificar a comunicação, pelo que informação irrelevante para aquilo que se quer comunicar deve ser deixada de fora.  Assim, em vez de desenhar um único diagrama com todas as classes e respectivas relações, é preferível desenhar vários diagramas, cada um com ênfase numa parte do modelo.

Por outro lado, embora estes diagramas se digam "de classes", podem e devem ser usados a um nível de abstracção mais alto, por exemplo para identificar os conceitos e respectivas relações no domínio do problema a resolver, antes mesmo de se começar a desenhar uma solução para esse problema.  Assim, embora aqui se use o termo "classe", deve-se levar em conta que as afirmações são também verdadeiras se se substituir esse termo pelo de "conceito".  No primeiro caso está-se no domínio da solução, e no segundo no domínio do problema.  No primeiro caso os diagramas representam o modelo de análise e no segundo o modelo de desenho.

Os diagramas de classes dizem-se diagramas estáticos, pois, no caso dos modelos de desenho, representam classes e relações que existem durante toda execução do programa: não variam no tempo.

2.1  Classes

As classes são representadas de uma forma simples: um rectângulo com o nome da classe no topo.  Por exemplo, a representação da classe Empregado é:

Se a classe for abstracta o seu nome escreve-se em itálico:

Quando se está a desenhar um diagrama à mão, não é fácil escrever em itálico.  Nesse caso acrescenta-se a propriedade {abstract} após o nome da classe:

Note-se que é suposto a UML simplificar a comunicação.  Assim, é lícito abusar da linguagem quando isso contribuir para facilitar a comunicação.  Esta disciplina aliás, abusa da UML, nomeadamente estendendo a notação dos objectos por forma a poder ser usada para os tipos elementares ou básicos.

2.1.1  Características

As instâncias de uma classe, ou seja, os respectivos objectos têm um comportamento (dados pelas suas operações) e um estado (dado pelos seus atributos).  Ao conjunto das operações e dos atributos de uma classe chama-se características, e é possível representá-los em UML, dividindo-se em "fatias" a representação das classes.  A divisão mais típica é em três fatias: a do topo contém o nome da classe, a intermédia os atributos e da base as operações.  Podem-se omitir quaisquer fatias com excepção da fatia do título:

Enquanto em C++ é típico dividir os membros de uma classe em públicos e privados, declarando-se primeiro os públicos pois formam a interface da classe, em UML a divisão estabelece-se entre atributos e operações, podendo existir características com qualquer visibilidade (o mesmo que categoria de acesso) quer na fatias das operações quer na fatia dos atributos.  É típico os atributos de uma classe serem privados (aliás é recomendável que o sejam, de acordo com o princípio do encapsulamento) e a maior parte das operações serem públicas.  Mas a verdade é que a notação UML não separa claramente características que pertencem à interface (públicas) das que pertencem à implementação (privadas).  Isso é deixado ao critério de quem desenha o diagrama.  Assim, é muitas vezes boa ideia representar num diagrama apenas as características públicas, deixando as privadas para alturas em que se tem de discutir pormenores de implementação.

Por outro lado, a ordem das fatias também não é a melhor, pois dá ênfase aos atributos face às operações, apesar de a distinção mais interessante que se pode fazer entre classes se basear no comportamento das respectivas instâncias, e não tanto no seu estado.  O comportamento das instâncias de uma classe tem a ver essencialmente com as respectivas operações, e não tanto com os respectivos atributos.  Estes, normalmente, não passam de um mero pormenor de implementação.

A visibilidade das características de uma classe pode ser representada em UML precedendo-as dos símbolos

como se pode ver nos exemplos acima.

Finalmente, as características podem ser (ou ter âmbito) de classe ou de instância, consoante se apliquem à classe como um todo (ou seja, ao conjunto de todas as instâncias da classe) ou às suas instâncias particulares.  As características com âmbito de classe representam-se sublinhadas.  Por exemplo, na classe abstracta Forma

existe uma operação nova() que, dado um canal de entrada, devolve um ponteiro do tipo Forma* para um tipo concreto de Forma.  Esta operação proporciona características de fábrica (de formas) à classe abstracta Forma.  Em C++ a respectiva declaração seria

class Forma {
  public:

    ...

    static Forma* nova(istream& entrada);

    ...

};

e a definição seria

Forma* Forma::nova(istream& entrada)
{
   
...
}

Uma possível utilização seria

...

ifstream entrada(nome.c_str());

Forma* forma = Forma::nova(entrada);

...

2.1.2  Notas, comentários e restrições

Por vezes é necessário adornar um elemento notacional UML com comentários, i.e., texto explicativo que não faz parte do modelo formal.  Para isso usam-se notas nas quais se insere qualquer texto.  A notação para as notas é simplesmente um rectângulo contendo texto e com o canto superior dobrado:

É possível ligar uma nota, por uma linha tracejada, a um ou mais elementos notacionais aos quais diz respeito:

Uma noção importante é a de condição invariante de classe.  Uma condição invariante é uma restrição imposta aos valores dos respectivos atributos.  Em UML, uma restrição representa-se colocando-a numa nota, mas entre chavetas:

O texto da restrição pode ser escrito em linguagem natural, mas também numa linguagem formal própria, o OCL (Object Constraint Language), que está fora do âmbito desta disciplina.  É possível usar restrições para representar pré-condições e condições objectivo de uma operação, como se verá, mas também para representar condições invariantes de classe, usando-se para isso o estereótipo «invariant»:

2.1.3  Operações

Na fatia das operações, cada linha corresponde a uma distinta operação.  A UML estabelece uma sintaxe para a representação das operações.  As operações podem corresponder a funções (devolvem um valor) ou a procedimentos (não devolvem qualquer valor).  A sintaxe usada para indicar as operações de uma classe em UML é bastante diferente da usada em C++, e semelhante à usada pelo Pascal e seus descendentes:

nome_de_função(lista_de_parâmetros): tipo_de_devolução
nome_de_procedimento(parâmetros)

A lista de parâmetros consiste numa lista de parâmetros separados por vírgulas.  Os parâmetros podem ter ou não valores por omissão e têm a seguinte sintaxe:

tipo_de_passagem nome: tipo
tipo_de_passagem nome: tipo = valor_por_omissão

O tipo de passagem pode ser: Operações abstractas podem ser apresentadas em itálico ou acrescentando a propriedade {abstract} à notação usada, como se viu no caso da classe Forma acima.

Uma operação que não modifique o estado do sistema, i.e., que não altere a instância da classe sobre a qual é efectuada nem qualquer outro objecto do sistema, diz-se de inspecção.  Pode-se representar uma operação com essas características acrescentando a propriedade {query} à notação apresentada.  Por exemplo:

Também é possível classificar como não virtuais as operações.  Por exemplo:

Uma forma mais clara de classificar as operações de uma classe é usando os estereótipos «constructor», «destructor», «query» e «update» (para operações modificadoras):

Finalmente, é possível usar restrições para assinalar pré-condições e condições objectivo de operações, fazendo-se uso para isso dos estereótipos «precondition» e «postcondition»:

2.1.4  Métodos

A cada operação declarada numa classe corresponde forçosamente um método, excepto se a operação for abstracta.  A presença numa subclasse da declaração de uma operação com a mesma assinatura de uma operação polimórfica declarada numa superclasse, correspondem a declarações redundantes da mesma operação (que é única) e assinalam o fornecimento de um método para essa operação pela classe em causa.

Pode-se indicar o corpo, ou seja, a implementação de um método colocando o respectivo código (e.g., em C++) numa restrição:

2.1.5  Atributos

Os atributos de uma classe especificam-se com uma sintaxe semelhante à dos parâmetros das operações:

nome: tipo
nome [multiplicidade]: tipo
nome: tipo = valor_inicial

É possível indicar que um determinado atributo não é variável, mas sim constante, usando a propriedade {frozen}.  No exemplo

define-se parcialmente uma classe PilhaFixaDe100Int que representa pilhas de inteiros com capacidade máxima fixa em 100 itens.  Na definição deixa-se claro que:

  1. há um atributo número_de_itens que é um inteiro inicializado com zero e que guarda o número de itens em cada instância da classe;
  2. há um atributo itens que guarda os itens da pilha, em quantidade de exactamente número_de_itens inteiros; e
  3. há um atributo constante número_máximo_de_itens inteiro, com valor 100, que é partilhado entre todas as instâncias da classe.

A multiplicidade de um atributo é colocada, como se viu, entre parêntese rectos e após a indicação do tipo do respectivo atributo, e pode ter as seguintes formas:

Note-se que a multiplicidade de um atributo se pode apresentar da várias formas.  A forma que se apresentou é a mais abstracta de todas.  Mas é possível usar formas de apresentação que se aproximam mais da implementação final da classe numa determinada linguagem de programação, apresentando o atributo sem multiplicidade explícita, mas indicando o seu tipo como um dos tipos da linguagem que suporte a multiplicidade desejada.  Ou seja, o exemplo acima tem a seguinte apresentação alternativa:

onde se indica directamente que o atributo itens corresponde a uma matriz clássica do C++ com exactamente número_máximo_de_itens (dos quais, presume-se, só se usam número_de_itens em cada instante).

Um outro exemplo, porventura mais elucidativo, é o de uma classe FormaComposta que representa formas que são composições de outras formas (instâncias da classe Forma).  Uma primeira representação, mais abstracta, acentua apenas o facto de uma FormaComposta ser composta por um número arbitrário de formas:

Uma representação menos abstracta reflecte já as particularidades da linguagem C++.  A primeira é que em C++ uma forma de guardar um número arbitrário de itens de um determinado tipo é usar vectores.  A segunda é que, como a classe Forma será provavelmente abstracta e, sobretudo, porque será o topo de uma hierarquia de classes polimórficas, o vector usado para guardar as formas deverá ser um vector de ponteiros para a classe Forma

Claro está que a forma menos abstracta de representar a classe é directamente em C++:

class FormaComposta: public Forma {
  public:

    ...

  private:
    vector<Forma*> formas;

    ...

};

A primeira representação, diz que uma forma composta é composta por um número arbitrário de formas.  A segunda, diz que uma forma composta é composta por um vector de ponteiros para formas.  A terceira e última, é código C++.  Enquanto as duas primeiras são modelos, o primeiro provavelmente um modelo de análise do problema e o segundo um modelo de desenho ou concepção, a última é já a implementação.

2.2  Relações

Viu-se que, nas classes, mais importante que os atributos, que representam o estado dos respectivos objectos, são as operações, que representam o respectivo comportamento.  O comportamento dos objectos normalmente depende do comportamento de outros objectos, da mesma ou de outras classes.  Por isso é muito importante ter uma forma de representar os vários tipos de relações que podem existir entre classes.

Existem essencialmente quatro formas de relação entre classes representáveis em UML.  Por ordem decrescente da "força de relacionamento" são:

  1. Generalização.
  2. Amizade.
  3. Associação.
  4. Dependência (fora do âmbito desta disciplina).

2.2.1  Generalização

A relação mais forte entre duas classes é a relação de generalização.  Como já se viu, a relação de generalização é apenas uma forma diferente de ver a relação é um.  Se se pode dizer que "qualquer objecto da classe A é também um objecto da classe B", então a classe B é uma generalização de A, o que quer dizer que em C++ a classe A deriva directa ou indirectamente da classe B, ou seja, B é uma superclasse de A e A uma subclasse de B.

Em UML a relação de generalização representa-se por uma seta com a ponta fechada e oca apontando da classe mais especializada para a classe mais generalizada.  Note-se que em UML só é lícito representar graficamente esta relação entre classes que sejam generalizações ou especializações directas uma da outra.  Por exemplo, uma classe Forma, representando o conceito de forma no contexto de uma aplicação de desenho, por exemplo, é uma generalização das classes Círculo, Rectângulo e forma composta, entre outras:

2.2.2  Amizade

Menos forte que a relação de generalização, mas mais forte que todos os outros tipos de relação, é a relação de amizade entre duas classes.  Esta representa-se usando a notação da dependência, a mais fraca das associações, como se verá, mas usando o estereótipo «friend».  É de notar que a relação de amizade é direccional, pelo que esta relação exige a existência de uma seta.  A seta aponta para a classe que declara a amizade e, por isso, permite o acesso irrestrito, pela classe que declara como amiga, a todos os seus membros privados.  Por exemplo, se existir uma classe ListaDeInt e uma classe (embutida) ListaDeInt::Iterador que é declarada como amiga da classe ListaDeInt, e por isso tem acesso aos seus membros privados, a notação a usar é:

Note-se que não existe em UML nenhuma notação para representar graficamente a relação de embutimento, i.e., a relação que existe entre uma classe embutida e a classe na qual está embutida.  Isso acontece porque essa relação não é uma relação semântica, e por isso importante do ponto de vista do modelo, mas meramente sintáctica.  A única forma de representar tal relação é através da qualificação do nome da classe embutida, tal como no diagrama acima (i.e., ListaDeInt::Iterador).

2.2.3  Associação

A relação mais fraca entre classes é a associação.  Uma associação também pode ter vários graus de "força de associação".  Por ordem decrescente de "força de associação":

  1. Associação de composição.
  2. Associação de agregação.
  3. Associação simples.

As associações normalmente são binárias, i.e., dizem respeito a apenas duas classes.  Neste texto lida-se apenas com associações binárias, embora a UML suporte associações de aridade arbitrária.

As associações são abstracções.  Dizer que duas classes estão associadas é o mesmo que dizer que os respectivos objectos (ou instâncias) estão de alguma forma ligados.  Ou seja, as associações são instanciadas em ligações entre objectos, da mesma forma que as classes são instanciadas em objectos.

2.2.3.1  Associação simples

Na associação simples não há conceito de posse.  Os objectos de cada uma das classes poderão possuir ligações entre si, mas estas ligações não introduzem qualquer noção de posse entre esses objectos.  Os tempos de vida dos objectos ligados entre si são, no caso da associação simples, totalmente independentes.

Este tipo de de associação corresponde a relações que podem ser expressas em português através de um verbo que não denote posse (e.g., possui um) nem composição (e.g., compõe-se de um), pois essas expressões correspondem às relações de agregação e composição.  Esse verbo pode ser, por exemplo, trabalhar para, por exemplo na expressão trabalha para um.

Muitas vezes este tipo de relação é expresso em português através da expressão "tem um".  No entanto, esta expressão é muito ambígua, podendo ser também usada para expressar relações de posse que correspondem á associação de agregação (ter no sentido de possuir) ou à relação de composição (ter no sentido de se compor de qualquer coisa) .

A notação para uma associação é simplesmente uma linha ligando as duas classes associadas.  Por exemplo, para representar a relação é chefiado por um ou chefia um entre um empregado e um chefe, pode-se usar uma associação simples:

É possível adornar esta associação de várias formas.  Em primeiro lugar com o nome da associação, que se coloca a meio da linha de associação acompanhada de um pequeno triângulo que indica a direcção em que a associação deve ser lida:

ou então

Também é possível adornar a notação com informação semântica adicional, nomeadamente de multiplicidade.  A informação de multiplicidade diz a quantos objectos de uma das classes pode estar associado um objecto da outra classe.  A notação usada para a multiplicidade é idêntica à usada para os atributos, e é colocada junto ao extremo da associação cuja multiplicidade se está a indicar.  É possível colocar multiplicidades em ambos os extremos da linha de associação.  Por exemplo, se se pretender indicar que um empregado pode ter zero ou um chefe, e que um chefe tem um número arbitrário de empregados, pode-se usar:

Os extremos da linha associação também podem ser adornados com o papel que o ou os objectos da classe nesse extremo da linha têm perante cada objecto da classe no outro extremo.  Por exemplo:

 

O papel corresponde normalmente ao atributo usado no código C++ para representar a associação em causa.  Assim, possíveis implementações das classes Empregado e Chefe seriam:

class Chefe;

class Empregado {
  public:

    ...

  private:

    ...

    Chefe* chefe;
};

class Chefe : public Empregado {
  public:

    ...

  private:

    ...

    vector<Empregado*> empregados;
};

Assim sendo, é possível indicar a visibilidade dos papéis na classe a que dizem respeito.  Supondo que quer os empregados de um chefe quer o chefe de um empregado são representados por atributos privados, o diagrama deverá ser:

É possível indicar a ou as direcções nas quais é possível navegar através da associação.  Para isso colocam-se setas no ou nos topos da linha de associação.  Caso não se coloque qualquer seta, admite-se geralmente que a navegação se pode fazer em ambos os sentidos, pelo que não é usual encontrarem-se setas em os extremos da linha.  Por exemplo, se for possível obter directamente os empregados de um dado chefe, mas não for possível obter directamente o chefe de um dado empregado, o diagrama acima deve ser modificado para:

(Note-se que este diagrama não corresponde ao código C++ acima, pois nele é claro que os empregados possuem forma directa de saber quem é o respectivo chefe.)

Finalmente, note-se que se podem representar no mesmo diagrama vários tipos de relações.  Assim, sabendo-se que qualquer chefe é um empregado, teria sido mais claro incluir uma relação de generalização entre as duas classes nos diagramas acima:

Este diagrama pode-se descrever em português como se segue:

Existem duas classes, Chefe e Empregado, relacionadas.  A classe Empregado é abstracta.  Qualquer chefe (da classe Chefe) é também um empregado (da classe Empregado).  Qualquer chefe chefia um número arbitrário de empregados, que conhece por empregados.  Qualquer empregado é chefiado por um ou nenhum chefe, que conhece por chefe.  A partir de um chefe é possível obter directamente os seus empregados, mas a partir dos empregados não é possível obter directamente o respectivo chefe.

Fica claro que um diagrama representa as relações entre as classes de uma forma muito mais clara e imediata que simples texto.

2.2.3.2  Agregação

A associação de agregação representa uma relação parte/todo ou possui um.  Ao contrário do que se passa na associação simples, a agregação pode implicar o controlo do tempo de vida de um objecto por outro.  Do ponto de vista semântico, a agregação é mais forte que a simples associação, mas mais fraca que a composição.  É a relação que existe, por exemplo, entre uma pessoa e a sua aliança: a pessoa possui a aliança, mas não é constituída por ela, mesmo que entre outras partes.

A notação para a relação de agregação é idêntica à da associação simples, embora com um losango vazio colocado do lado da classe que tem a posse.  Por exemplo, a relação de posse entre uma empresa e os veículos da sua frota pode ser vista como uma associação de agregação:

Este diagrama pode-se descrever em português como se segue:

Existem duas classes, Empresa e Veículo, relacionadas.  A classe Empregado é abstracta.  Qualquer empresa possui um conjunto da veículos, a que chama frota.  Qualquer veículo pertence no máximo a uma empresa.

Em c++. a associação de agregação, tal como a associação simples, representa-se normalmente através ponteiros ou referências.  Os ponteiros usam-se quando a ligação de um objecto a outro, correspondente a uma associação entre as classes correspondentes, pode ser alterada durante a vida dos objectos.  As referências usam-se quando essa ligação é permanente, mantendo-se durante toda a vida dos objectos em causa. 

Quando existir multiplicidade maior do que um, é necessário usar algum tipo de contentor de ponteiros, por exemplo um vector.

2.2.3.3  Composição

A associação de composição é a mais forte de todas as associações.  Ela corresponde à relação é composto por um.  É a relação que existe, por exemplo, entre uma pessoa e o seu dedo: vão-se os anéis, fiquem os dedos, diz-se...  

Numa relação de composição, os tempos de vida dos objectos envolvidos estão relacionados.  Os objectos que compõem um objecto são construídos depois de construído esse objecto, ou pelo menos ao mesmo tempo, e são destruídos antes de destruído esse objecto, ou quando muito ao mesmo tempo.  Normalmente a construção e destruição dos objectos que compõem o objecto é da sua exclusiva responsabilidade, embora ocasionalmente seja delegada em terceiros.

A notação é idêntica à da agregação, embora neste caso o losango seja cheio.  Por exemplo, a relação que existe entre a classe FormaComposta (ver Secção 2.1.5) e a classe abstracta Forma é de composição:

(A propriedade {incomplete} serve para indicar que no diagrama não estão representadas todas as classes que derivam da classe base.)

Numa relação de composição, um objecto só pode ser parte da composição de um outro objecto.  Por isso, a multiplicidade colocada do lado do objecto que é composto tem de ser inferior ou igual a um.

Este diagrama pode-se descrever em português como se segue:

Existe uma classe abstracta Forma.  Existe uma classe (concreta) FormaComposta.  Qualquer forma composta é composta por um número arbitrário de formas, a que chama formas.  Qualquer forma está no máximo numa forma composta.  Há mais classes derivadas da classe Forma, mas não são mostradas no diagrama.

A composição é muitas vezes representada através de atributos.  Aliás, os atributos de uma classe em C++ que não sejam ponteiros nem referências estabelecem uma associação de composição entre o seu tipo e a classe de que são atributos.  Normalmente estes atributos são de tipos básicos do C++ ou de TAD.  Aos atributos de classes propriamente ditas, particularmente se polimórficas, acede-se normalmente através de ponteiros.  Isso é muito claro no exemplo da FormaComposta, cujo código C++ pode ser visto na Secção 2.1.5.

Finalmente, é importante referir que a fronteira entre a composição e a agregação é difícil de estabelecer.  Há muitos casos em que não é fácil classificar um associação como sendo de composição ou de simples agregação.

Diagramas de objectos

Tal como a descrição do tipo de peças de Lego existentes e dos seus possíveis encaixes não mostra o aspecto final da construção a realizar, também os diagramas de classes, com a declaração de todas as classes e suas relações, são insuficientes para mostrar como funcionará o sistema depois de implementado.  É necessário dizer que peças são necessárias exactamente e qual a sua disposição na construção final.  Os diagramas de objectos têm essa finalidade: mostrar como as instâncias das classes, ou seja, os objectos, se ligam entre si no sistema em execução.  No entanto, ao contrário das construções em Lego, que são estáticas, um programa consiste num conjunto de objectos que interagem, podendo ser construídos e destruídos de acordo com as necessidades, e podendo as respectivas ligações variar ao longo do tempo.  Se os diagramas de classes são fundamentais para declarar que tipos de entidades e respectivas relações existem no sistema, e se os diagramas de objectos são importantes para mostrar o conjunto de objectos e respectivas ligações existentes num dado instante de tempo, são necessários diagramas adicionais que permitam representar a dinâmica do sistema.  Esses diagramas serão assunto da próxima secção.  Nesta secção apresenta-se brevemente a notação UML para os diagramas de objectos.

Os diagramas de objectos representam, portanto, os objectos e respectivas ligações existentes no programa (ou existentes na realidade que se pretende modelar) num determinado instante de tempo.  Os objectos correspondem a instâncias de classes.  As ligações entre os objectos correspondem a instâncias das associações entre classes.  É pelo facto de os diagramas de objectos dizerem respeito ao estado do programa num dado instante de tempo que se dizem diagramas estáticos.

Os diagramas de objectos são muito úteis, por exemplo, para representar a estrutura dos dados existentes num programa.  A sua notação é muito simples e é apresentada nas secções que se seguem:

3.1  Objectos ou instâncias

Os objectos representam-se através de 

Os objectos ou instâncias de uma dada classe são representadas de uma forma simples: um rectângulo contendo no seu topo o nome do objecto, seguido de dois pontos e do nome da classe, sendo todos estes elementos sublinhados, para distinguir claramente a notação dos objectos da notação usada para as classes.  Por exemplo, a representação de um objecto chamado da classe Humano é:

Durante a execução de um programa é usual existirem objectos ou instâncias sem nome associado.  É o caso das instâncias não declaradas em C++ (instâncias dinâmicas ou temporárias).  É possível, por isso, omitir o nome do objecto da notação:

Da mesma forma, ocasionalmente a classe de um dado objecto pode-se inferir a partir do contexto.  Nesse caso a notação pode ser simplificada:

Em qualquer dos casos pode-se usar a propriedade {frozen} para indicar que o objecto não pode mudar de valor, i.e., que é constante:

O estado de um objecto é representável através de um comportamento adicional, no qual se listam os atributos da classe respectiva seguidos do respectivo valor.  Por exemplo:

3.2  Ligações

Um objecto está ligado a outro se lhe conhecer o paradeiro.  As ligações são instâncias das associações entre as classes dos objectos respectivos.  Podem, por isso, ser ligações simples, de agregação ou de composição, embora esse facto seja raramente mostrado nos diagramas de objectos.  Em C++, a ligação entre um objecto e outro é tipicamente representada em C++:

  1. Por um atributo directo: o segundo objecto é atributo (directo) do primeiro objecto.  Este caso ocorre apenas para ligações que são instâncias de associações de composição.
  2. Por um atributo ponteiro: um ponteiro para o segundo objecto é atributo do primeiro objecto.  Este  caso ocorre se a ligação for alterável.
  3. Por um atributo referência: um ponteiro para o segundo objecto é atributo do primeiro objecto.  Este  caso ocorre se a ligação não for alterável.

Em qualquer dos casos, se a multiplicidade a associação respectiva for maior do que um, a ligação corresponde a um item de um contentor que é atributo do primeiro objecto.

A notação usada para as ligações binárias (as ligações com maiores aridades estão fora do âmbito deste texto) é simplesmente uma linha colocada entre os objectos ligados.  Esta linha pode ter os adornos da associação correspondente, embora isso seja pouco usual, se se exceptuar a navegabilidade.  A multiplicidade é sempre omitida numa ligação, pois faz sentido apenas no caso da correspondente associação.

O exemplo abaixo mostra o diagrama de objectos de uma forma composta por quatro outras formas, das quais uma é outra forma composta por três outras formas:

Diagramas de sequência

Os diagramas de sequência representam como que um filme das interacções entre os objectos para atingir um determinado fim.  Existe um outro tipo de diagrama que também mostra evoluções temporais: o diagrama de actividade.  Este diagrama já foi falado no semestre passado.

[Incompleto, ver páginas mais recentes.]

  1. Linha de vida
  2. Indicação de actividade (actividade recursiva)
  3. Invocação de operações
  4. Retorno
  5. Construção
  6. Destruição

Diagrama relativo às agências de aluguer.  Diagrama do caso do editor?  Diagrama do caso da folha de cálculo?

Diagramas de actividade

[Incompleto, ver páginas mais recentes.]

Mencionar que foram já vistos no semestre passado.

Correspondência entre linguagem natural, C++ e UML

As tabelas abaixo tentam sintetizar as relações que existem entre a linguagem natural, a linguagem C++ e a UML para descrever objectos, classes e suas relações.

No entanto, é necessário que se perceba que, enquanto a ponte entre linguagem natural e UML ocorre durante a análise de problema e resulta num modelo UML de análise do problema, a ponte entre UML e a linguagem C++ ocorre durante a implementação da solução, sendo nesse caso o modelo UML um modelo de desenho de uma solução para o problema.

Linguagem natural

Nome comum: "humano"

UML

Classe: 

C++

Classe:

class Humano {

    ...

};

 

Linguagem natural

Nome próprio: "Zé"

UML

Instância ou objecto: 

C++

Instância ou objecto:

 

Linguagem natural

"O Zé é um humano."

UML

Variável ou constante: 

C++

Definição de variável ou constante:

Humano zé;

Humano const zé;

 

Linguagem natural

"Um humano é um mamífero.", ou
"Qualquer humano é um mamífero.".

UML

Generalização: 

 

C++

Derivação pública  de classe:

class Humano : public Mamífero {

    ...

};

 

Linguagem natural

"Qualquer empregado é chefiado por (tem [fraco]) no máximo um chefe."

UML

Associação simples: 

 

C++

Atributo ponteiro (seria contentor de ponteiros se a multiplicidade fosse superior a um):

class Empregado {

    ...

  private:
    Chefe* chefe;
};

Atributo referência caso o chefe de um empregado nunca possa mudar (irrealista):

class Empregado {

    ...

  private:
    Chefe& chefe;
};

 

Linguagem natural

"Uma empresa possui (tem [médio]) um número arbitrário de veículos."

UML

Associação de agregação: 

 

C++

Atributo contentor de ponteiros (seria ponteiro se a multiplicidade fosse inferior ou igual a um):

class Empresa {

    ...

  private:
    vector<Veículo*> frota;
};

Note-se que a associação simples e a associação de agregação geralmente se representam em C++ da mesma forma.

 

Linguagem natural

"Um humano é composto por (tem [forte]) uma cabeça, dois braços e um número arbitrário de cabelos."

UML

Associação de composição: 

 

C++

Atributos directos ou contentores de instâncias, caso os tipos Cabelo, Braço e Cabeça sejam TAD:

class Humano {

    ...

  private:
    Cabeça cabeça;
    Braço braços[2];
    /*
ou 
       Braço braço_esquerdo;
       Braço braço_direito;
    */
    vector<Cabelo> cabelos;
};

Atributos ponteiros ou contentores de ponteiros, caso os tipos Cabelo, Braço e Cabeça sejam classes propriamente ditas:

class Humano {

    ...

  private:
    Cabeça* cabeça;
    Braço* braços[2];
    /*
ou 
       Braço* braço_esquerdo;
       Braço* braço_direito;
    */
    vector<Cabelo*> cabelos;
};

Viu-se nas tabelas acima que o papel de uma associação corresponde muitas vezes a um atributo na classe C++ respectiva.  De facto, embora se representem como atributos UML os atributos da classe C++ que sejam de TAD (tipos que põem o ênfase no valor e na igualdade), representam-se como papeis de associações UML os atributos da classe C++ que são de classes propriamente ditas (que põem o ênfase na existência e na identidade).

Desenvolvimento de um modelo

[Incompleto, ver páginas mais recentes.]

A capacidade de abstracção é muito importante para aumentar a clareza de um programa.  Num programa orientado para objectos os conceitos envolvidos na descrição do problema são tipicamente representados por classes.  As instâncias da classe são conhecidas por objectos e pretendem modelar os objectos envolvidos no problema a resolver.

A abstracção é usada em programação em muitos locais.  Pode-se inclusivamente dizer que os programa correspondem muitas vezes a camadas de abstracção que permitem ao programador preocupar-se em cada nível apenas com aquilo que é essencial.

É muito importante resistir à tentação de introduzir no modelo pormenores que são irrelevantes para o problema em causa.  Ao desenvolver um programa de gestão de recursos humanos, por exemplo, a cor dos olhos dos colaboradores de uma empresa é irrelevante.  Para quê introduzir esse atributo no modelo de um colaborador?  O sexo dos colaboradores ainda é relevante, mas presumivelmente apenas para automatizar a escrita da saudação nas cartas que lhes forem dirigidas (i.e., Sr. ou Sra.).

Modelo de análise ou de desenho?

Falar em "casos de uso" como forma de obter melhor informação acerca do problema e de desenvolver alguma intuição para o desenho.

Exemplo: Programa para gestão de uma agência de aluguer de veículos.  Veículo: Automóvel mercadoria/Automóvel ligeiro/Motociclo?  Classes?  O que os distingue?  Comportamento?  Pessoa/Condutor Pessoa/Cliente.  Clientes (identificados por B.I.)  Dizer que o exemplo irá acompanhar a discussão.

Veiculo (é parte da Frota)
Automóvel (é um Veículo)
Motociclo (é um Veículo)
Frota (é constituída por Veículos)
Agência (tem um frota de veículos que podem estar alugados ou livres)

7.1  Identificação de classes ou conceitos

[Incompleto, ver páginas mais recentes.]

Substantivos ou frases substantivas.

Nomes comuns podem por vezes ser "papeis" que um objecto tem relativamente a outro.  Nesse caso, não correspondem a classes, mas sim a atributos e a associações entre duas classes.  Exemplo: Veículo, dono, Pessoa...

Frases substantivas: muitas vezes correspondem a atributos de classes.

Nomes próprios: instâncias.

7.2  Identificação de relações

Identificando as relações: verificar frases com que unimos os conceitos.  Procurar "ser", "ter", "ser constituído por", etc.

Verbos que não denotam acção.  Verbos que denotam estados, por vezes.  Mas ver predicados abaixo.

Relação "é parecido com" e "é como" leva a identificar classes que são, provavelmente, "irmãs", ou seja, especializações de uma generalização comum, provavelmente abstracta.

7.3  Identificação de operações

[Incompleto, ver páginas mais recentes.]

Verbos.

Identificando as operações: verbos que denotam acções são modificadores, normalmente, verbos que denotam estados (temporários ou permanentes) são inspectores predicados, normalmente.

(Ver classificação aspectual de verbos.)

7.4  Identificação de propriedades

[Incompleto, ver páginas mais recentes.]

Noção de propriedade: inspectores.

Inspectores não correspondem forçosamente a atributos.

7.5  Identificação de atributos

[Incompleto, ver páginas mais recentes.]

Adjectivos: Mudam para um qualquer objecto?  Reflectem o estado do conceito.  Exemplo: Veiculo alugado/livre. Não mudam?  Existe uma classe especializada (exemplo FormaColorida).  Pôr isto em relações!

Alternativa quando muda: substantivizar (?) o adjectivo!  Um veiculo alugado é um veículo com um aluguer!  Logo, existe um conceito Aluguer!  E um o veiculo pode-se associar a zero ou um alugueres!  Há ainda a possibilidade de passarem a classe: alugado vs. associado a um aluguer.

alugado adj. = p.p. de alugar: verbo.  Alugar: associar a um aluguer.

Adjectivos.  O adjectivo "verde" é representado como?  "O Veículo é verde"... O adjectivo "alugado" como é representado como?  "O veículo está alugado"?

Adjectivo: valor de uma dada propriedade.  Carro verde.  Operação inspectora para a cor, ou predicado: carro.cor(), carro.éVerde() (propriedade permanente: atributo constante), carro.estáVerde() (propriedade transitória: atributo variável, ou propriedade calculável).  Propriedade diferenciadora do conceito: carro anfíbio.  I.e., que introduz novos comportamentos e propriedades inexistentes.  Nova classe CarroAnfíbio.

O adjectivo pode mudar ao longo do tempo para um dado objecto?  Então reflectem o estado do objecto.  Exemplo: Veiculo alugado/livre.  Veículo verde/azul (pode-se pintar).

Adjectivo não pode mudar ao longo do tempo para um dado objectos?  Então pode existir uma classe especializada (exemplo FormaColorida).  Alternativamente pode-se guardar um estado imutável: uma constante.  Se os veículos não se pudessem pintar, mais valia ter um veículo com uma cor constante que fazer classes especializadas VeiculoVerde, VeiculoAmarelo, etc...

Alternativa quando muda: substantivizar (?) o adjectivo!  Um veiculo alugado é um veículo com um aluguer!  Logo, existe um conceito Aluguer!  E um o veiculo pode-se associar a zero ou um alugueres!  E um aluguer pode-se referir a vários veículos!  Interessante!

Discutir um possível estado:
Veículos:
2 Motociclos e 2 Automóveis.
2 Alugueres:
1.  1 Motociclo
2.  2 Automóveis
3 Clientes, um deles sem aluguer.

Leitura recomendada

Capítulos 1, 4, 5 (só parte de diagramas de sequência), partes do Capítulo 6, e Capítulo 11 de [3].

Referências

[1]  "OMG Unified Modeling Language Specification", versão 1.4, Setembro de 2001.  http://www.omg.org/cgi-bin/doc?formal/01-09-67

[2]  James Rumbaugh, Ivar Jacobson e Grady Booch, "The Unified Modeling Language Reference Manual", Addison-Wesley, Reading, Massachusetts, 1999.

[3]  Martin Fowler e Kendall Scott, "UML Distilled: A Brief Guide to the Standard Object Modeling Language", 2ª edição, Addison-Wesley, Reading, Massachusetts, 1999.

[4]  Grady Booch, James Rumbaugh e Ivar Jacobson, "The Unified Modeling Language User Guide", Addison-Wesley, Reading, Massachusetts, 1999.