跨越开放封闭原则

7
我有一个简单的程序,基于用户提供的鼠标数据来绘制几何图形。我有一个处理鼠标跟踪的类(它获取带有鼠标移动历史记录的列表),还有一个名为Shape的抽象类。从这个类中,我派生出一些额外的形状,如圆形、矩形等——每个形状都重写了抽象的Draw()函数。
一切都很好,但问题在于当我希望用户能够手动切换所需的形状时。我获取鼠标数据并知道应该绘制哪种形状。问题是如何让代码“知道”应该创建哪个对象并传递适当的参数给构造函数。此时添加新的形状派生类也是不可能的,这显然是错误的。
显然,我不想编写以下代码:
List<Shape> Shapes = new List<Shape>();
// somwhere later 

if(CurrentShape == "polyline"){
    Shapes.Add(new Polyline(Points)); 
}
else if (CurrentShape == "rectangle"){
    Shapes.Add(new Rectangle(BeginPoint, EndPoint));
}
// and so on.

上面的代码明显违反了开闭原则。问题在于我没有任何好的想法来解决它。主要问题是不同的形状具有不同参数的构造函数,这使得它变得更加棘手。
我相信这是一个常见的问题,但我不知道如何克服它。你有什么想法吗?

2
那不是“开闭原则”。那只是多态性。 - Mitch Wheat
1
我希望 Shape 类的代码闭合于编辑,开放于扩展,因此我认为它符合 OCP 问题。 - Kamil T
1
你需要在谷歌上搜索工厂模式。 - Roger Rowland
1
我从未说过那样的话。两个点仍然是一个点列表,因此基本构造函数可以是 Shape(IEnumerable<Point> points)。矩形重载只需要检查是否提供了两个点。实际上,dasblinkenlight 在他的答案中提供了相同的想法。 - Simon Mourier
6
好的,你违反了开闭原则。这个原则对你的客户来说真的有意义吗?如果你的客户说你违反了OCP,你会失去任何销售吗?是不是必须要扩展性而保持开放呢?我经常违反OCP,因为严格遵守OCP通常会增加成本,而没有相应的价值。关注那些实际影响客户的问题即可。 - Eric Lippert
显示剩余9条评论
2个回答

6
当您需要创建所有派生自单个类或实现相同接口的对象时,一种常见的方法是使用工厂。然而,在您的情况下,简单的工厂可能不足够,因为工厂本身需要具有可扩展性。
实现它的一种方法如下:
interface IShapeMaker {
    IShape Make(IList<Point> points);
}
class RectMaker : IShapeMaker {
    public Make(IList<Point> points) {
        // Check if the points are good to make a rectangle
        ...
        if (pointsAreGoodForRectangle) {
            return new Rectangle(...);
        }
        return null; // Cannot make a rectangle
    }
}
class PolylineMaker : IShapeMaker {
    public Make(IList<Point> points) {
        // Check if the points are good to make a polyline
        ...
        if (pointsAreGoodForPolyline) {
            return new Polyline(...);
        }
        return null; // Cannot make a polyline
    }
}

使用这些Maker类,您可以制作一个制造商注册表(一个简单的List<IShapeMaker>),通过制造商传递它们的点,并在获得非空形状时停止。
该系统保持可扩展性,因为您可以添加一对NewShapeNewShapeMaker,并将它们“插入”到现有框架中:一旦NewShapeMaker进入注册表,系统的其余部分即立即准备好识别和使用您的NewShape

2
这种实现存在一个缺点 - 由于同一组点可能会被多个制造商接受,因此制造商的顺序突然变得很重要 - 更改顺序将使工厂返回不同的形状。原始实现没有这个问题,因为工厂客户端明确指定了它想要的形状。这可以通过向工厂方法传递此附加参数来轻松解决(请参见我的回答)。 - Wiktor Zychla
@WiktorZychla 额外的参数为调用者提供了显式控制,但每次通过添加新形状来扩展系统时,这也使得修改调用者成为必要(即调用者需要“学习”在每次新形状可用时传递新形状的名称)。我对问题的理解是OP想避免使用额外的参数。 - Sergey Kalinichenko
他写道:“我知道鼠标的数据和应该绘制的形状。” 由此可以看出,调用者知道确切的形状。我相信他拥有所有可用形状的工具箱或类似物。 - Wiktor Zychla
@WiktorZychla - 你说得对,用户有一个带有形状的工具栏(以及颜色,这与问题本身无关)。事实证明,你的答案对我的问题来说是一个稍微更好的解决方案。 - Kamil T
嗨,我想知道,如果这个答案也使用“string ShapeName”参数,那么相较于其他答案注入工人而不是扩展“ShapeFactory”,它有什么不足之处? - Blueriver

3

它需要一家工厂,但不仅仅是工厂,而是配备可注入员工的工厂。

public class Context {
   public Point BeginPoint;
   public Point EndPoint;
   public List Points;

   whatever else
}

public class ShapeFactory {

   List<FactoryWorker> workers;

   public Shape CreateShape( string ShapeName, Context context )
   {
      foreach ( FactoryWorker worker in workers )
         if ( worker.Accepts( ShapeName ) )
             return worker.CreateShape( context );
   }

   public void AddWorker( FactoryWorker worker ) {
      workers.Add( worker );
   }
 }

 public abstract class FactortWorker {
    public abstract bool Accepts( string ShapeName );
    puboic Shape CreateShape( Context context );
 }

 public class PolyLineFactoryWorker : FactoryWorker {

    public override bool Accepts( string ShapeName ) {
       return ShapeName == "polyline";
    }

    public Shape CreateShape( Context context ) { ... }

 }

这样代码就可以进行扩展——新的工厂工人可以自由创建并添加到工厂中。

嗨。我想知道,如果另一个答案也使用了“string ShapeName”参数,那么扩展ShapeFactory而不是注入工作者有什么优势? - Blueriver
@Blueriver:这将使两个答案等效。但是,另一个答案中没有工厂扩展。那里的制造商基本上就是这里的工人。工厂本身不应被修改,因为这将破坏OCP的Closed部分。 - Wiktor Zychla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接