软件设计原则之开闭原则

软件设计原则之开闭原则

如果模块仍可扩展,则称其为打开状态。例如,应该可以向其包含的数据结构添加字段,或者向其执行的功能集添加新元素。

技术开发 编程 技术框架 技术发展

 

软件设计原则之开闭原则

如果模块仍可扩展,则称其为打开状态。例如,应该可以向其包含的数据结构添加字段,或者向其执行的功能集添加新元素。

SOLID设计:开闭原则(OCP)

开闭原理(OCP)是众所周知的SOLID缩写词中的O。

伯特兰·迈耶(Bertrand Meyer)曾因创造了开放/封闭原则一词而广受赞誉,该原则出现在1988年的《面向对象的软件构造》一书中。它的原始定义是

  • 如果模块仍可扩展,则称其为打开状态。例如,应该可以向其包含的数据结构添加字段,或者向其执行的功能集添加新元素。

  • 如果某个模块可供其他模块使用,则将其称为已关闭。假设已为模块提供了良好定义的稳定描述(信息隐藏意义上的接口)

根据这些定义,通常以这种方式表达和总结原理:

  • 模块应该打开以进行扩展,而关闭则可以进行修改。

  • 关于这个简单定义的含义及其对在现实世界中使用面向对象编程(OOP)的含义的争论已经(并且仍然有很多)。

在这篇文章中,我将尽量具体而简洁。

解释OCP的经典代码示例

可以使用以下方式在C#中翻译解释OCP的经典代码示例:

public class Circle { }


public class Square { }


public static class Drawer {

   public static void DrawShapes(IEnumerable<object> shapes) {

      foreach (object shape in shapes) {

         if (shape is Circle) {

            DrawCircle(shape as Circle);

         } else if (shape is Square) {

            DrawSquare(shape as Square);

         }

      }

   }

   private static void DrawCircle(Circle circle) { /*Draw circle*/ }


   private static void DrawSquare(Square square) { /*Draw Square*/ }

}


public class Circle { }

 

public class Square { }

 

public static class Drawer {

   public static void DrawShapes(IEnumerable<object> shapes) {

      foreach (object shape in shapes) {

         if (shape is Circle) {

            DrawCircle(shape as Circle);

         } else if (shape is Square) {

            DrawSquare(shape as Square);

         }

      }

   }

   private static void DrawCircle(Circle circle) { /*Draw circle*/ }

 

   private static void DrawSquare(Square square) { /*Draw Square*/ }

}

我觉得这个例子有点毛骨悚然。我相信人们一定不知道编写这样的代码是什么OOP。它可以明显地进行重构,以这样的:


public interface IShape { void Draw(); }


public class Circle : IShape { public void Draw() { /*Draw circle*/ }}


public class Square : IShape { public void Draw() { /*Draw Square*/ } }


public static class Drawer {

   public static void DrawShapes(IEnumerable<IShape> shapes) {

      foreach (IShape shape in shapes) {

         shape.Draw();

      }

   }

}


public interface IShape { void Draw(); }

 

public class Circle : IShape { public void Draw() { /*Draw circle*/ }}

 

public class Square : IShape { public void Draw() { /*Draw Square*/ } }

 

public static class Drawer {

   public static void DrawShapes(IEnumerable<IShape> shapes) {

      foreach (IShape shape in shapes) {

         shape.Draw();

      }

   }

}

让我们看一下这种重构的含义:


我们引入了一个新的抽象IShape 来表示我们已经想到的一个概念。实际上,在第一个代码示例中,方法DrawShapes()已经接受了一系列形状。随着新IShape的抽象我们的设计现在是开放,接受更多的形状,例如三角形或Pentagone。

DrawShapes()方法将绘制任何新形状,而无需修改。换句话说,DrawShapes()方法的实现已关闭。

这是通常显示OCP的方式。这一切都是关于通过以下方式预测系统的未来变化:

发生更改时,现有代码保持不变。此处的现有代码是DrawShapes()具体方法主体,IShape接口以及Circle和Square类。

发生更改时,用于实现更改的新代码将写入实现现有抽象的新类中。这里的新类可能是Triangle和Pentagone。

变化点原理

查看OCP的另一种方法是变化点(PV)原则,其中指出:

  • 确定预测变化的点,并在它们周围创建稳定的界面。

  • 我发现PV比OCP更可理解,因为它是可行的。首先确定潜在的变化,然后围绕这些变化构建适当的抽象。

真正的挑战:期待

但是真正的挑战是期待,期待很难。如果预期容易,那么我们将成为比特币的亿万富翁。在现实世界中,当您预计到的风险很高时:

我们确实预期会有变化。这就是YAGNI原则所说的(您将不需要它):始终在真正需要它们时执行这些事情,永远不要在仅仅预见到需要它们时才执行。开发和维护抽象是有代价的,如果我们不需要它,则这是负数。

我们无法预期实际需要的变化的风险也很高。但是一旦变体需求成为现实,这就是开发人员的责任,即重构并创建正确的抽象以及将对这些抽象起作用的正确的稳定代码。这是一次愚弄我,不要愚弄我两次的想法:我不应该预见到我需要的东西,但是我应该确定然后在需要时编写正确的抽象。

现实世界中的OCP

这是一种实用的OCP方法:

无论如何,KISS原则都适用(保持简单愚蠢):不要低估预期的难度,不要浪费资源来创建不需要的抽象。

编写自动测试:编写测试的最大好处之一是,有一阵子,您必须从客户端的角度来看代码。如果您的代码包含某些难以通过测试覆盖的区域,则肯定意味着您的代码应进行重构以轻松进行100%可测试。经验表明,从可测试性差的代码重构为可完全测试的代码时,自然会出现对正确抽象的需求。

一些静态分析器可以帮助您查明典型的OCP违规情况:

当向下转换引用(即从基类或接口到子类或叶类的转换)时,

使用is或as运算符时(如上面的第一个示例)。

NDepend的规则基类不应使用派生类:此规则的匹配明显违反了OCP。

请记住一次欺骗我,不要两次欺骗我。一旦确定需要抽象一些概念,就必须重构代码。当然有时候,如果大量的客户端代码依赖于您的API是不可能的:在这种情况下,您不能轻松地进行重构,并且常常必须忍受错误的设计。这就是为什么公共API设计如此敏感的主题的原因:您别无选择,只能尽最大的努力去预期并接受过去的设计错误。

可以使用“访客”模式对多个变体开放

最后,我们强调一下,在现实世界中,数据对象(如此处的形状)不会实现自身的算法(例如绘图)。经验告诉我们,这明显违反了OCP,因为当对数据对象需要一种新算法时,例如在图形中添加持久性后,必须再次修改所有形状类。这也违反了单一职责原则(SRP,即SOLID中的S),因为形状类现在具有两个职责:1)保存形状数据2)绘制形状。

因此,我们现在有两种变体:我们需要一种抽象形状和应用于形状的算法的方法,以便编写诸如algorithm.ApplyOn(shape)之类的东西。这两个抽象类型的呼叫被命名为双调度呼叫:真正调用的实现既取决于IShape的对象的类型和IAlgorithm对象的类型。如果您有N个形状和M个算法,则需要[N x M]个实现。

幸运的是,访客模式有助于实现双重调度。具有新的持久性算法的代码将如下所示:

//

// Shapes elements

//

public interface IShape { void Accept(IVisitor visitor); }

public class Circle : IShape {

   public void Accept(IVisitor visitor) { visitor.Visit(this); }

}

public class Square : IShape {

   public void Accept(IVisitor visitor) { visitor.Visit(this); }

}


//

// Visitors algorithms on shapes elements

// don't use the IAlgorithm terminology to keep up with the classical visitor pattern terminology

//

public interface IVisitor {

   void Visit(Circle circle);

   void Visit(Square square);

}

public class DrawAlgorithm : IVisitor {

   public void Visit(Circle circle) { /*Draw circle*/}

   public void Visit(Square square) { /*Draw square*/}

}

public class PersistAlgorithm : IVisitor {

   public void Visit(Circle circle) { /*Persist circle*/}

   public void Visit(Square square) { /*Persist square*/}

}


public static class Program {

   public static void ApplyVisitorAlgorithmOnShapesElements(IEnumerable<IShape> shapes, IVisitor visitor) {

      foreach (IShape shape in shapes) {

         // Double dispatching:

         //   shape can be both: Circle or Square

         //   visitor can be both Draw or Persist

         shape.Accept(visitor);  

      }

   }

}

//

// Shapes elements

//

public interface IShape { void Accept(IVisitor visitor); }

public class Circle : IShape {

   public void Accept(IVisitor visitor) { visitor.Visit(this); }

}

public class Square : IShape {

   public void Accept(IVisitor visitor) { visitor.Visit(this); }

}

 

//

// Visitors algorithms on shapes elements

// don't use the IAlgorithm terminology to keep up with the classical visitor pattern terminology

//

public interface IVisitor {

   void Visit(Circle circle);

   void Visit(Square square);

}

public class DrawAlgorithm : IVisitor {

   public void Visit(Circle circle) { /*Draw circle*/}

   public void Visit(Square square) { /*Draw square*/}

}

public class PersistAlgorithm : IVisitor {

   public void Visit(Circle circle) { /*Persist circle*/}

   public void Visit(Square square) { /*Persist square*/}

}

 

public static class Program {

   public static void ApplyVisitorAlgorithmOnShapesElements(IEnumerable<IShape> shapes, IVisitor visitor) {

      foreach (IShape shape in shapes) {

         // Double dispatching:

         //   shape can be both: Circle or Square

         //   visitor can be both Draw or Persist

         shape.Accept(visitor);  

      }

   }

}

技术开发 编程 技术框架 技术发展