前言
最近在读《敏捷软件开发 原则、模式与实践》,里面提到了几大设计原则,于是想复习下,并且用C#把文中代码重新写一遍
开闭原则(OCP)
概念
对扩展开放,对修改封闭。
OCP的优势
开闭原则的核心价值在于:它使软件架构具有韧性。当需求变化时,我们通过写新代码而非改旧代码来适应变化,从而保护已通过测试的稳定代码,降低维护成本,提高可重用性。
原文中提到了更重要的一点:"对于应用程序中的每个部分都肆意地进行抽象也不是一个好主意。正确的做法是,开发人员应该仅仅对程序中呈现出频繁变化的那些部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。"这句话和性能优化的原则类似,拒绝过早优化。当你用开闭原则写代码像呼吸一样自然时,你就知道什么可以遵循开闭原则,什么时候可以先不用在意开闭原则。
先来看需求
我们有一个需要在标准的GUI上绘制圆和正方形的应用程序。圆和正方形必须要按照特定的顺序绘制。我们将创建一个列表,列表由按照适当的顺序排列的圆和正方形组成,程序遍历该列表,依次绘制出每个圆和正方形。
违反OCP的代码
public enum ShapeType
{
Circle,
Square
}
public struct Shape
{
public ShapeType ItsType;
}
public struct Circle
{
public ShapeType ItsType;
public double ItsRadius;
public Point ItsCenter;
}
public struct Square
{
public ShapeType ItsType;
public double ItsSide;
public Point ItsTopLeft;
}
public class ShapeDrawer
{
// 严重违反OCP:新增形状类型时必须修改此方法
public void DrawAllShapes(List<Shape> list)
{
foreach (var s in list)
{
switch (s.ItsType)
{
case ShapeType.Square:
DrawSquare((Square)(object)s);
break;
case ShapeType.Circle:
DrawCircle((Circle)(object)s);
break;
}
}
}
private void DrawSquare(Square s) => Console.WriteLine($"绘制正方形,边长:{s.ItsSide}");
private void DrawCircle(Circle c) => Console.WriteLine($"绘制圆形,半径:{c.ItsRadius}");
}
问题所在
-
每次新增类型时,都要增加一个enum,和switch项。
-
日后会导致,switch越来愈长,难以维护。
遵循OCP的代码
使用抽象和多态。通过抽象基类隔离变化。新增形状类型时,只需添加新类,无需修改现有代码。
public abstract class Shape
{
public abstract void Draw();
}
public class Square : Shape
{
public double Side { get; set; }
public Point TopLeft { get; set; }
public override void Draw()
{
Console.WriteLine($"绘制正方形:边长={Side}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public Point Center { get; set; }
public override void Draw()
{
Console.WriteLine($"绘制圆形:半径={Radius}");
}
}
public class ShapeDrawer
{
// 完全符合OCP:对扩展开放,无需修改即可支持新形状
public void DrawAllShapes(List<Shape> shapes)
{
foreach (var shape in shapes)
{
shape.Draw(); // 多态调用,无需知道具体类型
}
}
}
// 使用演示
// var drawer = new ShapeDrawer();
// var shapes = new List<Shape> { new Circle(), new Square(), new Triangle() }; // 可随意添加新类型
// drawer.DrawAllShapes(shapes);
当然,需求是会持续变化的
如果我们要求所有的圆必须在正方形之前绘制,那么DrawAlShapes函数会怎样呢?DrawAlIShapes函数无法对这种变化做到封闭。要实现这个需求,我们必须要修改DrawAIShapes的实现,使它首先扫描列表中所有的圆,然后再扫描所有的正方形。
如果是你,你会怎么优雅地完成这个需求呢?
终极OCP
使用“数据驱动”的方法获取封闭性。
依据原文中的解决方案,用C#实现。以下代码可直接复制运行。
var drawer = new ShapeDrawer();
var shapes = new List<Shape> {
new Triangle(),
new Circle(),
new Square()
};
drawer.DrawAllShapes(shapes);
public abstract class Shape : IComparable<Shape>
{
private static readonly string[] TypeOrderTable = new[]
{
"Circle", // 索引0:最先绘制
"Square", // 索引1
"Triangle" // 索引2
};
public abstract void Draw();
// 非虚方法:完全通用的排序逻辑,无需在每个派生类中重写
public bool Precedes(Shape other)
{
string thisType = this.GetType().Name;
string otherType = other.GetType().Name;
int thisOrder = Array.IndexOf(TypeOrderTable, thisType);
int otherOrder = Array.IndexOf(TypeOrderTable, otherType);
// 如果都不在表中,按名称排序;如果在,按表中的顺序
if (thisOrder == -1 && otherOrder == -1)
return string.Compare(thisType, otherType) < 0;
return thisOrder < otherOrder;
}
public int CompareTo(Shape other) => Precedes(other) ? -1 : (other.Precedes(this) ? 1 : 0);
}
// 现在添加新形状类型(如Triangle)无需修改任何现有代码!
public class Triangle : Shape
{
public override void Draw() => Console.WriteLine("绘制Triangle");
}
public class Circle : Shape
{
public override void Draw() => Console.WriteLine("绘制Circle");
}
public class Square : Shape
{
public override void Draw() => Console.WriteLine("绘制Square");
}
public class ShapeDrawer
{
public void DrawAllShapes(List<Shape> shapes)
{
foreach (var shape in shapes.OrderBy(s => s))
{
shape.Draw();
}
}
}
参考文献
敏捷软件开发 原则、模式与实践
329



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



