面向对象的程序设计原则之SOLID原则

Ò程序设计领域, SOLID (单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特•C•马丁在21世纪早期 引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软件维护和扩展的系统变得更加可能SOLID被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。

S 单一功能原则 :单一功能原则 认为对象应该仅具有一种单一功能的概念。

O 开闭原则 :开闭原则 认为“软件体应该是对于扩展开放的,但是对于修改封闭的”的概念。

L 里氏替换原则 :里氏替换原则 认为“程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的”的概念。

I 接口隔离原则 :接口隔离原则 认为“多个特定客户端接口要好于一个宽泛用途的接口”的概念。

I 接口隔离原则 :接口隔离原则 认为“多个特定客户端接口要好于一个宽泛用途的接口”的概念。

D 依赖反转原则: 依赖反转原则 认为一个方法应该遵从“依赖于抽象而不是一个实例” 的概念。依赖注入是该原则的一种实现方式。

单一功能原则的讨论

当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是让一个类只做一种类型责任,当这个类需要承担其他类型的责任的时候,就需要分解这个类。在所有的SOLID原则中,这是大多数开发人员感到最能完全理解的一条。严格来说,这也可能是违反最频繁的一条原则了。
单一责任原则可以看作是低耦合、高内聚在面向对象原则上的引申,将责任定义为引起变化的原因,以提高内聚性来减少引起变化的原因。责任过多,可能引 起它变化的原因就越多,这将导致责任依赖,相互之间就产生影响,从而极大的损伤其内聚性和耦合度。单一责任,通常意味着单一的功能,因此不要为一个模块实 现过多的功能点,以保证实体只有一个引起它变化的原因。

本原则的应用:

(1) Unix/Linux是这一原则的完美体现者。各个程序都独立负责一个单一的事。

(2) Windows是这一原则的反面示例。几乎所有的程序都交织耦合在一起。

(3) 另外举个例子:报表的内容和报表的格式都会变化改变,但是这两种变化的性质不同,一个是实质内在,一个是表面上的,SRP认为这是问题的两个方面, 其实代表不同的责任,应该将它们分离放入不同的类或模块中,而不应该放在一起,否则的话,因为不同原因发生变化,导致对方变动,比如报表格式变新的样式, 这个变化是不应该涉及到内容的。

开闭原则的讨论

(1) 软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。
(2) 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。

(3) 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

(4) 可以使用变化和不变来说明:封装不变部分,开放变化部分,一般使用接口继承实现方式来实现“开放”应对变化,说大白话就是:你不是要变化吗?,那么我就让你继承实现一个对象,用一个接口来抽象你的职责,你变化越多,继承实现的子类就越多。

(5) OCP的要点在于:作为开发人员,别人偶尔会向我们提供基类,偶尔也会为其他开发人员生成基类框架,供其使用。这些使用者应当仅能使用这些基类,但 不能对其进行修改。这一点是必要的,因为其他使用者也可能依赖于由基类提供的功能。如果允许使用者修改这些基类,可能会导致连锁反应,不仅会影响到应用程 序中的各方面,还会影响到企业内的应用程序。还有一个问题,使用者有时可能会收到基类的升级版本。使用者在升级之前,必须找出一种方法用来处理其对该基类 先前版本中所做的自定义。

于是,问题变为:"那么,如果我需要修改这个基类的工作方式,那应当怎么做呢?"OCP的另一部分中给出这一答案;基类应当开放,可进行扩充。在这里,扩充是指创建一个由此基类继承而来的派生类,它可以扩充或重载基类功能,以提供使用者所需要的特定功能。这样,使用者就能使用类的修改版本,而不会影响到类的其他使用者。使用者还可以在将来更轻松地使用基类的升级版本,因为他们不用担心丢失自己的修改内容。

本原则的应用:

对于面向对象来说,需要你依赖抽象,而不是实现,23个经典设计模式中的“策略模式”就是这个实现。对于非面向对象编程,一些API需要你传入一个你可以扩展的函数,比如我们的C语言的qsort()允许你提供一个“比较器”,STL中的容器类的内存分配,ACE中的多线程的各种锁。对于软件方面,浏览器的各种插件属于这个原则的实践。

里氏替换原则的讨论

(1) 软件工程大师Robert C. Martin把里氏替换原则最终简化为一句话:“Subtypes must be substitutable for their base types”。也就是,子类必须能够替换成它们的基类。即:子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。另外,不应该 在代码中出现if/else之类对子类类型进行判断的条件。里氏替换原则LSP是使代码符合开闭原则的一个重要保证。正是由于子类型的可替换性才使得父类 型的模块在无需修改的情况下就可以扩展。在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们一个判断和设计类之间关系的基准:需不需 要继承,以及怎样设计继承关系。

(2) 当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。继承对于OCP,就相当于多态性对于里氏替换原则。子类可以代替基类,客户使用基类,他们不需要知道派生类所做的事情。这是一个针对行为职责可替代的原则,如果S是T的子类型,那么S对象就应该在不改变任何抽象属性情况下替换所有T对象。

接口隔离原则

(1) 不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。注意:在代码中应用ISP并不一定意味着服务就是绝对安全的。仍然需要采用良好的编码实践,以确保正确的验证与授权。

(2) 这个原则起源于施乐公司,他们需要建立了一个新的打印机系统,可以执行诸如装订的印刷品一套,传真多种任务。此系统软件创建从底层开始编制,并实现了这些 任务功能,但是不断增长的软件功能却使软件本身越来越难适应变化和维护。每一次改变,即使是最小的变化,有人可能需要近一个小时的重新编译和重新部署。这 是几乎不可能再继续发展,所以他们聘请罗伯特Robert帮助他们。他们首先设计了一个主要类Job,几乎能够用于实现所有任务功能。只要调用Job类的 一个方法就可以实现一个功能,Job类就变动非常大,是一个胖模型啊,对于客户端如果只需要一个打印功能,但是其他无关打印的方法功能也和其耦合,ISP 原则建议在客户端和Job类之间增加一个接口层,对于不同功能有不同接口,比如打印功能就是Print接口,然后将大的Job类切分为继承不同接口的子 类,这样有一个Print Job类,等等。

依赖反转原则

(1) 依赖倒置原则(Dependency Inversion Principle,DIP)规定:代码应当取决于抽象概念,而不是具体实现。
(2) 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
(3) 抽象不应该依赖于细节,细节应该依赖于抽象
(4) 类可能依赖于其他类来执行其工作。但是,它们不应当依赖于该类的特定具体实现,而应当是它的抽象。这个原则实在是太重要了,社会的分工化,标准化都 是这个设计原则的体现。显然,这一概念会大大提高系统的灵活性。如果类只关心它们用于支持特定契约而不是特定类型的组件,就可以快速而轻松地修改这些低级 服务的功能,同时最大限度地降低对系统其余部分的影响。

本原则的应用:

举个例子,墙面的开关不应该依赖于电灯的开关实现,而是应该依赖于一个抽象的开关的标准接口,这样,当我们扩展程序的时候,我们的开关同样可以控制 其它不同的灯,甚至不同的电器。也就是说,电灯和其它电器继承并实现我们的标准开关接口,而我们的开关产商就可不需要关于其要控制什么样的设备,只需要关 心那个标准的开关标准。这就是依赖倒置原则。这就好像浏览器并不依赖于后面的web服务器,其只依赖于HTTP协议。

要获取更多Jerry的原创文章,请关注公众号"汪子熙":

发布了6767 篇原创文章 · 获赞 646 · 访问量 112万+

猜你喜欢

转载自blog.csdn.net/i042416/article/details/104514127