SÓLIDO, evitando domínios anêmicos, injeção de dependência?

33

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

David Robert Jones
fonte

Respostas:

15

Eu diria que nem uma pessoa nem um veículo devem saber se um pagamento é devido.

reexaminando o domínio do problema

Se você observar o domínio do problema "pessoas com carros": para comprar um carro, você tem algum tipo de contrato que transfere a propriedade de uma pessoa para outra, nem o carro nem a pessoa mudam nesse processo. Seu modelo está completamente sem isso.

experimento de pensamento

Supondo que haja um casal, o homem é dono do carro e está pagando os impostos. Agora, o homem morre, sua esposa herda o carro e paga os impostos. No entanto, seus pratos permanecerão os mesmos (pelo menos no meu país, eles continuarão). Ainda é o mesmo carro! Você deve conseguir modelar isso em seu domínio.

=> Propriedade

Um carro que não pertence a ninguém ainda é um carro, mas ninguém pagará impostos por ele. Na verdade, você não está pagando impostos pelo carro, mas pelo privilégio de possuir um carro . Então, minha proposta seria:

public class Person
{
    public string Name { get; set; }

    public string Surname { get; set; }

    public List<Ownership> getOwnerships();

    public List<Vehicle> getVehiclesOwned();
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Ownership currentOwnership { get; set; }

}

public class Car : Vehicle {}

public class Motorbike : Vehicle {}

public abstract class Ownership
{
    public Ownership(Vehicle vehicle, Owner owner);

    public Vehicle Vehicle { get;}

    public Owner CurrentOwner { get; set; }

    public abstract bool HasToPay();

    public DateTime LastPaidTime { get; set; }

   //Could model things like payment history or owner history here as well
}

public class CarOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class MotorbikeOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public class PublicAdministration
{
    public IEnumerable<Ownership> GetVehiclesThatHaveToPay()
    {
        return this.GetAllOwnerships().Where(HasToPay());
    }

}

Isso está longe de ser perfeito e provavelmente nem remotamente corrige o código C #, mas espero que você entenda (e alguém possa limpar isso). A única desvantagem é que você provavelmente precisaria de uma fábrica ou algo para criar o correto CarOwnershipentre um Care umPerson

sebastiangeiger
fonte
Eu acho que o veículo em si deve registrar os impostos pagos por ele e os indivíduos devem registrar impostos pagos por todos os seus veículos. Um código tributário pode ser escrito para que, se o imposto sobre um carro for pago em 1º de junho e vendido em 10 de junho, o novo proprietário possa se tornar responsável pelo imposto em 10 de junho, 1º de julho ou 10 de julho (e mesmo que seja atualmente, uma maneira de mudar). Se a regra de 30 dias for constante para o carro, mesmo com mudanças de propriedade, mas os carros que são devidos por ninguém não podem pagar impostos, sugiro que, quando for comprado um carro que não tenha sido proprietário por 30 dias ou mais, um pagamento de imposto ...
supercat
... sejam gravados de uma pessoa fictícia de "crédito tributário de veículo sem dono", para que, no momento da venda, o passivo tributário seja zero ou trinta dias, independentemente de quanto tempo o veículo permaneceu sem dono.
Supercat
4

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?

Erik Funkenbusch
fonte
Ok, digamos que você me convenceu e, pelo método GetVehiclesThatHaveToPay, inspecionamos o tipo de veículo. Quem deve ser responsável pelo LastPaidTime? LastPaidTime é um veículo ou uma administração pública? Porque se for um veículo, essa lógica de negócios não deveria residir no veículo? O que você pode me dizer sobre a pergunta 3.c?
@DavidRobertJones - Idealmente, eu pesquisaria o último pagamento no log do histórico de pagamentos, com base na licença do veículo. Um pagamento de imposto é uma preocupação fiscal, não um veículo.
Erik Funkenbusch
5
@DavidRobertJones Eu acho que você está se confundindo com sua própria metáfora. O que você tem não é um veículo real que precise fazer todas as coisas que um veículo faz. Em vez disso, você nomeou, por simplicidade, um Veículo tributável (ou algo semelhante) um Veículo. Todo o seu domínio é pago. Não se preocupe com o que os veículos reais fazem. E, se necessário, abandone a metáfora e crie uma mais apropriada.
3

O que eu não quero é (eu acho):

2.b) Um domínio anêmico, que significa lógica de negócios dentro de entidades de negócios.

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:

  1. Mude sua classe abstrata Vehicle para ser uma interface.
  2. Adicione uma interface ITaxPayment que os veículos devem implementar. Deixe seu HasToPay sair daqui.
  3. Remova as classes MotorbikeTaxPayment, CarTaxPayment, TaxPayment. São complicações / desordens desnecessárias.
  4. Cada tipo de veículo (carro, bicicleta, motorhome) deve implementar no mínimo um Veículo e, se for tributado, também deve implementar o ITaxPayment. Dessa forma, você pode delinear entre os veículos que não são tributados e os que são ... Supondo que essa situação exista.
  5. Cada tipo de veículo deve implementar, dentro dos limites da própria classe, os métodos definidos em suas interfaces.
  6. Além disso, adicione a data do último pagamento à sua interface ITaxPayment.
Eu não
fonte
Você está certo. Originalmente, a pergunta não foi formulada dessa maneira, excluí uma parte e o significado mudou. Está consertado agora. Obrigado!
2

Por que um veículo precisa saber se precisa pagar? Não é uma preocupação da Administração Pública?

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.

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;
    }
}

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.

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 é?

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.

Winston Ewert
fonte
1

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:

public interface ITaxPayment
{
    // Your base TaxPayment class will know the 
    // next due date. But you can easily create
    // one which uses (for example) fixed dates
    bool IsPaymentDue();
}

public interface IVehicle
{
    string Plate { get; }
    Person Owner { get; }
    ITaxPayment LastTaxPayment { get; }
} 

Cada vez que você recebe um pagamento, cria uma nova instância de ITaxPaymentacordo com as políticas atuais, que podem ter regras completamente diferentes das anteriores.

Groo
fonte
O fato é que o pagamento devido depende da última vez que o proprietário pagou (veículos diferentes, datas de vencimento diferentes). Como o ITaxPayment sabe qual foi a última vez que um veículo (ou proprietário) pagou para fazer o cálculo?
1

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 Ownershipclasse uma classe genérica:

public class Vehicle {}

public abstract class Ownership<T>
{
    public T Item { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract TimeSpan PaymentInterval { get; }

    public virtual bool HasToPay
    {
        get { return (DateTime.Today - this.LastPaidTime) >= this.PaymentInterval; }
    }
}

E use herança para especificar o intervalo de pagamento para cada tipo de veículo. Eu também sugiro usar a TimeSpanclasse fortemente tipada em vez de números inteiros simples:

public class CarOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(30, 0, 0, 0); }
    }
}

public class MotorbikeOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(60, 0, 0, 0); }
    }
}

Agora, seu código real precisa lidar apenas com uma classe - Ownership<Vehicle>.

public class PublicAdministration
{
    List<Ownership<Vehicle>> VehicleOwnerships { get; set; }

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return VehicleOwnerships.Where(x => x.HasToPay).Select(x => x.Item);
    }
}
user74130
fonte
0

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:

public class Vehicle
{
    public Boolean isMotorBike()
    public string PlateNumber { get; set; }
    public String Owner { get; set; }
    public DateTime LastPaidTime { get; set; }
}

public class TaxPaymentCalculator
{
    public List<Vehicle> GetVehiclesThatHaveToPay(List<Vehicle> all)
}

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.

Garrett Hall
fonte
4
Eu acho que sua solução não é particularmente escalável. Você precisaria adicionar um novo booleano para cada tipo de veículo adicionado. Essa é uma má escolha na minha opinião.
Erik Funkenbusch
@ Garrett Hall, gostei de você ter removido a classe Person do meu "domínio". Eu não teria essa aula em primeiro lugar, sinto muito por isso. Mas isMotorBike não faz sentido. Qual seria a implementação?
1
@DavidRobertJones: Concordo com Mystere, adicionar um isMotorBikemé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).
Groo
@MystereMan, um poderia facilmente cair as bools e usar umenum Vehicles
radarbob
+1. Tente não modelar o domínio do problema, mas modele a solução . Pithy . Memorável disse. E os princípios comentam. Eu vejo muitas tentativas de interpretação literal estrita ; Absurdo.
Radarbob
0

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:

[DaysBetweenPayments(30)]
public class Car : Vehicle
{
    // actual properties of the physical vehicle
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class DaysBetweenPaymentsAttribute : Attribute, IPaymentRequiredCalculator
{
    private int _days;

    public DaysBetweenPaymentAttribute(int days)
    {
        _days = days;
    }

    public bool HasToPay(Vehicle vehicle)
    {
        return vehicle.LastPaid.AddDays(_days) <= DateTime.Now;
    }
}

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.

pdr
fonte
O principal problema disso é que, se você quiser alterar o número de dias entre os pagamentos de um Veículo, precisará reconstruir e reimplementar, e se a frequência de pagamento variar com base nas propriedades da entidade (por exemplo, tamanho do motor), será difícil conseguir uma solução elegante para isso.
Ed James
@ EdWoodcock: Eu acho que a primeira questão é verdadeira para a maioria das soluções e realmente não me preocuparia. Se eles quiserem esse nível de flexibilidade posteriormente, eu daria a eles um aplicativo de banco de dados. No segundo ponto, discordo completamente. Nesse ponto, basta adicionar veículo como argumento opcional ao HasToPay () e ignorá-lo onde não for necessário.
pdr
Eu acho que depende dos planos de longo prazo que você tem para o aplicativo, além dos requisitos específicos do cliente, se vale a pena conectar um banco de dados se ele ainda não existir (eu tendem a supor que praticamente todo software é DB -Driven, que eu sei que nem sempre é o caso). No entanto, no segundo ponto, o qualificador importante é elegante ; Eu não diria que adicionar um parâmetro extra a um método em um atributo que leva uma instância da classe à qual o atributo está anexado é uma solução particularmente elegante. Mas, novamente, isso depende dos requisitos do seu cliente.
Ed2 de
@ EdWoodcock: Na verdade, eu estava pensando sobre isso e o argumento lastPaid terá que ser propriedade do Vehicle. Com o seu comentário em mente, pode valer a pena substituir esse argumento agora, e não mais tarde - será editado.
pdr