Subtipos e Subclasses - mit

Propaganda
Subtipos e Subclasses
Aula 15 do curso 6.170
15 de outubro de 2001
Sumário
1 Subtipos
32
2 Exemplo: Bicicletas
3 Exemplo: Quadrado e retângulo
4 Princípio de substituição
5 Subclasses e subtipos Java
33
37
38
40
6 Interfaces Java
41
Leituras necessárias: Capítulo 7 do livro Program Development in Java da Bárbara Liskov.
Verifique o seu livro texto Java sobre os detalhes de Java como, por exemplo, tipos abstratos (nem
todos os métodos são implementados, e nenhum objeto pode ser instanciado), detalhes sobre
interfaces, e modificadores de acesso (public, private, protected, default). Eles não serão discutidos
nesta aula.
1 Subtipos
Nós vimos que A é um B se todo objeto A também é um objeto B. Por exemplo, todo automóvel é um
veículo, e toda bicicleta é um veículo, e todo pula-pula é um veículo: todo veículo é um meio de
transporte, assim como todo animal de carga. Nós apresentamos este subconjunto de relacionamento
em um diagrama de dependência modular:
Este subconjunto de relacionamento é uma condição necessária, mas não suficiente para um
relacionamento de subtipificação. O tipo A é um subtipo do tipo B quando a especificação de A
implica na especificação de B. Isto é, qualquer objeto (ou classe) que satisfaça a especificação de A
também satisfaz a especificação de B, pois a especificação de B é mais fraca.
Outra maneira de explicar isto é que em qualquer lugar do código, se você espera um objeto B, um
objeto A é admissível. È garantido que o código escrito para funcionar com o objeto B (e para
depender de suas propriedades) continua a funcionar se o objeto A for fornecido ao invés dele; além
disso, o comportamento será o mesmo, se forem considerados apenas os aspectos do comportamento
de A que também estão inclusos no comportamento de B. (O A pode introduzir novos comportamentos
que o B não tenha, mas isto apenas modifica os comportamentos existentes de B em certas maneiras;
veja em seguida.)
2 Exemplo: Bicicletas
Suponha que nós tenhamos uma classe para representar as bicicletas. Aqui está uma implementação
parcial dessa classe:
class Bicycle{
private int framesize;
private int chainringGears;
private int freewheelGears;
...
// retorna o número de marchas da bicicleta
public int gears() { return chainringGears * freewheelGears; }
// retorna o preço da bicicleta
public float cost() { ... }
// retorna o imposto de venda que incide sobre a bicicleta
public float salesTax() { return cost() * .0825; }
// execução: transporta o ciclista do trabalho para casa
public void goHome() { ... }
...
}
Uma nova classe, representando bicicletas com lanternas dianteiras, pode se adaptar a situações
noturnas (ou as madrugadas).
class LightedBicycle{
private int framesize;
private int chainringGears;
private int freewheelGears;
private BatteryType battery;
...
// retorna o número de marchas da bicicleta
public int gears() { return chainringGears * freewheelGears; }
// retorna o preço da bicicleta
float cost() { ... }
// retorna o imposto de venda que incide sobre a bicicleta
public float salesTax() { return cost() * .0825; }
// execução: transporta o ciclista do trabalho para casa
public void goHome() { ... }
// execução: substitui a bateria existente pelo argumento b
public void changeBattery(BatteryType b);
...
}
Copiar todo o código é cansativo e passível de erro. (O erro pode ser causado pela falha na realização
de uma cópia correta ou pela falha na realização das alterações necessárias.) Adicionalmente, se um
erro é encontrado em uma versão, é fácil esquecer de realizar o conserto em todas as versões do
código. Finalmente, é muito difícil compreender a distinção entre duas classes apenas procurando por
diferenças em uma massa de similaridades.
A linguagem Java e outras linguagens de programação utilizam o conceito de subclasse para superar
essas dificuldades. A especialização de classes utilizando herança de classe permite reutilizar
implementações e sobrescrever os métodos.
Uma implementação melhor da classe LightedBicycle é a seguinte
class LightedBicycle extends Bicycle{
private BatteryType battery;
...
// retorna o preço da bicicleta
float cost() { return super.cost() + battery.cost(); }
// execução: transporta o ciclista do trabalho para casa
public void goHome() { ... }
// execução: substitui a bateria existente pelo argumento b
public void changeBattery(BatteryType b);
...
}
A LightedBicycle não precisa implementar os métodos e campos que aparecem na sua
superclasse Bicycle; as versões de Bicycle são automaticamente utilizadas pelo Java quando elas
não são sobrescritas na subclasse.
Considere a seguinte implementação do método goHome (juntamente com o fornecimento de
especificações mais completas). Se estas forem as únicas alterações, as classes LightedBicycle e
RacingBicycle são subtipos de Bicycle? (Mais adiante nós falaremos sobre o conceito de
subtipos; nós retornaremos as diferenças entre as subclasses Java, os subtipos Java, e os verdadeiros
subtipos mais tarde.)
class Bicycle{
...
// requer: velocidade_do_vento < 20mph && luz_do_dia
// execução: transporta o ciclista do trabalho para casa
public void goHome() { ... }
}
class LightedBicycle{
...
// requer: velocidade_do_vento < 20mph
// execução: transporta o ciclista do trabalho para casa
void goHome() { ... }
}
class RacingBicycle{
...
// requer: velocidade_do_vento < 20mph && luz_do_dia
// execução: transporta o ciclista do trabalho para casa
//
em um período de tempo < 10 minutos
//
&& faz o ciclista suar
void goHome() { ... }
}
Para responder a essa pergunta, relembre a definição de subtipificação: um objeto do subtipo pode ser
substituído em qualquer lugar onde o código espera um objeto do supertipo? Se sim, o relacionamento
de subtipificação é válido.
Neste caso, tanto LightedBicycle quanto RacingBicycle são subtipos de Bicycle. No
primeiro caso, as condições são relaxadas; no segundo caso, a execução é reforçada de uma maneira
que ela ainda satisfaça a execução da superclasse.
O método cost de LightedBicycle mostra outra capacidade da especialização de classes em
Java. Os métodos podem ser sobrescritos para fornecer uma nova implementação na subclasse. Isto
permite uma maior reutilização de código; em particular, a LightedBicycle pode reutilizar o
método salesTax de Bicycle. Quando o salesTax é chamado em uma LightedBicycle, a
versão da Bicycle é que é utilizada. Então, a chamada de cost de dentro de salesTax chama a
versão baseada no tipo em tempo de execução do objeto (LightedBicycle), assim a versão da
LightedBicycle é utilizada. Independentemente do tipo declarado de um objeto, a implementação
de um método, com muitas implementações (da mesma assinatura), é sempre escolhida baseada no
tipo de tempo de execução.
De fato, não existe nenhuma maneira para um cliente externo chamar a versão de um método,
especificado pelo tipo declarado ou qualquer outro tipo, que não seja o tipo de tempo de execução.
Esta é uma propriedade importante e muito agradável do Java (e outras linguagens orientadas a
objeto). Suponha que a subclasse mantenha alguns campos extras que sejam mantidos em sincronismo
com os campos da superclasse. Se os métodos da superclasse puderem ser chamados diretamente, é
possível que sejam realizadas alterações em campos da superclasse sem que os campos da subclasse
sejam alterados também, então a invariante de representação da subclasse seria quebrada.
De qualquer maneira, uma subclasse pode chamar métodos dos seus ancestrais pela utilização de
super. Algumas vezes isto é útil quando o método da subclasse necessita fazer apenas mais um
pouco de trabalho; relembre a implementação de LightedBicycle para cost:
class LightedBicycle extends Bicycle{
// retorna o preço da bicicleta
float cost() { return super.cost() + battery.cost(); }
}
Suponha que a classe Rider modele as pessoas que passeiam de bicicleta. Na ausência de
especializações de classe e subtipos, o diagrama de dependência modular iria se parecer com algo
assim:
O código para a Rider também precisaria testar que tipo de objeto teria sido passado, o que seria feio,
verboso, e passível de erro.
Com a subtipificação, as dependências do MDD se pareceriam com isso:
As várias dependências foram reduzidas para uma única dependência.
Quando as setas indicadoras de subtipo são adicionadas, o diagrama fica apenas mais um pouco
complicado:
Mesmo que existam várias setas, este diagrama é mais simples do que o original: restrições de
dependência complicam o projeto e a implementação mais do que outros tipos de restrição.
3 Exemplo: Quadrado e retângulo
Nós sabemos desde o ensino básico que todo quadrado é um retângulo. Suponha que nós queiramos fazer do
quadrado Square um subtipo do retângulo Rectangle, que inclui um método setSize:
class Rectangle{
...
// execução: define a largura width e a altura height com os valores
//
espeficiados (isto é, this.width’ = w && this.height’ = h)
void setSize(int w, int h);
}
class Square extends Rectangle{
...
}
Qual dos seguintes métodos é correto para o Quadrado?
// requer: w = h
void setSize(int w, int h);
void setSize(int edgeLenght);
// lança a exceção BadSizeException se w != h
void setSize(int w, int h) throws BadSizeException;
O primeiro não está certo, pois o método da subclasse requer mais do que o método da superclasse. Assim, os
objetos da subclasse não podem ser substituídos por objetos da superclasse, pois pode existir algum trecho de
código que chama o método setSize com argumentos diferentes.
O segundo não está certo (completamente), pois a subclasse ainda precisa especificar um comportamento para
setSize(int, int); esta é uma definição de um método diferente (cujo o nome é o mesmo mas cuja
assinatura é diferente).
O terceiro não está certo, pois ele lança uma exceção que não é mencionada pela superclasse. Assim,
novamente, ele possui um comportamento diferente e desta maneira o quadrado Square não pode ser
substituído pelo retângulo Rectangle. (Se a exceção BadSizeException for uma exceção não verificada,
então o Java irá permitir que o terceiro método seja compilado; mas então novamente, ele também irá permitir
que o primeiro método compile. A noção do Java sobre subtipo é mais fraca do que a noção de subtipo do 6.170.
Sem nenhuma arrogância, nós iremos chamar o último de “subtipos verdadeiros” para distingui-los dos subtipos
do Java.)
Não existe uma maneira de sair deste dilema sem modificar o supertipo. Algumas vezes os subtipos não estão de
acordo com a nossa intuição! Ou, a nossa intuição sobre o que é um bom supertipo está errada.
Uma solução plausível seria alterar o Rectangle.setSize para especificar que ele lança a exceção; é claro
que, na prática, somente o Square.size irá fazer isso. Outra solução seria eliminar o setSize e ao invés
dele ter o método
void scale(double scaleFactor);
que diminui ou aumenta uma figura. Outras soluções também são possíveis.
4 Princípio de substituição
O princípio da substituição é a sustentação teórica dos subtipos; ele fornece uma definição precisa de quando
dois tipos são subtipos. Informalmente, ele afirma que os subtipos devam ser substituídos por supertipos. Isto
garante que se o código depender de (qualquer aspecto de) um supertipo, mas um objeto de um subtipo for
substituído, o comportamento do sistema não será afetado. (O compilador Java também requer que as cláusulas
extends ou implements nomeiem o pai para que os subtipos sejam usados no lugar dos supertipos.)
Os métodos de um subtipo devem suportar certos relacionamentos com os métodos do supertipo, e o subtipo
deve garantir que qualquer propriedade do supertipo (como as invariantes de representação ou restrições de
especificação) não seja violada pelo subtipo.
Métodos Existem duas propriedades necessárias:
1. O subtipo deve possuir um método correspondente, para cada método do supertipo. (É permitido
que o subtipo introduza novos métodos adicionais que não apareçam no supertipo.)
2. Cada método do subtipo que corresponda a um método do supertipo:
•
requer menos (possui uma pré-condição mais fraca)
-
existem menos cláusulas “requires”, e cada uma delas é menos rigorosa do que uma
no método do supertipo.
-
os tipos dos argumentos devem ser um dos supertipos do supertipo. Isto é dito ser
uma contra-variância, e parece um pouco controverso, pois os argumentos dos
métodos do subtipo são supertipos dos argumentos dos métodos do supertipo.
Entretanto, isto faz sentido, pois é garantido que qualquer argumento passado para
o método do supertipo é um argumento válido para o método do subtipo.
•
garante mais (possui uma pós-condição mais forte)
-
não existem mais exceções
-
existem menos variáveis modificadoras
-
na descrição do resultado e/ou do estado resultante, existem mais cláusulas, e elas
descrevem propriedades mais fortes
-
o tipo do resultado deve ser um dos subtipos do supertipo. Isto é dito ser uma
covariância: o tipo de retorno do método do subtipo é um subtipo do tipo de retorno
do método do supertipo.
(Todas as descrições acima devem permitir a uniformidade; por exemplo, “requer menos” deveria ser
“não requer mais”, e “menos rigorosa” deveria ser “não mais rigorosa”. Elas foram colocadas desta
forma para permitir uma fácil leitura.)
O método do subtipo não deve se comprometer em fornecer resultados a mais ou diferentes; ele deve
apenas se comprometer a fazer o que o método do supertipo faz, assim como possivelmente garantir
propriedades adicionais. Por exemplo, se um método de um supertipo retorna um número maior do que
o seu argumento, um método do subtipo pode retornar um número primo maior do que o seu argumento.
Como um exemplo de restrições de tipo, se A é um subtipo de B, então a seguinte redefinição (que é o
mesmo que sobrescrever) seria válida:
Bicycle B.f(Bicycle arg);
RacingBicycle A.f(Vehicle arg);
O método B toma uma bicicleta Bicycle como seu argumento, entretanto A.f pode aceitar qualquer
veículo (o que inclui todas as bicicletas). O método B.f retorna uma bicicleta Bicycle como resultado,
entretanto A.f retorna uma bicicleta de corrida RacingBicycle (que é propriamente uma bicicleta).
propriedades Quaisquer propriedades garantidas por um supertipo, como restrições sobre os valores que
possam aparecer nos campos de especificação, também devem ser garantidas pelo subtipo. (É permitido
que o subtipo reforce essas restrições.)
Como um exemplo simples do livro texto, considere o FatSet, que é sempre não vazio.
class FatSet{
// restrições de especificação: o objeto corrente this deve
//
sempre conter pelo menos um elemento
...
// execução: se o objeto this contiver x e this.size > 1,
//
remova x de this
void remove(int x);
}
O tipo SuperFatSet com um método adicional
// execução: remove x do objeto this
void reallyRemove(int x)
não é um subtipo de FatSet. Mesmo que não exista nenhum problema com qualquer método de
FatSet – o reallyRemove é um novo método, então as regras sobre métodos correspondentes não
se aplicam – este método viola a restrição.
Se o objeto do subtipo for considerado puramente como um objeto do supertipo (isto é, apenas os
métodos e campos do supertipo são consultados), então o resultado deve ser o mesmo que se um objeto
do supertipo tivesse sempre sido manipulado ao invés dele.
Na seção 7.9, o livro texto descreve o princípio de substituição como a colocação de restrições em
•
assinaturas: isto são essencialmente as regras de contra-variância e covariância que foram descritas
acima. (A assinatura de um procedimento é composta pelo seu nome, tipos de argumentos, tipos de
retorno, e exceções.)
•
métodos: isto são restrições de comportamento, ou todos os aspectos de uma especificação que não
podem ser expressos em uma assinatura
•
propriedades: como acima
5 Subclasses e subtipos Java
Os tipos Java são as classes, interfaces, ou primitivas. O Java possui a sua própria noção de subtipo (que
envolve apenas as classes e interfaces). Esta é uma noção mais fraca do que a noção de subtipos verdadeiros que
foi descrita anteriormente; os subtipos Java não satisfazem necessariamente o princípio de substituição. Além
disso, uma definição de subtipo que satisfaça o princípio de substituição pode não ser permitida em Java, e não
irá compilar.
Para um tipo ser um subtipo Java de outro tipo, o relacionamento deve ser declarado (pela sintaxe Java
extends ou implements), e os métodos devem satisfazer a duas propriedades similares a, entretanto mais
fracas que, aquelas para os subtipos verdadeiros:
1. O subtipo deve possuir um método correspondente, para cada método do supertipo. (É
permitido que o subtipo introduza novos métodos adicionais que não apareçam no supertipo.)
2. Para cada método do subtipo que corresponda a um método do supertipo
•
os argumentos devem possuir os mesmos tipos
•
o resultado deve possuir o mesmo tipo
•
não devem existir mais declarações de exceções
O Java não possui nenhuma noção de especificação comportamental, assim ele não realiza nenhuma verificação
e não pode dar nenhuma garantia quanto ao comportamento. A exigência de uniformidade de tipos para os
argumentos e o resultado é mais forte do que o estritamente necessário para garantir a proteção do tipo. Isto
proíbe alguns trechos de código que nós gostaríamos de escrever. Entretanto, isto simplifica a sintaxe e a
semântica da linguagem Java.
A especialização de classes possui várias vantagens, todas elas provenientes da reutilização:
•
As implementações de subclasses não precisam repetir os campos e métodos não alterados, mas podem
reutilizar os da superclasse
•
Os clientes (aqueles que executam as chamadas) não precisam modificar o código quando novos
subtipos são adicionados, mas podem reutilizar o código existente (o trecho que não menciona os
subtipos totalmente, apenas o supertipo)
•
O projeto resultante possui uma melhor modularidade e uma reduzida complexidade, pois os projetistas,
os programadores, e os usuários somente têm que entender o supertipo, não todos os subtipos; isto é
reutilização de especificação.
Um mecanismo chave que permite esses benefícios é a redefinição, que especializa o comportamento para
alguns métodos. Na ausência de redefinição, qualquer alteração no comportamento (mesmo uma compatível)
pode forçar uma completa re-implementação. A redefinição permite que parte de uma implementação seja
alterada sem modificar outras partes que dependam dela. Isto permite uma maior reutilização de código e
especificação, por ambos a implementação e o cliente.
Uma potencial desvantagem da especialização de classes é a oportunidade que ela apresenta para a reutilização
inapropriada. As subclasses e superclasses podem depender umas das outras (explicitamente pelo nome do tipo
ou implicitamente pelo conhecimento da implementação), particularmente se as subclasses tiverem acesso às
partes protegidas da implementação da superclasse. Essas dependências extras complicam o MDD, o projeto, e a
implementação, fazendo com seja mais complicado implementar, entender, e modificar.
6 Interfaces Java
Algumas vezes você quer garantias sobre comportamento sem compartilhar o código. Por exemplo, você pode
querer requerer que os elementos de um contêiner específico sejam ordenados ou suportem uma operação
particular, sem fornecer uma implementação padrão (porque todo relacionamento de ordem possui uma
implementação diferente). O Java fornece interfaces para preencher essas necessidades. As interfaces garantem
nenhuma reutilização de código. Outra vantagem das interfaces é que uma classe pode implementar múltiplas
interfaces e uma interface pode estender múltiplas interfaces; em oposição a isto, uma classe só pode estender
uma classe. Isto se confirma na prática, a implementação de múltiplas interfaces e a extensão de uma única
superclasse fornece a maioria dos benefícios da herança arbitrária, mas com uma implementação e uma
semântica de linguagem mais simples. Uma desvantagem das interfaces é que elas não fornecem uma maneira
para especificar a assinatura (ou o comportamento) de um construtor.
Download