SOLID,避免贫血领域,依赖注入?


33

尽管这可能是与编程语言无关的问题,但我对针对.NET生态系统的答案很感兴趣。

这是这种情况:假设我们需要为公共管理开发一个简单的控制台应用程序。该应用程序是关于车辆税的。他们(仅)具有以下业务规则:

1.a)如果车辆是汽车,而车主最后一次缴税是在30天前,则车主必须再次缴税。

1.b)如果车辆是摩托车,且车主上次缴税是60天前,则车主必须再次缴税。

换句话说,如果您有汽车,则必须每30天支付一次费用;如果您有摩托车,则必须每60天支付一次费用。

对于系统中的每辆车,应用程序都应测试这些规则并打印不满足这些规则的车辆(车牌号和车主信息)。

我想要的是:

2.a)符合SOLID原则(尤其是开放/封闭原则)。

我不想要的是(我认为):

2.b)一个贫血的领域,因此业务逻辑应该放在业务实体内部。

我从这个开始:

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:保证开/关原则;如果他们想要您仅从Vehicle继承的自行车税,那么您可以重写HasToPay方法,然后完成。通过简单继承即可满足打开/关闭原则。

“问题”是:

3.a)为什么车辆必须知道是否必须付款?这不是公共行政问题吗?如果公共管理部门改变了汽车税,为什么还要改变汽车?我认为您应该问:“那为什么要在Vehicle中放入HasToPay方法?”,答案是:因为我不想在PublicAdministration中测试车辆类型(typeof)。您看到更好的选择了吗?

3.b)毕竟,纳税可能是与车辆有关的问题,或者我最初的Person-Vehicle-Car-Motorbike-PublicAdministration设计完全是错误的,或者我需要一个哲学家和一个更好的业务分析师。一个解决方案可能是:将HasToPay方法移至另一个类,我们可以将其称为TaxPayment。然后,我们创建两个TaxPayment派生类;CarTaxPayment和MotorbikeTaxPayment。然后,将一个抽象的Payment属性(类型为TaxPayment)添加到Vehicle类中,然后从Car和Motorbike类中返回正确的TaxPayment实例:

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

我们以这种方式调用旧过程:

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
    }

但是现在此代码将无法编译,因为CarTaxPayment / MotorbikeTaxPayment / TaxPayment内部没有LastPaidTime成员。我开始看到CarTaxPayment和MotorbikeTaxPayment更像是“算法实现”,而不是业务实体。现在,这些“算法”以某种方式需要LastPaidTime值。当然,我们可以将值传递给TaxPayment的构造函数,但这会违反车辆封装或职责,或者违反那些OOP宣传人员的说法,不是吗?

3.c)假设我们已经有一个实体框架ObjectContext(使用我们的域对象; Person,Vehicle,Car,Motorbike,PublicAdministration)。您将如何从PublicAdministration.GetAllVehicles方法获取ObjectContext引用,从而实现功能性?

3.d)如果对于CarTaxPayment.HasToPay我们还需要相同的ObjectContext引用,但对于负责“注入”的MotorbikeTaxPayment.HasToPay也需要一个不同的ObjectContext引用,或者您将如何将这些引用传递给支付类?在这种情况下,避免出现贫乏的领域(从而避免出现服务对象),是不是会导致我们灾难?

在这种简单情况下,您有哪些设计选择?显然,我展示的示例对于完成一项简单的任务而言“太复杂了”,但与此同时,其思想是遵守SOLID原理并防止贫血域(已委托它们销毁)。

Answers:


15

我要说的是,人和车辆都不应该知道是否应付款。

重新检查问题域

如果您查看问题域“拥有汽车的人”:为了购买汽车,您需要某种合同,将所有权从一个人转移到另一个人,在此过程中,汽车和人都不会改变。您的模型完全缺乏这一点。

思想实验

假设有一对已婚夫妇,该人拥有汽车,他正在纳税。现在该男子去世了,他的妻子继承了这辆汽车,并将纳税。但是,您的车牌将保持不变(至少在我的国家中会如此)。它仍然是同一辆车!您应该能够在您的域中对其进行建模。

=>所有权

没有任何人拥有的汽车仍然是汽车,但是没有人会为此纳税。实际上,您不是在为汽车缴税,而是为拥有汽车而特权。所以我的主张是:

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

}

这远非完美,甚至可能无法远程纠正C#代码,但我希望您能理解(有人可以清理)。唯一的缺点是您可能需要工厂或其他东西才能CarOwnership在a Car和a 之间创建正确的位置Person


我认为车辆本身应记录为此支付的税款,而个人应记录其所有车辆所支付的税款。可以编写一个税法代码,以便如果汽车税于6月1日支付,并于6月10日出售,则新所有者可能会在6月10日,7月1日或7月10日对税负(即使目前可能会改变的一种方式)。如果即使通过所有权变更,汽车的30天规则仍然不变,但是没有人欠的汽车不能纳税,那么我建议当购买了30天或更长时间没有所有权的汽车时,纳税...
supercat 2015年

...从假想的“无人车辆税收抵免”人那里记录下来,因此,自售出之日起,无论车辆多长时间不拥有,税收责任将为零或三十天。
supercat 2015年

4

我认为在这种情况下,您希望业务规则存在于目标对象之外,对于类型的测试是可以接受的。

当然,在许多情况下,应该使用策略模式,让对象自己处理事情,但这并不总是令人满意的。

在现实世界中,业务员会查看车辆的类型并基于此查找其税收数据。为什么您的软件不应该遵循相同的业务规则?


好的,假设您说服了我,并通过GetVehiclesThatHaveToPay方法检查了车辆类型。谁应该负责LastPaidTime?LastPaidTime是车辆问题还是公共管理部门?因为如果是车辆,那么业务逻辑是否不应该存在于车辆中?关于问题3.c,您能告诉我什么?

@DavidRobertJones-理想情况下,我会根据车辆牌照在“付款历史”日志中查找最后一笔付款。纳税是税务问题,而不是车辆问题。
Erik Funkenbusch '02

5
@DavidRobertJones我认为您正在将自己与自己的隐喻混淆。您拥有的并不是一辆真正的车辆,它需要完成车辆的所有工作。相反,为简单起见,您已将TaxableVehicle(或类似名称)命名为Vehicle。您的整个域是纳税。不用担心实际的车辆会做什么。并且,如有必要,可以删除比喻,并提出一个更合适的比喻。

3

我不想要的是(我认为):

2.b)贫血域,这意味着业务实体内部的业务逻辑。

你有这个倒退。贫血领域是逻辑在业务实体之外的领域。使用附加类来实现逻辑是一个不好的主意,原因有很多。以及为什么要这么做的只有几个。

因此,它被称为贫血的原因是:该类中没有任何东西。

我会做什么:

  1. 将抽象的Vehicle类更改为接口。
  2. 添加车辆必须实现的ITaxPayment接口。让您的HasToPay挂在这里。
  3. 删除MotorbikeTaxPayment,CarTaxPayment,TaxPayment类。它们是不必要的并发症/杂物。
  4. 每种车辆类型(汽车,自行车,房车)都应至少实施“车辆”,如果要征税,也应实施“ ITaxPayment”。这样,您就可以在那些未征税的车辆和那些已征税的车辆之间划定界限...假设这种情况甚至存在。
  5. 每种车辆类型都应在类本身的范围内实现在接口中定义的方法。
  6. 另外,将最后一次付款日期添加到您的ITaxPayment界面。

你是对的。最初,问题不是这样提出的,我删除了一部分,含义也随之改变。现在已修复。谢谢!

2

为什么车辆必须知道是否必须付款?这不是公共行政问题吗?

不。据我了解,您的整个应用都在征税。因此,对于车辆是否应该征税的担忧不再是公共行政部门的关注,而是整个计划中的其他任何问题。除了这里,没有理由将其放置在其他任何地方。

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

这两个代码段的区别仅在于常量。而不是覆盖整个HasToPay()函数,而是覆盖一个int DaysBetweenPayments()函数。调用它而不是使用常量。如果这是它们实际不同的全部,那么我将放弃这些类。

我还建议摩托车/汽车应该是车辆的类别属性,而不是子类。这为您提供了更大的灵活性。例如,它可以轻松更改摩托车或汽车的类别。当然,现实生活中不太可能将自行车转变为汽车。但是您知道,车辆输入错误,以后需要进行更换。

当然,我们可以将值传递给TaxPayment的构造函数,但这会违反车辆封装或职责,或者违反那些OOP宣传人员的说法,不是吗?

并不是的。故意将其数据作为参数传递给另一个对象的对象并不是很大的封装问题。真正关心的是其他对象到达该对象内部以拉出数据或操纵该对象内部的数据。


1

您可能走在正确的轨道上。正如您所注意到的那样,问题是TaxPayment中没有LastPaidTime成员。这就是它的所属位置,尽管根本不需要公开它:

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

每次收到付款时,您都会ITaxPayment根据当前策略创建一个新的a实例,该实例的规则可以与上一个完全不同。


问题在于,应付款取决于所有者上次付款的时间(不同的车辆,不同的到期日期)。ITaxPayment如何知道车辆(或所有者)最后一次付款以进行计算?

1

我同意@sebastiangeiger回答,即问题实际上与车辆拥有权有关,而不是车辆。我将建议使用他的答案,但要进行一些更改。我将使Ownership该类成为通用类:

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

并使用继承指定每种车辆类型的付款间隔。我还建议使用强类型TimeSpan类而不是普通整数:

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

现在,您的实际代码只需要处理一个类- 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);
    }
}

0

我不愿意将它交给您,但是您有一个贫乏的领域,即对象外部的业务逻辑。如果这是您想要的,那可以,但是您应该阅读避免这种情况的原因。

尝试不对问题域建模,而对解决方案建模。这就是为什么最终会出现并行继承层次结构和比您需要的复杂性更高的原因。

该示例仅需要这样的东西:

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

如果您想遵循SOLID,那就太好了,但是正如Bob叔叔说的那样,它们只是工程原理。并不是要严格应用它们,而是应考虑的准则。


4
我认为您的解决方案不是特别可扩展。您需要为添加的每种车辆添加一个新的布尔值。我认为这是一个糟糕的选择。
Erik Funkenbusch '02

@Garrett Hall,我喜欢您从我的“域”中删除了Person类。我首先不会上那堂课,对此感到抱歉。但是isMotorBike没有意义。哪个会执行?

1
@DavidRobertJones:我同意Mystere,添加isMotorBike方法不是OOP设计的好选择。这就像用类型代码(尽管是布尔值)代替多态。
Groo 2012年

@MystereMan,人们很容易掉落的bool和使用enum Vehicles
radarbob

+1。尝试不对问题域建模,而对解决方案建模精辟。令人难忘的说。和原则评论。我看到了很多严格的字面解释的尝试 ; 废话。
2013年

0

我认为您是对的,付款期限不是实际的汽车/摩托车的财产。但这是车辆类别的一个属性。

尽管可以对对象的类型进行if / select的选择,但这并不是很可扩展。如果以后再添加卡车和Segway并推自行车,公共汽车和飞机(这似乎不太可能,但对于公共服务您可能一无所知),那么情况就会变得更大。而且,如果您想更改货车的规则,则必须在该列表中找到它。

那么,为什么不从字面意义上使它成为一个属性并使用反射将其拉出呢?像这样:

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

您也可以使用静态变量,如果这样更方便的话。

缺点是

  • 那会稍微慢一点,我假设您必须处理很多车辆。如果这是一个问题,那么我可能会采取更为务实的看法,并坚持使用abstract属性或接口方法。(尽管如此,我也考虑按类型进行缓存。)

  • 您将必须在启动时验证每个Vehicle子类具有IPaymentRequiredCalculator属性(或允许使用默认值),因为您不希望它处理1000辆汽车,而只是由于Motorbike没有PaymentPeriod而崩溃。

好处是,如果您以后遇到日历月或年度付款,则可以编写一个新的属性类。这就是OCP的定义。


与此相关的主要问题是,如果您想更改车辆付款之间的天数,则需要重建和重新部署,并且如果付款频率根据实体的属性(例如引擎大小)而变化,很难获得一个完美的解决方案。
Ed James

@EdWoodcock:我认为对于大多数解决方案来说,第一个问题是正确的,这真的不会让我担心。如果他们以后想要这种灵活性,我可以给他们一个数据库应用程序。关于第二点,我完全不同意。那时,您只需将Vehicle作为HasToPay()的可选参数添加,并在不需要时将其忽略。
pdr 2012年

我想这取决于您对应用程序的长期计划,以及客户端的特殊要求,以决定是否值得(如果尚不存在)插入数据库(我倾向于认为几乎所有软件都是数据库)驱动,但我知道并非总是如此。然而,第二点,重要的限定词是优雅;我不会说向属性的方法中添加一个额外的参数,然后接受该属性所附加的类的实例是一个特别优雅的解决方案。但是,这再次取决于您的客户要求。
Ed James

@EdWoodcock:实际上,我只是在考虑这个问题,并且lastPaid参数已经必须是Vehicle的属性。考虑到您的评论,可能值得现在而不是以后替换该论点-将进行编辑。
pdr 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.