DEV Community

Marlon Marques
Marlon Marques

Posted on • Updated on

Composite - Design Pattern

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.

Composite Diagram of Class

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
}
Enter fullscreen mode Exit fullscreen mode
interface EventStatus {
  parent?: EventStatus
  readonly isDisabled: boolean

  disable(event?: EventStatus): boolean | void
}
Enter fullscreen mode Exit fullscreen mode

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')
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
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')
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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 {}
}
Enter fullscreen mode Exit fullscreen mode

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)
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

Vamos desabilitar primeiro o facebook e executar o view e add do composite.

 composite.disable(facebook)

 composite.view()
 composite.add()
Enter fullscreen mode Exit fullscreen mode

Saída:

"Firebase: view" 
"Firebase: add" 
Enter fullscreen mode Exit fullscreen mode

Agora vamos desabilitar somente o analytics.

 composite.disable(analytics)

 composite.view()
 composite.add()
Enter fullscreen mode Exit fullscreen mode

Saída:

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)