假设我有一个抽象类鸟,它的一个函数是飞(int高度)。
我有许多不同的鸟类,每个类都有自己不同的飞的实现,这个函数在整个应用程序中被广泛使用。
有一天,我的老板来了,要求我添加一只鸭子,它做其他鸟类所做的一切,只是它不飞,而是在应用程序的池塘里游泳。
将duck添加为bird的子类型违反了Liskov替换规则,因为在调用duck时。我们要么抛出异常,要么什么也不做,要么违反正确性原则。
在牢记坚实的设计原则的同时,您将如何引入这一变化?
所以你的问题是
(1) 鸭子是一只鸟。
(2) 但它不能飞。
所以真正的问题是,
解决方案:
因此,这里要采用的最简单和最明智的解决方案是为方法名使用通用名称,以便双方能够达成一致。因此,在这里您可以将方法Bird.fly()重命名为Bird.move()。
所以Duck.move()和Crow.move()都有意义,但它们有自己的方式。
所有其他可用的解决方案都将消除多态性(使用两个不同的基类)。这意味着您将无法通过一种方法同时调用Duck和Crow移动(分别是swime和fly)。(像鸟一样,移动)。
当你的应用程序建立在它之上时,你会花费很多,因为你必须通过显式或隐式类型转换来区分它们,这是非常糟糕的。这会浪费您的时间,使您的应用程序很难扩展和维护。:)
我看你有3个选择:
>
使用Bird
作为公共功能的抽象基类,并从中派生FlyingBird
和Aquatic Bird
。
使用Zoran Horvat描述的对象组合和访问者模式:https://vimeo.com/195774910(在任何情况下,这都值得一看)-尽管如此,在手头的案例中,这似乎是一种过度的杀伤力。
使用您描述的解决方案。
最后,一切都是关于平衡的。如果你期待许多不同能力的鸟加入,那么你应该认真考虑选项2,否则取决于你如何使用这些类,在1和3之间选择。
根据Gilads关于选项2(对象组成)的评论进行更新
这里最主要的事情是,你可以有一个类鸟
并将能力附加到它,例如飞行
和游泳
,也许以后你会决定你有其他能力或某种分类。您可以通过创建不同的接口(IFlyable
,ISwimable
)来实现这一点,并具有以下内容:
public class SomeFlyingBird : BirdBase, IFlyable
{
....
}
public class Duck : BirdBase, ISwimmable
{
....
}
然后使用访问者模式访问具体的动作。
或者,如果你寻找更健壮和优雅的东西,你可以根据能力创建不同的类,并将它们附加到鸟类:
public class Bird/Animal
{
string name;
List<Ability> abilities = new List<Ability>();
public Bird (string name)
{
this.name = name;
}
public void Add (Ability ability)
{
this.abilities.Add(ability);
}
}
您可以稍后使用一些静态类来创建鸟:
public static Bird CreateDuck ()
{
var duck = new Bird("Donald");
duck.Add(new SwimAbility());
return duck;
}
能力和游泳能力可以如下所示:
public abstract class Ability
{
public virtual void Accept(AbilityVisitor visitor) =>
visitor.Visit(this);
}
public class SwimAbility : Ability
{
public override void Accept(AbilityVisitor visitor)
{
base.Accept(visitor);
visitor.Visit(this);
}
}
我没有附上完整的代码,因为它不是我的,而且对于这个答案来说太长了。我再说一遍,对于你所描述的案件来说,这似乎是一种过度的杀伤力。
我强烈建议您观看Zoran Horvat的视频,并下载和播放他提供的代码:http://www.postsharp.net/blog/post/webinar-recording-object-composition
您发现的问题是由于模型的以下不兼容功能造成的。
您需要更改其中一个以使语句一致。更改哪一个将是一个最适合您使用的设计问题,以及进行更改所涉及的问题。
你可以通过引入分类来区分会飞的和不会飞的鸟来解决这个问题。也许为了使变化变小,你的鸟类继续飞行,所以鸭子不是鸟,而是不会飞的鸟。或者你可以扩展Bird来创建flyingbird类,并将fly方法移动到那里——两者都涉及到对现有代码的更改。
抛出异常有点像在第3点作弊——你的鸭子仍然没有真正飞起来,它只是把问题变成了运行时的问题,而不是设计时的问题。这可能很快,但这不是一个非常安全的方法,它需要调用代码,避免调用碰巧是鸭子的鸟上的苍蝇。
允许fly对ducks不做任何事情是否合适取决于您的调用代码通常期望fly做什么-实际上,您正在通过将第1点中fly的含义更改为“所有鸟类都可以被要求飞(尽管有些不会飞)”来修复不一致性。在这种情况下,fly真的变成了Flyifoucan,这可能是设计不完美的迹象,但可能是务实的解决方案——特别是在修改现有设计时。
事实上,您建议的“不做任何事情”选项可能表明了一条最不受影响的路线,因为如果您让fly对鸭子不做任何事情,那么您仍然需要一些鸭子特定的代码来让它游动,因此您可以接受在实际世界中鸭子可以飞,并且让鸭子特定的代码调用swim而不是fly-不如fly。
更一般地说,我认为您实际描述的是第1点的变化,从“所有的鸟都能飞”到“所有的鸟都能移动”,然后非鸭子实现像苍蝇一样移动,鸭子实现像游泳一样移动(无论它们是否也有飞的方法)。这可能涉及将一些现有呼叫更改为“飞”到“移动”呼叫。
我试图通过反复阅读维基百科条目来确定我对上述原则的理解。 撇开仍然让我悲伤的协变和逆变的概念不谈,wikipedia还提到超类型的不变量必须保留在子类型和历史约束或历史规则中。基于最后两个概念,我提出了一个小例子: 所以我的问题是:基于上述两个概念,我用这个例子是否违反了原则?若否,原因为何? 事先非常感谢。
来自维基百科, Liskov的行为子类型概念定义了对象的可替代性概念;也就是说,如果S是T的子类型,则程序中T类型的对象可以替换为S类型的对象,而不改变该程序的任何期望属性(例如正确性)。 假设以下类层次结构: 基本抽象类-。它有一个只读属性,在后继程序中被重写。 基类的继承者-,它重写并返回灰色。 Cat的继任者-,它覆盖并返回带条纹的。 然后我们声明一个方法,参数类型为(不是)。 向该方法发送
我在理解这两个原则时有些困难。这是一个有点长的阅读问题,所以要有耐心。 假设我们有一个类 和接口 然后我们创建了两个子类 现在我们将使implements 并在儿童课程中做出改变 并为该结构创建测试函数(如下LSP): 在这里我想停下来,因为实际上在下一步我被绊倒了。如果我们要创建第三个类? 圆没有边,所以为这个孩子实现听起来很可笑。好的,我们可以只将实现移到四边形和三角形,但在这种情况下LSP将
Liskov替代原则(LSP)和界面分离原则(ISP)之间有什么核心区别吗?最终,这两种方法都是为了设计具有通用功能的界面,并在您具有特殊功能时引入新的界面。
在创建我的班级结构时,我努力坚持利斯科夫替代原则。我想在Day类中存储一组日历项。需要有几种不同类型的日历项,例如: 任命项目 备注项目 轮换项目 它们都共享一些抽象基类CalendarItem中的常见功能: 但例如RotaItem有一些额外的功能: 其他类也添加了自己的逻辑等。 我有一组CalendarBaseItem用于我的日课: 但在回顾这一点时,我可以看到我正在打破LSP原则,因为我必须检
Liskov替代原则要求 子类型中的前提条件不能加强 任何人都可以发布一个违反上述每一点的例子和另一个解决这些问题的例子吗?