É uma boa idéia definir uma grande função privada em uma classe para manter um estado válido, ou seja, atualizar os membros de dados do objeto?

18

Embora no código abaixo seja usada uma compra simples de um item em um site de comércio eletrônico, minha pergunta geral é sobre a atualização de todos os membros de dados para manter os dados de um objeto em estado válido o tempo todo.

Encontrei "consistência" e "estado é mau" como frases relevantes, discutidas aqui: https://en.wikibooks.org/wiki/Object_Oriented_Programming#.22State.22_is_Evil.21

<?php

class CartItem {
  private $price = 0;
  private $shipping = 5; // default
  private $tax = 0;
  private $taxPC = 5; // fixed
  private $totalCost = 0;

  /* private function to update all relevant data members */
  private function updateAllDataMembers() {
    $this->tax =  $this->taxPC * 0.01 * $this->price;
    $this->totalCost = $this->price + $this->shipping + $this->tax;
  }

  public function setPrice($price) {
      $this->price = $price;
      $this->updateAllDataMembers(); /* data is now in valid state */
  }

  public function setShipping($shipping) {
    $this->shipping = $shipping;
    $this->updateAllDataMembers(); /* call this in every setter */
  }

  public function getPrice() {
    return $this->price;
  }
  public function getTaxAmt() {
    return $this->tax;
  }
  public function getShipping() {
    return $this->shipping;
  }
  public function getTotalCost() {
    return $this->totalCost;
  }
}
$i = new CartItem();
$i->setPrice(100);
$i->setShipping(20);
echo "Price = ".$i->getPrice(). 
  "<br>Shipping = ".$i->getShipping().
  "<br>Tax = ".$i->getTaxAmt().
  "<br>Total Cost = ".$i->getTotalCost();

Alguma desvantagem, ou talvez melhores maneiras de fazer isso?

Esse é um problema recorrente em aplicativos do mundo real apoiados por um banco de dados relacional e se você não usar procedimentos armazenados extensivamente para enviar toda a validação para o banco de dados. Eu acho que o armazenamento de dados deve apenas armazenar dados, enquanto o código deve executar todo o estado de tempo de execução, mantendo o trabalho.

EDIT: esta é uma pergunta relacionada, mas não possui uma recomendação de boas práticas em relação a uma única função grande para manter o estado válido: /programming/1122346/c-sharp-object-oriented-design-maintaining- estado-objeto-válido

EDIT2: Embora a resposta da @ eignesheep seja a melhor, esta resposta - /software//a/148109/208591 - é o que preenche as linhas entre a resposta da @ eigensheep e o que eu queria saber - o código só deve processar, e o estado global deve ser substituído pela passagem de estado habilitada para DI entre os objetos.

site80443
fonte
Evito ter variáveis ​​que são porcentagens. Você pode aceitar uma porcentagem do usuário ou exibir uma para um usuário, mas a vida é muito melhor se as variáveis ​​do programa forem proporções.
precisa

Respostas:

29

Sendo tudo igual, você deve expressar seus invariantes em código. Nesse caso, você tem o invariante

$this->tax =  $this->taxPC * 0.01 * $this->price;

Para expressar isso em seu código, remova a variável do membro do imposto e substitua getTaxAmt () por

public function getTaxAmt() {
  return $this->taxPC * 0.01 * $this->price;
}

Você deve fazer algo semelhante para se livrar da variável de membro de custo total.

Expressar seus invariantes em seu código pode ajudar a evitar erros. No código original, o custo total está incorreto se marcado antes de chamar setPrice ou setShipping.

eigensheep
fonte
3
Muitos idiomas têm getters, de modo que essas funções fingem que são propriedades. O melhor de ambos!
precisa saber é o seguinte
Ponto excelente, mas meu caso de uso geral é onde o código busca e armazena dados em várias colunas em várias tabelas em um banco de dados relacional (principalmente MySQL) e não quero usar procedimentos armazenados (discutível e outro tópico por si só). Levando sua idéia de invariantes em código adiante, isso significa que todos os cálculos devem ser "encadeados": getTotalCost()chamadas getTaxAmt()e assim por diante. Isso significa que nós sempre armazenamos coisas não calculadas . Estamos nos movendo um pouco para a programação funcional? Isso também complica o armazenamento de entidades calculadas em tabelas para acesso rápido ... Precisa de experimentação!
site80443
13

Alguma desvantagem [?]

Certo. Esse método depende de todos sempre se lembrando de fazer algo. Qualquer método que confie em todos e sempre está sujeito a falhar às vezes.

talvez melhores maneiras de fazer isso?

Uma maneira de evitar o ônus da lembrança da cerimônia é calcular as propriedades do objeto que dependem de outras propriedades, conforme necessário, como sugeriu @eigensheep.

Outra é tornar o item do carrinho imutável e calculá-lo no método construtor / fábrica. Você normalmente usaria o método "calcular conforme necessário", mesmo se você tornasse o objeto imutável. Mas se o cálculo consumir muito tempo e for lido muitas e muitas vezes; você pode escolher a opção "calcular durante a criação".

$i = new CartItem();
$i->setPrice(100);
$i->setShipping(20);

Você deveria se perguntar; Um item do carrinho sem preço faz sentido? O preço de um item pode mudar? Depois de criado? Após o imposto calculado? etc Talvez você deva tornar CartItemimutável e atribuir preço e frete no construtor:

$i = new CartItem(100, 20);

Um item do carrinho faz sentido sem o carrinho ao qual pertence?

Se não, eu esperaria $cart->addItem(100, 20).

abuzittin gillifirca
fonte
3
Você aponta a maior desvantagem: confiar nas pessoas que se lembram de fazer as coisas raramente é uma boa solução. A única coisa em que você pode confiar em um ser humano é que ele se esqueça de fazer algo.
corsiKa
@corsiKlause Ho Ho Ho e abuzittin, ponto sólido, não podem argumentar com isso - as pessoas esquecem invariavelmente . No entanto, o código que escrevi acima é apenas um exemplo, há casos de uso substanciais em que alguns membros de dados são atualizados posteriormente. A outra maneira que vejo é normalizar ainda mais - criar classes de modo que membros de dados atualizados independentemente estejam em outras classes e colocar a responsabilidade da atualização em algumas interfaces - para que outros programadores (e você depois de algum tempo) tenham que escrever um método - o O compilador lembra que você precisa escrevê-lo. Mas isso adicionaria muito mais classes ...
site80443
1
@ site80443 Com base no que vejo, essa é a abordagem errada. Tente modelar seus dados para que apenas os dados validados contra eles mesmos sejam incluídos. Por exemplo, o preço de um item não pode ser negativo, apenas depende de si mesmo. Se um item tiver desconto, não leve em consideração o preço - decore-o com um desconto mais tarde. Armazene os US $ 4,99 para o item e o desconto de 20% como uma entidade separada e o imposto de 5% como mais uma entidade. Na verdade, parece que você deve considerar o padrão Decorator se os exemplos representam seu código da vida real.
corsiKa