function Extends(clazz) {
return class extends clazz {
// ...
}
}
Deixe-me explicar como funciona. Em vez de herança regular, usamos o mecanismo acima. Em seguida, especificamos a classe base apenas ao criar um objeto:
const Class = Extends(Base)
const object = new Class(...args)
Vou tentar convencê-lo de que este é o filho da amiga da minha mãe para herança de classe e uma maneira de devolver a herança ao título de verdadeira ferramenta OOP (logo após a herança prototípica, é claro).
Quase não offtopic
, , , pet project , pet project'. , .
Vamos combinar os nomes: vou chamar essa técnica de mixin, embora isso ainda signifique um pouco diferente . Antes de me dizerem que esses são mixins do TS / JS, usei o nome LBC (classes de ligação tardia).
Os "problemas" da herança de classes
Todos nós sabemos como "todos" "não gostam" da herança de classes. Quais são seus problemas? Vamos descobrir e ao mesmo tempo entender como os mixins os resolvem.
A herança de implementação quebra o encapsulamento
A principal tarefa do OOP é vincular dados e operações nele (encapsulamento). Quando uma classe herda de outra, essa relação é quebrada: os dados estão em um lugar (pai), as operações em outro (herdeiro). Além disso, o herdeiro pode sobrecarregar a interface pública da classe, de modo que nem o código da classe base, nem o código da classe herdada separadamente podem dizer o que acontecerá com o estado do objeto. Ou seja, as classes são acopladas.
Os mixins, por sua vez, reduzem bastante o acoplamento: do comportamento de qual classe base o herdeiro deve depender, se simplesmente não houver classe base no momento de declarar a classe herdada? No entanto, graças a este limite tardio e à sobrecarga do método, o "problema do Yo-Yo"permanece. Se você usar herança em seu projeto, a partir dele não pode escapar, mas, por exemplo, em palavras
open
- chave Kotlin e override
deve facilitar muito a situação (eu não sei, não estou muito familiarizado com Kotlin).
Herdando métodos desnecessários
Um exemplo clássico com uma lista e uma pilha: se você herdar a pilha de uma lista, os métodos da interface da lista entrarão na interface da pilha, o que pode violar a invariante da pilha. Eu não diria que este é um problema de herança, porque, por exemplo, em C ++ há herança privada para isso (e métodos individuais podem ser tornados públicos usando
using
), então este é um problema de linguagens individuais.
Falta de flexibilidade
- , : . , , : , , cohesion . , .
- ( ), . , : , .
- . - , . , , ? - — , .. .
- . - , - . , , .
Se uma classe herda da implementação de outra classe, alterar essa implementação pode interromper a classe herdada. Em este artigo não é uma boa ilustração dos problemas
Stack
e MonitorableStack
.
Com mixins, o programador deve levar em consideração que a classe herdada que ele escreve deve trabalhar não apenas com alguma classe base específica, mas também com outras classes que correspondem à interface da classe base.
Banana, gorila e selva
OOP promete composibilidade, ou seja, a capacidade de reutilizar classes individuais em diferentes situações e até mesmo em diferentes projetos. No entanto, se uma classe herda de outra classe, a fim de reutilizar o herdeiro, você precisa copiar todas as dependências, a classe base e todas as suas dependências e sua classe base…. Essa. queria uma banana e tirou um gorila, e depois a selva. Se o objeto foi criado com o Princípio de Inversão de Dependências em mente, as dependências não são tão ruins - apenas copie suas interfaces. No entanto, isso não pode ser feito com a cadeia de herança.
Os mixins, por sua vez, tornam possível (e obrigatório) o uso de DIPs em relação à herança.
Outras amenidades dos Mixins
As vantagens dos mixins não param por aí. Vamos ver o que mais você pode fazer com eles.
Morte da hierarquia de herança
As classes não dependem mais umas das outras: dependem apenas de interfaces. Essa. a implementação torna-se as folhas do gráfico de dependência. Isso deve tornar a refatoração mais fácil - o modelo de domínio agora é independente de sua implementação.
Morte de classes abstratas
Classes abstratas não são mais necessárias. Vejamos um exemplo do padrão Factory Method em Java emprestado do guru da refatoração :
interface Button {
void render();
void onClick();
}
abstract class Dialog {
void renderWindow() {
Button okButton = createButton();
okButton.render();
}
abstract Button createButton();
}
Sim, claro, os métodos de fábrica evoluem para padrões de construtor e estratégia. Mas você pode fazer isso com mixins (vamos imaginar por um segundo que Java tenha mixins de primeira classe):
interface Button {
void render();
void onClick();
}
interface ButtonFactory {
Button createButton();
}
class Dialog extends ButtonFactory {
void renderWindow() {
Button okButton = createButton();
okButton.render();
}
}
Você pode fazer esse truque com quase qualquer classe abstrata. Um exemplo em que não funciona:
abstract class Abstract {
void method() {
abstractMethod();
}
abstract void abstractMethod();
}
class Concrete extends Abstract {
private encapsulated = new Encapsulated();
@Override
void method() {
encapsulated.method();
super.method();
}
void abstractMethod() {
encapsulated.otherMethod();
}
}
Aqui, o campo é
encapsulated
necessário tanto na sobrecarga method
quanto na implementação abstractMethod
. Ou seja, sem quebrar o encapsulamento, a classe Concrete
não pode ser dividida em filho Abstract
e "superclasse" Abstract
. Mas não tenho certeza se este é um exemplo de bom design.
Flexibilidade comparável aos tipos
O leitor atento notará que todos são muito semelhantes aos traços de Smalltalk / Rust. Existem duas diferenças:
- As instâncias do Mixin podem conter dados que não estavam na classe base;
- Mixins não modificam a classe da qual herdam: para usar a funcionalidade do mixin, você precisa criar explicitamente o objeto mixin, não a classe base.
A segunda diferença leva ao fato de que, digamos, mixins agem localmente, em contraste com traits que agem em todas as instâncias da classe base. O quão conveniente é depende do programador e do projeto, não vou dizer que minha solução seja definitivamente melhor.
Essas diferenças aproximam os mixins da herança normal, então essa coisa me parece uma troca engraçada entre herança e características.
Contras de mixins
Oh, se fosse tão simples. Os mixins definitivamente têm um pequeno problema e uma falta de gordura.
Interfaces explodindo
Se você pode herdar apenas da interface, obviamente, haverá mais interfaces no projeto. Claro, se o DIP for observado no projeto, mais algumas interfaces não farão o clima, mas nem todas seguem SÓLIDOS. Este problema pode ser resolvido se, a partir de cada classe, for gerada uma interface contendo todos os métodos públicos e, ao citar o nome da classe, distinguir se a classe se destina a ser uma fábrica de objetos ou uma interface. Algo semelhante é feito no TypeScript, mas por algum motivo os campos e métodos privados são mencionados na interface gerada.
Construtores complexos
Se você usa mixins, a tarefa mais difícil é criar um objeto. Considere duas opções, dependendo se o construtor está incluído na interface da classe base:
- , , . , - . , .
- , . :
interface Base { new(values: Array<int>) } class Subclass extends Base { // ... } class DoesntFit { new(values: Array<int>, mode: Mode) { // ... } }
DoesntFit
Subclass
, - .Subclass
DoesntFit
,Base
. - Na verdade, existe outra opção - passar para o construtor não uma lista de argumentos, mas um dicionário. Isso resolve o problema acima, porque
{ values: Array<int>, mode: Mode }
obviamente se encaixa no padrão{ values: Array<int> }
, mas leva a uma colisão imprevisível de nomes em tal dicionário: por exemplo, tanto a superclasseA
quanto o herdeiroB
usam os mesmos parâmetros, mas esse nome não é especificado na interface da classe base paraB
.
Em vez de uma conclusão
Tenho certeza de que perdi alguns aspectos dessa ideia. Ou o fato de que este já é um acordeão de botão selvagem e há vinte anos havia uma linguagem que usava essa ideia. Em qualquer caso, estou te esperando nos comentários!
Lista de fontes
neethack.com/2017/04/Why-inheritance-is-bad
www.infoworld.com/article/2073649/why-extends-is-evil.html
www.yegor256.com/2016/09/13/inheritance-is- procedural.html
refactoring.guru/ru/design-patterns/factory-method/java/example
scg.unibe.ch/archive/papers/Scha03aTraits.pdf