Você deve ter ouvido a famosa citação do popular livro "Padrões de Projeto: Elementos de Software Orientado a Objetos Reutilizáveis", escrito pela "Gangue dos Quatro" em 1994(Design Patterns: Elements of Reusable Object-Oriented Software", escrito pela "Gang of Four" em 1994:): "Favorecer a composição de objetos em vez da herança de classes".
O que isso realmente significa e como posso implementar isso em JavaScript?
Vamos imaginar que você está criando uma livraria online. As pessoas podem se inscrever para se tornarem clientes e comprarem livros.
Parece muito fácil! Vamos em frente e criar as classes e relacionamentos de que precisamos.
//book.js
class Book {
constructor(title, price) {
this.title = title
this.price = price
}
getPrice() {
return this.price
}
}
//user.js
class User {
constructor(username) {
this.username = username
this.balance = 100
}
login() {
console.log('Logging in!')
}
}
//customer.js
class Customer extends User {
constructor(...args) {
super(...args)
}
getPaymentDetails() {
return {
cardNumber: 'xxxx-xxxx-xxxx-xxxx',
cvc: 123,
}
}
purchase(book) {
this.balance -= book.getPrice()
}
}
Os usuários recebem $100 iniciais para gastar na loja, e permitimos que comprem livros. Parece que nossa livraria está pronta para produção!
Observação: é claro que existem outros componentes em uma livraria online real, mas omiti o que não considero relevante para o ponto que estou tentando fazer.
Um mês depois, seu chefe chega e lhe diz que temos alguns clientes muito importantes que devem ser capazes de fazer tudo o que os clientes regulares podem fazer, exceto que recebem um desconto de 20% em cada compra!
É justo, vamos apenas criar outra classe:
class VIPCustomer extends Customer {
constructor() {
super()
this.discount = 20
}
purchase(book) {
this.balance -= book.getPrice(this.discount)
}
}
//Atualize a classe Book para oferecer descontos:
class Book {
constructor(title, price) {
this.title = title
this.price = price
}
getPrice(discount) {
return discount
? this.price - (this.price / 100) * discount
: this.price
}
}
Excelente! Os nossos clientes VIP obtêm os seus merecidos descontos e todos ficam felizes.
Mais um mês se passa e o negócio está realmente prosperando, então seu chefe tem uma surpresa para todos os funcionários da livraria; a partir de agora terão os mesmos descontos que os clientes VIP!
Podemos modelar esse novo recurso de duas maneiras.
- Apenas deixe os funcionários serem VIPCustomers
- Crie uma nova classe, Employee, que subtrairá o desconto de 20% das compras - como o VIPCustomer
- Crie uma nova classe, Employee, mova a lógica de desconto para uma nova classe da qual Employee e VIPCustomer herdam.
1 Tecnicamente funcionaria por enquanto, mas parece um pouco estranho. Além disso, quaisquer recursos que os clientes VIP potencialmente obteriam no futuro também contariam automaticamente para os funcionários.
2 Parece uma abordagem mais limpa, mas a desvantagem é que teríamos que manter a lógica de desconto em sincronia entre a classe Employee e a classe VIPCustomer. E se aumentarmos o desconto para clientes VIP, mas esquecermos de aumentá-lo para os funcionários?
3 parece uma boa opção. Obtemos a separação clara entre um funcionário e um cliente VIP E podemos reutilizar a lógica para calcular descontos. Vamos dar uma chance a isso.
Primeiro, extraímos a funcionalidade de fazer compras com desconto em sua própria classe:
//discountUser.js
class DiscountedUser extends User {
constructor(...args) {
super(...args)
this.discount = 20
}
purchase(book) {
this.balance -= book.getPrice(this.discount)
}
}
Em seguida, criamos uma classe para representar um funcionário:
class Employee extends DiscountedUser {
}
//E, finalmente, remova a lógica de desconto de VIPCustomer e //faça com que seja herdada de DiscountedUser :
class VIPCustomer extends DiscountedUser {
}
Ok, agora tanto os funcionários quanto os clientes VIP têm descontos, mas fizemos uma alteração importante. Nosso VIPCustomer não estende mais a classe Customer e, portanto, não temos mais acesso aos detalhes de pagamento. Além disso, a Employee classe precisa ter acesso a detalhes de pagamento, bem como, mas JavaScript(com class) não suporta herança múltipla, nós não podemos fazer VIPCustomer e Employee estender tanto DiscountedUser e Cliente.
Provavelmente, existem outras maneiras de resolver esse problema específico, mas você sabe onde isso vai dar. Quanto mais funcionalidades adicionamos, mais profundamente nossa árvore de herança cresce e mais frágil nosso código se torna.
E se pudéssemos obter os benefícios da herança clássica (reutilização de código), mas sem as desvantagens (relacionamentos frágeis)?
Modele as coisas com base no que fazem, e não no que são
Em nossa implementação inicial, modelamos nosso aplicativo de acordo com o que as coisas são. Usuário, cliente, empregado etc .
E se, em vez disso, arquitetássemos nosso aplicativo em torno da funcionalidade que eles fornecem?
Se dividirmos a funcionalidade que cada uma de nossas classes fornece, é basicamente isso:
Login na livraria
Compre um livro
Compre um livro com desconto
Obtenha informações de pagamento
Em vez de usar classes, vamos simplesmente usar funções simples e antigas para criar os objetos de que precisamos - compondo pequenas partes de funcionalidade.
Vamos criar uma função para cada funcionalidade em nosso aplicativo:
const withPurchasing = (state = {}) => {
let balance = 100
return {
...state,
purchase: book => {
balance -= book.getPrice()
},
}
}
const withDiscount = (state = {}) => {
const discount = 20
return {
...state,
purchase: book => {
state.balance -= book.getPrice(discount)
},
}
}
const withLogin = (state = {}) => {
return {
...state,
login: () => {
console.log('Logged in!')
},
}
}
const withPaymentInfo = (state = {}) => {
return {
...state,
getPaymentDetails: () => {
return {
cardNumber: 'xxxx-xxxx-xxxx-xxxx',
cvc: 123,
}
},
}
}
Cada uma das funções aceita um parâmetro de "estado". Você pode nomeá-lo como achar que faz mais sentido para você, mas basicamente representa o objeto que você deseja aumentar. Eles retornam uma cópia do objeto "state" que você fornece, com os métodos adicionais adicionados.
Criando nossos objetos
Excelente! Temos os diferentes recursos separados em suas próprias funções, agora vamos criar alguns objetos!
Nota: Se você quer saber o que as funções 'pipe()' fazem e se parecem, confira este artigo: https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/
const createCustomer = (username, balance) => {
const customer = { username, balance }
return pipe(
withLogin,
withPurchasing,
withPaymentInfo
)(customer)
}
Em vez de criar uma classe Customer e usar a palavra-chave new para criar objetos de cliente, temos a chamada função de fábrica, uma função que cria objetos de cliente para nós. Pegamos as entradas (o que seriam argumentos do construtor no exemplo da classe), username e balance e anexamos os recursos que um cliente deve ter, neste caso withLogin , withPurchasing , withPaymentInfo
Vamos usar a função createCustomer para criar um objeto de cliente e verificar se ele possui as propriedades corretas
const bob = createCustomer('bob99', 100);
console.log(bob);
// output
{
username: 'bob99',
balance: 100,
login: [Function: login],
purchase: [Function: purchase],
getPaymentDetails: [Function: getPaymentDetails]
}
Se registrarmos nosso objeto no console, veremos que ele possui os métodos login(), purchase() e getPaymentDetails() que esperávamos.
Então, como criamos um cliente VIP? Fácil!
const createVipCustomer = (username, balance) => {
const vipCustomer = { username, balance }
return pipe(
withLogin,
withPaymentInfo,
withPurchasing,
withDiscount
)(vipCustomer)
}
A única diferença é o recurso adicionado withDiscount .
Os funcionários têm os mesmos recursos que os clientes VIP por enquanto, então eles serão os mesmos - no entanto, agora é muito fácil estender o comportamento dos clientes VIP e dos funcionários.
const createEmployee = (username, balance) => {
const employee = { username, balance }
return pipe(
withLogin,
withPaymentInfo,
withPurchasing,
withDiscount
)(employee)
}
Finalmente, nossa função createBook:
const createBook = (title, price) => {
return {
title,
price,
}
}
Adicionando novos recursos
Então, seu chefe aparece novamente e diz que todos os clientes devem poder convidar outros usuários para a loja, e os funcionários devem poder convidar usuários, mas também banir usuários. Com nossa nova arquitetura, implementar isso é trivial. Vamos criar duas funções que encapsulam usuários convidando e banindo usuários.
const withUserInviting = (state = {}) => {
return {
...state,
inviteUser: user => {
// Send the invitation
}
}
}
const withUserBanning = (state = {}) => {
return {
...state,
banUser: user => {
// Ban!
}
}
}
Em seguida, atualizamos as funções createCustomer() e createEmployee()
class VIPCustomer
const createCustomer = (username, balance) => {
const customer = { username, balance }
return pipe(
withLogin,
withPurchasing,
withPaymentInfo,
withUserInviting,
)(customer)
}
const createEmployee = (username, balance) => {
const employee = { username, balance }
return pipe(
withLogin,
withPaymentInfo,
withPurchasing,
withDiscount,
withUserInviting,
withUserBanning,
)(employee)
}
E terminamos!
Resumo
Modelar seu software de acordo com o que as coisas fazem em vez do que são, oferece os benefícios da herança clássica sem as desvantagens - a capacidade de reutilizar propriedades/comportamentos enquanto mantém seu código adaptável às mudanças de requisitos
Este texto é uma tradução do original abaixo:
https://mskutle.dev/blog/object-composition-in-javascript
Top comments (0)