A extensibilidade como ideia orientadora da escrita de programas orientados pelos objetos.
O uso de testes explícitos de tipo compromete a extensibilidade dos programas. A falta de modularidade compromete a extensibilidade dos programas.
Ser minimalista.
Fatorizar na medida certa.
Para começar, parte do código é concebido para ser reutilizado em mais do que um contexto dentro do próprio programa, porventura também no futuro.
Outro objetivo é que a evolução do programa deve ser conseguida sem se alterar o que já foi escrito e verificado. Nós não gostamos de alterar código já escrito e verificado, porque isso obriga a retomar o contexto mental da escrita original do código, porventura alguns anos mais tarde. É grande a probabilidade de nos esquecermos de alguns invariantes e de introduzir erros.
Quanto precisamos de adicionar a um programa funcionalidade nova que ele não está preparado para receber, então temos de refatorizar o programa (isto é restruturar os conceitos e adaptar o código). Isso costuma ser trabalhoso e introduzir bugs. A grande vantagem da extensibilidade é que o programa aguenta mais tempo antes de precisar de ser refatorizado.
A extensibilidade obtém-se pela via da abstração:
Não é obrigatório escrever código extensível, mas muitas vezes desejamos escrever software extensível. As principais razões são as seguintes:
Curiosamente, muitas vezes escreve-se código extensível, mesmo quando há convencimento de que não haverá extensões futuras do programa. Porquê? Por causa dos três últimos pontos referidos atrás.
O sistema organizado em torno de abstrações naturais fica em princípio extensível assumindo que não há interação entre objetos, ou seja as operações disponíveis numa classe aplicam-se a this, sem envolver outros objetos. Esta é a situação que apareceu nos exemplos finais da aula teórica 21 (exemplo das classes Point1, Point2, Point3 e exemplo das Expressões Algébricas). Também irá aparecer nos exercícios da aula prática 12 (exercícios sobre sucessões matemáticas).
Eis agora um exemplo dum pequeno programa em Java que se considera extensível. Em linguagens como o OCaml ou o C, não existe suporte assumido para extensibilidade. Contudo podemos tentar organizar o mesmo programa em torno de abstrações naturais e obter algo que se pode chamar de extensibilidade limitada. Eis um exemplo do programa reescrito em C usando extensíbilidade limitada. Repare que existiu o cuidade de definir o tipo Shape, que representa um conceito abstrato.
Agorta atençãpo: quando as operações envolvem o objeto this mais outros objetos de tipo abstrato então a fatorização, só por si, não é suficiente para obter extensibilidade. Provavelmente, esta será a primeira vez que você vai ver este problema a ser discutido com detalhe.
Tal código nunca pode ser extensível, pois a sua lógica está comprometida com os tipos concretos existentes: ou seja, esse código não conseguirá lidar com novos tipos concretos a criar no futuro. Nessa situação, para incorporar novos tipos, seria necessário reescrever o código para tratar mais casos concretos, o que significa que o código não seria extensível.
Por outras palavras: cada objeto trata de si próprio e respeita o que compete aos outros objetos.
Código duma classe A que está escrito com uma lógica que implica impor decisões aos objetos duma classe B, se calhar vai ter de ser reescrito quando se adicionar uma nova classe C, pois o mesmo tipo de decisões poderá precisar de se estender aos objetos da classe C.
Vejamos algumas técnicas que permitem evitar os testes explícitos de tipo concreto.
Por exemplo, num jogo baseado numa matriz bidimensional, em que vários monstros perseguem um herói, o que é que um monstro deverá fazer quando se cruza com outra personagem? Imagine que se trata duma função meets(vizinho) da classe Monstro.
Em conclusão, conseguimos escrever código extensível introduzindo o conceito abstrato de apetitoso.
Relativamente a detalhes de implementação, tipicamente define-se o método isTasty na raiz da hierarquia de classes retornando o valor false. Desta forma, por omissão, as personagens não são apetitosas. As personagens que forem apetitosas redefinem o método para retornar true.
Há uma grande tendência natural para programar de acordo com a visão do mundo fechado, porque geralmente o enunciado descreve personagens concretas e nós deixamo-nos levar por isso. É preciso fazer um esforço para programar o que o enunciado pede, mas usando conceitos abstratos inventados por nós, tais como o conceito de apetitoso.
interface Tasty { /* empty */ } class Hero extends Actor implements Tasty { ... }Depois para testar se o vizinho é apetitoso escreve-se:
vizinho instanceof TastyRepare que não há nada de errado em usar instanceof, neste caso. Aqui não estamos a testar o tipo concreto dum objeto. Aqui estamos a testar uma propriedade abstrata. Estas interfaces vazias especiais costumam ser designadas de marker interfaces.
Esta técnica não funciona em JavaScript. Podíamos tentar usar classes abstratas, mas uma classe não pode herdar de duas classes e nunca poderíamos ter um objeto com duas propriedades simultâneas e autónomas.
Neste caso, convém associar um nível alimentar a cada tipo de personagem e estabelecer a seguinte regra: uma personagem pode comer outra só no caso do nível alimentar da primeira ser superior ao da segunda. Concretamente, um objeto pode comer o seu vizinho se:
this.feedLevel() > vizinho.feedLevel()
Por exemplo, para efeitos de reprodução, um animal pode precisar de saber se um outro animal, seu vizinho, é do mesmo tipo. Como fazer isso de forma geral, sem ter de referir o tipo concreto?
Faz-se assim em JavaScript:
this.constructor == vizinho.constructorEm Java:
getClass() == vizinho.getClass()Em C++:
#include <typeinfo> typeid(*this) == typeid(*vizinho)Em C:
this.kind == vizinho.kind
Por exemplo, no contexto dum jogo, uma personagem P, controlada pelo teclado, empurra outra personagem Q.
Com a abordem correta, estamos preparados para criar novas personagens com comportamentos diferentes no método push, sem ser preciso estar a reescrever o código do objeto P.
Na inicialização, temos de abrir uma exceção no requisito da extensibilidade.
Por vezes há a tentação de começar a introduzir funcionalidades adicionais "porque poderão ser precisas no futuro", apesar de não serem precisas imediatamente. A partir duma certa altura, já não se está a programar, digamos, um jogo concreto, mas sim uma framework genérica para um determinado tipo de jogos. Isso é uma perda de tempo e só vai complicar o programa desnecessariamente.
Recomenda-se a escrita dum programa que faça apenas aquilo que o enunciado pede. Recorre-se a um pequeno conjunto conceitos abstratos, inventados por nós, mas sem introduzir nenhum conceito abstrato desnecessário. O programa fica preparado para crescer. Mas o futuro é que dirá a forma como vai crescer.
No limite da fatorização, pode fazer-se migrar todo o código para a classe na raiz da hierarquia, deixando apenas os construtores nas outras classes. A classe de raiz fica muito geral e muito complexa, cheia de variáveis que servem para representar as diferentes propriedades de diversos tipos de entidades ao mesmo tempo. Os métodos também ficam muito complexos e cheios de ifs porque têm de lidar com muitos casos.
Este código é uma confusão por não ter uma estratificação lógica dos conceitos. Também não é extensível, pois para incorporar um novo tipo de entidade, seria preciso alterar o código da raiz para tratar mais um caso.