Composite - Design Pattern
Marlon Marques
Posted on October 31, 2022
Problema
Imagine o seguinte cenário: sua aplicação possui diversas integrações com plataformas de envio de eventos, todas seguindo o mesmo padrão de conteúdo de envio. Sua aplicação começa a ficar repleta de integrações de envio de eventos como por exemplo: Facebook, Firebase Analytics etc. O que nos leva ao seguinte problema:
- Código "redundante", sua aplicação começa a enviar vários eventos com o mesmo padrão de código.
- Se por algum motivo você desejar remover/desligar algum evento específico, ou vários eventos ao mesmo tempo, você terá um árduo caminho pela frente.
Esse foi um dos cenários encontrados ao se ter uma aplicação integrada com vários envios de eventos. Estávamos perdendo o controle 🤯.
Solução
Queríamos ter um maior controle sobre os nossos eventos, que vão desde:
- Remover a "redundância" de envio de eventos no código;
- Controlar os eventos enviados, desde um evento específico à um grupo de eventos;
Em conjunto com a equipe, ao se deparar com o problema, percebemos que a solução do problema se encaixava muito bem no Padrão de Projeto Composite.
Então como podemos resolver o problema usando o Composite?
Primeiro vamos ver a proposta de solução para o problema através de um Diagrama de Classes UML, e assim, discutir um pouco baseando-se nele.
Vamos lá, primeiro vamos começar resolvendo os nossos problemas passo a passo.
De inicio queremos poder enviar eventos de add e de view, através de plataformas como Facebook, Firebase etc. Para isso criamos uma interface EventActions que contém as ações mencionadas, fazendo com que o Facebook e o Firebase respeitem esse contrato.
Porém, como mencionado, queremos poder ter não só eventos por plataformas específicas, mas como também, eventos baseados por escopo. Para isso criamos o Analytics que também respeita o contrato com EventActions. Se pararmos para pensar, o Facebook e o Firebase tem certa relação com o Analytics por se tratarem de eventos de análise. Só que ainda tem um ponto... Precisamos de alguma forma fazer essa referência entre "pai" e "filho". Vamos resolver esse probleminha no próximo passo.
Chegamos a mencionar que queríamos poder desabilitar os eventos quando necessário. Para isso criamos a interface EventStatus, onde ela contém a função de poder desabilitar o evento, todas as nossas classes possuem um contrato com ela também. Mas não só isso, vamos aproveitar para adicionar a responsabilidade de poder atribuir um pai para a classes que respeitam esse contrato, assim, resolvendo o problema mencionado no passo acima.
No entanto, tem algo faltando... Comentamos da possibilidade de poder desabilitar os eventos especificamente ou por características/escopo. Para conseguirmos isso, vamos criar um EventComposite que tem um contrato com EventStatus e EventActions. Além disso, possui também a característica de conter uma lista de eventos.
Acho que agora ficou um pouco mais claro a solução para o nosso problema.
Vamos ver como funciona na prática
Seguindo o passo a passo acima, vamos criar as nossas classes e interfaces, conforme diagrama UML.
Primeiro criamos as nossas interfaces que vão definir as regras que as classes devem seguir.
interface EventActions {
add(): void
view(): void
}
interface EventStatus {
parent?: EventStatus
readonly isDisabled: boolean
disable(event?: EventStatus): boolean | void
}
Agora, criamos as nossas classes responsáveis pelo envio de eventos.
class Facebook implements EventActions, EventStatus {
parent?: EventStatus
isDisabled: boolean
disable(event?: EventStatus): boolean {
this.isDisabled = true
return this.isDisabled
}
add(): void {
if(!this.isDisabled) {
console.log('Facebook: add')
}
}
view(): void {
if(!this.isDisabled) {
console.log('Facebook: view')
}
}
}
class Firebase implements EventActions, EventStatus {
parent?: EventStatus
isDisabled: boolean
disable(event?: EventStatus | undefined): boolean {
this.isDisabled = true
return this.isDisabled
}
add(): void {
if(!this.isDisabled) {
console.log('Firebase: add')
}
}
view(): void {
if(!this.isDisabled) {
console.log('Firebase: view')
}
}
}
Aqui temos um caso em particular, como o Analytics não é de fato resposável por enviar eventos, e sim, um tipo mais abrangente, a implementação é mínima.
class Analytics implements EventActions, EventStatus {
parent?: EventStatus
isDisabled: boolean
disable(event?: EventStatus | undefined): boolean {
this.isDisabled = true
return this.isDisabled
}
add(): void {}
view(): void {}
}
E aqui temos a cereja do bolo, o nosso EventComposite, responsável por realizar a composição dos nossos eventos.
type EventUseCase = EventStatus & EventActions
class EventComposite implements EventActions, EventStatus {
parent?: EventStatus
isDisabled: boolean
constructor(private readonly events: Array<EventUseCase>) {}
disable(event?: EventStatus): boolean | void {
if(event?.parent === undefined) {
this.completeForEachEventsWith((eventOfList) => {
if(eventOfList.parent === event) {
eventOfList.disable()
}
})
}
if(event?.parent) {
event?.disable()
}
}
add(): void {
this.completeForEachEventsWith((event) => {
event.add()
})
}
view(): void {
this.completeForEachEventsWith((event) => {
event.view()
})
}
private completeForEachEventsWith(complete: (event: EventUseCase) => void) {
this.events.forEach((event) => {
complete(event)
})
}
}
Agora vamos brincar um pouco. Aqui criamos os nosso eventos e adicionamos ao nosso composite. Preste atenção quando atribuímos um pai para o facebook e firebase.
const main = () => {
const analytics = new Analytics();
const facebook = new Facebook()
facebook.parent = analytics
const firebase = new Firebase()
firebase.parent = analytics
const composite = new EventComposite([analytics, facebook, firebase])
}
Vamos desabilitar primeiro o facebook e executar o view e add do composite.
composite.disable(facebook)
composite.view()
composite.add()
Saída:
"Firebase: view"
"Firebase: add"
Agora vamos desabilitar somente o analytics.
composite.disable(analytics)
composite.view()
composite.add()
Saída:
Você pode estar se perguntando, o que aconteceu com a saída? Como o analytics é pai do facebook e firebase, ao desabilitarmos ele, automaticamente desabilitamos os seus filhos. Eis a mágica do Composite 🤌🏻.
Como podemos ver, o Composite é uma boa solução para resolver problemas onde precisamos compor os objetos em um formato de árvore e conseguir tratá-los de forma singular.
Aqui você encontrará o repo do código utilizado como exemplo: https://github.com/MarlonBeloMarques/composite-designpattern
É isso galera, até a próxima.
Posted on October 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.