目录
前言
最近在读《敏捷软件开发 原则、模式与实践》,里面提到了几大设计原则,于是想复习下,并且用C#把文中代码重新写一遍
Liskov替换原则(LSP)
概念
子类必须能够替换掉他们的基类。
违反LSP的简单例子
很多情况下,违反LSP会导致违反OCP(开闭原则,上一篇博客有具体讲到)。
特别是在运行时用ifelse去区分一个对象的类型,以采取不同的行为,大概率会违反LSP。
using System;
namespace LSPExample
{
// 违反LSP的例子:使用类型检查而不是多态
public enum ShapeType
{
Square,
Circle
}
public class Shape
{
public ShapeType Type { get; protected set; }
public Shape(ShapeType type)
{
Type = type;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public Point Center { get; set; }
public Circle() : base(ShapeType.Circle) { }
public void Draw()
{
Console.WriteLine($"Drawing circle at ({Center.X}, {Center.Y}) with radius {Radius}");
}
}
public class Square : Shape
{
public double Side { get; set; }
public Point TopLeft { get; set; }
public Square() : base(ShapeType.Square) { }
public void Draw()
{
Console.WriteLine($"Drawing square at ({TopLeft.X}, {TopLeft.Y}) with side {Side}");
}
}
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public Point(double x, double y)
{
X = x;
Y = y;
}
}
public class Program
{
// 违反OCP和LSP的函数:需要知道所有具体类型
public static void DrawShape(Shape shape)
{
// 使用类型检查而不是多态
if (shape.Type == ShapeType.Square)
{
((Square)shape).Draw();
}
else if (shape.Type == ShapeType.Circle)
{
((Circle)shape).Draw();
}
// 每次添加新形状都需要修改这里!
}
}
}
问题分析
-
DrawShape函数必须知道所有具体的形状类型
-
添加新形状需要修改DrawShape函数,违反开闭原则
-
使用类型检查而不是多态,代码难以维护
正方形和矩形的微妙违规
在数学中,正方形是一种特殊的矩形,那么是否正方形就要继承矩形呢。
using System;
namespace RectangleSquareExample
{
// 初始设计:违反LSP
public class Rectangle
{
public virtual double Width { get; protected set; }
public virtual double Height { get; protected set; }
public virtual void SetWidth(double width)
{
Width = width;
}
public virtual void SetHeight(double height)
{
Height = height;
}
public double Area => Width * Height;
}
// 正方形继承自矩形 - 看似合理,实则违反LSP
public class Square : Rectangle
{
public override void SetWidth(double width)
{
base.SetWidth(width);
base.SetHeight(width); // 保持正方形特性
}
public override void SetHeight(double height)
{
base.SetWidth(height);
base.SetHeight(height); // 保持正方形特性
}
}
public class Program
{
// 这个函数假设长方形的宽和高可以独立修改
public static void TestRectangle(Rectangle rect)
{
rect.SetWidth(5);
rect.SetHeight(4);
// 预期:面积 = 20
Console.WriteLine($"Expected area: 20, Actual area: {rect.Area}");
// 如果传入Square,这里会失败!
// 因为SetHeight(4)也会把宽度设为4,实际面积是16
Assert.Equal(20, rectangle.Area);
}
}
}
问题分析
Square的设计者,没有考虑到Square和Rectangle在对象行为上的不同。把数学中的正方形是矩形,和面向对象的继承概念混为一谈。
基于契约的设计(Design by Contract)
概念
使用基于契约的设计,类的编写者显式地规定针对该类的契约。客户代码的编写者可以通过该契约了解可以依赖的行为方式。契约是通过为每个方法声明的前置条件(preconditions)和后置条件(postconditions)来指定的。要使一个方法得以执行,前置条件必须要为真。执行完毕后,该方法要保证后置条件为真。
Rectangle基于契约的设计
public class RectangleWithContract
{
private double width;
private double height;
public double Width
{
get => width;
set
{
// 前置条件:值必须为正数
Contract.Requires(value > 0, "Width must be positive");
// 后置条件:设置后Width等于新值,Height不变
double oldHeight = height;
width = value;
// 验证后置条件
Contract.Ensures(Width == value);
Contract.Ensures(Height == oldHeight);
}
}
public double Height
{
get => height;
set
{
Contract.Requires(value > 0, "Height must be positive");
double oldWidth = width;
height = value;
Contract.Ensures(Height == value);
Contract.Ensures(Width == oldWidth);
}
}
public double Area => Width * Height;
}
此时,Rectangle的契约:设置Width时需要保持Height不变。但Square无法满足,因为修改Width需要同时修改Height。
基于契约的设计可以第一时间让开发者提前发现错误。
当然,正方形和矩形的例子可能比较简单,一般人可能也不会犯这种错误。但在工作中,遇到一些复杂需求时,接受你代码的人可能没有完全理解你的代码就开始工作了,此时,基于契约的设计和单元测试的好处就体现出来了,代码即文档。
可能违反LSP的情况
原文还举了很多例子,但本文有点太长了。直接总结下可能违反LSP的情况和实现方式。
-
用ifelse区分对象类型
-
子类的方法退化或为空实现,基类使用者期望的功能被静默丢弃
-
子类加强了前置条件或减弱了后置条件
实现LSP的方式
-
提取公共抽象层,提取两个不适合继承的类的基类。
-
使用组合替代继承,两个不适合继承的类,应实现一个共享接口。
-
基于契约设计(Design By Contract)。
-
通过单元测试明确契约,测试即文档,帮助开发者理解正确使用方式。
参考文献
敏捷软件开发 原则、模式与实践



被折叠的 条评论
为什么被折叠?



