Augusto Pascutti

Orientação a Objetos – Tell Don't Ask

Num post anterior vimos quanta informação do desenvolvedor os
métodos de acesso escondem, não dos objetos em si mas de como os desenvolvedores
podem usar esses objetos no dia a dia.

Continuando a mesma série de exemplos, vamos ver como unir dado e comportamento
para melhorar a comunicação com nossos usuários (desenvolvedores).

Dados vs Comportamentos

Nos examplos anteriores, tínhamos um valor de desconto em dinheiro. Um desconto
só é útil se aplicado à alguma coisa, certo? Como nossa empresa fictícia vende livros,
vamos ver como seria usar dar desconto em um livro:

namespace MyAppPromotion;

$book = new Product('Object Thinking (Developer Reference)', 60.00);
$blackFriday = new DiscountMoney(10.00);

$finalPrice = $book->getPrice() - $blackFriday->getValue();
$book->setPrice($finalPrice);

No exemplo acima temos duas classes representando dois dados diferentes: desconto
e produto. Veja que o comportamento de cálculo do desconto não esta encapsulado
em nada, vamos resolver isso:

namespace MyAppPromotion;

class DiscountCalculator
{
    public function applyDiscountOnProduct(
        DiscountMoney $discount,
        Product $book
    ) {
        if ($discount->getValue() >= $book->getPrice()) {
            $message = sprintf(
                'Discount (%01.2f) is greater than book price (%01.2f).',
                $discount->getValue(),
                $book->getPrice()
            );
            throw new UnexpectedValueException($message);
        }

        return $book->getPrice() - $discount->getValue();
    }
}

Agora temos nosso comportamento agindo em dois dados diferentes. Apesar de ser
um exemplo com classes, nada impede os seus dados de serem um array ou qualquer
outra estrutura de dados (apesar de saber que você prefere objetos 😉).

Vamos ver como fica o uso deles com nossa classe tilanga:

namespace MyAppPromotion;

$calculator = new DiscountCalculator;
$book = new Product('Object Thinking (Developer Reference)', 60.00);
$guguFriday = new DiscountMoney(50.00);

$customerCart = $calculator->applyDiscountOnProduct($guguFriday, $book);

Repare como a calculadora depende sempre dos dois objetos mas principalmente
do desconto, que é quando ela é necessária. Separar dados de comportamento pode
ser útil em alguns casos mas existe um grande poder (de comunicação) em unir ambos,
além de ser um dos fundamentos da Orientação a Objetos:

Código procedural pega uma informação e toma uma decisão. Código orientado a
objetos pede que objetos façam coisas.

Alec Sharp

Separando dado de comportamento você cobra do usuário o conhecimento dos
comportamentos associados ao dado. Toda vez que alguém quiser saber o preço que
algum cliente pagou (ou vai pagar) por algo, ela precisa saber dos descontos e
aplicá-los. Você acha mesmo que elas vão sempre saber disso?!

Papai Noel não existe, nem o coelhinho da páscoa. #prontofalei

Tell, don’t ask

O Tell, don’t ask basicamente diz que juntando os dois (dado e
comportamento) em uma única classe, o desenvolvedor precisa saber menos e o
código tende a ficar mais conciso (e coeso, como consequência).

Como ficaria nosso exemplo de desconto em dinheiro usando ele?

namespace MyappPromotionDiscount;

class Money
{
    private $amount = 0.00;

    public function __construct($amountToDiscount)
    {
        $this->amount = $amountToDiscount;
    }

    public function calculateProductFinalPrice(Product $product)
    {
        $newPrice = $product->getPrice() - $this->amount;
        if ($newPrice <= 0.00) {
            $message = sprintf(
                'Discount (%01.2f) is greater than book price (%01.2f).',
                $this->amount(),
                $product->getPrice()
            );
            throw new UnexpectedValueException($message);
        }

        return $newPrice;
    }
}

Pronto. Jogamos a calculadora fora, tiramos o método de acesso pra retornar o
valor do desconto e colocamos o método da calculadora dentro do desconto.

Antes o desenvolvedor precisava saber o que fazer com aquele valor, agora ele
precisa decidir se o que ele precisa é o valor do produto com o desconto aplicado
ou não. É justamente desse fato que sai o nome do princípio: antes de
permitir algum usuário de pedir alguma coisa (dado), diga/ofereça a ele as
ações (comportamentos) que ele pode usar naquele objeto (dado+comportamento).

$book = new Product('Object Thinking (Developer Reference)', 60.00);
$blackFriday = new DiscountMoney(10.00);

$customerCartValue = $blackFriday->calculateProductFinalPrice($book);

Pensando sempre em quem vai usar seu código, inclusive você, o código acima é
mais simples e menos propenso aos erros de interpretação no futuro.

Como todo comportamento de desconto está disponível e pronto pra usar, expor o
valor de desconto através de um método de acesso é desnecessário. Esse menor
nível de exposição torna o encapsulamento dos algoritmos melhor e portanto, mais
fáceis de evoluir.

É comum nas discussões de segregar (ou não) dado de comportamento, o pessoal
a favor da união de ambos citar o argumento dos modelos anêmicos
em DDD mas, acho que ninguém precisa ir tão longe pra defender os benefícios
desse princípio.

Agora queremos descontos em porcentagem!

Agora precisamos aplicar um desconto em porcentagem. Refletindo sobre o
problema, não temos muito a fazer além de criar outra classe de desconto
e mudar ou pouco como o valor final do livro é calculado.

Acho sempre válido você pensar nas soluções por você, se possível envie
elas pra mim depois. Como agora não tenho muita opção, segue minha solução
pro problema:

namespace MyAppPromotionDiscount;

class Percentage
{
    private $fraction = 0.00;

    public function __construct($percentageToDiscount)
    {
        $value = $percentageToDiscount / 100;
        $this->fraction = (float) $value;
    }

    public function calculateProductFinalPrice(Product $product)
    {
        $discountValue = $product->getPrice() * $this->fraction;
        $newPrice = $product->getPrice() - $discountValue;
        if ($newPrice <= 0.00) {
            $message = sprintf(
                'Discount (%01.2f%%) makes product cost nothing!',
                $this->fraction * 100
            );
            throw new UnexpectedValueException($message);
        }

        return $newPrice;
    }
}

Usar o princípio força você a imaginar como um objeto vai ser usado
e nas informações que o usuário terá quando for consumir a funcionalidade
da sua classe. Pra resolver isso, eu costumo tentar fazer o código imitar
uma conversa entre duas pessoas
.

Quando você recebe um desconto, como a pessoa comunica esse desconto a você?

  1. Você tem 25% de desconto em qualquer produto.
  2. Você só vai pagar 75% do produto.
  3. Você vai pagar o valor do produto menos o valor dele multiplicado por 0.25.

Imaginando que as três conversas fossem um código de verdade, eu penso
imediatamente nos códigos abaixo:

namespace MyAppPromotion;

// 1. Você tem 25% de desconto em qualquer produto.
$blackFriday = new DiscountPercentage(25);

// 2. Você só vai pagar 75% do produto.
$blackFriday = new DiscountPercentage(75);

// 3. Você vai pagar o valor do produto menos o valor dele multiplicado por 0.25.
$blackFriday = new DiscountPercentage(0.25);

Qualquer um dos três códigos pode funcionar, mas qual é a real expectativa do
desenvolvedor quando for consumir o código existente?
Deixar implementação próxima da forma como vocês conversam sobre uma
funcionalidade dentro da empresa
evita diversos problemas. Como na nossa
empresa fictícia nos comunicamos usando o primeiro caso, implementamos ele.

Repare no nome do primeiro argumento: $percentageToDiscount. Ele tira toda a
ambiguidade do processo e elimina a necessidade do desenvolvedor precisar ler o
resto da classe. Através de um código limpo estamos sempre evitando a ambiguidade
e a dúvida onde pudermos.

As diferenças entre os casos 1 e 2 são a forma como vocês conversam sobre a
funcionalidade. Já no caso 3 a coisa se degringola um pouco: Você expõe a conta
de porcentagem pro cliente, o que pode ser prejudicial no futuro, como no caso
dos métodos de acesso
.

Defendendo a separação de comportamento e dados

Existem alguns vários argumentos a favor de manter os dois separados mas de forma alguma
vou esgotar essa discussão, nem que eu fosse presunçoso o suficiente pra achar
que tenho essa capacidade, então minha intenção é te dar opções (ou novos problemas)
pra ao menos facilitar sua escolha.

O primeiro argumento contra a separação costuma ser o da violação de SRP, o
que é ótimo se a pessoa se embasar na questão de responsabilidade. É ótimo porque
a questão de responsabilidade em nada viola a unidade de dado e comportamento.
“Um motivo pra mudar” é o que define “responsabilidade” no SRP, se você
fizer um bom trabalho de design você consegue ter ambos. Ninguém disse que ia
ser fácil.

O segundo argumento é chato e costuma tocar objetos que precisam ser mapeados ou
serializados. Nesse caso, você tem uma classe que possui informações além de um
método que retorna o mapamento daquele objeto pra outro. Você pode isolar o
mapeamento em outra classe e sempre que a estrutura de dados mudar, mudar essa
classe-mapa também – resumindo, uma bosta. Você pode usar Reflection e fazer
esse mapeamento com Inflexão, é um jeito melhor de resolver esse
determinado problema e tenho certeza que não é o único. Aqui o argumento é chato
porque provavelmente você vai cair nos trade-offs: analisar diferentes soluções
e adotar a “menos pior” pro seu caso específico.

Em todos os casos, repare que a opinião pouco importa. Usar o “Tell, don’t ask
não precisa violar nenhum outro princípio independente da situação mas te convido
a antes de pensar nos princípios, pensar em quem utilizará o código e se todas
as implicações de negócio estarão tão claras pra eles quanto estão pra você. Esqueça
disso e use todos os princípios, você terá problemas. Não disse que os princípios
são ruins, só disse que eles por si só não resolverão todos os seus problemas.

PS: Se você pensou em usar alguns desses argumentos pra defender Active Record,
eu tenho um argumento contra você: SRP 😄. Eu sou um filho da puta, to ligado. 😘

Conclusões

Assim como o uso inconsicente dos métodos de acesso pode ser ruim, separar
dado dos seus comportamentos também é.

Dar aos usuários do seu código ações a serem feitas em cima de determinados dados,
junto com eles, vai melhorar a comunicação do seu código com eles. É essa melhora
de comunicação que impede os problemas de acontecerem.

Eu não falei quase nada sobre o TDA, então vá ler mais a respeito:

PS: Meu obrigado pro João, Nelson e Cobucci pelo review a mais um
post. Eles são uns 👼 !

Powered by WPeMatico