Terminologia: vou me referir à construção da linguagem interface
como 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 sort
rotina 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 sort
abstraçã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.LinkedList
suporta tanto oList
interface que trata da abstração "coleção ordenada e indexável" quanto a Queue
interface 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:
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.
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 Comparable
interface:
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 cmp
e 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 Comparable
interface 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 -2
e 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.