¿Es una buena idea definir una gran función privada en una clase para mantener un estado válido, es decir, actualizar los miembros de datos del objeto?

18

Aunque en el siguiente código se usa una compra simple de un solo artículo en un sitio de comercio electrónico, mi pregunta general es sobre la actualización de todos los miembros de datos para mantener los datos de un objeto en estado válido en todo momento.

Encontré "consistencia" y "estado es malo" como frases relevantes, discutidas aquí: 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();

¿Alguna desventaja, o quizás mejores maneras de hacer esto?

Este es un problema recurrente en las aplicaciones del mundo real respaldadas por una base de datos relacional, y si no utiliza los procedimientos almacenados de forma exhaustiva para insertar toda la validación en la base de datos. Creo que el almacén de datos solo debería almacenar datos, mientras que el código debería hacer todo el estado de tiempo de ejecución manteniendo el trabajo.

EDITAR: esta es una pregunta relacionada, pero no tiene una recomendación de mejores prácticas con respecto a una sola función grande para mantener un estado válido: /programming/1122346/c-sharp-object-oriented-design-maintaining- estado-objeto-válido

EDIT2: Aunque la respuesta de @ eignesheep es la mejor, esta respuesta - /software//a/148109/208591 - es lo que llena las líneas entre la respuesta de @ eigensheep y lo que quería saber: el código solo debe procesarse, y el estado global debe sustituirse por el paso de estado habilitado por DI entre objetos.

sitio80443
fuente
Evito tener variables que son porcentajes. Puede aceptar un porcentaje del usuario o mostrar uno a un usuario, pero la vida es mucho mejor si las variables del programa son proporciones.
Kevin Cline

Respuestas:

29

En igualdad de condiciones, debe expresar sus invariantes en código. En este caso tienes la invariante

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

Para expresar esto en su código, elimine la variable de miembro de impuestos y reemplace getTaxAmt () con

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

Debe hacer algo similar para deshacerse de la variable de miembro de costo total.

Expresar sus invariantes en su código puede ayudar a evitar errores. En el código original, el costo total es incorrecto si se marca antes de llamar a setPrice o setShipping.

Eigensheep
fuente
3
Muchos lenguajes tienen captadores para que tales funciones pretendan que son propiedades. ¡Lo mejor de ambos!
curiousdannii
Excelente punto, pero mi caso de uso general es cuando el código obtiene y almacena datos en múltiples columnas en múltiples tablas en una base de datos relacional (MySQL principalmente) y no quiero usar procedimientos almacenados (discutibles y otro tema por sí solo). Llevando su idea de invariantes en código más allá, esto significa que todos los cálculos deben estar "encadenados": getTotalCost()llamadas, getTaxAmt()etc. Esto significa que solo almacenamos cosas no calculadas . ¿Nos estamos moviendo un poco hacia la programación funcional? Esto también complica el almacenamiento de entidades calculadas en tablas para un acceso rápido ... ¡Necesita experimentación!
site80443
13

¿Alguna desventaja [?]

Seguro. Este método se basa en que todos siempre recuerden hacer algo. Cualquier método que dependa de todos y siempre siempre fallará .

tal vez mejores maneras de hacer esto?

Una forma de evitar la carga de recordar la ceremonia es calcular las propiedades del objeto que dependen de otras propiedades según sea necesario, como sugirió @eigensheep.

Otro es hacer que el artículo del carro sea inmutable y calcularlo en el método del constructor / fábrica. Normalmente iría con el método "calcular según sea necesario", incluso si hizo que el objeto sea inmutable. Pero si el cálculo lleva demasiado tiempo y se leería muchas, muchas veces; puede elegir la opción "calcular durante la creación".

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

Deberías preguntarte a ti mismo; ¿Tiene sentido un artículo de carrito sin precio? ¿Puede cambiar el precio de un artículo? Después de que se crea? Después de su impuesto calculado? etc. Tal vez debería hacer que el CartItemprecio y el envío sean inmutables y asignados en el constructor:

$i = new CartItem(100, 20);

¿Tiene sentido un artículo de carrito sin el carrito al que pertenece?

Si no, esperaría en su $cart->addItem(100, 20)lugar.

abuzittin gillifirca
fuente
3
Usted señala la mayor desventaja: confiar en las personas que recuerdan hacer las cosas rara vez es una buena solución. Lo único en lo que puede confiar en un humano es que se olviden de hacer algo.
corsiKa
@corsiKlause Ho Ho Ho y abuzittin, punto sólido, no pueden discutir eso: la gente olvida invariablemente . Sin embargo, el código que escribí anteriormente es solo un ejemplo, hay casos de uso sustanciales en los que algunos miembros de datos se actualizan más tarde. La otra forma que veo es normalizar aún más: crear clases de manera que los miembros de datos actualizados independientemente estén en otras clases y llevar la responsabilidad de la actualización a algunas interfaces, de modo que otros programadores (y usted mismo después de un tiempo) tengan que escribir un método: el El compilador te recuerda que debes escribirlo. Pero eso agregaría muchas más clases ...
site80443
1
@ site80443 Según lo que veo, ese es el enfoque equivocado. Intente modelar sus datos de modo que solo se incluyan los datos que se validan contra sí mismos. Por ejemplo, el precio de un artículo no puede ser negativo solo depende de sí mismo. Si un artículo tiene descuento, no incluya el descuento en el precio; decórelo con un descuento más adelante. Almacene los $ 4.99 para el artículo y el 20% de descuento como una entidad separada, y el 5% de impuestos como otra entidad más. En realidad, parece que debería considerar el patrón Decorador si los ejemplos representan su código de la vida real.
corsiKa