S.O.L.I.D要点遗漏点?

时间:2012-12-17 17:38:01

标签: c# design-patterns inversion-of-control solid-principles liskov-substitution-principle

我读了很多关于此的文章,但我还有2个问题。

问题#1 - 关于Dependency Inversion

  

它指出高级别的课程不应该依赖于低级别   类。两者都应该取决于抽象。抽象不应该   取决于细节。细节应该取决于抽象。

例如:

public class BirthdayCalculator
{
    private readonly List<Birthday> _birthdays;

    public BirthdayCalculator()
    {
        _birthdays = new List<Birthday>();// <----- here is a dependency
    }
...

修复:把它放在一个ctor。

public class BirthdayCalculator
{
    private readonly IList<Birthday> _birthdays;

    public BirthdayCalculator(IList<Birthday> birthdays) 
    {
        _birthdays = birthdays;
    }
  • 如果它在ctor中 - 我每次使用课时都必须发送它。所以在调用BirthdayCalculator课时我必须保留它。它可以这样做吗?

  • 我可以说,在修复之后,仍然 - IList<Birthday> _birthdays不应该在那里(Birthday中的IList) - 但它应该是{{1} }。我对吗 ?

问题#2 - 关于Liskov Substitution

  

派生类必须可替代其基类

或更准确:

  

设q(x)是可证明关于类型T的对象x的属性。然后q(y)   对于S类型的对象y应该为true,其中S是T的子类型。

(已阅读this

示例:

IList<IBirthday>

我有一个班级:

public abstract class Account
{
     public abstract void Deposit(double amount);
}

银行想要开立一个按揭账户 - 所以:

public class CheckingAccount : Account
{
     public override void Deposit(double amount)
    {    ...
        _currentBalance += amount;
    }
}

当有一个函数接受抵押public class MortgageAccount : Account { public override void Deposit(double amount) { _currentBalance -= amount; //<-----notice the minus } } 并存款时,就会出现问题。

Account

所以在这里,它违反了LSP。

但是我不明白。

每个重写的方法在重写时都会执行不同的代码,因此它永远不会 100%可替换!

定义没有谈到“逻辑应该继续像基类一样(总是存入(添加)正数)”

示例:

如果public class Bank { public void ReceiveMoney(Account account, double amount) { double oldBalance = account.CurrentBalance; account.Deposit(amount); //oopssss????? } } 类和CheckingAccount类都存储正数而MortgageAccount也记录到数据库,该怎么办?它还会打破LSP吗?中断/不制动LSP的界限是什么?

定义应该定义什么是边界。它没有说什么。

我错过了什么?

6 个答案:

答案 0 :(得分:5)

LSP表示基类所做的任何承诺,子类也必须这样做。在帐户案例中,是的,这意味着Deposit 从抵押贷款余额中减去相当混乱。对此的一种解决方法是,如果您认为余额是客户欠您的金额与您欠他的金额之间的差额。 (我大约54%肯定这是“平衡”的原始含义。)积极的平衡可能意味着客户钱,而负面的平衡可能意味着他钱。如果您无法解决它,以便您可以类似地处理这两个帐户,那么它们不应该相关 - 或者至少,不应该在基类上定义“存款”方法。

就DIP而言,它没有真正提到的是它并不适用于每行代码。最终你拥有在某个地方拥有一个具体的课程。关键是要将对这些具体类的细节的依赖限制在绝对 以了解它们的代码部分,并保持这些部分的数量和大小尽可能小。这意味着尽可能使用尽可能多的通用接口。例如,如果您不需要List所做的所有保证(枚举顺序,重复项存在,空值等),那么您可以将_birthdays声明为IEnumerable 。如果构造函数是唯一知道你的IEnumerable实际上是List的东西,那么你或多或少地遵守这个原则。

(但请注意:这并不意味着您可以通过简单地将所有内容声明为Object并根据需要进行向下转换来遵守DIP。向下转换本身可能会被视为违反DIP,因为您不是更长时间依赖于您提供的界面;您正试图获得更具体的界面。)

答案 1 :(得分:2)

  

如果它将在ctor中 - 我必须在每次使用课程时发送它。所以在调用BirthdayCalculator类时我必须保留它。它可以这样做吗?

我会检索一次生日列表,制作一个BirthdayCalculator,然后传递它。如果您的生日列表发生变化,或者它以某种方式保持状态,您应该只需要实例化一个新的BirthdayCalculator

  

我可以说,在修复之后,仍然 - IList<Birthday> _birthdays不应该在那里(IList中的生日) - 但它应该是IList<IBirthday>。我是对的吗?

这取决于。生日只是一个数据对象吗?它中是否包含任何逻辑?如果它只是一堆属性/字段,那么界面就会过度。

如果生日确实有逻辑,那么我会选择IEnumerable<IBirthday>List<Birthday>无法分配IList<IBirthday>,因此您可能会遇到问题。如果您不需要操纵它,请坚持使用IEnumerable


关于你的第二个问题,我唯一遇到的问题是你的命名约定。你没有向Mortage存款。我想如果你要重命名那个方法可能会更有意义。

此外,支票账户和抵押账户是两种截然不同的动物。一个是存款账户,另一个是贷款。我会非常小心地尝试互换使用它们。

修改

其他人已经注意到您可以在_currentBalance中将MortageAccount设为负值,然后添加到其中。如果您更容易理解,那就去吧。但是,MortageAccount如何处理其平衡的内部结构并不重要,只要GetBalance或您使用检查/显示它的任何方法,都可以正确处理它。

答案 2 :(得分:1)

<强>#1

  • 是的,每次创建对象时都必须调用它。这是FactoriesInversion of Control框架派上用场的地方,可以删除您必须执行的所有管道。

  • 是的,您应该使用IBirthday界面。

<强>#2

你的逻辑存在缺陷。当您将钱存入账户时,您需要为其注入资金。抵押贷款账户中没有账户中的资金(正余额),并且您不会通过存款取出资金。抵押贷款账户余额为负值,存入账户会降低余额(增加负余额)。

答案 3 :(得分:0)

关于LSP: 在我看来,LSP主要是关于层次结构的(公共)合同。 你有一个账户(对象),你可以贷记/借记资金(合同)。 对于抵押贷款帐户信用将导致减法,借方将导致额外。 对于支票账户,它是相反的。

重要的是,给定一个帐户,您可以随时要求它进行信用/借记操作,并且帐户对象的所有子类都将支持这些操作。

答案 4 :(得分:0)

依赖性倒置:

  

如果它将在ctor中 - 我必须在每次使用课程时发送它。所以在调用BirthdayCalculator课时我必须保留它。它可以这样做吗?

在使用对象时,但在构建 BirthdayCalendar对象时,您不需要它。将IList<Birthday>移动到构造接口可确保BirthdayCalendar的所有依赖项与调用方的依赖项相同(即,没有新的依赖项),这就是我们要实现的目标。

  

我可以说,在修复之后,仍然 - IList<Birthday> _birthdays不应该在那里(Birthday中的IList) - 但它应该是IList<IBirthday>。我是对的吗?

这与依赖倒置原则没有直接关系。您已将依赖关系卸载到调用方,现在您要询问此依赖关系的详细信息。使用IBirthday来访问列表的内部而不是Birthday可能是一个好主意,但这是一个与接口反转无关的接口问题。

  

每个重写的方法在重写时都会执行不同的代码,因此它永远不会100%可替换!   该定义没有谈到“逻辑应该继续像在基类中一样(总是存入(添加)正数)”

     

例如:   如果CheckingAccount类和MortgageAccount类都存储正数而MortgageAccount也记录到数据库,该怎么办?它还会中断LSP吗?中断/不制动LSP的边界是什么?   定义应该定义边界的内容。它没有说什么。

Liskov Substitution Principle适用于基类与派生类的关系。 您的示例处理从共享库继承的2个类,因此您的关系是派生类1与派生类2 。这与LSP所讨论的完全相反,正是由于您提到的原因 - 两个派生类的逻辑都不同。 LSP是您定义的继承有效性的度量。如果你可以肯定地回答问题是我的派生类对象可以透明地用于我的基类对象的所有地方吗?,你就不会破坏LSP。否则,你正在打破它。

答案 5 :(得分:0)

关于第一点,可以使用特定的低级别类作为另一个类的私有实现细节;如果要说SOLID,代码可能会更IList<Birthday> _birthdays = new List<Birthday>;,但List<Birthday> _birthdays = new List<Birthday>的效果可能略好一些。如果您想使用IBirthday而不是Birthday,那么如果您每次倾向于使用IBirthday时都使用Birthday,除非在调用构造函数或静态时使用Birthday方法。请注意,在使用IBirthday vs List<T>IList<T> vs List<T>时,您需要保持一致。

请注意,私人成员不必遵守与公共成员相同的规则。如果某个类型接受List<T>类型的参数,则会强制外部代码使用该类型。从IList<T>切换到List<T>可能需要在许多不同的程序集中进行许多更改,并使旧版本的程序集与新程序集不兼容。相比之下,如果一个类具有类型IList<T>的私有成员,它永远不会与外部代码共享,那么将其更改为使用{{1}}的效果将限制在类本身内,并且没有对该班的消费者产生影响。