什么是设计原则?
设计原则是基本的工具,应用这些规则可以使你的代码更加灵活、更容易维护,更容易扩展。
基本原则
封装变化
面向接口编程而不是实现
优先使用组合而非继承
SRP: The single responsibility principle 单一职责
系统中的每一个对象都应该只有一个单独的职责,而所有对象所关注的就是自身职责的完成。只能让一个类有且仅有一个职责。
每一个职责都是一个设计的变因,需求变化的时候,需求变化反映为类职责的变化。当你系统里面的对象都只有一个变化的原因的时候,你就已经很好的遵循了SRP原则。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起。一个职责的变化就可能削弱或者抑制这个类其它职责的能力。这种设计会导致脆弱的设计。当变化发生的时候,设计会遭到意想不到的破坏。
SRP 让这个系统更容易管理维护,因为不是所有的问题都搅在一起。
内聚Cohesion 其实是SRP原则的另外一个名字.你写了高内聚的软件其实就是说你很好的应用了SRP原则。
怎么判断一个职责是不是一个对象的呢?你试着让这个对象自己来完成这个职责,比如:“书自己阅读内容”,阅读的职责显然不是书自己的。
仅当变化发生时,变化的轴线才具有实际的意义,如果没有征兆,那么应用SRP或者任何其它的原则都是不明智的。
DRY : Don't repeat yourself Principle
通过抽取公共部分放置在一个地方避免代码重复.
DRY 很简单,但却是确保我们代码容易维护和复用的关键。
你尽力避免重复代码候实际上在做一件什么事情呢?是在确保每一个需求和功能在你的系统中只实现一次,否则就存在浪费!系统用例不存在交集,所以我们的代码更不应该重复,从这个角度看DRY可就不只是在说代码了。
DRY 关注的是系统内的信息和行为都放在一个单一的,明显的位置。就像你可以猜到正则表达式在.net中的位置一样,因为合理所以可以猜到。
DRY 原则:如何对系统职能进行良好的分割!职责清晰的界限一定程度上保证了代码的单一性。
OCP : Open-Close Principle开闭原则
类应该对修改关闭,对扩展打开。一个软件的实体应当对扩展开放,对修改关闭。
我的理解是,对于一个已有的软件,如果需要扩展,应当在不需修改已有代码的基础上进行。
OCP 关注的是灵活性,改动是通过增加代码进行的,而不是改动现有的代码;
OCP的应用限定在可能会发生的变化上,通过创建抽象来隔离以后发生的同类变化
OCP原则传递出来这样一个思想:一旦你写出来了可以工作的代码,就要努力保证这段代码一直可以工作。这可以说是一个底线。稍微提高一点要求,一旦我们的代码质量到了一个水平,我们要尽最大努力保证代码质量不回退。这样的要求使我们面对一个问题的时候不会使用凑活的方法来解决,或者说是放任自流的方式来解决一个问题;比如代码添加了无数对特定数据的处理,特化的代码越来越多,代码意图开始含混不清,开始退化。
OCP 背后的机制:封装和抽象;封闭是建立在抽象基础上的,使用抽象获得显示的封闭;继承是OCP最简单的例子。除了子类化和方法重载我们还有一些更优雅的方法来实现比如组合;
怎样在不改变源代码(关闭修改)的情况下更改它的行为呢?答案就是抽象,OCP背后的机制就是抽象和多态
没有一个可以适应所有情况的贴切的模型!一定会有变化,不可能完全封闭.对程序中的每一个部分都肆意的抽象不是一个好主意,正确的做法是开发人员仅仅对频繁变化的部分做出抽象。拒绝不成熟的抽象和抽象本身一样重要。
OCP是OOD很多说法的核心,如果这个原则有效应用,我们就可以获更强的可维护性可重用灵活性健壮性 LSP是OCP成为可能的主要原则之一
LSP: The Liskov substitution principle
子类必须能够替换基类。所有引用基类的地方必须能透明地使用其子类的对象。
LSP关注的是怎样良好的使用继承.
必须要清楚是使用一个Method还是要扩展它,但是绝对不是改变它。
LSP清晰的指出,OOD的IS-A关系是就行为方式而言,行为方式是可以进行合理假设的,是客户程序所依赖的。
LSP让我们得出一个重要的结论:一个模型如果孤立的看,并不具有真正意义的有效性。模型的有效性只能通过它的客户程序来表现。必须根据设计的使用者做出的合理假设来审视它。而假设是难以预测的,直到设计臭味出现的时候才处理它们。
对于LSP的违反也潜在的违反了OCP
DIP:依赖倒置原则
高层模块不应该依赖于底层模块二者都应该依赖于抽象。要针对接口编程,不要针对实现编程。
我的理解是,对于不同层次的编程,高层次暴露给低层次的应当只是接口,而不是它的具体类。
A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. 抽象不应该依赖于细节,细节应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象
什么是高层模块?高层模块包含了应用程序中重要的策略选择和业务模型。这些高层模块使其所在的应用程序区别于其它。
如果高层模块依赖于底层模块,那么在不同的上下文中重用高层模块就会变得十分困难。然而,如果高层模块独立于底层模块,那么高层模块就可以非常容易的被重用。该原则就是框架设计的核心原则。
这里的倒置不仅仅是依赖关系的倒置也是接口所有权的倒置。应用了DIP我们会发现往往是客户拥有抽象的接口,而服务者从这些抽象接口派生。
这就是著名的Hollywood原则:"Don't call us we'll call you."底层模块实现了在高层模块声明并被高层模块调用的接口。
通过倒置我们创建了更灵活更持久更容易改变的结构
DIP的简单的启发规则:依赖于抽象;这是一个简单的陈述,该规则建议不应该依赖于具体的类,也就是说程序汇总所有的依赖都应该种植于抽象类或者接口。
如果一个类很稳定,那么依赖于它不会造成伤害。然而我们自己的具体类大多是不稳定的,通过把他们隐藏在抽象接口后面可以隔离不稳定性。
依赖倒置可以应用于任何存在一个类向另一个类发送消息的地方
依赖倒置原则是实现许多面向对象技术多宣称的好处的基本底层机制,是面向对象的标志所在。
ISP:接口隔离原则
不应该强迫客户程序依赖它们不需要的使用的方法。接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。
接口不是高内聚的,一个接口可以分成N组方法,那么这个接口就需要使用ISP处理一下。
接口的划分是由使用它的客户程序决定的,客户程序是分离的接口也应该是分离的。
一个接口中包含太多行为时候,导致它们的客户程序之间产生不正常的依赖关系,我们要做的就是分离接口,实现解耦。
应用了ISP之后,客户程序看到的是多个内聚的接口。
怎样在实践中遵守这些原则?
使用三种视角思考问题就是答案之一。
本文内容包括:
1.为什么我们过早的纠缠于细节?问题的本质是什么?
2.救命稻草--Martin Fowler的三层视角理论
3.三层视角--回头再说OO设计原则
为什么我们过早的纠缠于细节?问题的本质是什么?
做设计时过早的关注细节几乎是多数程序员的泥沼,也是我自己的顽疾。就像我刚开始工作不久要做一个自动更新的系统,设计会议上开发组老大定了使用FTP协议完成,你知道我脑袋里面想的是什么?--“Indy组件好像不支持中文”…...
过早的关注细节,大体上可能有两种原因:1.经验丰富,举一反三,纲举目张,各种技术玄妙如数家珍 2.没有什么经验,只知道点技术细节,难以跳出思维桎梏;我知道我是后者,以前是现在也是。
人是有惰性的,人们习惯性的做自己熟练精通的事情。所以做设计的时候,当对大框架缺少把握能力的时候,潜意识里我更愿意去思考那些细节。这是在偷懒,所有的技术细节、问题都是没有疑问的(我们不是做科研),或者是有疑问你可以很容易获得解答无论是开发文档还是在社区。细节的解决方案总是显而易见,但并非肯定是最好的入手点。
有时候我真的要做设计了,我想要避免陷入“细节泥沼”可是我还是无意中把细节扯进来,这是为什么?剖析自己,我知道这是因为我的思考是平铺的,是没有层次的,所有的问题搅在一起,做设计的时候难免拖泥带水,泥沙俱下。
思考没有层次这就是问题本质所在,我要做好设计,而思维方式上的缺陷成为我的命门所在,我该怎么办?说实话我一直在走弯路,而且不知道现在的这条路是否对头。
“善良的人在追求的中纵然迷茫,却终将意识到有一条正途.”-----《浮士德》
救命稻草--Martin Fowler的三层视角理论
Martin Fowler在他的著作《UML Distilled》中提到了三层视角(perspective):概念视角,规约视角,实现视角。
使用三种视角看软件开发,我们可以得到这样的描述:
概念视角:呈现所研究领域中的各种概念,得出概念模型的时候应该尽量少德或者不考虑它的实现,这个视角要回答的问题是:软件要负责什么?是策略性的结论
规约视角:我们现在考虑的是软件,但是我们关注的是软件的接口而不是实现。规约视角要回答的问题是:怎么使用软件?这个层次关注的是软件各部分的交流。
实现:这时我们考虑的是代码本身但是许多方面我们使用规约视角可能会更好,软件在规约层交流在实现层执行。
视角帮助我们将问题划分层次,隔离
从上面的描述我们很明显得看到“软件开发”所设计牵扯的问题已经被划分到三个不同的层次,在每一个层次我们都要有特定的思考成果。在高层没有思考成熟的时候我们不往下一个层次进行,按照这样一个原则,细节被隔离在思维的围墙之外。