Aula 8: Modelos de Objetos e Invariantes
Esta aula irá consolidar muitas das idéias fundamentais das aulas anteriores, nestas aulas foram
abordados assuntos sobre objetos, representações e abstração. Iremos explicar em detalhes a
notação gráfica de modelagem de objetos e rever as invariantes de representação, funções de
abstração e exposição de representação. Após a leitura desta aula, talvez você queira retornar às
aulas anteriores para revisá-las, pois elas contém mais detalhes a respeito dos exemplos discutidos
aqui.
8.1 Modelos de Objetos
Um modelo de objeto é uma descrição de uma coleção de configurações. Na aula de hoje, iremos
analisar modelos de objetos na forma de código, nos quais as configurações são estados de um
programa. Veremos mais tarde no curso que a mesma notação pode ser usada de maneira mais
genérica para descrever qualquer tipo de configuração – como o formato de um sistema de arquivos,
uma hierarquia de segurança, uma topologia de rede, etc.
As noções básicas que estão por trás dos modelos de objetos são incrivelmente simples: conjuntos
de objetos e relações entre eles. O que os estudantes acham difícil é aprender como construir um
modelo útil: como capturar as partes interessantes e mais complicadas de um programa, e como não
ser levado a modelar partes irrelevantes, terminando com um modelo imenso, mas com pouca
abrangência ou, pelo contrário, acabar com um modelo bastante simples, mas que é inútil.
Os modelos de objetos (object model - OM) e módulos de diagrama de dependência (module
dependency diagram - DMM) contêm, ambos, retângulos e flechas. A similaridade termina aqui.
Bem, eu admito que existam mais conexões entre um OM e um MDD de um mesmo programa.
Mas, à primeira vista, é melhor encará-los como completamente diferentes. O MDD aborda a
estrutura sintática – quais descrições textuais existem, e como elas estão relacionadas umas com as
outras. O OM aborda a estrutura semântica – quais configurações são criadas em tempo de
execução e quais as propriedades que estas configurações possuem.
8.1.1 Classificação
Um modelo de objeto expressa dois tipos de propriedades: classificação de objetos e
relacionamentos entre objetos. Para expressar classificação, desenhamos um retângulo para cada
classe de objetos. Em um modelo de objeto na forma de código, estes retângulos irão corresponder a
classes e interfaces em Java; em uma definição mais geral, eles representam classificações
arbitrárias.
86
Uma flecha com uma ponta grande e fechada da classe A para a classe B indica que A representa um
subconjunto de B: ou seja, todo A, também é um B. Para mostrar que dois retângulos representam
subconjuntos distintos, fazemos com que eles compartilhem a mesma ponta de flecha. No diagrama
mostrado acima, LinkedList e ArrayList são subconjuntos disjuntos de List.
Em Java, todas as declarações implements e extends resultam em um relacionamento de
subconjuntos de um modelo de objetos. Esta é uma propriedade do sistema de tipos: se um objeto o
é criado com um construtor de uma classe C, e C estende D, então o é considerado como também
possuindo o tipo D. O diagrama acima mostra o modelo de objetos à esquerda.
O diagrama à direita é um diagrama de dependência modular. Seus retângulos representam
descrições textuais – o código das classes. As flechas, como você se lembra, representam a relação
‘satisfaz’. Portanto, a flecha que parte de ArrayList para List indica que o código de ArrayList
satisfaz à especificação List. Em outras palavras, objetos da classe ArrayList se comportam como
listas abstratas. Esta é uma propriedade tênue que só é verdadeira por causa dos detalhes do código.
Como veremos adiante na aula sobre derivação, é fácil enganar-se com esta característica, e criar
uma classe que estende ou implementa uma outra sem que haja uma relação ‘satisfaz’ entre elas.
(no MDD, o compartilhamento da ponta da flecha não tem significado).
8.1.2 Campos
Uma flecha com a ponta aberta que parte de A para B indica um relacionamento entre objetos de A e
de B. Por poder haver muitos relacionamentos entre duas classes, portanto, atribuímos nomes a
estes relacionamentos e rotulamos as flechas com estes nomes. Um campo f do tipo B em uma
classe A resulta em uma flecha de A para B rotulada de f (o nome do campo). A parte da flecha onde
fica sua ponta é denominado ‘fim da flecha’, ou target, e é para onde ela aponta; a parte da flecha
onde não há ponta é denominado ‘início da flecha’, ou source, e é a partir de onde ela aponta. Uma
flecha rotulada com o nome de um campo de um objeto é denominada ‘flecha de campo’, ou flecha
que corresponde a um campo.
87
Por exemplo, o seguinte código produz estruturas que podem ser ilustradas pelo diagrama mostrado
abaixo (ignore, por enquanto, as marcas no final das flechas):
class LinkedList implements List {
Entry header;
…
}
class Entry {
Entry next;
Entry prev;
Object elt;
…
}
8.1.3 Multiplicidade
Até aqui, vimos classificação de objetos em classes, e relações que demonstram que objetos de uma
classe podem estar relacionados com objetos de uma outra classe. Uma questão fundamental sobre
uma relação entre classes é a multiplicidade: quantos objetos em uma classe podem estar
relacionados com um determinado objeto de uma outra classe.
88
Os símbolos de multiplicidade são:
· * (zero ou mais)
· + (um ou mais)
· ? (zero ou um)
· ! (exatamente um).
Quando um símbolo é omitido, * é o símbolo padrão assumido (o que não indica muita coisa). A
interpretação destas marcas é: quando há uma marca n no fim B de uma flecha de campo f que parte
da classe A para a classe B, existem n membros da classe B associados por f com cada A. A
interpretação funciona da mesma forma para o outro lado: se há uma marca m no início A de uma
flecha de campo f que parte de A para B, cada B é mapeado por m membros da classe A.
No final da flecha – onde fica a ponta da flecha – a multiplicidade indica quantos objetos uma
variável pode referenciar. Por enquanto, não temos uso nenhum para as marcas * e +, mas veremos,
mais adiante, como elas são utilizadas com campos abstratos. A escolha de ? ou ! depende da
possibilidade de um campo poder, ou não, ser nulo.
No início da flecha, a multiplicidade indica quantos objetos podem apontar para um dado objeto.
Em outras palavras, ela nos dá informações a respeito de compartilhamento. Vamos analisar
algumas das flechas e ver o que suas multiplicidades indicam:
· Para o campo header, o símbolo ! no final da flecha indica que todo objeto da classe List está
relacionado a exatamente um objeto na classe Entry pelo campo header. O ? no início da
flecha indica que cada objeto Entry é o objeto header de no máximo um objeto List.
· Para o campo element, o ? no final da flecha indica que o campo element de um objeto Entry
aponta para zero ou um objeto da classe Object. Em outras palavras, ele pode ser null: um
objeto List pode armazenar referências nulas. A falta de um símbolo no início da flecha indica
que um objeto pode ser apontado pelo campo element de qualquer número de objetos Entry.
Em outras palavras, uma lista pode armazenar duplicatas.
· Para o campo next, o ! no final e no início da flecha indica que o campo next de todo objeto
Entry aponta para um objeto Entry, e todo objeto Entry é apontado pelo campo next de um
objeto Entry.
8.1.4 Mutabilidade
Até aqui, todas as características do modelo de objeto que descrevemos fazem restrições a estados
individuais. Restrições de mutabilidade descrevem como os estados podem ser alterados. Para
demonstrar que uma restrição de multiplicidade é violada, precisamos mostrar apenas um único
estado, mas para demonstrar que uma restrição de mutabilidade é violada, precisamos mostrar dois
estados, representando o estado antes e o estado depois da alteração global de estado.
89
Restrições de mutabilidade podem ser aplicadas para ambos, conjuntos e relações, mas, por
enquanto, iremos considerar apenas uma forma limitada na qual uma barra (veja a figura acima)
opcional pode ser usada para marcar o final de uma flecha de um campo. Quando presente, esta
marca indica que um objeto com o qual um determinado objeto está relacionado, deve ser sempre o
mesmo. Neste caso, dizemos que o campo é imutável, estático ou, mais precisamente, target static
(ou estático no final da flecha, pois mais tarde iremos dar significado para uma barra no início de
uma flecha).
Em nosso diagrama, por exemplo, a barra no final da relação header indica que um objeto List, uma
vez criado, sempre aponta através de seu campo header para o mesmo objeto Entry. Um objeto é
imutável se todos os seus campos são imutáveis. Uma classe é dita ser imutável se seus objetos são
imutáveis.
8.1.5 Diagramas de Instância
O significado de um modelo de objeto é uma coleção de configurações – todas as que satisfazem as
restrições do modelo. Estas configurações podem ser representadas através de diagramas de
instância, ou através de snapshots (um snapshot pode ser entendido como uma representação
simplificada), que são, simplesmente, grafos que consistem de objetos e referências que os
conectam. Cada objeto é rotulado com a classe (a mais específica) da qual pertencem. Cada
referência é rotulada com o campo que representa. O relacionamento entre um snapshot e um
modelo de objeto é como o relacionamento entre uma instância de um objeto e uma classe, ou o
relacionamento entre uma sentença e uma gramática.
A figura mais abaixo mostra um snapshot legal (que pertence à coleção representada pelo modelo
de objeto dos exemplos acima) e um snapshot ilegal (que não pertence). Existe, claro, um infinito
número de snapshots legais, pois se pode ter uma lista de qualquer tamanho.
Um exercício útil para se checar a compreensão do significado do modelo de objeto é examinar o
snapshot ilegal e determinar quais restrições são violadas. As restrições são as restrições de
multiplicidade e as restrições implícitas na colocação das flechas. Por exemplo, já que a flecha do
campo header parte de List para Entry, um snapshot que contém uma flecha de campo partindo de
um Entry para um Entry deve estar errada. Note que as restrições de mutabilidade não são
relevantes aqui; elas indicam quais transições são permitidas.
8.2 Modelos de Programas Completos
Um modelo de objeto pode ser usado para mostrar qualquer porção do estado de um programa. No
90
exemplo List, logo abaixo, nosso modelo de objeto mostrou apenas os objetos envolvidos na
representação do tipo abstrato List. Mas, de fato, modelos de objetos são muito úteis quando eles
incluem objetos de muitos tipos, pois eles capturam o inter relacionamento entre os objetos, o que,
muitas vezes, é a essência de um projeto orientado a objetos.
Suponha, por exemplo, que estamos construindo um programa para controlar preços de ações da
bolsa de valores. Podemos projetar um tipo de dado denominado Portfolio que representa a carteira
de um determinado tipo de ações da bolsa. Um Portfolio contém uma lista de objetos do tipo
Position, cada um dos quais possuindo um símbolo Ticker para uma dada ação, uma contagem do
número de ações detidas na carteira, e um valor corrente para aquela ação. O objeto Portfolio
também mantém o valor total de todas as posições indicadas pelos objetos Positions.
91
O modelo de objeto abaixo mostra este esquema. Perceba, agora, como os objetos Entry apontam
para objetos Position: eles pertencem a uma lista (objeto List) de objetos Position, que não é uma
lista qualquer. Devemos permitir diversos retângulos com o rótulo List no mesmo diagrama,
correspondendo a diferentes tipos de List. E, conseqüentemente, devemos ser um pouco cuidadosos
sobre como interpretar as restrições implícitas em uma flecha correspondente a um campo. A flecha
rotulada element que parte de Entry para Position em nosso diagrama, por exemplo, não significa
que todo objeto Entry do programa aponta para um objeto Position, mas, ao invés disso, significa
que todo objeto Entry que está contido em um objeto List que está contido em um objeto Portfolio
aponta para um objeto Position.
92
8.3 Pontos de Vista Abstratos e Concretos
Suponha que queiramos implementar um conjunto na forma de um tipo de dado abstrato. Em
algumas circunstâncias – por exemplo, quando temos um monte de conjuntos bem pequenos –
representar um conjunto como uma lista é uma opção aceitável. A figura abaixo mostra três
modelos de objetos. Os dois primeiros são duas versões de um tipo denominado Set, um
representado com um LinkedList e um com um ArrayList. (Uma questão para o leitor astuto: por
que o campo header na representação com LinkedList é imutável, enquanto que o campo
elementData na representação com ArrayList não é?).
Se o que nos interessa é como o tipo Set é representado, podemos querer mostrar estes modelos de
objetos. Mas se nosso interesse é o papel desempenhado pelo tipo Set em um programa maior, e não
queremos nos preocupar com a escolha da representação, iríamos preferir um modelo de objeto que
não exiba a diferença entre estas duas versões. O terceiro modelo de objeto, o que está do lado
direito, é o modelo que procuramos. Ele substitui todos os detalhes da representação de Set com um
único campo denominado elements que conecta objetos Set diretamente a seus elementos. Este
campo não corresponde a um campo que é declarado em Java na classe Set; trata-se de um campo
abstrato ou de especificação.
93
Existem, portanto, muitos modelos de objeto que podem ser desenhados para o mesmo programa.
Você pode escolher o quanto modelar dos estados do programa e, para aquela parte dos estados, o
quanto abstrata será sua representação. No entanto, existe em nível particular de abstração que pode
ser considerado normativo. Este é o nível de abstração que é apresentado pelos métodos no código.
Por exemplo, se algum método da classe Set retorna um objeto do tipo LinkedList, não teria muito
sentido realizar-se a abstração da classe LinkedList. Mas, do ponto de vista de um cliente de Set, é
impossível saber qual está sendo usado, um LinkedList ou um ArrayList. Teria mais sentido exibir,
ao invés disso, o campo abstrato elements.
Um tipo abstrato pode ser representado por muitas representações distintas. Da mesma forma, um
tipo pode ser usado para representar muitas diferentes abstrações. Uma lista encadeada, por
exemplo, pode ser usada para implementar uma pilha: diferente da interface genérica List,
LinkedList oferece os métodos addLast e removeLast. E, segundo seu projeto, LinkedList
implementa a interface List, que representa uma seqüência de elementos abstrata. Podemos,
portanto, encarar a classe LinkedList, por si própria, de maneira mais abstrata com um campo
elems[] escondendo a estrutura interna Entry, na qual o [] indica que o campo elems representa uma
seqüência indexada. A figura acima mostra estes relacionamentos: uma flecha significa “pode ser
usado para representar”. Obviamente, o relacionamento não é simétrico. O tipo concreto,
geralmente, possui mais informação no seu conteúdo: uma lista pode representar um conjunto, mas
um conjunto não pode representar uma lista, pois um conjunto não pode conter informação de
ordenação ou duplicatas. Perceba também que nenhum tipo é, inerentemente, abstrato ou concreto.
Estes conceitos são relativos. Uma lista é abstrata com relação a uma lista encadeada utilizada para
representá-la, mas é concreta com relação a um conjunto que representa.
8.3.1 Funções de Abstração
Podemos demonstrar como os valores do tipo concreto são interpretados como valores abstratos por
94
uma função de abstração, assim como foi explicado na aula anterior. Lembre-se que o mesmo valor
concreto pode ser interpretado de diferentes maneiras, portanto, a função de abstração não é
determinada pela escolha dos tipos abstrato e concreto. Trata-se de uma decisão do projeto, e
determina como o código é escrito para métodos do tipo abstrato. Em uma linguagem sem objetos
mutáveis, na qual não temos que nos preocupar com compartilhamento, podemos interpretar os
valores abstratos e os valores concretos como sendo apenas isto - valores. A função de abstração é,
então, uma função matemática direta. Pense, por exemplo, nas várias formas através das quais os
inteiros são representados como bitstrings. Cada uma destas representações pode ser descrita como
uma função de abstração de bitstring para integer. Uma codificação que coloca o menos
significativo dos bits primeiro, por exemplo, pode ter uma função de mapeamento como:
A (0000) = 0
…
A (0001) = 8
A (1001) = 9
Mas em um programa orientado a objetos, no qual temos que nos preocupar em como as alterações
em um objeto através de um caminho (um método, por exemplo) podem afetar a visão deste objeto
através de um outro caminho, os ‘valores’ são, de fato, como pequenos subgrafos. O caminho mais
direto para se definir a função de abstração nestas circunstâncias é fornecer uma regra para cada
campo abstrato, explicando como ele é obtido a partir dos campos concretos. Por exemplo, na
representação LinkedList de Set, podemos escrever:
s.elements = s.list.header.*next.element
para expressar que para cada objeto s da classe, os objetos apontados pelo campo abstrato elements
são os objetos obtidos ao se percorrer list (o objeto List), em seguida header (para o primeiro objeto
Entry), e, então, uma ou mais travessias pelo campo next (até os demais objetos Entry) e, para cada
um deles, seguir o campo element uma vez (até o elemento Object do objeto Entry). Perceba que
esta regra é, por si só, uma espécie de modelo de objeto invariante: a regra diz a você onde é legal
colocar flechas rotuladas element dentro de um snapshot.
Em geral, um tipo abstrato pode ter qualquer número de campos abstratos, e a função de abstração é
especificada ao se fornecer uma regra para cada um destes campos.
Na prática, exceto para alguns poucos tipos container, funções de abstração são geralmente mais
problemáticas do que úteis. No entanto, compreender a idéia de uma função de abstração é valioso,
pois elas lhe ajudam a solidificar sua compreensão a respeito da abstração de dados. Portanto, você
deve estar pronto para escrever uma função de abstração se for necessário. A fórmula booleana em
95
CNF da aula 6 é um bom exemplo de um tipo abstrato que, realmente, necessita de uma função de
abstração. Neste caso, sem uma firme compreensão da função de abstração, será difícil alcançar um
código correto.
8.3.2 Invariantes de Representação
Um modelo de objeto é uma espécie de invariante: uma restrição válida para toda a vida de um
programa. Uma invariante de representação, ou 'invariante rep', como discutido na aula 6, é uma
espécie particular de invariante que indica se uma representação de um objeto abstrato está bem
formada. Alguns aspectos da invariante rep podem ser expressos em um modelo de objeto. Mas
existem outros aspectos que não podem ser expressos graficamente. E nem todas as restrições de
um modelo de objeto são invariantes rep.
Uma invariante rep é uma restrição que pode ser aplicada para um único objeto de um tipo abstrato,
indicando a você se a representação deste objeto está bem formada. Portanto, a invariante envolve
sempre exatamente um objeto do tipo abstrato em questão, e quaisquer objetos que possam ser
alcançados a partir de sua representação.
Podemos traçar um contorno ao redor de uma parte do modelo de objeto para indicar que
uma determinada invariante de representação se refere a esta parte. Este contorno agrupa os
objetos de uma representação em torno do objeto abstrato destes objetos. Por exemplo, para
a invariante rep de LinkedList vista como um List (isto é, uma seqüência de elementos
abstrata), este contorno inclui os elementos Entry. Não surpreendente, as classes dentro do contorno
são, exatamente, as classes abstraídas pelo campo elems[]. E, similarmente, a invariante para a
classe ArrayList engloba o array nela contido.
96
Os detalhes das invariantes rep foram discutidos na aula 6: para LinkedList, por exemplo, as
invariantes incluem restrições como a necessidade dos objetos Entry formarem um ciclo, o header
estar sempre presente, além de ser necessário um campo element com valor null, etc.
Vamos agora lembrar por que a invariante rep é útil - e por que não é apenas um conceito teórico,
mas uma ferramenta prática:
· A invariante rep informa, para um determinado local, qual valor legal leva à uma representação
bem formada. Se você estiver modificando código de um tipo abstrato de dados, ou escrevendo
um novo método, você precisa saber quais invariantes precisam ser estabelecidas e em quais
você pode se basear. A invariante rep indica a você tudo que você precisa saber; isto é o que se
pretendo com o raciocínio modular. Se não há registro explícito de nenhuma invariante rep,
você precisa ler o código de todos os métodos!
· A invariante rep captura a essência do projeto de representação. A presença da entidade header
e a forma cíclica do Java LinkedList, por exemplo, são boas decisões do projeto que fazem os
métodos mais fáceis de serem codificados e de maneira uniforme.
· Como veremos em uma aula subseqüente, a invariante rep pode ser usada para detectar bugs
em tempo de execução em uma espécie de 'programação defensiva'.
8.3.3 Exposição de Representação
A invariante rep possibilita o raciocínio modular, desde que a representação seja modificada apenas
dentro da classe do tipo abstrato de dado. Se existe a possibilidade de alterações via código externo
à classe, será necessário examinar o programa inteiro para se certificar que a invariante rep está
sendo mantida. Esta situação incômoda é denominada exposição de representação. Em aulas
passadas já vimos exemplos claros em pequenos exemplos. Um exemplo simples ocorre quando um
tipo abstrato de dados fornece acesso direto a um dos objetos internos abrangidos pela invariante
rep. Por exemplo, toda a implementação da interface List (aliás, da interface mais geral denominada
Colecction) deve fornecer um método:
public Object [] toArray ()
que retorna a lista como um array de elementos. A especificação deste método diz:
O array retornado será ‘seguro’ no sentido de que nenhuma referência a ele é mantida por este
objeto collection. (em outras palavras, este método deve alocar um novo array mesmo que o
objeto seja baseado em um array já existente). O código que o invoca, portanto, é livre para
modificar o array.
A implementação do ArrayList, portanto, é implementado como:
97
private Object elementData[];
…
public Object[] toArray() {
Object[] result = new Object[size];
System.arraycopy(elementData, 0, result, 0, size);
return result;
}
perceba como o array interno é copiado para que se produza o resultado. Se, ao invés disso, o array
fosse retornado imediatamente, da seguinte forma:
public Object[] toArray() {
return elementData;
}
teríamos uma exposição de representação. Modificações subseqüentes no array, ocorridas do lado
de fora do tipo abstrato iriam afetar a representação interna (de fato, neste caso, há um vazamento
fraco de invariante rep, tal que uma alteração no array não irá comprometer sua consistência, apenas
produzirá o efeito de se ver os valores da lista abstrata serem modificados sempre que o array for
modificado externamente. Mas pode-se imaginar uma versão do ArrayList que não armazena
referências nulas; neste caso, atribuir-se um valor nulo para um elemento do array iria arruinar a
invariante rep).
Aqui apresentaremos um exemplo mais refinado. Suponha que queiramos implementar um tipo
abstrato para listas que não possuem duplicatas, e que vamos definir o conceito de duplicação
através do método equals implementado pelos elementos Objects. Agora nossa invariante rep irá
expressar, para a representação de uma lista encadeada, por exemplo, que nenhum par de objetos
Entry possa ter elementos cujo teste de igualdade (equals) retorne true. Se os elementos são
mutáveis, e o método equals examina campos internos, é possível que uma alteração de um
elemento irá fazer com que o elemento alterado se torne igual a um outro. Portanto, o acesso
propriamente dito aos elementos irá constituir uma exposição de representação.
Na verdade, este exemplo não é diferente do exemplo anterior, no sentido de que o problema do
acesso a um objeto que está dentro do contorno, ou seja, dentro da abrangência da invariante rep. A
invariante, neste caso, possui um contorno que engloba os elementos do tipo Object, pois ela
depende do estado interno dos elementos. A igualdade cria problemas complicados; como veremos
na próxima aula.
98
Download

Aula 8: Modelos de Objetos e Invariantes