[设计模式]之六大设计原则

RandellAlbe 8年前
   <p>就一个类而言,应该仅有一个引起它变化的原因。</p>    <p>假设现在要在iPhone上做一个图片编辑工具。功能有裁剪图片,旋转图片,缩放移动照片等等。</p>    <p>呐,我们可以写一个功能集类,然后把这些所有操作视为功能集的一部分,把代码全部写进这个类里面。</p>    <p>这么看来似乎可以,因为这是作为一个单独的模块嘛,把相关功能写进一个工具类里,用哪个功能调用哪个函数就好了。但这带来了一个问题就是这个工具类包含过多功能显得非常臃肿,不容易维护。而且在一个类里往往容易出现几个函数共用一个全局变量的情况,功能之间耦合度太大,难以复用。</p>    <p>举个最直接的例子:如果我想把这个功能移植到Android上去怎么办。这个移植过程麻烦之处并不在于语言语法变化,而是两个系统有着完全不同的手势传递机制,我要用手旋转,缩放图片这段代码完全没法复用,唯一能用的裁剪代码,也可能因为和其他代码耦合过大导致需要重新修改,退一步说,裁剪算法就算没有耦合,代码可以直接用,但关系到手势的代码对我来说都成为冗余代码,这对于代码复用就是灾难。</p>    <p>如果一个类承担的职责过多,就等于把这些职责偶合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。</p>    <p>所以这里在设计的时候,就要考虑一下把这些功能分类。比如裁剪功能需要知道裁剪框大小,位置。那就分离出一个类,专门负责计算裁剪框四个点的坐标变化。旋转缩放图片需要知道图片的大小,缩放率,显示方向等信息,那就再分离出一个类,负责计算图片形态的变化。最后剩下手势再封装一个类,处理手势的逻辑,在不同情况下获取不同的手势数据,作为参数交给上面两个算法类进行计算输出。</p>    <p>这样一来,每个类的职责就变得单一了,维护就容易多了。后面再移植代码的话,算法类只需要切换语法,手势类只要去重写触发手势的条件,而不必修改逻辑。代码很快就可以改好,并且不会破坏原有的项目结构。</p>    <p>软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。判断是否要分离出类的方法就是,如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责,就应该考虑类的职责分离。</p>    <p>优点</p>    <ul>     <li>可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多</li>     <li>提高类的可读性,提高系统的可维护性</li>     <li>变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响</li>    </ul>    <h3>里氏替换原则 Liskov Substitution Principle - LSP</h3>    <p>子类型必须能够替换掉他们的父类型</p>    <p>通俗的讲,一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。即,在软件里,把父类都替换成它的子类,程序的行为没有变化。</p>    <p>里氏替换原则的重点在不影响原功能,而不是不覆盖原方法。</p>    <p>所以正常遵从该原则的处理办法是在需要覆盖父类方法时应该首先考虑使用super调用父类的同名方法以保证父类同名方法会被调用。</p>    <p>如果确实不需要调用父类方法,则不加此语句。</p>    <p>这个原则很重要,编码时要注意。</p>    <h3>依赖倒置原则 Dependence Inversion Principle - DIP</h3>    <p>抽象不应该依赖细节,细节应该依赖抽象</p>    <p>通俗的说,就是要 <strong>针对接口编程,不要对实现编程</strong> 。呐,比如说电脑主板,CPU,内存,硬盘这些硬件的设计就是依赖接口设计的。单拿CPU来说,CPU有各种厂家设计的各种型号,这些型号的内部设计实现都不相同,但他们的接口是一样的,这样主板就可以随意更换CPU了。</p>    <p>关于倒置,比如说我有一个高层模块,模块实现对SQLite读写的功能依赖一个控制访问SQLite的低层模块。一旦我要求把SQLite改为MySQL,那这个低层模块就无法正常工作,进而倒置上层模块也无法正常工作。依赖倒置就是说设计代码不再是上层依赖下层,而是两层都去依赖接口去实现,这样两层的运行状态便不会互相影响。</p>    <p>依赖倒转其实可以说是面向对象设计的标志,用哪种语言来编写程序不重要,如果编写时考虑的都是如何针对抽象编程而不是针对细节编程,即程序中所有的依赖关系都是终止于抽象类或者接口,那就是面向对象的设计,反之那就是过程化的设计了。</p>    <p>依赖倒置原则的实现可以参考策略模式: <a href="http://www.open-open.com/lib/view/open1463406425399.html">设计模式之二:策略模式</a></p>    <p>例子中的收取现金的不同方式可以看做CPU的不同型号。调用收现金的方法可看做主板插上不同型号的CPU。就是这么个思想。</p>    <p>遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。</p>    <p>根据该原则,编程中要注意</p>    <ul>     <li>低层模块尽量都要有抽象类或接口,或者两者都有</li>     <li>变量的声明类型尽量是抽象类或接口</li>     <li>使用继承时遵循里氏替换原则</li>    </ul>    <h3>接口隔离原则 Interface Segregation Principle - ISP</h3>    <p>客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上</p>    <p>看图,图一是未遵循该原则的结构:</p>    <p><img src="https://simg.open-open.com/show/fbf229ea13832f7ef8350bce8defdf3b.jpg"> <img src="https://simg.open-open.com/show/3537a06721b109ad2b2c312b0567d348.jpg"></p>    <p>接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。</p>    <p>接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。</p>    <p>注意事项</p>    <ul>     <li>接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度</li>     <li>为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系</li>     <li>提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情</li>    </ul>    <h3>迪米特法则 Law Of Demeter - LOD</h3>    <p>一个对象应该对其他对象保持最少的了解。</p>    <p>如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法,可以通过第三者转发这个调用。</p>    <p>迪米特法则首先强调的前提是在 <strong>类的结构设计上,每一个类应当尽量降低成员的访问权限</strong> ,也就是要降低类之间的耦合。类之间的耦合越弱,越有利于复用,修改类相互之间的影响也会降到最低。</p>    <p>迪米特法则还有一个更简单的定义:只与 <strong>直接的朋友</strong> 通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。</p>    <p>迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。</p>    <h3>开闭原则 Open Close Principle - OCP</h3>    <p>软件实体(类,模块,函数等)应该可以拓展,但是不可修改</p>    <p>这个原则有两点:</p>    <ul>     <li>对于拓展是开放的 Open for extension</li>     <li>对于更改是封闭的 Closed for modification</li>    </ul>    <p>在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。所以当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。</p>    <p>但在设计软件的时候,无论模块是多么的封闭,都会存在无法对之封闭的变化,因为你不可能在编码前就考虑到所有情况。所以在设计代码时就必须先猜测出最可能发生变化的种类,然后构造抽象来隔离变化。在编码之后,一旦遇到发生变化的地方,那就应该首先考虑要不要对这里进行结构的修改。也就是 <strong>遇到变化发生时要立即采取行动</strong> 。</p>    <p>比如现在在客户端类中写了一个加法程序,后来说要增加减法,那么这时就应该立即抽象出来一个运算类。虽然说直接在客户端增加减法算法很快,但考虑到以后也许会拓展更多的算法,而且代码改得越晚修改代码的范围就越大。立即修改代码结构的代价似乎比以后去改的代价要小很多。</p>    <p>我们希望的是在开发工作展开不久就知道可能发生的变化。查明可能发生的变化所等待的时间越长,要创建正确的抽象就越困难开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护,可拓展,可复用,灵活性好。开发人员应该对程序中呈现出频繁变化的那些部分作出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。</p>    <p>参考</p>    <p><a href="/misc/goto?guid=4959673181080134544" rel="nofollow,noindex">http://blog.csdn.net/zhengzhb/article/category/926691/</a></p>    <p>via: http://www.wossoneri.com/2016/05/16/[Design-Pattern]Principles/</p>