6

我已经阅读了很多关于此的文章,但我仍然有 2 个问题。

问题 #1 - 关于依赖倒置

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

例如 :

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不应该在那里(BirthdayIList) - 但它应该是IList<IBirthday>。我对吗 ?

问题 #2 - 关于Liskov 替换

派生类必须可以替代它们的基类

或更准确:

令 q(x) 是关于 T 类型的对象 x 的可证明性质。那么 q(y) 对于 S 类型的对象 y 应该为真,其中 S 是 T 的子类型。

(已经读过这个

例子 :

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并做存款时,问题就出现了。

public class Bank
{
    public void ReceiveMoney(Account account, double amount)
    {
        double oldBalance = account.CurrentBalance;
        account.Deposit(amount); //oopssss?????
    }
}

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

我不明白。

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

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

例子:

如果CheckingAccount class 和MortgageAccount class 都存入正数但MortgageAccount 也登录到 db 怎么办?它仍然会破坏 LSP 吗?休息/不刹车 LSP 的边界是什么?

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

我错过了什么?

4

6 回答 6

5

LSP 说基类做出的任何承诺,子类也必须做出。在 Account 案例中,是的,这意味着从 Mortgage 的余额中Deposit 减去是相当混乱的。一种解决方法是,如果您认为余额是客户欠您的金额与您欠他的金额之间的差额。(我大约有 54% 的把握这就是“余额”的原始含义。)正余额可能意味着客户有钱,负余额可能意味着他欠钱。如果您无法解决它以便您可以类似地对待这两个帐户,那么它们不应该是相关的——或者至少,“存款”方法不应该在基类上定义。

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

(但请注意:这并不意味着您可以通过简单地将所有内容声明为 aObject并根据需要进行向下转换来遵守 DIP。向下转换本身可能被视为违反 DIP,因为您不再依赖于您拥有的接口已提供;您正在尝试获得更具体的内容。)

于 2012-12-17T18:00:23.680 回答
2

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

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

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

这取决于。生日只是一个数据对象吗?里面有什么逻辑吗?如果只是一堆属性/字段,那么一个接口就大材小用了。

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


关于您的第二个问题,我唯一遇到的问题是您的命名约定。您不向抵押贷款存款。我认为如果您要重命名该方法,它可能更有意义。

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

编辑

其他人已经指出,您可以先输入_currentBalance负值MortageAccount,然后再添加。如果这对你来说更容易理解,那就去吧。但是,a 如何MortageAccount处理其余额的内部结构并不重要,只要GetBalance您使用检查/显示它的任何方法或任何方法,都可以正确处理它。

于 2012-12-17T17:54:06.160 回答
1

#1

  • 是的,每次创建对象时都必须调用它。这是工厂控制反转框架派上用场的地方,以消除您必须做的所有管道。

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

#2

你的逻辑在这里有缺陷。当您将钱存入帐户时,您就是在向它添加钱。抵押贷款账户中没有资金(正余额),您不会通过存款从其中取出资金。抵押账户有负余额,存入账户会降低余额(增加负余额)。

于 2012-12-17T18:01:25.103 回答
0

关于 LSP:在我看来,LSP 主要是关于层次结构的(公共)合同。你有一个账户(对象),你可以贷记/借记它(合约)。对于抵押账户,贷方将导致减法,借方将导致加法。对于支票账户,情况正好相反。

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

于 2012-12-17T18:02:14.590 回答
0

依赖倒置:

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

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

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

这与依赖倒置原则没有直接关系。您已将依赖项卸载给调用者,现在您正在询问此依赖项的详细信息。IBirthday使用list 而不是访问内部结构可能是一个好主意Birthday,但这是一个与依赖倒置无关的接口问题。

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

示例:如果CheckingAccountclass 和MortgageAccountclass 都存入正数但MortgageAccount也登录到 db 怎么办?它仍然破裂LSP吗?休息/不刹车 LSP 的边界是什么?定义应该定义什么是边界。它并没有说什么。

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

于 2012-12-17T18:08:12.410 回答
0

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

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

于 2012-12-17T18:09:23.713 回答