Por que as interfaces são mais úteis que as superclasses para obter acoplamentos soltos?

15

( Para o propósito desta pergunta, quando digo 'interface', quero dizer a construção da linguageminterface , e não uma 'interface' no outro sentido da palavra, ou seja, os métodos públicos que uma classe oferece ao mundo externo para se comunicar com e manipulá-lo. )

O acoplamento frouxo pode ser obtido fazendo com que um objeto dependa de uma abstração em vez de um tipo concreto.

Isso permite um acoplamento flexível por duas razões principais: 1 - as abstrações têm menos probabilidade de mudar do que os tipos concretos, o que significa que o código dependente tem menos probabilidade de quebrar. 2- tipos diferentes de concreto podem ser usados ​​em tempo de execução, porque todos se encaixam na abstração. Novos tipos de concreto também podem ser adicionados posteriormente, sem a necessidade de alterar o código dependente existente.

Por exemplo, considere uma classe Care duas subclasses Volvoe Mazda.

Se o seu código depende de a Car, ele pode usar a Volvoou a Mazdadurante o tempo de execução. Mais tarde, subclasses adicionais podem ser adicionadas sem a necessidade de alterar o código dependente.

Além disso, Car- que é uma abstração - é menos provável que mude do que Volvoou Mazda. Os carros geralmente são os mesmos há algum tempo, mas Volvos e Mazdas são muito mais propensos a mudar. Ou seja, as abstrações são mais estáveis ​​que os tipos concretos.

Tudo isso foi para mostrar que eu entendo o que é o acoplamento solto e como ele é alcançado, dependendo das abstrações e não das concreções. (Se eu escrevi algo impreciso, diga isso).

O que eu não entendo é o seguinte:

Abstrações podem ser superclasses ou interfaces.

Em caso afirmativo, por que as interfaces são elogiadas especificamente por sua capacidade de permitir acoplamentos soltos? Não vejo como é diferente do que usar uma superclasse.

As únicas diferenças que vejo são: 1- As interfaces não são limitadas por herança única, mas isso não tem muito a ver com o tópico de acoplamento flexível. 2- As interfaces são mais "abstratas", pois não possuem lógica de implementação. Ainda assim, não vejo por que isso faz uma diferença tão grande.

Por favor, explique-me por que as interfaces são ótimas para permitir acoplamentos soltos, enquanto superclasses simples não são.

Aviv Cohn
fonte
3
A maioria das linguagens (por exemplo, Java, C #) que possuem "interfaces" suportam apenas herança única. Como cada classe pode ter apenas uma superclasse imediata, as superclasses (abstratas) são muito limitadas para que um objeto suporte várias abstrações. Confira os traços (por exemplo, os papéis de Scala ou Perl ) para uma alternativa moderna que também evita o " problema do diamante " com herança múltipla.
amon
@amon Então, você está dizendo que a vantagem das interfaces em relação às classes abstratas ao tentar obter um acoplamento flexível não está sendo limitada pela herança única?
Aviv Cohn
Não, eu quis dizer caro em termos de o compilador tem mais a ver quando lida com uma classe abstrata , mas isso provavelmente pode ser negligenciado.
pastosa
2
Parece que @amon está no caminho certo, encontrei este post onde é dito que: interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class(o que me leva à comparação com C ++, onde interfaces são apenas classes com funções virtuais puras).
26514 pasty
Por favor, diga quem diz que as superclasses são ruins.
Tulains Córdova

Respostas:

11

Terminologia: vou me referir à construção da linguagem interfacecomo interface e à interface de um tipo ou objeto como superfície (por falta de um termo melhor).

O acoplamento frouxo pode ser obtido fazendo com que um objeto dependa de uma abstração em vez de um tipo concreto.

Corrigir.

Isso permite um acoplamento frouxo por dois motivos principais: 1 - as abstrações têm menos probabilidade de mudar do que os tipos concretos, o que significa que o código dependente tem menos probabilidade de quebrar. 2 - tipos diferentes de concreto podem ser usados ​​em tempo de execução, porque todos se encaixam na abstração. Novos tipos de concreto também podem ser adicionados posteriormente, sem a necessidade de alterar o código dependente existente.

Não está correto. Os idiomas atuais geralmente não antecipam que uma abstração seja alterada (embora existam alguns padrões de design para lidar com isso). Separar detalhes de coisas gerais é abstração. Isso geralmente é feito por alguma camada de abstração . Essa camada pode ser alterada para outras especificidades sem quebrar o código que se baseia nessa abstração - o acoplamento flexível é alcançado. Exemplo fora da OOP: Uma sortrotina pode ser alterada do Quicksort na versão 1 para Tim Sort na versão 2. O código que depende apenas do resultado que está sendo classificado (ou seja, é baseado na sortabstração) é, portanto, desacoplado da implementação de classificação real.

O que chamei de superfície acima é a parte geral de uma abstração. Agora acontece no OOP que um objeto às vezes deve suportar várias abstrações. Um exemplo não muito ideal: o Java's java.util.LinkedListsuporta tanto oList interface que trata da abstração "coleção ordenada e indexável" quanto a Queueinterface que (em termos gerais) trata da abstração "FIFO".

Como um objeto pode suportar múltiplas abstrações?

O C ++ não possui interfaces, mas possui várias heranças, métodos virtuais e classes abstratas. Uma abstração pode então ser definida como uma classe abstrata (ou seja, uma classe que não pode ser instanciada imediatamente) que declara, mas não define métodos virtuais. Classes que implementam as especificidades de uma abstração podem herdar dessa classe abstrata e implementar os métodos virtuais necessários.

O problema aqui é que a herança múltipla pode levar ao problema do diamante , onde a ordem na qual as classes são pesquisadas para uma implementação de método (MRO: ordem de resolução do método) pode levar a "contradições". Existem duas respostas para isso:

  1. Defina uma ordem sensata e rejeite as ordens que não podem ser sensivelmente linearizadas. O C3 MRO é bastante sensível e funciona bem. Foi publicado em 1996.

  2. Siga o caminho mais fácil e rejeite a herança múltipla.

Java pegou a última opção e escolheu uma herança comportamental única. No entanto, ainda precisamos da capacidade de um objeto para suportar múltiplas abstrações. Portanto, é necessário usar interfaces que não suportam definições de métodos, apenas declarações.

O resultado é que o MRO é óbvio (basta olhar para cada superclasse em ordem) e que nosso objeto pode ter várias superfícies para qualquer número de abstrações.

Isso acaba sendo bastante insatisfatório, porque muitas vezes um pouco de comportamento faz parte da superfície. Considere uma Comparableinterface:

interface Comparable<T> {
    public int cmp(T that);
    public boolean lt(T that);  // less than
    public boolean le(T that);  // less than or equal
    public boolean eq(T that);  // equal
    public boolean ne(T that);  // not equal
    public boolean ge(T that);  // greater than or equal
    public boolean gt(T that);  // greater than
}

Isso é muito fácil de usar (uma API agradável com muitos métodos convenientes), mas tedioso de implementar. Gostaríamos que a interface incluísse apenas cmpe implementasse os outros métodos automaticamente em termos desse método necessário. Mixins , mas mais importante Características [ 1 ], [ 2 ] resolvem esse problema sem cair nas armadilhas da herança múltipla.

Isso é feito definindo uma composição de características para que elas não participem do MRO - em vez disso, os métodos definidos são compostos na classe de implementação.

A Comparableinterface pode ser expressa em Scala como

trait Comparable[T] {
    def cmp(that: T): Int
    def lt(that: T): Boolean = this.cmp(that) <  0
    def le(that: T): Boolean = this.cmp(that) <= 0
    ...
}

Quando uma classe usa essa característica, os outros métodos são adicionados à definição da classe:

// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
    override def cmp(that: Inty) = this.x - that.x
    // lt etc. get added automatically
}

Então Inty(4) cmp Inty(6)seria -2e Inty(4) lt Inty(6)seriatrue .

Muitos idiomas têm algum suporte para características, e qualquer linguagem que tenha um "Metaobject Protocol (MOP)" pode ter características adicionadas a ele. A atualização recente do Java 8 adicionou métodos padrão que são semelhantes aos traços (os métodos nas interfaces podem ter implementações de fallback, sendo opcional para a implementação de classes implementar esses métodos).

Infelizmente, os traços são uma invenção bastante recente (2002) e, portanto, são bastante raros nas grandes línguas tradicionais.

amon
fonte
Boa resposta, mas gostaria de acrescentar que linguagens de herança única podem falsificar a herança múltipla usando interfaces com composição.
4

O que eu não entendo é o seguinte:

Abstrações podem ser superclasses ou interfaces.

Em caso afirmativo, por que as interfaces são elogiadas especificamente por sua capacidade de permitir acoplamentos soltos? Não vejo como é diferente do que usar uma superclasse.

Primeiro, subtipagem e abstração são duas coisas diferentes. Subtipagem significa apenas que eu posso substituir valores de um tipo por valores de outro tipo - nenhum dos tipos precisa ser abstrato.

Mais importante, as subclasses dependem diretamente dos detalhes de implementação de sua superclasse. Esse é o tipo mais forte de acoplamento que existe. De fato, se a classe base não for projetada com herança em mente, alterações na classe base que não mudam seu comportamento ainda podem quebrar subclasses, e não há como saber a priori se ocorrerá uma quebra. Isso é conhecido como o frágil problema da classe base .

A implementação de uma interface não o acopla a nada, exceto a própria interface, que não contém comportamento.

Doval
fonte
Obrigado por responder. Para entender se: quando você deseja que um objeto chamado A dependa de uma abstração denominada B, em vez de uma implementação concreta dessa abstração denominada C, geralmente é melhor que B seja uma interface implementada por C, em vez de uma superclasse estendida por C. Isso ocorre porque: C subclasse B acopla firmemente C a B. Se B muda - C muda. No entanto, C implementando B (B sendo uma interface) não acopla B a C: B é apenas uma lista de métodos que C deve implementar, portanto, não há acoplamento rígido. No entanto, com relação ao objeto A (o dependente), não importa se B é uma classe ou interface.
Aviv Cohn
Corrigir? ..... .....
Aviv Cohn
Por que você consideraria uma interface acoplada a qualquer coisa?
Michael Shaw
Eu acho que essa resposta acertou na cabeça. Eu uso bastante o C ++ e, como foi dito em uma das outras respostas, o C ++ não possui interfaces, mas você o falsifica usando superclasses com todos os métodos restantes como "virtual puro" (ou seja, implementado por crianças). O ponto é que é fácil criar classes base que fazem algo junto com a funcionalidade delegada. Em muitos, muitos, muitos casos, eu e meus colegas de trabalho descobrimos que, ao fazer isso, um novo caso de uso aparece e invalida esse pouco de funcionalidade compartilhada. Se houver necessidade de funcionalidade compartilhada, é fácil fazer uma classe auxiliar.
J Trana
@Prog Sua linha de pensamento está correta, mas novamente, abstração e subtipagem são duas coisas separadas. Quando você diz you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named Cque está assumindo que as aulas não são abstratas. Uma abstração é qualquer coisa que oculte detalhes da implementação; portanto, uma classe com campos privados é tão abstrata quanto uma interface com os mesmos métodos públicos.
Doval 28/04
1

Há um acoplamento entre as classes pai e filho, uma vez que o filho depende do pai.

Digamos que temos uma classe A e a classe B herda dela. Se formos para a classe A e mudarmos as coisas, a classe B também será alterada.

Digamos que temos uma interface I e a classe B a implementa. Se mudarmos a interface I, embora a classe B não possa mais implementá-la, a classe B permanece inalterada.

Michael Shaw
fonte
Estou curioso para saber se os desobedientes tiveram um motivo ou se estavam apenas tendo um dia ruim.
Michael Shaw
1
Não reduzi o voto, mas acho que pode ter a ver com a primeira frase. As classes filho são acopladas às classes pai, e não o contrário. Os pais não precisam saber nada sobre o filho, mas precisam de um conhecimento íntimo sobre os pais.
@ JohnGaughan: Obrigado pelo feedback. Editado para maior clareza.
Michael Shaw