前言
大约在一年前学习过一段时间的设计模式, 但是当时自己的学习方式比较低效, 也没有深刻的去理解, 运用所学的知识.
所以现在准备系统的再重新学习一遍, 写一个关于设计模式的系列博客.
废话不多说, 正文开始.
1. 设计模式是什么
设计模式是一套被反复使用, 多数人知晓的, 经过分类编目的, 代码设计经验的总结, 使用设计模式是为了可重用代码, 让代码更容易被他人理解并且保证代码可靠性.
2. 学习设计模式的好处
提高开发效率, 使用设计模式可以避免我们做一些重复工作
减少开发人员的沟通成本.
阅读源码, 更深入的理解使用的框架和类库
自己写出灵活性高, 易维护, 易扩展和易复用的代码
3. 设计模式的分类
根据用途设计模式可分为创建型, 结构型和行为型三种
3.1 创建型设计模式
创建型模式是处理对象创建的设计模式, 试图根据实际情况使用合适的方式创建对象.
5 种创建型设计模式:
简单工厂模式
工厂方法模式
单例模式
原型模式
建造者模式
3.2 结构型设计模式
结构型设计模式是借由一以贯之的方式来了解元件间的关系, 以简化设计.
一以贯之: 指做人做事, 按照一个道理, 从一而终, 出自《论语. 里仁》.
注: 以上是百科的解释, 一以贯之是我自己百度贴上的, 又能学到技术又能学到成语, 看这篇博客赚翻了有没有.
七种结构型设计模式:
适配器模式
桥接模式
组合模式
装饰者模式
外观模式
享元模式
代理模式
3.3 行为型设计模式
行为型设计模式是用来识别对象之间的常用交流模式并加以实现. 如此, 可在进行这些交流活动时增强弹性.
十一种行为型设计模式:
职责链模式
命令模式
解释器模式
迭代器模式
中介者模式
备忘录模式
观察者模式
状态模式
策略模式
模板方法模式
访问者模式
4. 学习设计模式的一些其他准备工作
学习设计模式还需要一些其他的知识储备, 例如:
UML 类图相关知识(部分示例使用 UML 类图演示, 如没有相关知识, 请移步我的上一篇博客 UML 类图简介)
了解面向对象设计原则
5. 面向对象设计原则
面向对象设计原则是从设计模式中总结出来的指导性原则, 也就是说设计模式遵循了面向对象设计原则. 我们平时在开发软件的时刻也要尽量遵循面向对象设计原则进行开发.
面向对象设计原则为支持可维护性复用而诞生.
最常见的七种面向对象设计原则:
单一职责
开闭原则
里氏代换原则
依赖倒转原则
接口隔离原则
合成复用原则
迪米特法则
5.1 单一职责
定义: 一个类只负责一个功能领域中的相应职责, 或者可以定义为: 就一个类而言, 应该只有一个引起变化的原因.
使用单一职责的原因: 如果一个类承担的职责太多, 它被复用的可能性就越小, 而且一个类承担的职责过多, 就相当于将这些职责耦合在一起, 当其中一个职责变化时, 可能影响其他职责的运作, 因此要将这些职责分离. 将不同的职责封装在不同的类中.(如果多个职责总是同时发生改变则可以将他们封装在同一个类中)
单一职责原则是实现高内聚, 低耦合的指导方针.
内聚: 内聚是从功能角度来度量模块内的联系, 一个好的内聚模块应当恰好做一件事. 它描述的是模块内的功能联系.
耦合: 耦合是软件结构中各模块之间相互连接的一种度量, 耦合强弱取决于模块间接口的复杂程度, 进入或访问一个模块的点以及通过接口的数据.
示例: 有一个汽车的类, 有几个方法分别是开门, 关门, 前进, 后退, 修车, 维护, 洗车的功能
按照单一职责的定义一个类只负责一个功能领域中的相应职责, 我们可以对汽车这个类进行优化, 将修车, 维护, 洗车的工作抽离到修车厂的类中.
优化后的类:
5.2 开闭原则
开闭原则是面向对象的可复用设计的第一基石, 它是最重要的面向对象设计原则.
定义: 一个软件实体应当对扩展开放, 对修改关闭. 即软件实体应该尽量在不修改原有代码的情况下进行扩展.
为了满足开闭原则, 需要对系统进行抽象化设计, 抽象化是开闭原则的关键. 使用接口, 抽象类定义系统的抽相层, 再通过具体类来进行扩展.
如果需要修改系统的行为, 无须对抽象层进行任何改动, 只需要增加新的具体类来实现新的业务功能即可, 实现在不修改已有代码的基础上扩展系统的功能, 达到开闭原则的要求.
示例: 超市举办促销活动, 打折策略是满 200 打八折. 我们来看看打折策略的设计
代码:
- /**
- * @author liuboren
- * @Title: 打折策略类
- * @Description: 具体的打折实现
- * @date 2019/7/11 14:39
- */
- public class DiscountStrategy {
- /*
- * 消费超过 200, 打八折
- * */
- public Double strategy(Double money){
- if(money> 200){
- money = money * 0.8;
- }
- return money;
- }
- }
- /**
- * @author liuboren
- * @Title: 结账功能
- * @Description: 使用打折策略结账
- * @date 2019/7/11 14:37
- */
- public class SettleAccounts {
- public Double Buy(Double money,DiscountStrategy strategy){
- // 返回打折后的金额
- return strategy.strategy(money);
- }
- }
一切都看上去很完美, 但是过了几个月, 超市决定换一种打折策略, 消费满 500 立减 200.
这时候如果直接去改打折策略的类, 就违反了开闭原则, 而且如果过几天打折策略又要还回去, 或者同时增加新的打折策略, 也没有办法很好的扩展.
实现开闭原则的关键在于面向接口编程, 我们来更改一下代码.
新增打折策略接口, 结账类使用接口进行结算:
- /**
- * @author liuboren
- * @Title: 打折接口
- * @Description: 声明打折方法, 具体有实现类去实现
- * @date 2019/7/11 14:48
- */
- public interface DiscountStrategyInterface {
- // 打折策略
- Double strategy(Double money);
- }
- /**
- * @author liuboren
- * @Title: 满 200 打八折实现类
- * @Description: 具体的打折实现
- * @date 2019/7/11 14:39
- */
- public class TwentyPercentStrategy implements DiscountStrategyInterface{
- /*
- * 消费超过 200, 打八折
- * */
- @Override
- public Double strategy(Double money){
- if(money> 200){
- money = money * 0.8;
- }
- return money;
- }
- }
- /**
- * @author liuboren
- * @Title: 结账功能
- * @Description: 使用打折策略结账
- * @date 2019/7/11 14:37
- */
- public class SettleAccounts {
- public Double Buy(Double money,DiscountStrategyInterface strategy){
- // 返回打折后的金额
- return strategy.strategy(money);
- }
- public static void main(String[] args) {
- SettleAccounts settleAccounts = new SettleAccounts();
- /*
- * 这样很灵活, 有新的打折策略的时候, 只需要添加新的实现类,
- * 并传入购买方法, 开闭原则得到了很好的实现
- * */
- settleAccounts.Buy(300d,new TwentyPercentStrategy());
- }
- }
修改后我们的代码在增加新的打折策略的时候变得很容易扩展, 而且还不需要修改原来的类了.
5.3 里氏代换原则
定义: 所有引用基类 (父类) 的地方必须能透明地使用其子类的对象.
里氏代换原则告诉我们, 在软件中将一个基类对象替换成它的子类对象时, 程序将不会产生任何错误和异常, 反过来则不成立.
里氏代换原则是实现开闭原则的重要方式之一, 由于使用基类对象的地方都可以使用子类对象, 因此在程序中尽量使用基类类型来对对象定义, 而在运行时再确定其子类类型, 用子类对象来替换基类对象.
使用里氏代换原则需要注意的问题:
子类的所有方法必须在父类中声明, 或子类必须实现父类中声明的所有方法. 根据里氏代换原则, 为了保证系统的扩展性, 在程序中通常使用父类来进行定义, 如果一个方法只存在子类中, 在父类中不提供相应的声明, 则无法在父类定义的对象中使用该方法.
尽量把父类设计为抽象类或接口, 让子类继承父类或实现父接口, 并实现在父类中声明的方法, 运行时, 子类实例替换父类实例, 我们可以很方便地扩展系统的功能, 同时无须修改原有子类的代码, 增加新的功能可以通过增加一个新的子类来实现. 里氏代换原则是开闭原则的具体实现之一
实例: 还看超市的例子
- /**
- * @author liuboren
- * @Title: 结账功能
- * @Description: 使用打折策略结账
- * @date 2019/7/11 14:37
- */
- public class SettleAccounts {
- public Double Buy(Double money,DiscountStrategyInterface strategy){
- // 返回打折后的金额
- return strategy.strategy(money);
- }
- public static void main(String[] args) {
- SettleAccounts settleAccounts = new SettleAccounts();
- /*
- * 这样很灵活, 有新的打折策略的时候, 只需要添加新的实现类,
- * 并传入购买方法, 开闭原则得到了很好的实现
- * */
- settleAccounts.Buy(300d,new TwentyPercentStrategy());
- }
- }
Buy 方法使用的参数是 DiscountStrategyInterface 接口, 但是在 main 方法使用的是其子类, 这就是父类出现的地方都可以被子类替换的里氏代换原则.
5.4 依赖倒转原则
如果说开闭原则是面向对象设计的目标的话, 那么依赖倒转原则就是面向对象设计的主要实现机制之一, 它是系统抽象化的具体实现.
定义: 抽象不应该依赖于细节, 细节应该依赖于抽象. 换言之, 要针对接口编程, 而不是针对实现编程.
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中, 尽量引用层次高的抽象层类, 即使用接口和抽象类进行变量类型声明, 参数类型声明, 方法返回类型声明, 以及数据类型的转换等, 而不要用具体类来做这些事情.
为了确保该原则的应用, 一个具体类应当只实现接口或者抽象类中声明过的方法, 而不要给出多余的方法, 否则将无法调用到在子类中增加的新方法.
示例: 同上面.. 一句话面对接口编程.
5.5 接口隔离原则
定义: 使用多个专门的接口, 而不使用单一的总接口, 即客户端不应该依赖那些它不需要的接口.
根据接口隔离原则, 当一个接口太大时我们需要将它分割成一些更细小的接口, 使用该接口的客户端仅需知道与之相关的方法即可. 每个接口都应该承担一种相对独立的角色, 不该干的事不干, 该干的事都要干.
接口有两种含义, 一种是指一个类型所具有的方法特征的集合, 仅仅是一种逻辑上的抽象例如上面的 Animal 接口; 另一种是值某种语言具体的 "接口" 定义, 有严格的定义和机构, 比如 Java 语言中的 interface; 对这两种不同的含义, 接口隔离原则的表达方式以及含义都有所不同:
把 "接口" 理解成一个类型所提供的的所有方法特征的集合的时候, 这就是一种逻辑上的概念, 接口的划分将直接带来类型的划分. 可以把接口理解成角色, 一个接口只能代表一个角色, 每个角色都有它特定的一个接口, 此时, 这个原则可以叫做 "角色隔离原则". 例如动物可以抽象成一个接口, 接口封装动物的一些特性和行为.
如果把 "接口" 理解成狭义的特定语言的接口, 那么接口隔离原则的意思是指接口仅仅提供客户端需要的行为, 客户端不需要的行为则隐藏起来.
应当为客户提供尽可能小的单独的接口, 而不要提供大的总接口.
在面向对象编程语言中, 实现一个接口就需要实现该接口中定义的所有方法, 因此大的总接口使用起来不一定很方便, 为了使接口的职责单一, 需要将大接口中的方法根据其职责不同分别放在不同的小接口中, 以确保每个接口使用起来都较为方便, 并都承担某一单一角色.
接口应该尽量细化, 同时接口中的方法应该尽量少, 每个接口中只包含一个客户端所需的方法即可. 和单一职责有异曲同工之妙.
5.6 合成复用原则 / 聚合复用原则
定义: 尽量使用对象组合, 而不是继承来达到复用的目的.
合成复用原则就是在一个新的对象里通过关联关系 (包括组合关系和聚合关系) 来使用一些已有的对象, 使之成为新对象的一部分; 新对象通过委派调用已有对象的方法达到复用功能的目的.
简言之: 复用时要尽量使用组合 / 聚合关系(关联关系), 少用继承.
组合 / 聚合和继承都可以复用已有的设计和实现, 但是应该优先考虑使用组合 / 聚合. 因为组合 / 聚合可以使系统更加灵活, 降低类与类之间的耦合度, 一个类的变化对其他类造成的影响相对较少. 其次再考虑继承, 在使用继承时, 需要严格遵循里氏代换原则, 有效使用继承有助于对问题的理解, 降低复杂度, 而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度, 因此需要慎重使用继承复用.
继承的坏处:
通过继承来复用的主要问题在于继承复用会破坏系统的封装性. 因为继承会将基类的实现细节暴露给子类, 由于基类的内部细节对子类来说是可见的, 所以这种复用又称 "白箱" 复用, 如果基类发生改变, 那么子类的实现也不得不发生改变.
从基类继承而来的实现是静态的, 不可能在运行时发生改变, 没有足够的灵活性.
类没有声明 final 才能被继承, 使用条件有限.
组合 / 聚合的好处:
组合 / 聚合将已有对象纳入新对象中, 使之成为新对象的一部分, 因此新对象可以调用已有对象的功能. 这样做可以使得成员对象的内部实现细节对于新对象不可见. 所以这种复用又称为 "黑箱" 复用, 相对于继承而言, 其耦合度相对较低, 成员对象的变化对新对象的影响不大, 可以再新对象中根据实际需要有选择性的调用成员对象的方法.
合成复用可以在运行时动态进行, 新对象可以动态地引用与成员对象类型相同的其他对象.
继承和组合 / 聚合的选择: 像之前超市打折的例子中, 可以提高程序的灵活性才使用继承 / 实现, 否则优先使用组合.
5.7 迪米特法则
定义: 一个软件实体应当尽可能少地与其他实体发生相互作用.
如果一个系统符合迪米特法则, 那么当其中某一个模块发生修改时, 就会尽量少的影响其他模块, 扩展会相对容易, 这是对软件实体之间通信的限制, 迪米特法则要求限制软件实体时间通信的宽度和深度. 迪米特法则可以降低系统的耦合度, 使类与类之间保持松散的耦合关系.
迪米特法则要求对象只与朋友通信,"不要和陌生人说话", 朋友包括以下几类:
当前对象自身(this);
以参数形式传入到当前对象方法中的对象;
当前对象的成员对象;
如果当前对象的成员对象是一个集合, 那么集合中的元素也都是朋友;
当前对象所创建的对象.
在应用迪米特法则时, 一个对象只能与直接朋友发生交互, 不要与 "陌生人" 发生直接交互, 这样做可以降低系统的耦合度, 一个对象的改变不会给太多其他对象带来影响.
迪米特法则要求我们在设计系统时, 应当尽量减少对象之间的交互, 如果两个对象之间不必彼此直接通信, 那么这两个对象就不应该发生任何直接的相互作用, 如果其中的一个对象需要调用另一个对象的某一个方法, 可以通过第三者转发这个调用.
简言之, 就是通过引入一个合理的第三者来降低现有对象之间的耦合度.
在将迪米特法则运用到系统设计中时, 要注意下面的几点:
在类的划分上, 应当尽量创建松耦合的类, 类之间的耦合度越低, 就越有利于复用, 一个处在松耦合中的类一旦被修改, 不会对关联的类造成太大波及.
在类的设计结构上, 每一个类都应当尽量降低其成员变量和成员函数的访问权限.
在类的设计上, 只要有可能, 一个类型应当设计成不变类.
在对其他类的引用上, 一个对象对其他对象的引用应当降到最低.
示例: 有一个客户关系管理系统包含很多业务操作窗口, 某些界面控件之间存在复杂的交互关系, 一个控件事件的触发将导致很多其他界面产生响应.
例如, 当一个按钮 (button) 被单击时, 对应的列表框 (List), 组合框(ComboBox), 文本框(TextBox), 文本标签(Label) 等都将发生改变.
由于界面空间之间的交互关系复杂, 导致在该窗口增加新的界面控件时需要修改与之交互的其他控件的源代码, 系统扩展性较差, 也不便于增加和删除新控件.
改良方法:
引入一个专门用于控制控件交互的中间类 (Mediator) 来降低界面控件的耦合度.
引入中间类后, 界面控件之间不再发生直接引用, 而是将请求先转发给中间类, 再有中间类来完成对其他控件的调用.
当需要增加或删除新的控件时, 只需修改中间类即可, 无须修改新控件或已有控件的代码.
6. 主要参考文献
大话设计模式
Java 设计模式
来源: https://www.cnblogs.com/xisuo/p/11160184.html