我们知道对于很多数学问题,经常会有多种不同的解法
而且这其中可能会有一种比较通用简便高效的方法
我们在遇到类似的问题或者同一性质的问题时,也往往采用这一种通用的解法
将话题转移到程序设计中来
对于软件开发人员, 在软件开发过程中, 面临的一般问题的解决方案就是设计模式(准确的说是OOP中)
当然,如同数学的解题思路一样,设计模式并不是公式一样的存在
设计模式(Design pattern)代表了最佳的实践
是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的宝贵经验
是解决问题的思路
总之,设计模式是一种思想,思想,思想。
起源
随着面向对象编程语言的发展,以及软件开发规模的不断扩大
编写良好的OOP程序变得困难,而编写可复用的OOP程序则更是困难
在 1994 年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides
四人合著出版了一本名为 Design Patterns - Elements of Reusable Object-Oriented Software(中文译名:设计模式 - 可复用面向对象软件的基础) 的书
该书首次提到了软件开发中设计模式的概念。
四位作者合称 GOF(四人帮,全拼 Gang of Four)
这就是设计模式四个字的起源
当然,即使在这本书出版之前,肯定也已经有很多有经验的OOP程序员已经在使用自己的经验(设计模式)了
但是这本书将OOP的设计经验作为设计模式记录下来
使我们能够更加简单方便的复用成功的设计经验和体系结构
设计原则
"随着面向对象编程语言的发展,以及软件开发规模的不断扩大
编写良好的OOP程序变得困难,而编写可复用的OOP程序则更是困难"
设计模式的起源, 正是需要设计模式的根本原因
借助于设计模式,可以更好地实现代码的复用,增加可维护性
怎么才能更好地实现代码复用呢?
面向对象有几个原则:
根本原则
开闭原则(Open Closed Principle,OCP) 一个软件实体应当对扩展开放,对修改关闭 。
即软件实体应尽量在不修改原有代码的情况下进行扩展
|
在开闭原则的定义中,软件实体可以指一个软件模块、一个由多个类组成的局部结构或一个独立的类
不修改已有代码的基础上扩展系统的功能的形式,就是符合开闭原则的
开闭原则的关键是抽象
比如,一个方法中
if(){
//...
}else if(){
//...
}
如果新增加一个逻辑功能点,则需要增加新的else 或者 else if ,势必修改了已有代码
而如果面向抽象的接口或者抽象类进行编程,扩展增加新的功能,只需要传递新的子类即可,原有的代码功能不会有任何的修改
再比如
实际项目开发的时候,我们会把一些配置写入到配置文件中,而不是"硬编码"到代码中
修改参数设置的时候,源代码无需更改,这也是符合开闭原则
开闭原则作为根本原则,并不限定某种具体场景,只要是符合了这一含义,就是符合开闭原则
总之,开闭原则就是别因为新增功能扩展改(老)代码
六大原则
开闭原则是根本纲领,它是面向对象设计的终极目标
除了根本原则另外还有六大原则 , 则可以看做是开闭原则的实现方法
- 单一职责原则 (Single Responsiblity Principle SRP)
- 里氏替换原则(Liskov Substitution Principle,LSP)
- 依赖倒转原则(Dependency Inversion Principle,DIP)
- 接口隔离原则(Interface Segregation Principle,ISP)
- 合成/聚合复用原则(Composite/Aggregate Reuse Principle,C/ARP)
- 迪米特法则(Principle of Least Knowledge,PLK,也叫最小知识原则)
单一职责原则 (Single Responsiblity Principle SRP)
一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因
单一职责的原则很简单,就是一个实体(一个类或者一个功能模块)不要承担过多的责任
承担了过多的责任也就意味着多个功能的耦合
堆积木时, 到底是一块积木比较容易利用, 还是多块积木拼接起来的"一大块" 更容易利用? 结果显而易见
而且,承担了过多的责任,也就是可能会因为多个原因修改这段代码
随之而来的是不稳定性以及维护成本的增加,也就是将会有多个原因引起他变化
单一职责原则的根本在于控制类的粒度大小
里氏替换原则(Liskov Substitution Principle,LSP)
里氏替换原则是以提出者 Barbara Liskov 的名字命名的
定义:
如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2
使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化
那么类型 T2 是类型 T1 的子类型
简单说就是 如果 一个程序P(T1) ,如果将输入T1 替换为T2 ,而且 P(T1) = P(T2)
那么T2 是T1的子类型
再简单的概述就是:
所有引用基类的地方必须能透明地使用其子类的对象
透明也就意味着不感知,不受任何影响
听起来好像很自然的就可以做到
假如子类覆盖了父类的方法呢?假如子类覆盖了父类的方法并且改变了父类方法的原有功能逻辑呢?
比如,原来传递来两个参数进行加法运算,子类覆盖后,进行减法运算,会发生什么?
里氏代换原则的根本,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常
想要透明的使用子类,满足里氏替换原则
需要注意应该尽可能的将父类设计为抽象类或者接口
让子类继承父类或实现父接口,并实现在父类中声明的方法,这样可以做到满足开闭原则
子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法,也就是父类定义,子类实现
而且,子类不应该破坏父类的契约,也就是不能更改原有的方法的逻辑含义
里氏替换是继承复用的基石,只有当子类可以替换父类,且软件单位的功能不受到影响时
父类才能真正被复用,而子类也能够在基类的基础上增加新的行为
里氏代换原则是对开闭原则的补充。
实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范
依赖倒转原则(Dependency Inversion Principle, DIP)
抽象不应该依赖于细节,细节应当依赖于抽象; 换言之,要针对接口编程,而不是针对实现编程
也就是使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不是使用具体的类
在需要时,将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中
在引入抽象层后,程序中尽量使用抽象层进行编程, 系统将具有很好的灵活性 并且将具体类写在配置文件中
如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件
而无须修改原有系统的源代码 , 扩展系统的功能无需修改原来的代码,满足开闭原则的要求
接口隔离原则(Interface Segregation Principle,ISP)
使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可
接口隔离根本在于不要强迫客户端程序依赖他们不需要使用的方法
合成/聚合复用原则(Composite/Aggregate Reuse Principle,C/ARP)
复用一个类有两种常用形式,继承和组合
尽量使用对象组合,而不是继承来达到复用的目的,因为继承子类可以覆盖父类的方法,将细节暴露给子类
而且会建立强耦合关系,是一种静态关系,不能再运行时更改等等弊端
个人建议,对于继承的态度是不滥用,不弃用,带着脑子用!
迪米特法则(Principle of Least Knowledge,PLK,也叫最小知识原则)
一个软件实体应当尽可能少地与其他实体发生相互作用
也就是一个对象应当对其他对象有尽可能少的了解
再设计系统时,应该尽可能的减少对象之间的交互
有一个形象的说法"不要和“陌生人”说话、只与你的直接朋友通信"
下面这些一般被认为是朋友
"不要和“陌生人”说话、只与你的直接朋友通信" 就能够最大程度的降低耦合性
类之间的耦合度越低,就越有利于复用
如果两个对象之间不是必须要直接通信,那么这两个对象就可以不发生任何直接的相互作用
而是可以通过第三者转发这个调用,通过引入第三者将耦合度降低
设计原则总结
设计原则要求
设计原则是指导思想,将规则落实到具体的类/接口的设计、功能逻辑的划分上,可以转化成以下要求
所有的要求都有一个前提:如果可以,应该优先考虑,尽可能的
- 面向抽象(抽象类、接口)编程,而不是面向实现编程
- 接口和类的功能要尽可能的单一,避免大而全的类和接口
- 优先使用组合,而不是继承
- 子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法
- 子类应该尽可能的与父类保持一致,不要重写父类原有逻辑
- 如果类之间没必要直接交互,可以通过“中介”,而不是直接交互,降低耦合性
- 实现和细节可以通过DI的方式,最大程度减少“硬编码”
- 如果没有什么明显弊端,类应该被设计成不变的
- 降低其他类对自身的访问权限,不要暴露内部属性成员,如果需要提供相应的访问器(属性)
设计模式与设计原则
设计原则是软件开发过程中,前人以“高内聚,低耦合” “提高复用性”“提高可维护性”为根本目标
在实践中总结出来的经验,进而演化出来的具体的行为准则
就好似要做“好”一件事情,那么“好”的标准是什么?
按照经验总结归纳出来的一些“好”的标准,就是程序设计中的设计原则
设计原则是站在不同的维度与角度思考问题的, 他们的根本目的是相同的
本质都是为了设计一个“易维护、可复用、高内聚低耦合”的程序
比如单一职责原则与接口隔离原则,本质都是要职责专一
类提供单一的功能的实现,接口不要有大而全的功能约定
职责专一就能降低耦合,就更有可能被复用
使用组合而不是继承可以避免子类对父类的修改这种情况也就符合了里氏替换原则,也就符合了开闭原则
依赖倒置原则要求面向抽象进行编程而不是面向具体细节,而且依赖注入DI的思想也是如日中天Spring的根本
“易维护、可复用、高内聚低耦合”是目标
设计原则是为了达到目标的具体规则
而设计模式则是符合设计规则的具体的类/接口的设计解决方案
也就是设计原则的具体化形式
更准确的说,一个设计良好的程序应该遵循的是设计原则,而并非一定是某个设计模式
所有的原则都是指导方针,而不是硬性规则
是在很多场景下一种优秀的解决方案,而并不是一成不变的
在实际的项目中,你既不能完全放弃使用继承,也可能让一个类完全不同“陌生人”讲话
也不可能子类完全不重写父类的方法
面向抽象进行编程,你也不可能让项目中所有的类都有抽象对应,这也是不可能的,也不能是被允许的
设计模式设计原则是经验之谈,当然是非常宝贵的经验,也是经过实践检验过的
但是最大的忌讳就是生搬硬套,矫枉过正,那将是最失败的设计模式的应用方式
设计模式和面向对象的设计原则是解决问题的一般思路
而不是像交规一样,必须遵守,严格执行
不遵守设计原则与设计模式也不会编译失败
但是希望能够尽最大可能的遵守, 当然,还需要因地制宜而不能生搬硬套
或许,你从来不遵守原则,也不使用设计模式,你的代码可能看起来仍旧好好地
但是
你的代码出问题的概率
却会比使用了设计模式遵循了设计原则的代码
要大得多
设计模式和设计原则正是为了能够更加简单便利的复用代码,尽可能的减少问题的出现
就好像一条浅浅的小河,可能有无数种趟过去方案
但是,那条走的人最多的,可能它并不是最好的
但是他肯定是比较合适的一条途径,不会出现碎玻璃,沙坑等陷阱.
到底是站在巨人的肩膀上还是一定要自己摸着石头过河?
简单说来就是:我们知道软件的目标“正确、健壮、灵活、可重用、高效....”等等,总之都是往“优秀”“好”的方向
然后发现了好的软件的一些特性,所以作为了设计原则
但是还是过于抽象,于是针对于不同的场景,按照设计原则,整理出来一套好的解决方法,这就是设计模式。
设计模式分类
- 将系统所使用的具体类的信息封装起来
- 隐藏了类的实例是如何被创建和组织的
行为型
设计模式之间并不是孤立的,他们也会相互使用,下图为《设计模式 - 可复用面向对象软件的基础》一书中的描述
各个模式之间的区别和联系是一个“悟”的过程,不要试图对下下图进行任何记忆
另外还有范围准则的概念,指定模式主要是用于类还是用于对象
类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,编译时刻便已经确定下来了
对象模式处理对象之间的关系,这些关系在运行时是可以变化的,更具有动态性
其实如果较真,很多的模式都有涉及到继承/实现
所以说设计模式中常说的“类模式”只是指那些集中于处理类间关系的模式
大部分模式都属于对象模式
比如对于创建型来说分为 类创建型模式和对象创建型模式
概念只是为了更好的描述问题,类模式和对象模式的概念也来自《设计模式 - 可复用面向对象软件的基础》
本人认为对于设计模式一般的学习与理解,这个概念无所谓
总结
设计模式是设计原则在解决具体问题时实践中的运用
所以根本是要理解设计原则的含义
随着技术发展,会出现更多的不同的问题场景,基于设计原则,可能拓展出来更多的设计模式
事实上到目前为止,也不仅仅是23种
所以说设计模式的根本是设计原则,而设计原则又是为了达到实现一个“优秀”软件的行为准则。
在你还不能灵活的运用设计原则时,设计模式则是你的垫脚石,让你在具体的问题面前能够写出更好地代码
设计模式是理论层次的研究学习,自然是枯燥的
而且很难能够一开始就高屋建瓴的自顶而下的深入理解
也很难彻底领悟设计原则本身
所以,从一个一个模式的学习中慢慢品味设计原则的精髓
类、接口之间的层级结构是可以变换的,设计模式的根本是设计原则
所以说在学习中要领悟设计模式的根本思想使用场景
在实践中,不要生搬硬套的应用模式,也无需同设计模式中的类、接口设计层级结构一模一样
可能你应用了某个模式,但是可能又根据实际业务有一些变动或调整
有人说,你这不是设计模式,那又如何?
只要能够满足需求符合设计原则,往“可复用/易维护/高内聚/低耦合”的目标前进,就好~
设计模式将“只可意会,不可言传”转变为“不只意会,还可以言传~”