Aunque esta podría ser una pregunta independiente del lenguaje de programación, estoy interesado en respuestas dirigidas al ecosistema .NET.
Este es el escenario: supongamos que necesitamos desarrollar una aplicación de consola simple para la administración pública. La aplicación es sobre el impuesto de vehículos. Ellos (solo) tienen las siguientes reglas comerciales:
1.a) Si el vehículo es un automóvil y la última vez que su propietario pagó el impuesto fue hace 30 días, entonces el propietario debe pagar nuevamente.
1.b) Si el vehículo es una motocicleta y la última vez que su propietario pagó el impuesto fue hace 60 días, entonces el propietario tiene que pagar nuevamente.
En otras palabras, si tiene un automóvil, debe pagar cada 30 días o si tiene una motocicleta, debe pagar cada 60 días.
Para cada vehículo en el sistema, la aplicación debe probar esas reglas e imprimir aquellos vehículos (número de placa e información del propietario) que no los satisfacen.
Lo que quiero es:
2.a) Conforme a los principios SÓLIDOS (especialmente el principio Abierto / cerrado).
Lo que no quiero es (creo):
2.b) Un dominio anémico, por lo tanto, la lógica empresarial debe ir dentro de las entidades comerciales.
Empecé con esto:
public class Person
// You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
{
public string Name { get; set; }
public string Surname { get; set; }
}
public abstract class Vehicle
{
public string PlateNumber { get; set; }
public Person Owner { get; set; }
public DateTime LastPaidTime { get; set; }
public abstract bool HasToPay();
}
public class Car : Vehicle
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
}
}
public class Motorbike : Vehicle
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
}
}
public class PublicAdministration
{
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.HasToPay());
}
private IEnumerable<Vehicle> GetAllVehicles()
{
throw new NotImplementedException();
}
}
class Program
{
static void Main(string[] args)
{
PublicAdministration administration = new PublicAdministration();
foreach (var vehicle in administration.GetVehiclesThatHaveToPay())
{
Console.WriteLine("Plate number: {0}\tOwner: {1}, {2}", vehicle.PlateNumber, vehicle.Owner.Surname, vehicle.Owner.Name);
}
}
}
2.a: El principio abierto / cerrado está garantizado; si quieren el impuesto sobre la bicicleta que usted acaba de heredar del vehículo, entonces anula el método HasToPay y listo. Principio abierto / cerrado satisfecho por simple herencia.
Los "problemas" son:
3.a) ¿Por qué un vehículo tiene que saber si tiene que pagar? ¿No es una preocupación de la Administración Pública? Si la administración pública rige el cambio del impuesto sobre el automóvil, ¿por qué el automóvil tiene que cambiar? Creo que debería preguntarse: "¿por qué pones un método HasToPay dentro del vehículo?", Y la respuesta es: porque no quiero probar el tipo de vehículo (tipo de) dentro de PublicAdministration. ¿Ves una mejor alternativa?
3.b) Tal vez el pago de impuestos es una preocupación del vehículo después de todo, o tal vez mi diseño inicial de Administración de Persona-Vehículo-Automóvil-Moto-Público es simplemente incorrecto, o necesito un filósofo y un mejor analista de negocios. Una solución podría ser: mover el método HasToPay a otra clase, podemos llamarlo TaxPayment. Luego creamos dos clases derivadas de TaxPayment; CarTaxPayment y MotoTaxPayment. Luego agregamos una propiedad de pago abstracta (del tipo TaxPayment) a la clase Vehicle y devolvemos la instancia correcta de TaxPayment de las clases Car y Motorbike:
public abstract class TaxPayment
{
public abstract bool HasToPay();
}
public class CarTaxPayment : TaxPayment
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
}
}
public class MotorbikeTaxPayment : TaxPayment
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
}
}
public abstract class Vehicle
{
public string PlateNumber { get; set; }
public Person Owner { get; set; }
public DateTime LastPaidTime { get; set; }
public abstract TaxPayment Payment { get; }
}
public class Car : Vehicle
{
private CarTaxPayment payment = new CarTaxPayment();
public override TaxPayment Payment
{
get { return this.payment; }
}
}
public class Motorbike : Vehicle
{
private MotorbikeTaxPayment payment = new MotorbikeTaxPayment();
public override TaxPayment Payment
{
get { return this.payment; }
}
}
E invocamos el viejo proceso de esta manera:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Pero ahora este código no se compilará porque no hay un miembro de LastPaidTime dentro de CarTaxPayment / MotorbikeTaxPayment / TaxPayment. Estoy empezando a ver CarTaxPayment y MotorbikeTaxPayment más como "implementaciones de algoritmos" que como entidades comerciales. De alguna manera ahora esos "algoritmos" necesitan el valor LastPaidTime. Claro, podemos pasar el valor al constructor de TaxPayment, pero eso violaría la encapsulación del vehículo, o las responsabilidades, o como lo llamen esos evangelistas de OOP, ¿no?
3.c) Supongamos que ya tenemos un ObjectContext de Entity Framework (que usa nuestros objetos de dominio; Persona, Vehículo, Coche, Moto, Administración Pública). ¿Cómo haría para obtener una referencia ObjectContext del método PublicAdministration.GetAllVehicles y, por lo tanto, implementar la funcionalidad?
3.d) Si también necesitamos la misma referencia de ObjectContext para CarTaxPayment.HasToPay pero diferente para MotorbikeTaxPayment.HasToPay, ¿quién está a cargo de la "inyección" o cómo pasaría esas referencias a las clases de pago? En este caso, evitando un dominio anémico (y, por lo tanto, sin objetos de servicio), ¿no nos lleva al desastre?
¿Cuáles son sus opciones de diseño para este escenario simple? Claramente, los ejemplos que he mostrado son "demasiado complejos" para una tarea fácil, pero al mismo tiempo la idea es adherirse a los principios SÓLIDOS y prevenir dominios anémicos (de los cuales se ha encargado destruir).
fuente
Creo que en este tipo de situación, donde desea que las reglas de negocio existan fuera de los objetos de destino, las pruebas de tipo son más que aceptables.
Ciertamente, en muchas situaciones, debe usar un patrón de estrategia y dejar que los objetos manejen las cosas por sí mismos, pero eso no siempre es deseable.
En el mundo real, un empleado miraría el tipo de vehículo y buscaría sus datos fiscales basándose en eso. ¿Por qué su software no debería seguir la misma regla de busienss?
fuente
Tienes esto al revés. Un dominio anémico es aquel cuya lógica está fuera de las entidades comerciales. Hay muchas razones por las que usar clases adicionales para implementar la lógica es una mala idea; y solo un par de por qué deberías hacerlo.
De ahí la razón por la que se llama anémica: la clase no tiene nada.
Que haría yo:
fuente
No. Según tengo entendido, toda su aplicación se trata de impuestos. Por lo tanto, la preocupación acerca de si un vehículo debe pagar impuestos no es más una preocupación de la Administración Pública que cualquier otra cosa en todo el programa. No hay razón para ponerlo en otro lugar que no sea aquí.
Estos dos fragmentos de código difieren solo por la constante. En lugar de anular toda la función HasToPay (), anule una
int DaysBetweenPayments()
función. Llame a eso en lugar de usar la constante. Si esto es todo en lo que realmente difieren, abandonaría las clases.También sugeriría que Moto / Coche debería ser una propiedad de categoría de vehículo en lugar de subclases. Eso te da más flexibilidad. Por ejemplo, hace que sea fácil cambiar la categoría de una motocicleta o automóvil. Por supuesto, es poco probable que las bicicletas se transformen en automóviles en la vida real. Pero usted sabe que un vehículo va a ingresar mal y debe cambiarse más tarde.
Realmente no. Un objeto que pasa deliberadamente sus datos a otro objeto como parámetro no es una gran preocupación de encapsulación. La verdadera preocupación es que otros objetos lleguen dentro de este objeto para extraer datos o manipularlos dentro del objeto.
fuente
Puede que estés en el camino correcto. El problema es, como se habrá dado cuenta, que no hay un miembro de LastPaidTime dentro de TaxPayment . Ahí es exactamente a donde pertenece, aunque no necesita exponerse públicamente en absoluto:
Cada vez que recibe un pago, crea una nueva instancia de
ITaxPayment
acuerdo con las políticas actuales, que puede tener reglas completamente diferentes a la anterior.fuente
Estoy de acuerdo con la respuesta de @sebastiangeiger de que el problema es realmente sobre la propiedad de vehículos en lugar de los vehículos. Voy a sugerirle que use su respuesta pero con algunos cambios. Haría que la
Ownership
clase sea una clase genérica:Y use la herencia para especificar el intervalo de pago para cada tipo de vehículo. También sugiero usar la
TimeSpan
clase fuertemente tipada en lugar de enteros simples:Ahora su código real solo necesita tratar con una clase -
Ownership<Vehicle>
.fuente
Odio decírtelo, pero tienes un dominio anémico , es decir, lógica de negocios fuera de los objetos. Si esto es lo que está buscando, está bien, pero debe leer sobre las razones para evitarlo.
Intente no modelar el dominio del problema, pero modele la solución. Es por eso que está terminando con jerarquías de herencia paralelas y más complejidad de la que necesita.
Algo como esto es todo lo que necesita para ese ejemplo:
Si quieres seguir a SOLID, eso es genial, pero como dijo el tío Bob, son solo principios de ingeniería . No están destinados a ser aplicados estrictamente, pero son pautas que deben considerarse.
fuente
isMotorBike
método no es una buena opción de diseño de OOP. Es como reemplazar el polimorfismo con un código de tipo (aunque sea booleano).enum Vehicles
Creo que tiene razón en que el período de pago no es propiedad del automóvil / moto real. Pero es un atributo de la clase de vehículo.
Si bien podría seguir el camino de tener un if / select en el Tipo de objeto, esto no es muy escalable. Si luego agrega un camión y un Segway y empuja la bicicleta, el autobús y el avión (puede parecer poco probable, pero nunca se sabe con los servicios públicos), la condición se agrava. Y si desea cambiar las reglas de un camión, debe encontrarlo en esa lista.
Entonces, ¿por qué no convertirlo, literalmente, en un atributo y usar la reflexión para sacarlo? Algo como esto:
También podría ir con una variable estática, si eso es más cómodo.
Los inconvenientes son
será un poco más lento y supongo que tienes que procesar muchos vehículos. Si eso es un problema, entonces podría tener una visión más pragmática y seguir con la propiedad abstracta o el enfoque interconectado. (Aunque, también consideraría el almacenamiento en caché por tipo).
Tendrá que validar en el inicio que cada subclase de Vehículo tiene un atributo IPaymentRequiredCalculator (o permitir un valor predeterminado), porque no desea que procese 1000 autos, solo para colisionar porque Moto no tiene un PaymentPeriod.
La ventaja es que si luego se encuentra con un mes calendario o un pago anual, puede escribir una nueva clase de atributo. Esta es la definición misma de OCP.
fuente