Saltar para o conteúdo

Design Patterns: Métodos importantes utilizados no mundo do desenvolvimento

Fonte: Wikiversidade

O que são design patterns?

[editar | editar código-fonte]

Os design patterns oferecem soluções gerais e reutilizáveis para problemas recorrentes no design de software, simplificando a manutenção e a escalabilidade do código. Eles se dividem em três categorias principais: padrões de criação, padrões estruturais e padrões comportamentais. Cada categoria engloba uma variedade de métodos amplamente utilizados no desenvolvimento de software, proporcionando ferramentas eficazes para lidar com diferentes desafios de design.

De que se trata cada categoria de design pattern?

[editar | editar código-fonte]
  • Padrões de criação: Providenciam mecanismos de criação para garantir flexibilidade e reutilização de código.
  • Padrões estruturais: Explicam como unir objetos e classes em estruturas maiores, ao mesmo tempo em que mantém essas estruturas flexíveis e eficientes.
  • Padrões de comportamento: Tomam conta de comunicação efetiva e da gestão de responsabilidades entre objetos.


Nesse tutorial, exploraremos alguns exemplos de design patterns mais comuns de cada uma dessas categorias, dando exemplos de implementação com a linguagem Kotlin.

Padrões de criação

[editar | editar código-fonte]

O padrão de design Singleton é um padrão criacional que assegura que uma classe tenha apenas uma instância e fornece um ponto global de acesso a essa instância. Ele é útil quando você precisa que uma única instância de uma classe coordene ações em todo o sistema.

Para implementar a estrutura de Singleton, basta criarmos uma classe com construtor privado, o que impede que outros objetos criem instâncias da classe. Em algumas linguagens, armazenamos a instância única do objeto dentro da classe e a inicializamos quando o usuário desejar utilizá-la.

Usando Kotlin, basta utilizarmos a keyword object:

object Singleton {
    fun doSomething() = "Doing something"
}

Factory Method

[editar | editar código-fonte]

O padrão de design Factory Method define uma interface para criar um objeto, mas permite que as subclasses alterem o tipo de objetos que serão criados. O objetivo principal é delegar a responsabilidade da criação dos objetos para subclasses, promovendo a flexibilidade e a extensibilidade do código.

Em Factory Method, chamamos a 'fábrica" criadora de objetos de Creator e os objeto retornado pela mesma como Product. Normalmente, o Creator é representado no código como uma classe abstrata que possui um método que retorna uma instância de Product. Já o Product é uma interface que representa as classes que podem ser retornadas por Creator. As subclasses de Creator ficam responsabilizadas em sobrescrever o método de criação de objeto, e retornaram seu respectivo objeto concreto designado.

Em Kotlin, podemos implementar Factory Method da seguinte forma:

interface Product
class ConcreteProduct1 : Product
class ConcreteProduct2 : Product

interface ProductFactory {
    fun createProduct() : Product
}
 
class ProductFactory1 : ProductFactory {
    override fun createProduct() : Product {
        return ConcreteProduct1()
    }
}

class ProductFactory2 : ProductFactory {
    override fun createProduct() : Product {
        return ConcreteProduct2()
    }
}

Outra forma de implementação bastante comum de Factory Method pode ser feita da seguinte forma:

interface Product
class ConcreteProduct1 : Product
class ConcreteProduct2 : Product

class ProductFactory {
    fun createProduct(productType : String) : Product? {
        when (productType) {
            "1" -> return ConcreteProduct1()
            "2" -> return ConcreteProduct2()
        }
        return null
    }
}

Padrões estruturais

[editar | editar código-fonte]

O padrão de design Adapter permite a colaboração entre interfaces inicialmente incompatíveis. Ele é especialmente útil ao utilizar bibliotecas e aplicativos de terceiros que geram inputs e outputs em diferentes formatos. Por exemplo, em um aplicativo de monitoramento de mercado de investimentos que recebe dados em formato .xml e precisa usar uma biblioteca gráfica que lida apenas com .json, um adaptador pode ser usado para converter .xml para .json sem alterar o código existente do aplicativo.

O adaptador implementa a interface de um objeto e encapsula outro, permitindo que classes com interfaces incompatíveis possam se comunicar. A estrutura inclui o Cliente, que contém a lógica do programa; a Interface, que é o protocolo para interação; o Serviço, que possui métodos úteis mas incompatíveis com o Cliente; e o Adaptador, que traduz os requisitos do Cliente para o Serviço. Dessa forma, o adaptador recebe os requisitos do Cliente e os adapta para o Serviço, permitindo uma integração suave sem modificar o código original.

Podemos demonstrar a utilização de Adapters em Kotlin com o seguinte código exemplo:

// Suponha que você tenha duas classes com interfaces compatíveis:
// BuracoRedondo e PecaRedonda.
class BuracoRedondo(private val raio: Double) {

    fun obterRaio(): Double {
        // Retorna o raio do buraco.
        return raio
    }

    fun encaixa(peca: PecaRedonda): Boolean {
        return this.obterRaio() >= peca.obterRaio()
    }
}

open class PecaRedonda(private val raio: Double) {

    open fun obterRaio(): Double {
        // Retorna o raio do pino.
        return raio
    }
}

// Mas há uma classe incompatível: PecaQuadrada.
class PecaQuadrada(private val lado: Double) {

    fun obterLado(): Double {
        // Retorna a largura do pino quadrado.
        return lado
    }
}

// Uma classe adaptadora permite encaixar pinos quadrados em buracos redondos.
// Ela estende a classe PecaRedonda para permitir que os objetos adaptadores
// atuem como pinos redondos.
class AdaptadorPecaQuadrada(private val peca: PecaQuadrada) : 
    PecaRedonda(peca.obterLado() * Math.sqrt(2.0) / 2) 

// Em algum lugar no código do cliente.
fun main() {
    val buraco = BuracoRedondo(5.0)
    val pecaRedonda = PecaRedonda(5.0)
    println(buraco.encaixa(pecaRedonda)) // true

    val pecaQPeq = PecaQuadrada(5.0)
    val pecaQGra = PecaQuadrada(10.0)
    // buraco.encaixa(pecaQPeq)
    // Isto não compilará (tipos incompatíveis)

    val adaptadorPecaQPeq = AdaptadorPecaQuadrada(pecaQPeq)
    val adaptadorPecaQGra = AdaptadorPecaQuadrada(pecaQGra)
    println(buraco.encaixa(adaptadorPecaQPeq)) // true
    println(buraco.encaixa(adaptadorPecaQGra)) // false
}

O design pattern Decorator permite adicionar novos comportamentos a objetos através do uso de objetos encapsuladores que contêm esses comportamentos. Um exemplo comum é quando se deseja enviar uma mesma mensagem para diferentes redes comunicacionais como email, SMS, Slack, Facebook e Instagram. Em vez de criar uma notificação para cada tipo de aplicação, pode-se utilizar uma única classe Notificador, onde os métodos de notificação para cada aplicação são decoradores que modificam o comportamento de envio da mensagem conforme necessário.

O padrão Decorator não faz uso de herança, mas sim de agregação, onde um objeto decorador contém o mesmo conjunto de métodos do objeto alvo e delega os requisitos, alterando os resultados conforme necessário. A estrutura básica inclui o Componente, que declara a interface comum para objetos encapsuladores e encapsulados; o Componente Concreto, que define o comportamento básico; o Decorador Base, que referencia um objeto encapsulado e delega operações; e os Decoradores Concretos, que adicionam dinamicamente novos comportamentos. O cliente pode encapsular os componentes em várias camadas de decoradores, permitindo flexibilidade e extensão dos comportamentos sem modificar os métodos de notificação originais.

// A interface do componente define operações que podem ser
// alteradas por decoradores.
interface FonteDeDados {
    fun escreverDados(dados: String)
    fun lerDados(): String
}

// Componentes concretos fornecem implementações padrão para as
// operações. Pode haver várias variações dessas classes em um
// programa.
class FonteDeDadosArquivo(private val nomeDoArquivo: String) : FonteDeDados {

    override fun escreverDados(dados: String) {
        // Escrever dados no arquivo.
    }

    override fun lerDados(): String {
        // Ler dados do arquivo.
        return ""
    }
}

// A classe decoradora base segue a mesma interface que os
// outros componentes. O principal objetivo desta classe é
// definir a interface de encapsulamento para todos os
// decoradores concretos. A implementação padrão do código de
// encapsulamento pode incluir um campo para armazenar um
// componente encapsulado e os meios para inicializá-lo.
open class DecoradorFonteDeDados(protected val fonte: FonteDeDados) : FonteDeDados {

    override fun escreverDados(dados: String) {
        fonte.escreverDados(dados)
    }

    override fun lerDados(): String {
        return fonte.lerDados()
    }
}

// Decoradores concretos devem chamar métodos no objeto
// encapsulado, mas podem adicionar algo ao resultado. Decoradores
// podem executar o comportamento adicional antes ou depois da
// chamada ao objeto encapsulado.
class DecoradorCriptografia(fonte: FonteDeDados) : DecoradorFonteDeDados(fonte) {

    override fun escreverDados(dados: String) {
        // 1. Criptografar os dados passados.
        // 2. Passar os dados criptografados para o método escreverDados do encapsulado.
    }

    override fun lerDados(): String {
        // 1. Obter dados do método lerDados do encapsulado.
        // 2. Tentar descriptografá-los se estiverem criptografados.
        // 3. Retornar o resultado.
        return ""
    }
}

// Você pode encapsular objetos em várias camadas de decoradores.
class DecoradorCompressao(fonte: FonteDeDados) : DecoradorFonteDeDados(fonte) {

    override fun escreverDados(dados: String) {
        // 1. Comprimir os dados passados.
        // 2. Passar os dados comprimidos para o método escreverDados do encapsulado.
    }

    override fun lerDados(): String {
        // 1. Obter dados do método lerDados do encapsulado.
        // 2. Tentar descomprimir se estiverem comprimidos.
        // 3. Retornar o resultado.
        return ""
    }
}

// Opção 1. Um exemplo simples de montagem de decoradores.
class Aplicacao {

    fun exemploDeUsoSimples() {
        var fonte: FonteDeDados = FonteDeDadosArquivo("algum_arquivo.dat")
        fonte.escreverDados("dados_salariais")
        // O arquivo de destino foi escrito com dados simples.

        fonte = DecoradorCompressao(fonte)
        fonte.escreverDados("dados_salariais")
        // O arquivo de destino foi escrito com dados comprimidos.

        fonte = DecoradorCriptografia(fonte)
        // A variável fonte agora contém o seguinte:
        // Criptografia > Compressão > FonteDeDadosArquivo
        fonte.escreverDados("dados_salariais")
        // O arquivo foi escrito com dados comprimidos e criptografados.
    }
}

// Opção 2. Código cliente que usa uma fonte de dados externa.
// Objetos GerenciadorDeSalarios não sabem nem se importam com
// especificidades de armazenamento de dados. Eles trabalham com
// uma fonte de dados pré-configurada recebida do configurador do app.
class GerenciadorDeSalarios(private val fonte: FonteDeDados) {

    fun carregar(): String {
        return fonte.lerDados()
    }

    fun salvar() {
        fonte.escreverDados("dados_salariais")
    }
    // ...Outros métodos úteis...
}

// O app pode montar diferentes pilhas de decoradores em tempo
// de execução, dependendo da configuração ou do ambiente.
class ConfiguradorDeAplicacao {

    fun exemploDeConfiguracao() {
        var fonte: FonteDeDados = FonteDeDadosArquivo("salarios.dat")
        if (habilitarCriptografia) {
            fonte = DecoradorCriptografia(fonte)
        }
        if (habilitarCompressao) {
            fonte = DecoradorCompressao(fonte)
        }

        val gerenciador = GerenciadorDeSalarios(fonte)
        val salarios = gerenciador.carregar()
    }

    companion object {
        var habilitarCriptografia: Boolean = true
        var habilitarCompressao: Boolean = true
    }
}

Padrões comportamentais

[editar | editar código-fonte]

Chain of Responsibility

[editar | editar código-fonte]

O padrão de design Chain of Responsibility permite que um request (pedido) passe por uma cadeia de handlers (manipuladores) até que um deles trate o pedido. Este padrão promove o desacoplamento entre o emissor do pedido e os seus receptores, permitindo que múltiplos handlers tenham a oportunidade de processar o pedido sem que o emissor precise conhecer a estrutura da cadeia.

Tal padrão pode ser utilizado para lidar com a interação com um botão dentro de uma estrutura de interface de usuário. Verificamos a classe do botão apertado e as classes que contém o botão (subpainéis, painéis e até mesmo a janela inteira do programa) até encontrarmos uma classe responsável por lidar com o comportamento decorrente do apertar do botão.

Utilizando Kotlin, podemos programar Chain of Responsibility da seguinte forma:

class Request
class Response(status: Boolean)

interface Handler {
	fun setNext(next: Handler)
    fun handle(request: Request) : Response
    fun canHandle(request: Request) : Boolean
}

abstract class BaseHandler : Handler {
    protected lateinit var nextHandler : Handler
    
    override fun setNext(next: Handler) {
		nextHandler = next
    }
    
    override fun handle(request: Request) : Response {
        if (canHandle(request)) return Response(true)
        if (nextHandler != null) return nextHandler.handle(request)
        return Response(false)
    }
}

class Handler1 : BaseHandler() {
    override fun canHandle(request: Request) : Boolean {
        // Escreva aqui condições de manipulação handler1
        return true
    }
}

class Handler2 : BaseHandler() {
    override fun canHandle(request: Request) : Boolean {
        // Escreva aqui condições de manipulação handler2
        return false
    }
}

O design pattern Observador permite que você crie um mecanismo de subscrição que alerta diversos objetos sobre qualquer evento que ocorra em relação ao objeto sendo observado. Claramente, as newsletters de forma geral fazem uso desse tipo de padrão.

Em questão estrutural,a figura principal é a de um Editor (Publisher) que produz eventos que alertam outros objetos. Esses eventos ocorrem nas mudanças de estado ou execução de alguns comportamentos do editor. Eles contém uma lista de inscrições que permite a entrada de interessados. Quando um novo evento ocorre o editor percorre a lista de inscrições e chama o método de notificação declarado na interface de cada objeto inscrito.

Além disso, o Inscrito declara a interface de notificação como descrito. Na maior parte dos casos é um simples método de update. Os Inscritos Concretos realizam ações respondendo às notificações enviadas pelo editor. Finalmente, o Cliente cria o editor e os inscritos separadamente e registra os interessados nos updates de interesse.

// A classe base do editor inclui o código de gerenciamento de assinaturas
// e métodos de notificação.
class GerenciadorDeEventos {
    private val ouvintes = mutableMapOf<String, MutableList<EventListener>>()

    fun inscrever(tipoEvento: String, ouvinte: EventListener) {
        ouvintes.computeIfAbsent(tipoEvento) { mutableListOf() }.add(ouvinte)
    }

    fun desinscrever(tipoEvento: String, ouvinte: EventListener) {
        ouvintes[tipoEvento]?.remove(ouvinte)
    }

    fun notificar(tipoEvento: String, dados: String) {
        ouvintes[tipoEvento]?.forEach { it.atualizar(dados) }
    }
}

// O editor concreto contém a lógica de negócios real que é interessante
// para alguns assinantes. Poderíamos derivar essa classe do editor base,
// mas isso nem sempre é possível na vida real porque o editor concreto pode
// já ser uma subclasse. Nesse caso, você pode ajustar a lógica de assinatura
// com composição, como fizemos aqui.
class Editor {
    val eventos = GerenciadorDeEventos()
    private lateinit var arquivoPath: String

    fun abrirArquivo(caminho: String) {
        arquivoPath = caminho
        eventos.notificar("abrir", arquivoPath.substringAfterLast("/"))
    }

    fun salvarArquivo() {
        val conteudo = arquivoPath
        eventos.notificar("salvar", arquivoPath.substringAfterLast("/"))
    }
}

// Aqui está a interface do assinante. Se sua linguagem de programação
// suporta tipos funcionais, você pode substituir toda a hierarquia de
// assinantes por um conjunto de funções.
interface EventListener {
    fun atualizar(nomeArquivo: String)
}

// Os assinantes concretos reagem às atualizações emitidas pelo editor
// ao qual estão anexados.
class OuvinteDeLog(private val logPath: String, private val mensagem: String) : EventListener {
    override fun atualizar(nomeArquivo: String) {
        val conteudoAtual = try {
            ""
        } catch (e: Exception) {
            ""
        }
    }
}

class OuvinteDeAlertasEmail(private val email: String, private val mensagem: String) : EventListener {
    override fun atualizar(nomeArquivo: String) {
        // Simulando envio de email
        println("Enviando email para $email: ${mensagem.replace("%s", nomeArquivo)}")
    }
}

// Uma aplicação pode configurar editores e assinantes em tempo de execução.
class Aplicacao {
    fun configurar() {
        val editor = Editor()

        val logger = OuvinteDeLog("/path/to/log.txt", "Alguém abriu o arquivo: %s")
        editor.eventos.inscrever("abrir", logger)

        val alertasEmail = OuvinteDeAlertasEmail("admin@example.com", "Alguém alterou o arquivo: %s")
        editor.eventos.inscrever("salvar", alertasEmail)
    }
}

- https://refactoring.guru/design-patterns

- https://sourcemaking.com/design_patterns

- https://en.wikipedia.org/wiki/Software_design_pattern