Modelos GRASP: especialista em informações

Olá, Habrovsk. Em contato com Vladislav Rodin. Atualmente, estou ministrando cursos sobre arquitetura de software e arquitetura de software de alta carga no portal OTUS. Dessa vez, decidi escrever um pouco de material com direitos autorais, antecipando o início de um novo curso “Arquitetura e Design Patterns” . Gostar de ler.





Introdução


Descritos no livro de Craig Larman, Applying UML and patterns, 3rd edition, GRASP patterns são uma generalização de padrões GoF, bem como uma conseqüência direta dos princípios de POO. Eles complementam a etapa que falta na escada lógica, o que permite que você obtenha os padrões GoF a partir dos princípios do POO. Os modelos GRASP provavelmente não são padrões de design (como os do GoF), mas princípios fundamentais para a distribuição de responsabilidades entre classes. A prática mostra que eles não são muito populares; no entanto, a análise de classes projetadas usando o conjunto completo de padrões GRASP é um pré-requisito para escrever um bom código.

Uma lista completa dos modelos GRASP consiste em 9 elementos:

  • Especialista em informação
  • O Criador
  • Controlador
  • Baixo acoplamento
  • Coesão alta
  • Polimorfismo
  • Fabricação pura
  • Indirection
  • Variações protegidas

Sugiro considerar o padrão mais óbvio e mais importante da lista: especialista em informações.

Especialista em informação


Redação


Evitando formulações científicas, a essência desse padrão pode ser expressa da seguinte maneira: as informações devem ser processadas onde estão contidas.

Exemplo de violação


Apesar da aparente simplicidade e obviedade, estou certo de que no código de qualquer projeto você pode encontrar muitas violações a esse princípio.

Considere o sistema de classes mais simples: Order (order), contendo uma lista de OrderItem'ov (linhas de pedido), cujos elementos, por sua vez, contêm Good (produto) e sua quantidade, e o produto pode conter, por exemplo, preço, nome, etc .:

@Getter
@AllArgsConstructor
public class Order {
    private List<OrderItem> orderItems;
    private String destinationAddress;
}

@Getter
@AllArgsConstructor
public class OrderItem {
    private Good good;
    private int amount;
}

@Getter
@AllArgsConstructor
public class Good {
    private String name;
    private int price;
}


Temos uma tarefa simples: calcular o valor do pedido. Se você abordar a solução para esse problema sem muita reflexão, poderá escrever imediatamente algo assim no código do cliente que funciona com objetos da classe Order:

public class Client {
    public void doSmth() {
        
    }
    
    private int getOrderPrice(Order order) {
        List<OrderItem> orderItems = order.getOrderItems();
        
        int result = 0;
        
        for (OrderItem orderItem : orderItems) {
            int amount = orderItem.getAmount();
            
            Good good = orderItem.getGood();
            int price = good.getPrice();
            
            result += price * amount;
        }
        
        return result;
    }
}


Vamos analisar esta solução.

Em primeiro lugar, se começarmos a adicionar lógica de negócios relacionada a preços, o código do método Client :: getOrderPrice não apenas crescerá inevitavelmente, mas também será cercado por todos os tipos de if-s (desconto para aposentados, desconto em feriados, desconto devido a compras no atacado), o que no final levará ao fato de que esse código será impossível de ler e muito menos de alterações.

Em segundo lugar, se você criar um diagrama UML, poderá descobrir que existe uma dependência da classe Client em até 3 classes: Order, OrderItem e Good. Ele desenha toda a lógica de negócios para trabalhar com essas classes. Isso significa que, se quisermos reutilizar OrderItem ou Good separadamente do Order (por exemplo, para calcular o preço das mercadorias restantes nos armazéns), simplesmente não podemos fazer isso, porque a lógica de negócios está no código do cliente, o que levará à duplicação inevitável do código.

Neste exemplo, como em quase todos os lugares onde há uma cadeia de get'ov, o princípio do Information Expert é violado, porque o código do cliente processa as informações e contém seu Pedido.

Exemplo de aplicação


Vamos tentar redistribuir responsabilidades de acordo com o princípio:

@Getter
@AllArgsConstructor
public class Order {
    private List<OrderItem> orderItems;
    private String destinationAddress;
    
    public int getPrice() {
        int result = 0;
        
        for(OrderItem orderItem : orderItems) {
            result += orderItem.getPrice();
        }
        
        return result;
    }
}

@Getter
@AllArgsConstructor
public class OrderItem {
    private Good good;
    private int amount;

    public int getPrice() {
        return amount * good.getPrice();
    }
}

@Getter
@AllArgsConstructor
public class Good {
    private String name;
    private int price;
}

public class Client {
    public void doSmth() {
        Order order = new Order(new ArrayList<>(), "");
        order.getPrice();
    }
}


Agora as informações são processadas na classe que a contém, o código do cliente depende apenas de Order, sem suspeitar de nada sobre sua estrutura interna, e as classes Order, OrderItem e Good ou OrderItem e Good podem ser montadas em uma biblioteca separada que pode ser usada em diferentes partes do projeto.

Conclusão


O Information Expert, resultante do encapsulamento, é um dos princípios mais fundamentais do compartilhamento de responsabilidades do GRASP. Sua violação pode ser facilmente determinada e eliminada, aumentando a simplicidade da percepção do código (o princípio da menor surpresa), acrescentando a possibilidade de reutilização e reduzindo o número de conexões entre classes.

Convidamos você a um webinar gratuito, no âmbito do qual será possível estudar os recursos de um aplicativo monolítico, arquiteturas de vários níveis e sem servidor. Dê uma olhada no sistema orientado a eventos, no sistema orientado a serviços e na arquitetura de microsserviço.

All Articles