敏捷软件开发 – OCP 开放-封闭原则

软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。

  如果程序中的一处改动就会产生连锁反应,导致一系列相关模块的改动,那么设计就具有僵化性的臭味。OCP建议我们应该对系统进行重构,这样以后对系统再进行这样那样的改动时,就不会导致更多的修改。如果正确地应用OCP,那么以后再进行同样的改动时,就只需要添加新的代码,而不必改动已经正常运行的代码。

OCP概述
  遵循开放-封闭原则设计出的模块具有两个主要的特征。它们是:

  1. 对于扩展是开放的(open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。换句话说,我们可以改变模块的功能。
  2. 对于修改是封闭的(closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者.EXE文件,都无需改动。

  这两个特征好像是互相矛盾的。扩展模块的行为的通常方式,就是修改该模块的源代码。不允许修改的模块常常都认为具有固定的行为。
  怎样可能在不改动模块源代码的情况下去更改它的行为呢?如果不改变一个模块,又怎么能够去改变它的功能呢?
  答案是抽象。在C#或者其他任何的OOPL(面向对象程序设计语言)中,可以创建出固定却能够描述一组任意个可能行为的抽象体。这个抽象体就是抽象基类。而一组任意个可能的行为则表现为可能的派生类。
  模块可能对抽象体进行操作。由于模块依赖于一个固定的抽象体,所以它对于更改可以是封闭的。同时,通过从这个抽象体派生,可以扩展此模块的行为。
  下图展示了一个简单的不遵循OCP的设计。Client类和Server类都是具体类。Client类使用Server类。如果我们希望Client类对象使用另外一个不同的服务器对象,那么就必须要把Client类中使用Server类的地方更改为新的服务器类。

  下图展示了一个针对上述问题的遵循OCP的设计(通过使用STRATEGY模式)。在这个设计中,ClientInterface是一个拥有抽象成员函数的抽象接口。Client类使用这个抽象接口。然而Client类的对象却使用派生的Server类的对象。如果我们希望Client对象使用一个新的服务器类,那么只需要从ClientInterface派生一个新的类。无需从Client类做任何改动。

  Client需要实现一些功能,它可以根据ClientInterface抽象接口去描绘那些功能。ClientInterface的子类型可以以任何它们所选择的方式去实现这个接口。这样,就可以通过创建ClientInterface的新的子类型的方式去扩展、更改Client中指定的行为。
  也许你想知道为何把抽象接口命名为ClientInterface。为何不把它命名为AbstractServer呢?因为抽象类和它们的客户的关系要比和实现它们的类的关系更密切一些。

Shape应用程序
  Shape示例在许多讲述面向对象设计的书中都提到过。这个声名狼藉的例子常常用来展示多态的工作原理。
  我们有一个需要在标准的GUI上绘制圆和正方形的应用程序。圆和正方形必须按照特定的顺序绘制。我们将创建一个列表,列表由按照适当顺序排列的圆和正方形组成,程序遍历该列表,依次绘制出每一个圆和正方形。

违反OCP

//--shape.h---------------------------------------
enum ShapeType
{
    circle,
    square
};

struct Shape
{
    ShapeType itsType;
};

//--circle.h---------------------------------------
struct Circle
{
    ShapeType itsType;
    double itsRadius;
    Point itsCenter;
};

void DrawCircle(struct Circle*);

//--square.h---------------------------------------
struct Square
{
    ShapeType itsType;
    double itsSide;
    Point itsTopLeft;
};

void DrawSquare(struct Square*);

//--drawAllShapes.cc-------------------------------
typedef struct Shape *private ShapePointer ;
void DrawAllShapes(ShapePointer list[]private ,private int n )
{
    int i;
    for (i = 0; i < n; i++)
    {
      struct Shape* s = list[i];
        switch (s->itsType)
        {
            case square:
                DrawSquare((struct Square* )s);
                break;
            case circle:
                DrawCircle((struct Circle* )s);
                break;
        }
    }
}

  DrawAllShapes函数不符合OCP,因为它对于新的形状类型的添加不是封闭的。如果希望这个函数能够绘制包含三角形的列表,就必须变更这个函数。事实上,每增加一种新的形状类型,都必须要更改这个函数。

  当然这只是一个简单的例子。在实际应用程序中,类似DrawAllShape函数中的switch语句会在应用程序的许多函数中不断地重复出现,每个函数中switch语句负责完成的工作差别甚微。这些函数中,可能有负责拖拽形状对象的,有负责拉伸形状对象的,有负责移动形状对象的,有负责删除形状对象的,等等。在这样的应用程序中增加一种新的形状类型,就意味着要找出所有包含上述switch语句(或者链式if/else语句)的函数,并在每一处都添加对新增的形状类型的判断。

  同样,在进行上述改动时,我们必须要在ShapeType中添加一个新的成员。由于所有不同种类的形状都依赖于这个枚举声明,所以我们必须要重新编译所有的形状模块。并且也必须要重新编译所有依赖于Shape类的模块。

遵循OCP

public interface Shape
{
    void Draw();
}
public class Square : Shape
{
    public void Draw()
    {
        //draw a square
    }
}
public class Circle : Shape
{
    public void Draw()
    {
        //draw a circle
    }
}
public void DrawAllShapes(IList shapes)
{
    foreach (Shape shape in shapes)
    {
        shape.Draw();
    }
}

  这个程序是符合OCP的。对它的改动是通过增加新代码进行的,而不是更改现有的代码。因此,它就不会引起像不遵循OCP的程序那样的连锁改动。所需要的改动仅仅是增加新的模块,以及为了能够实例化新类型的对象而进行的围绕main的改动。

  如果我们要求所有的Circle必须在Square之前绘制,那么代码中的DrawAllShapes函数会怎么样呢?DrawAllShapes函数无法对这种变化做到封闭。要实现这个需求,我们必须要修改DrawAllShapes的实现,使它首先扫描列表中所有的Circle,然后再扫描所有的Square。

预测变化和“贴切的”结构

  如果我们预测到了这种变化,那么就可以设计出一个抽象来隔离它。我们在代码中所选定的抽象对于这种变化来说反倒成为一种障碍。可能你会觉得奇怪:还有什么比定义一个Shape类,并从它派生出Square类和Circle类更贴切的结构呢?为何这个贴切的模型不是最优的呢?很明显,模型对于一个形状的顺序比形状更具有重要意义的系统来说,就不再那么贴切了。

  这就导致了一个麻烦的结果,一般而言,无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。没有对于所有的情况都贴切的模型!

  遵循OCP的代价是昂贵的。创建适当的抽象是要花费开发时间和精力的。同时,那些抽象也增加了软件设计的复杂性。我们希望把OCP的应用限定在可能会发生的变化上。

  那么我们如何知道哪个变化有可能发生呢?我们进行适当的调查,提出正确的问题,并且使用我们的经验和一般常识。最终,我们会一直等到变化发生时才采取行动!

使用抽象获得显示封闭

  封闭是建立在抽象的基础上的。因此,为了让DrawAllShapes对于绘制顺序的变化是封闭的。我们需要一种“顺序抽象体”。这个抽象体定义了一个抽象接口,通过这个接口可以表示任何可能的排序策略。

  一个排序策略意味着,给定两个对象,可以推导出先绘制哪一个。C#提供了这样的抽象。IComparable是一个接口,它只提供一个方法:CompareTo。这个方法以一个对象作为输入参数,当接受消息的对象小于、等于、大于参数数对象时,该方法分别返回-1,0,1 。

扩展了IComparable接口的Shape类

public interface Shape : IComparable
{
    void Draw();
}

依次绘制DrawAllShape函数

public void DrawAllShapes(ArrayList shapes)
{
    shapes.Sort();
    foreach (Shape shape in shapes)
    {
        shape.Draw();

    }
}

对Circle排序

public class Circle : Shape
{
    public int CompareTo(object obj)
    {
        if (obj is Square)
        {
            return -1;
        }
        else
        {
            return 0;
        }
}

  显然这个函数以及所有的Shape类的派生类中的CompareTo函数都不符合OCP。没有办法使得这些函数对于Shape类的新派生类做到封闭。每次创建一个新的Shape类的派生类时,所有的CompareTo函数都需要改动。如果需要频繁地创建新的Shape类的派生类,那么这个设计就会遭到沉重的打击。

使用“数据驱动”的方法获取封闭性

public class ShapeComparer : IComparer
{
    private static Hashtable priorities = new Hashtable();
    static ShapeComparer()
    {
        priorities.Add(typeof(Circle), 1);
        priorities.Add(typeof(Square), 2);
    }
    private int PriorityFor(Type type)
    {
        if(priorities.Contains(type))
            return (int)priorities[type];
        else
            return 0;
    }
    public int Compare(object o1, object o2)
    {
        int priority1 = PriorityFor(o1.GetType());
        int priority2 = PriorityFor(o2.GetType());
        return priority1.CompareTo(priority2);
    }
}

  通过这种方法,我们成功地做到了一般情况下DrawAllShapes函数对于顺序问题的封闭,也使得每一个Shape派生类对于新的Shape派生类的创建或者基于类型的Shape对象排序规则的改变是封闭的。(比如,改变顺序为Square必须最先绘制)

结论

  在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处:灵活性、可重用性以及灵活性。然而,并不是说使用一种面向对象的语言就是遵循了这个原则。对于应用程序中的每个部分都肆意地进行抽象同样不是一个好主意。正确的做法是,开发人员仅仅对程序中出现频繁变化的那些部分作出抽象。拒绝不成熟的抽象和抽象本身一样重要。

摘录自:[美]RobertC.Martin、MicahMartin著,邓辉、孙鸣译 敏捷软件开发原则、模式与实践(C#版修订版) [M]、人民邮电出版社,2013、93-101、

时间: 2024-10-24 13:26:32

敏捷软件开发 – OCP 开放-封闭原则的相关文章

[敏捷设计]3.OCP开放封闭原则

一.定义 软件实体(类.模块.函数等)应该是可以扩展的,但是不可修改. 如果正确的应用了OCP原则,那么 以后在进行同样的改动时,就只需要添加新的代码,不必修改已经正常运行的代码. 二.OCP概述 1.对于扩展是开放的 这意味着模块的行为是可以扩展的.当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为.换句话说,我们可以改变模块的功能. 2.对于修改是封闭的 对模块进行扩展时,不必改动模块的源代码或者二进制代码. 3.有何问题 这两个特征好像是互相矛盾的.扩展模块行为的通常

敏捷软件开发的12个原则

作为一个软件工程师,软件设计和开发是最重要的技能,但是,从整个产品的角度上讲,项目管理能力比开发能力更重要,本文摘自Robert大叔的<敏捷软件开发>,粗体是Robert大叔的话,细体是我的理解. 1.持续.尽早交付有价值的软件以满足客户,是我们优先要做的首要任务. 以逐渐增加功能的方式经常性地交付系统和最终质量之间有非常强的相关性.交付得越频繁,最终产品的质量就越高. 自顶向下地设计软件,按照功能优先级逐步开发,定期交付可运行的版本.这个原则看起来简单,但是对软件设计有非常高的要求,因为随着

敏捷软件开发 – LSP Liskov替换原则

Liskov替换原则:子类型(subtype)必须能够替换掉它们的基类型(basetype). 违反LSP的情形 对于LSP的违反常常会导致以明显违反OCP的方式使用运行时类型检查.通常,会使用一个显式的if语句或者if/else链去确定一个对象的类型,以便于可以选择针对该类型的正确行为. struct Point { double x, y;} public enum ShapeType { square, circle }; public class Shape { private Shap

敏捷软件开发 – SRP 单一职责原则

SRP:单一职责原则  一个类应该只有一个发生变化的原因. 为何把两个职责分离到单独的类中很重要呢?因为每一个职责都有变化的一个轴线.当需求变化时,该变化会反映为类的职责的变化.如果一个类承担了多于一个的职责,那么引起它变化的原因就会有多个. 如果一个类承担的职责过多,就等于把这些职责耦合在了一起.一个职责发生变化可能会削弱或抑制这个类完成其他职责的能力.这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏. 有两个不同的应用程序使用Rectangle类.一个应用程序是有关计算几何

敏捷软件开发 – ISP 接口隔离原则

如果类的接口不是内聚的,就表示该类具有“胖”接口.换句话说,类的“胖”接口可以分解成多组方法.每一组方法服务于一组不同的客户程序. ISP承认有一些对象确实需要有非内聚的接口,但是ISP建议客户程序不应该看到它们作为单一的类存在.相反,客户程序看到的应该是多个具有内聚接口的抽象基类. 接口污染 考虑一个安全系统.在这个系统中,有一些Door对象,可以被加锁和解锁,并且Door对象知道自己是开着还是关着.这个Door编码成一个接口,这样客户程序就可以使用那些符合Door接口的对象,而不需要依赖于D

敏捷软件开发 – DIP 依赖倒置原则

DIP 依赖倒置原则 高层模块不应该依赖于低层模块.二者都应该依赖于抽象. 抽象不应该依赖于细节.细节应该依赖于抽象. 依赖于低层模块的高层模块意味着什么?正是高层模块包含了应用程序中重要的策略选择和业务模型.这些高层模块使得其所在的应用程序区别于其他.然而,如果这些高层模块依赖于低层模块,那么对于低层模块的改动会直接影响到高层模块,从而迫使它们依次做出改动.如果高层模块独立于低层模块,那么高层模块就可以非常容易地被重用.该原则是框架设计的核心原则. 层次化 糟糕的层次关系. 更为适合的模型.每

开放封闭原则(Open Closed Principle)

在面向对象的设计中有很多流行的思想,比如说 "所有的成员变量都应该设置为私有(Private)","要避免使用全局变量(Global Variables)","使用运行时类型识别(RTTI:Run Time Type Identification,例如 dynamic_cast)是危险的" 等等.那么,这些思想的源泉是什么?为什么它们要这样定义?这些思想总是正确的吗?本篇文章将介绍这些思想的基础:开放封闭原则(Open Closed Princi

敏捷软件开发:原则、模式与实践——第9章 OCP:开放-封闭原则

第9章 OCP:开放-封闭原则 软件实体(类.模块.函数等)应该是可以扩展的,但是不可修改. 9.1 OCP概述 遵循开放-封闭原则设计出的模块具有两个主要特征: (1)对于扩展是开放的(open for extension).这意味着模块的行为是可以扩展的.当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为. (2)对于修改是封闭的(closed for modification).对模块进行扩展时,不必改动模块的源代码或者二进制代码.模块的二进制可执行版本,无论是可链接

敏捷软件开发——开放—封闭原则(OCP)

由来: 怎么样的设计才能面对需求的改变却可以保持相对稳定,从而使得系统可以在第一版本以后不断推出新的版本呢?bertrand meyer 在1988年提出的著名的开发-封闭原则(the open-closed princle)为我们提供了指引. 遵循开放-封闭原则设计出的模块具有两个主要特征: 1. "对于扩张是开放的"(open for extension) 这以为着模块的行为是可以扩展的.当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的行为.也就是说,我们可以改变