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).