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).
fonte
Eu acho que nesse tipo de situação, em que você deseja que as regras de negócios existam fora dos objetos de destino, testar o tipo é mais do que aceitável.
Certamente, em muitas situações, você deve usar um padrão de estratégia e deixar que os objetos manejem as coisas, mas isso nem sempre é desejável.
No mundo real, um funcionário examinaria o tipo de veículo e pesquisaria seus dados fiscais com base nisso. Por que seu software não deve seguir a mesma regra de negócios?
fonte
Você tem isso ao contrário. Um domínio anêmico é aquele cuja lógica está fora das entidades de negócios. Há muitas razões pelas quais usar classes adicionais para implementar a lógica é uma má idéia; e apenas alguns por que você deveria fazer isso.
Daí a razão pela qual é chamada de anêmica: a classe não tem nada.
O que eu faria:
fonte
Não. Pelo que entendi, todo o seu aplicativo é sobre tributação. Portanto, a preocupação sobre se um veículo deve ser tributado não é mais uma preocupação da Administração Pública do que qualquer outra coisa em todo o programa. Não há razão para colocá-lo em outro lugar, exceto aqui.
Esses dois trechos de código diferem apenas pela constante. Em vez de substituir a função HasToPay () inteira, substitua uma
int DaysBetweenPayments()
função. Chame isso em vez de usar a constante. Se é isso que eles realmente diferem, eu abandonaria as aulas.Eu também sugeriria que Motorbike / Car deveria ser uma propriedade de categoria de veículo em vez de subclasses. Isso lhe dá mais flexibilidade. Por exemplo, facilita a alteração da categoria de uma moto ou carro. É claro que as bicicletas dificilmente serão transformadas em carros na vida real. Mas você sabe que um veículo vai entrar errado e precisa ser alterado mais tarde.
Na verdade não. Um objeto que deliberadamente passa seus dados para outro objeto como parâmetro não é uma grande preocupação de encapsulamento. A preocupação real é que outros objetos cheguem dentro deste objeto para extrair dados ou manipulá-los dentro dele.
fonte
Você pode estar no caminho certo. O problema é que, como você notou, não há membro LastPaidTime no TaxPayment . É exatamente onde ele pertence, embora não precise ser exposto publicamente:
Cada vez que você recebe um pagamento, cria uma nova instância de
ITaxPayment
acordo com as políticas atuais, que podem ter regras completamente diferentes das anteriores.fonte
Concordo com a resposta do @sebastiangeiger de que o problema é realmente a propriedade dos veículos e não os veículos. Vou sugerir o uso da resposta dele, mas com algumas mudanças. Eu faria da
Ownership
classe uma classe genérica:E use herança para especificar o intervalo de pagamento para cada tipo de veículo. Eu também sugiro usar a
TimeSpan
classe fortemente tipada em vez de números inteiros simples:Agora, seu código real precisa lidar apenas com uma classe -
Ownership<Vehicle>
.fonte
Eu odeio dizer isso a você, mas você tem um domínio anêmico , ou seja, lógica de negócios fora dos objetos. Se é para isso que você está indo, tudo bem, mas você deve ler sobre os motivos para evitar um.
Tente não modelar o domínio do problema, mas modele a solução. É por isso que você está terminando com hierarquias de herança paralelas e mais complexidade do que precisa.
Algo assim é tudo o que você precisa para esse exemplo:
Se você deseja seguir o SOLID, isso é ótimo, mas como o próprio tio Bob disse, eles são apenas princípios de engenharia . Eles não devem ser aplicados rigorosamente, mas são diretrizes que devem ser consideradas.
fonte
isMotorBike
método não é uma boa opção de design de POO. É como substituir o polimorfismo por um código de tipo (embora um código booleano).enum Vehicles
Acho que você está certo de que o período de pagamento não é propriedade do carro / moto real. Mas é um atributo da classe de veículo.
Embora você possa ter um if / select no tipo de objeto, isso não é muito escalável. Se você adicionar caminhão e Segway mais tarde e empurrar bicicleta, ônibus e avião (pode parecer improvável, mas você nunca sabe com serviços públicos), a condição se tornará maior. E se você deseja alterar as regras de um caminhão, você deve encontrá-lo nessa lista.
Então, por que não torná-lo, literalmente, um atributo e usar a reflexão para retirá-lo? Algo assim:
Você também pode usar uma variável estática, se isso for mais confortável.
As desvantagens são
que será um pouco mais lento, e eu suponho que você precise processar muitos veículos. Se isso é um problema, eu posso ter uma visão mais pragmática e me ater à propriedade abstrata ou à abordagem com interface. (Embora eu também considere o cache por tipo.)
Você precisará validar na inicialização que cada subclasse Veículo possui um atributo IPaymentRequiredCalculator (ou permitir um padrão), porque você não deseja que ele processe 1000 carros, apenas trava porque o Motorbike não possui um PaymentPeriod.
A vantagem é que, se você encontrar um mês ou pagamento anual mais tarde, poderá escrever uma nova classe de atributo. Esta é a própria definição de OCP.
fonte