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