Embora essa possa ser uma questão independente da linguagem de programação, estou interessado em respostas direcionadas ao ecossistema .NET.
Este é o cenário: suponha que precisamos desenvolver um aplicativo de console simples para a administração pública. A aplicação é sobre imposto sobre veículos. Eles (apenas) têm as seguintes regras de negócios:
1.a) Se o veículo for um carro e a última vez que seu proprietário pagou o imposto há 30 dias, o proprietário deverá pagar novamente.
1.b) Se o veículo é uma moto e a última vez que o proprietário pagou o imposto há 60 dias, o proprietário deve pagar novamente.
Em outras palavras, se você tem um carro, paga a cada 30 dias ou se tem uma moto, paga a cada 60 dias.
Para cada veículo no sistema, o aplicativo deve testar essas regras e imprimir os veículos (número da placa e informações do proprietário) que não as satisfazem.
O que eu quero é:
2.a) Conformidade com os princípios do SOLID (especialmente o princípio Aberto / Fechado).
O que eu não quero é (eu acho):
2.b) Um domínio anêmico, portanto, a lógica de negócios deve entrar nas entidades de negócios.
Comecei com isso:
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: O princípio Aberto / Fechado é garantido; se eles querem imposto de bicicleta, você acaba de herdar Vehicle, então você substitui o método HasToPay e pronto. Princípio aberto / fechado satisfeito por herança simples.
Os "problemas" são:
3.a) Por que um veículo precisa saber se deve pagar? Não é uma preocupação da Administração Pública? Se as regras da administração pública para alteração do imposto sobre carros, por que o carro precisa mudar? Eu acho que você deveria estar se perguntando: "então por que você coloca um método HasToPay dentro de Vehicle?", E a resposta é: porque eu não quero testar o tipo de veículo (typeof) dentro de PublicAdministration. Você vê uma alternativa melhor?
3.b) Talvez o pagamento de impostos seja uma preocupação do veículo, afinal, ou talvez meu projeto inicial de Pessoa-Veículo-Carro-Moto-Administração Pública esteja completamente errado, ou eu preciso de um filósofo e um melhor analista de negócios. Uma solução poderia ser: mover o método HasToPay para outra classe, podemos chamá-lo de TaxPayment. Em seguida, criamos duas classes derivadas de TaxPayment; CarTaxPayment e MotorbikeTaxPayment. Em seguida, adicionamos uma propriedade abstrata Payment (do tipo TaxPayment) à classe Vehicle e retornamos a instância TaxPayment correta das classes Car e 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 o antigo processo desta maneira:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Mas agora esse código não será compilado porque não há membro LastPaidTime no CarTaxPayment / MotorbikeTaxPayment / TaxPayment. Estou começando a ver CarTaxPayment e MotorbikeTaxPayment mais como "implementações de algoritmos" em vez de entidades de negócios. De alguma forma, agora esses "algoritmos" precisam do valor LastPaidTime. Certamente, podemos passar o valor para o construtor do TaxPayment, mas isso violaria o encapsulamento ou responsabilidades do veículo, ou o que os evangelistas da OOP chamam, não?
3.c) Suponha que já tenhamos um ObjectContext do Entity Framework (que usa nossos objetos de domínio; Pessoa, Veículo, Carro, Moto, Administração Pública). Como você faria para obter uma referência ObjectContext do método PublicAdministration.GetAllVehicles e, portanto, implementar a funcionalidade?
3.d) Se também precisarmos da mesma referência ObjectContext para CarTaxPayment.HasToPay, mas outra diferente para MotorbikeTaxPayment.HasToPay, quem é responsável por "injetar" ou como você passaria essas referências para as classes de pagamento? Nesse caso, evitando um domínio anêmico (e, portanto, nenhum objeto de serviço), não nos leva ao desastre?
Quais são as suas escolhas de design para esse cenário simples? Claramente, os exemplos que mostrei são "muito complexos" para uma tarefa fácil, mas, ao mesmo tempo, a idéia é aderir aos princípios do SOLID e impedir domínios anêmicos (dos quais foram contratados para destruir).