本帖最后由 LiuKang 于 2013-11-18 20:30 编辑
定义:软件实体(类、模块以及功能等)对扩展是开放的,对修改是封闭的。"一个对象一旦被定义好,并公开给其他对象调用,我们就不能轻易地修改它。
优点:
通过扩展已有软件系统,可以提供新的行为,以满足对软件的新的需求,使变化中的软件有一定的适应性和灵活性。
已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。
例子:玉帝招安美猴王
当年大闹天宫便是美猴王对玉帝的新挑战。美猴王说:"'皇帝轮流做,明年到我家。'只教他搬出去,将天宫让于我!"对于这项挑战,太白金星给玉皇大帝提出的建议是:"降一道招安圣旨,宣上界来…,一则不劳师动众,二则收仙有道也。"
换而言之,不劳师动众、不破坏天规便是"闭",收仙有道便是"开"。招安之道便是玉帝天庭的"开放-封闭"原则。
招安之法的关键便是不允许更改现有的天庭秩序,但允许将妖猴纳入现有秩序中,从而扩展了这一秩序。用面向对象的语言来讲,不允许更改的是系统的抽象层,而允许更改的是系统的实现层。
这里所谓的"修改",可以分为两个层次来分析。一个层次是对抽象定义的修改,如对象公开的接口,包括方法的名称、参数与返回类型。我们必须保证一个接口,尤其要保证被其他对象调用的接口的稳定;否则,就会导致修改蔓延,牵一发而动全身。从某种程度上讲,接口就是标准,要保障接口的稳定,就应该对对象进行合理的封装。一般的设计原则之所以强调方法参数尽量避免基本类型,原因正在于此。比较如下两个方法定义:
//定义1
bool Connect(string userName, string password, string ftpAddress, int port);
//定义2
bool Connect(Account account);
public class Account
{
public string UserName { get; set; }
public string Password { get; set; }
public string FtpAddress { get; set; }
public string int Port { get; set; }
}
相比较前者,后者虽然多了一个Account类的定义,但Connect()方法却明显更加稳定。倘若需要为Connect()方法提供一个Ftp服务器的主目录名,定义1必须修改该方法的接口,对应的,所有调用Connect()方法的对象都会受到影响;而定义2只需要修改Account类,由于Connect()方法的接口保持不变,只要Connect()方法的调用者并不需要主目录名,这样的修改就完全不会影响调用者。即使需要主目录名,我们也可以在Account类的构造函数中为主目录名提供默认的实现,从而降低需求变化带来的影响。我认为,这样的设计对修改就是封闭的。
另一个层次是指对具体实现的修改。"对修改封闭"是开放封闭原则的两个要素之一。原则上,要做到避免对源代码的修改,即使仅修改具体实现,也需要慎之又慎。这是因为具体实现的修改,可能会给调用者带来意想不到的结果,这一结果并非我们预期的,甚至可能与预期相反。如果确实需要修改具体的实现,就需要做好达到测试覆盖率要求的单元测试。根据我的经验,设计要做到完全对修改封闭,几乎是不可能完成的任务。我们只能尽量将代码修改的影响降到最低,其核心指导原则就是封装与充分的测试。
"对扩展开放"的关键是"抽象",而对象的多态则保证了这种扩展的开放性。开放原则首先意味着我们可以自由地增加功能,而不会影响原有系统。这就要求我们能够通过继承完成功能的扩展。其次,开放原则还意味着实现是可替换的。只有利用抽象,才可以为定义提供不同的实现,然后根据不同的需求实例化不同的实现子类。例如排序算法的调用,对照图2-5与图2-6之间的区别。
图2-5 违背开放封闭原则
图2-6 遵循开放封闭原则
图2-5的设计无法支持排序算法的扩展,因为Client直接调用了冒泡排序算法实现的BubbleSort类,一旦要求支持快速排序算法,就束手无策了。图2-6由于引入了排序算法的共同抽象ISortable接口,只要排序算法实现了该接口,就可以被Client调用。
开放封闭原则还可以统一起来理解。由于我们对扩展实现了开放,才能够保证对修改是封闭的。开放利用了对象的抽象,封闭则在一定程度上利用了封装。最佳的做法仍然是要做到分离对象的变与不变,将对象不变的部分封装起来,并遵循良好的设计原则以保障接口的稳定;至于对象中可能变的部分,则需要进行抽象,以建立松散的耦合关系。实际上,开放封闭原则也体现了依赖倒置原则的思想。 |
|