漫谈面向对象基石之开闭原则(OCP)

开闭原则的意思是软件实体应该对扩展开发,对修改关闭(Software entities should be open for extension,but closed for modification)。实现开闭原则的途径是抽象,将需要扩展的部分抽象出来,并留出扩展接口。打个比方,比如电脑机箱上有usb的插口,这些插口就是可扩展的部分,我们可以在这些usb插口上插上鼠标,键盘,U盘,还可以插上网银的U盾等等。电脑硬件上对于usb接口的这个设计就是一个符合开闭原则的设计。

为什么要遵循开闭原则呢?因为开闭原则可以使软件系统更容易复用,更容易维护,当某个软件实体,不适合了,我可以重新做另外一种实现,并将现有的实现替换掉。比如说统计个税的算法发生了一些变化,我可以在不改变原有代码的情况下,重新实现一个算法将原有的算法替换下来。比如说杀毒软件,在出现一种新的病毒时,开发出一个查杀这种病毒的新模块,可以只开发更新这个查杀模块,而不需要改变原有系统的内容。

开闭原则这么好,如何实现符合开闭原则的软件系统呢?答案是抽象,将可能发生变化的功能点进行抽象,并留出变化的接口。设计模式中很多模式都可以帮我们实现开闭原则,个人的理解设计模式是对抽象用法的一种总结。其实我们在项目已经为开闭原则做了一些工作了,比如说我们进行三层开发,将数据层抽象出来,并定义个数据处理的接口,我们可以通过新开发一个数据层把刚开始将数据存放到sql server中的实现,修改为将数据存放到my sql中的实现;我们将业务逻辑中的代码从UI代码中分离出来,这就为我们复用业务逻辑的代码提供了可能,我们可以开发一个专门为手机使用的UI层出来,当用户用手机访问我们的系统时,智能的切换到手机UI层的代码上去执行。

实现开闭原则的例子,其实我都不好意思自己举例子了,因为我正在使用office 2007写这篇博客,在Office2007的快捷工具栏中就有一项是加载项,就是说Office 2007能将插件加载进来使用,如下图所示:
 

Snagit在word中添加了一个插件,这种插件技术就是一种遵循OCP的实现;再说我们整天使用的Visual Studio 它的可扩展程度更高,可以开发很多类型的工具对他进行扩展。

为了本文的完整性,我还是厚着脸皮,用重构的方式举一个遵循开闭原则的微不足道的实现。
下面的举例实现的场景是个税的计算:我的第一个版本是这样子的

class Program
{
    static void Main(string[] args)
    {
        float salary = 10000;

        Console.WriteLine("收入是{0}的人应缴个税是{1},",salary, GetTax(salary));
    }

    static float GetTax(float salary)
    {
        return (float)(salary * 0.03);
    }
}


这个版本中我未做任何抽象,直接调用静态方法算了,可是一不小心开两会了,个税要调整了,于是个税的算法要进行调整了,怎么办呢,因为要少缴税,我很高兴的就要来重构上面的代码了,既然个税的计算方法是一个变化的东西,我就把它抽象出来吧。

class Program
{
    static void Main(string[] args)
    {
        float salary = 10000;

        Console.WriteLine("收入是{0}的人应缴个税是{1},",salary, GetTax(salary));
    }

    static float GetTax(float salary)
    {
        ITaxCalculateStrategy strategy = GetTaxCalculateStrategy();
        return strategy.GetTax(salary);        
    }

    /// <summary>
    /// 获得应该使用的个税计算方法
    /// </summary>
    /// <returns>个税计算方法实现实例</returns>
    static ITaxCalculateStrategy GetTaxCalculateStrategy() {
        string typeName = ConfigurationManager.AppSettings["TaxCalculateStrategyType"];
        if (string.IsNullOrEmpty(typeName))
            throw new ConfigurationErrorsException("请配置TaxCalculateStrategyType");

        Type type = Type.GetType(typeName);
        if (type == null) throw new ConfigurationErrorsException("TaxCalculateStrategyType错误");

        return (ITaxCalculateStrategy)Activator.CreateInstance(type);
    }
}

/// <summary>
/// 定义个税计算的接口
/// </summary>
public interface ITaxCalculateStrategy
{
    float GetTax(float salary);
}

/// <summary>
/// 两会前个税计算办法的实现
/// </summary>
public class TaxCalculateBefore2Conference : ITaxCalculateStrategy
{
    float ITaxCalculateStrategy.GetTax(float salary)
    {
        return (float)(salary * 0.03);
    }
}

/// <summary>
/// 两会后个税的计算方法
/// </summary>
public class TaxCalculateAfter2Conference:ITaxCalculateStrategy
{
    float ITaxCalculateStrategy.GetTax(float salary)
    {
        return (float)(salary * 0.020);
    }
}


因为要少缴税,所以我很愉快的重构了之前的代码,可以转眼两会开完了,结果并非如我预期的个税变化,咋办呢?没关系我们重新开发一个个税计算方法,修改下配置就可以仍旧使用之前的个税计算办法了。

开闭原则实现的关键点在于抽象,也许我们刚开始不知道该把那部分抽象出来,但是这并不是问题,我们可以遵循简单设计的原则,当变化来了的时候,再重构代码,做到一种满足开闭原则的设计。

切忌到处都抽象,如果到处都抽象就会导致系统过度设计,过度复杂。这反而是不利于系统的维护。完全的开闭原则是不可能实现的,所以请保持简单设计,在需要的时候做符合开闭原则的设计。

我的微博地址是:http://weibo.com/yukaizhao 我会把一些技术心得碎片写到微博中,欢迎关注。
posted @ 2010-06-29 09:23 玉开 阅读(2337) 评论(20) 编辑 收藏

 回复 引用 查看   
#1楼2010-06-29 09:46 | xming4321      
当变化来了的时候,再重构代码


是说我们只做满足现状的设计吗?


前期我们不知道会变的部分都不做抽象??

 回复 引用 查看   
#2楼[楼主]2010-06-29 09:50 | 玉开      
@xming4321
如果我们前期做设计的时候肯定某个地方肯定是一个变化点,就要对这个点进行抽象。

如果我们不知道或者不确认是否是个变化的点,那么我倾向于在变化来了的时候再做抽象,这样可以保持设计的简单性。

这需要权衡。

 回复 引用 查看   
#3楼2010-06-29 10:52 | 普若伽门      
@玉开
我也倾向这样的设计,不过往往在项目中众人意见不一,结果就是处处抽象,增加项目维护复杂性

 回复 引用 查看   
#4楼[楼主]2010-06-29 10:58 | 玉开      
引用普若伽门:
@玉开
我也倾向这样的设计,不过往往在项目中众人意见不一,结果就是处处抽象,增加项目维护复杂性


这种情况可以在团队中引入代码复查环节,这样大家可以相互讨论,去掉不必要的抽象。

 回复 引用 查看   
#5楼2010-06-29 11:16 | 徐少侠      
敏捷编程里面对于何时重构,以及避免过度设计有类似的描述

当的确不知道有变化的时候,就不要做抽象设计,硬编码也可以的么
而当出现变化的时候,运用模式进行重构。

这样能随需应变,又不至于早早的过度设计。
反正用户那里没这个需求,就别太热心去设计。



 回复 引用 查看   
#6楼[楼主]2010-06-29 11:20 | 玉开      
大家在做设计时会在意开闭原则吗?
 回复 引用 查看   
#7楼2010-06-29 11:59 | 麦舒      
引用玉开:
@xming4321
如果我们前期做设计的时候肯定某个地方肯定是一个变化点,就要对这个点进行抽象。

如果我们不知道或者不确认是否是个变化的点,那么我倾向于在变化来了的时候再做抽象,这样可以保持设计的简单性。

这需要权衡。

很赞同你这种看法。

 回复 引用 查看   
#8楼2010-06-29 12:07 | virus      
@玉开
要控制设计的度,不要设计过度,漫无边际,但是也要有所考虑,这需要经验来说明

 回复 引用 查看   
#9楼[楼主]2010-06-29 12:15 | 玉开      
@virus
对呀,做设计就是需要平衡,就像单一职责原则,如果绝对了,就成了一个类只包含一个方法了,如何划分职责也需要平衡。

 回复 引用 查看   
#10楼2010-06-29 13:09 | 诺贝尔      

一个良好的面向对象,自身就是符合开闭原则的。

所谓的设计到底是什么?用面向对象本身,已经是设计了。

 回复 引用 查看   
#11楼[楼主]2010-06-29 13:25 | 玉开      
@诺贝尔
你的回复和你的名字一样高深,我还没有达到这种高度,所以也没有明白,还望详细解释下。

 回复 引用 查看   
#12楼2010-06-29 21:18 | 诺贝尔      
@玉开
诺贝尔,发明炸药的那个人,这都不认识啊。

当然我只是碰巧同名,绝对不是故意模仿名人的。

你这个开闭原则,是什么?你要用代码的实现去认识,而不是基于理论的论述自我解释。

在我看来,一个良好设计的面向对象,就是开闭原则的最好实践。同时也是最基础最简洁的设计之一。

 回复 引用 查看   
#13楼2010-06-30 02:50 | 无待      
个人感觉,原则是众多实际经验的总结和提炼,但往往也是最难把握的,因为它不能解决具体的问题,就像中国菜谱中的“少许”二字,有点儿玄。在应用中,我们最难的还是找不到哪里需要开,哪里需要闭,这既和需求分析有关,更和设计经验有关。
 回复 引用 查看   
#14楼[楼主]2010-06-30 08:06 | 玉开      
@诺贝尔
文中代码的例子不可以说明开闭原则吗?

“在我看来,一个良好设计的面向对象,就是开闭原则的最好实践。同时也是最基础最简洁的设计之一。”这句话好笼统呀。




 回复 引用 查看   
#15楼[楼主]2010-06-30 08:07 | 玉开      
@无待
软件设计确实需要经验,学习;有了经验才能更好的把握分寸。

 回复 引用 查看   
#16楼2010-06-30 15:07 | 诺贝尔      
@玉开
你那个是模式,我说的是范式.

 回复 引用 查看   
#17楼2010-07-24 17:40 | 缪军      
一个很简单的事情,被弄得这么复杂,
从属性到方法,再到对象,如果开发者养成了
先声明,后实现的开发习惯和思维方式,
那么,自然而然程序骨架都是依赖抽象(或者说依赖设计概念)的,

其实这就是建模,
利用现成的或者自己开发的建模工具,
真正的软件设计人员可以不用关心编程语言,
就把所有者视图转换成设计者视图(也就是从概念模型到物理模型),
而建模工具可以自动生成所有的声明代码,
项目经理用建模工具就可以轻松地分割项目和派工,

而接下来的实现部分,才轮到代码开发工具上场,
当然,很多建模工具和代码开发工具是整合在一起的,
并且,很多开发工具还提供了很多实现代码模型,以提高coder的效率,
其实Coder可能不知道,那些模型也是继承自声明的

这恰恰让很多程序员产生了误解,他们以为代码就是那样拖拖弄弄搞出来的

 回复 引用 查看   
#18楼2010-07-24 17:52 | 缪军      
设计是遵从SOP的,所谓过度设计就是没有SOP,导致了不恰当的设计,
那楼主的GetTax方法举例:
如果这是个私有方法,那完全是class内部事务,
不存在抽象这个说法,该怎么写怎么写,项目组根本不会关心coder怎么实现这个方法的,只要class的公用接口被测试通过,就可以交差;

如果这是个公用方法,那肯定不是任由coder写上去的,
一定是事先声明的,
差别在于designer可能会一层层隔离和切割声明,
直到那个具体实现的位置,跟其他代码没有任何多余的耦合


 回复 引用 查看   
#19楼[楼主]2010-07-25 11:15 | 玉开      
@缪军
建模工具是不是银弹了?你是个设计理想主义者

 回复 引用 查看   
#20楼2010-07-25 21:39 | 缪军      
我研发的建模工具是以我们的业务领域和我们的技术条件为基础的,
很可能不适用别的团队,
越成熟的工业,建模工具的同质化就越严重,
由于软件工业的落后,所以目前各种建模工具五花八门,所适用的业务领域也很有限,所以看上去就是"没有银弹",
更何况,即便是传统工业,
成熟的工厂只购买最基础的生产工具,
高级的生产工具(比如生产线)基本上都是工厂自行研发的,
而那些落后的厂家要引进人家淘汰的生产工具,还要长期支付各种专利费用,

我所说的生产方式,是我所在的团队的开发方式,
并且跟我们有交流的好几个软件公司也具备这样的生产能力,
不仅如此,根据我们这些不起眼的团队的经验,
我觉得,稍微有些项目经验的团队都可以用2到3个月的工期,仅仅针对自己的业务领域设计出一个轻量级的建模和开发工具,
有了建模工具,你会发现,开发人员可以随时得到最新版的项目文档,
而且开发人员没有重复劳动

发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1767217 XvwwgOMcgYY=