软件设计原则
软件的特点
1.软件需求变更法则
软件不断变更法则:真实世界中使用的程序必须进行变更,否则它在环境中的作用就会越来越小.
2.增加一个功能特性的成本
(1)原有代码的理解成本
(2)设计修改成本(编码)
(3)测试成本
(4)发布成本
3.对软件需求变化的态度
通过提高团队的能力,设计的弹性,流程的灵活性来适应变化.
专注于团队和个人的技能和才华来拥抱变化而不是试图拒绝变化.
4.拙劣软件的表现
僵化性:设计难以改变。很难对系统进行改动,因为每个改动都会迫使系统其他部分的改动。如果单一的改动也会导致有依赖关系的模块连锁改动,那么设计就是僵化的,必须要改的模块越多,设计越僵化。
脆弱性:设计易遭到破坏。对系统的改动会导致系统中和改动概念上无关的许多部分出现问题。在进行一个改动时,程序就会可能出现问题,出现问题的地方和改动的地方常常没有概念上的关联,要修正这些问题又会引入新的问题。
牢固性:设计难以重用。很难解开系统纠结,使之成为一些其它系统中重用的组件。设计中包含了对其它系统有用的部分,但是把这些部分从系统中分离出来需的努力和风险是具大的。
粘滞性: 难以做正确的事情。做正确的事情比做错误的事件要困难。软件粘滞性:当面临一个改动时,开发人员会有多种改动方法,其中一些方法会保持设计,而另一些方法会破坏设计,当保持设计比破坏设计更难应用时,就说明设计具有很高的粘滞性。环境粘滞性:开发环境迟钝低效产生,比如编译很慢,就会引诱开发人员去做不会导致大规模重新编译的改动。
不必要的复杂性:过分设计。设计中包含有不具任何好处的基础数据结构。如果设计中包含了当前没有用的组成部分,它就含有不必要有复杂性。开发人员预测可能的需求变化,从而放置了潜在变化的代码,以保持灵活性,看起来是好事,但结果常常正好相反,致使设计中含有不绝不用到的结构,使软件变得复杂,难于理解。
不必要的重复:设计中包含重复的结构,而该结构本可以使用单一的抽象体进行统一。当同样的代码以稍微不同的形式反复出现时,就表示开发人员忽略了抽象。当系统中有重复的代码时,对系统的改动会变得困难,重复的错误必须在每个重复体中一一修正,但是重复的代码之间有细微的差别,修正的方式也不总是相同。
晦涩性:很难阅读理解,没有很好的表现意图。代码随着时间的变化,会变得越来越晦涩,为了使代码晦涩性保持最低,就需要持续的保持代码清晰并具有表现力。当开发人员编写代码时,代码对于他们来说也许是清晰的,这是由于他们使自己专注于代码的编写,并且他们对代码非常熟悉,当熟悉退化后,他们或许回过头来再去看那个模块,他们会想怎么会有如些糟糕的代码,为了防止这种情况,开发人员必须站在阅读者的角度,对代码进行重构。
5.优秀设计的目标
可扩展性:新的功能可以很容易的加入到系统之中
灵活性:可以容许代码平稳修改,而不会波及到很多其它的模块
可插入性:可以很容易的将一个类抽出去,同时将一个有同样接口的类加入进来
6.优秀设计
(1)发现变化/封装变化
(2)隔离变化
(3)动态绑定
(4)解耦具体实现
设计原则
1.单一职责原则(变化分离)
就一类而言,应该有且只有引起它变化的原因.
如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责.
2.里氏替换原则(解释如何进行继承)
基类可以出现的地方都可以用子类进行替换,而不会引起任何不适应的问题.
里氏替换原则是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为.
里氏代换原则是对“开-闭”原则的补充.实现“开-闭”原则的关键步骤就是抽象化.
覆盖或实现父类的方法时,子类返回值类型可以是父类返回值类型的子类.
3.依赖倒置原则(针对接口编程)
高层模块不应该依赖于低层模块,他们都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象.
(低层模块,不可分隔的原子逻辑即为低层模块;高层模块,原子逻辑的再组装形成高层模块)
针对接口编程,依赖于抽象而不依赖于具体.降低客户与实现模块间的耦合.
4.接口隔离原则(恰当的划分角色和接口)
客户端不应该依赖于它不需要的接口;类间的依赖关系应该建立在最小的接口上.
因此,使用多个隔离接口比使用单个接口好.
一个接口代表一个角色,不应当将不同的角色都交给一个接口.
没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染.
不应该强迫客户依赖于它们不用的方法.
接口属于客户,不属于它所在的类层次结构.
5.迪米特法则(最小知道原则)
一个对象应该对其它对象有最小的了解.
一个软件实体应尽可能少的与其它实体发生相互作用,使得系统模块功能相对独立.
每一个软件单位对其它的单位都只有最少了解,而且局限于那些与本单位密切相关的软件单位.
类成员函数应该只调用以下对象:
对象自身,函数参数,成员对象,成员集合中的元素,函数中创建的对象.
6.开闭原则(可变性封装)
软件组成实体(类,模块,函数等)应该是可扩展的,但是是不可修改的.即软件实体可以通过增加新代码实现扩展,但是不能修改已有的代码.
任何系统在其生命周期中都会发生变化,如果我们希望开发出的系统不会在第一版发布后就被抛弃,就必须牢记这一点.
把会变化的部分抽取并封装起来,以便以后可以轻易地改动和扩充,而不影响不需要变化的其它部分.
禁忌:
有些时候使用多态设计形成的变化点在实际中不一定会发生或从未发生过,因此这种付出是不必要的.
在投入灵活的设计前,一定要现实地对待可变性的真实可能性.不要过度设计!
设计原则实践运用
1发现变化/封装变化
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起.
2抽象稳定接口,针对接口
抽象接口:创建出固定却能描述一组任意个可能行为的抽象体,这个抽象体就像抽象基类或接口,任意个可能的行为就是可能的派生类.
针对接口(抽象)设计:由于子模块间依赖于一个固定的抽象,所以它对于更改可以是关闭的.
3合成复用原则
尽量使用动态绑定(组合/聚合),而不是继承关系达到复用的目的.
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过向这些对象的委派达到复用已有功能的目的.
4静态绑定与动态绑定
静态绑定:代码结构在编译时刻就被确定下来了,比如由继承关系固定的类组成,或者硬编码实现等.
动态绑定:程序运行时刻的结构是由快速变化的对象实现,编译时两个结构是彼此独立的.
5创建与使用分离
对象要么构造对象,要么使用对象,而不应该兼而有之.如果遵守这一约束,就能加强内聚和降低耦合.
如果你吃了一只鸡蛋觉得味道不错,难道一定要见到那只母鸡?
6针对接口编程
(1)每个类尽量都有接口或抽象类,或两者兼有.这是依赖倒置原则的基本要求,有了抽象才能依赖倒置.
(2)变量的表面类型尽量是接口或抽象类.
(3)任何类都不应该从具体类派生.
(4)尽量不重写基类的方法.
(5)接口负责public属性和方法,并且声明与其它对象之间的依赖关系;抽象类负责公共构造部分的实现;实现类准确实现业务逻辑.
7告诉而不是询问
应该尽量告诉对象去做的事情,而不要询问它们的状态之后做出决定,最后才告诉它们做什么事情.对象拥有了数据,就应该让它们自己去完成.
8过程式设计与面向对象设计
过程式程序获取信息然后决策,面向对象则告诉对象做某事件.
过程式设计(使用数据结构)便于在不改动既有数据结构的前提下添加新函数.因此,过程化代码难以添加新的数据结构,因为必须修改所有函数.
面向对象设计便于在不改动既有函数的前提下添加新类.因此,难以添加新函数,因为必须修改类.
在复杂系统中,需要添加新数据类型而不是新函数的时候,适合面向对象;需要添加新函数而不是新类型的时候,适合过程代码和数据结构结合的方式.