# head-first-design-patterns-note **Repository Path**: TrZoey/head-first-design-patterns-note ## Basic Information - **Project Name**: head-first-design-patterns-note - **Description**: 《head First 设计模式 》学习笔记 及 练习代码 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2019-07-12 - **Last Updated**: 2021-04-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # head-first-design-patterns-note ## 介绍 《head First 设计模式》、《设计模式之禅(第2版)》学习笔记 及 练习代码 ## 笔记 ### Tips 1.良好的OO设计必须具备可复用、可扩充、可维护三个特性 2. 找出应用中可能需要变化的地方,把它们独立出来,不要和那些不需要变化的代码混在一起 3. 针对接口编程,而不是针对实现编程 4. 多用组合,少用继承 5. 为了交互对象之间的松耦合设计而努力 ### 设计原则清单 #### “开闭原则”:类应该对扩展开放,对修改关闭 * todo... #### 依赖倒置原则(Dependence Inversion Principle, DIP) * 定义:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions. * 高层模块不应该依赖低层模块,两者都因该依赖抽象 * 抽象不应该依赖细节 * 细节应该依赖抽象 * 核心: * 面向接口编程 * 实践方针: 1. 变量不要持有具体类的引用(用工厂替代new) 2. 类不要派生自具体类(应派生自抽象——接口或抽象类) 3. 不要覆盖基类中已实现的方法(破坏基类的抽象性,基类中已实现的方法,应由所有子类共享) * 我们不可能完全遵守上边这些方针,但其实这只是一种思维方式的形成与锻炼,让我们在设计时进行充分考虑。一个不怎么会改变的类,那么直接在代码中实例化是没有问题的。比如我们用了无数次的String。 * 作用: * 减少类之间的耦合,提高系统稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 * 示例: * 张三是你的司机,负责开你的宝马。(这时候就有了Driver对象,内部有一个驾驶BMW对象的方法)。 * 但有一天你又买了辆奔驰,这下完蛋了,张三没有开奔驰的方法。 * 显然你不能再招一个专门开奔驰的司机,或者说去修改张三的基因,让他会开奔驰。 * 正确的做法是抽象出汽车对象,让宝马和奔驰实现汽车抽象。然后让Driver依赖汽车抽象。 * 这样只要在出差前,注入相应的汽车,张三就可以开动了。即使以后你买了兰博基尼也不用去动张三。 * 注入依赖的三种方法:(Car是汽车的抽象) * 构造函数注入 ``` public class Driver { private Car car; public Driver(Car car){ this.car = car; } public void drive() { this.car.run(); } } ``` * Setter方法注入 ``` public class Driver { private Car car; public void setCar(Car car) { this.car = car; } public void drive() { this.car.run(); } } ``` * 接口声明注入 ``` public class Driver { public void drive(Car car) { car.run(); } } ``` #### 单一职责原则(Single Responsibility Principle) * 定义 * There should never be more than one reason for a class to change. * 应该有且仅有一个原因引起类的变更。 * 盘一下 * 感觉盘起来会罗里吧嗦,而且还没有人家盘的清楚。强烈建议大家看《设计模式之禅》中这一部分的原文。 * 记录一些要点 * 优点: * 类的复杂性降低,实现什么职责都有清晰明确的定义。 * 可读性提高 * 可维护性提高 * 变更引起的风险降低 * 每一种职责都是一个接口,一个承担多个职责的类可以实现多个接口。这样当某个接口改变时,只对该接口的实现类有影响,对其他接口无影响。这对系统的扩展性、可维护性有非常大的帮助。 * 职责的划分很难确定且没有唯一正确答案。最终都是结合实际项目的各个方面综合考量的结果。而结果往往违背单一职责原则。职责和变化都是不可度量的,要因项目而定,因环境而定。 * 方法一定要做到单一职责原则。比如修改用户名称就是changeUserName(String name)方法,不要写一个changeUserInfo(String field, Object value)方法。这样职责清晰明确,开发简单,更易于维护。 * 建议:接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化 ### 设计模式 #### 策略模式 ##### 定义 * 定义算法族,分别封装起来,让他们可以互相替换,此模式让算法的变化独立与使用算法的客户 ##### 适用范围 * 几个类的主要逻辑相同,只在部分逻辑的算法和行为上稍有区别的情况 * 有几种相似的行为(算法),客户端需要动态决定使用哪一种 ##### 优点 * 动态改变对象的行为 * 扩展性良好 ##### 缺点 * 客户端必须知道所有的策略类,并自行决定使用哪一个 * 会产生很多策略类 * 只适合扁平的结构,策略之间层级平等,不能互相嵌套 ##### 结构 * 抽象策略:抽象类或接口,约束具体策略的行为 * 具体策略:抽象策略的实现 * 上下文类:负责具体交互,相当于一个容器,持有具体策略实现的引用 ##### 执行顺序 * 创建具体策略实现 * 创建上下文,并注入策略 * 通过上下文处理数据 ##### 代码 * [策略模式-鸭子](src/main/java/com/prik/strategy/duck/strategy_duck.md) * [策略模式-折扣](src/main/java/com/prik/strategy/discount/strategy_discount.md) ##### 参考资料 * [深入解析策略模式](https://www.cnblogs.com/lewis0077/p/5133812.html) * [JAVA设计模式学习17——策略模式](https://alaric.iteye.com/blog/1920714) #### 观察者模式 ##### 定义 观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他的所有依赖者都会收到通知并自动更新 ##### 结构 * Subject:主题接口 * ConcreteSubject:具体主题 * Observer:观察者接口 * ConcreteObserver:具体观察者 ##### 优点 * 具体主题和具体观察者之间为松耦合关系 * 符合“开闭原则” ##### 缺点 * 没有相应的机制使观察者知道主题是如何发生变化的 * 如果观察者之间存在循环调用,会导致系统崩溃,需要特别注意 ##### 适用范围 * 一个对象的改变需要触发其他多个对象的改变,但不知道具体有多少需要改变的对象,降低对象间的耦合 * 一个对象需要通知很多其他对象,但不需要知道他们是谁 ##### 与发布-订阅模式的区别 * 发布-订阅模式中,发布者不直接和订阅者通信,他们甚至不知道对方的存在。他们通过第三方信息中介进行通信 * 观察者模式大多是同步的,而发布-订阅模式大多是异步的(消息队列) ##### 代码 * [观察者模式-订单付款](src/main/java/com/prik/observer/order/observer_order.md) * [观察者模式-气象台](src/main/java/com/prik/observer/weather/observer_weather_station.md) ##### 其他 * 在另一个例子中:气象台(主题)发布实时数据(间隔很短,假设1秒一次), 布告板(观察者)显示气温(有可能实时,也有可能每天一次,或者统计月平均数据)。 这种情况下观察者需要将接收到的数据缓存下来,然后在各自设定的时间对外展示。 * 在JDK的java.util包中,提供了Observable类以及Observer接口,它们构成了Java语言对观察者模式的支持。 #### 装饰者模式 ##### 定义 动态地将责任附加到对象身上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。 ##### 特点 * 装饰者和被装饰着有共同的超类,装饰过的对象可以替代原始对象使用 * 可以用一个或多个装饰者包装一个对象 * 装饰者可以在所委托被装饰者的行为之前、之后,加上自己的行为,来达到特定目的 ##### 优点 * 可以很灵活地扩展对象功能,扩展时符合“开闭原则” ##### 缺点 * 会产生很多对象,增加系统复杂度,加大学习理解成本 * 使用时更容易出错,错误排查也更加困难(但结合工厂模式和生成器模式后会得到很大改善) ##### 注意 * 这里用到继承的目的不是“继承行为”,而是“类型匹配”。对象的行为来自于对象的组合。这并不违反之前提到的设计原则。 ##### 代码 * [装饰者模式-咖啡](src/main/java/com/prik/decorator/decorator.md) ##### Java中的装饰者模式 * java.io 包中有茫茫多的类,但仔细观察就会发现,其中很多类都是装饰者。 这也体现了装饰者模式的缺点:对于不明所以的人来说,大料API看起来会很困扰。 * 感兴趣的话可以自己编写一个装饰者来装饰io,比如把输入流中的所有小写字母转为大写。 #### 简单工厂(Simple Factory Pattern) ##### 定义 * 定义一个类来专门负责创建其他类的实例,被创建的实例通常具有共同的父类。简单工厂可以根据参数的不同返回一个相应的实例 * 严格来讲,简单工厂模式不属于设计模式,而是属于一种编程习惯 ##### 结构 * Factory:工厂角色 * Product:抽象产品 * ConcreteProduct:具体产品 ##### 优点 * 简单工厂实现了责任的分隔,客户端免除了创建产品对象的责任。 * 客户端无需知道对象创建细节,只需要知道什么参数对应什么对象即可。 * 通过配置文件,可以在不修改客户端代码的情况下更换、增加新的产品类,提高了灵活性。 ##### 缺点 * 工厂类集中了所有产品的创建逻辑,一旦无法正常工作,整个系统都会受影响 * 扩展困难。增加修改产品类都需要修改工厂类。如果产品类数量过多,工厂类会很难维护。 * 工厂方法一般都是静态的,工厂无法进行继承。 ##### 适用环境 * 工厂类需要创建的对象数量比较少 * 客户端不关心独享创建细节,只知道参数。 ##### 代码 * [简单工厂-披萨店](src/main/java/com/prik/factory/simple/simple_factory_pattern_pizza.md) ##### 参考资料 * [图说设计模式——简单工厂模式](https://design-patterns.readthedocs.io/zh_CN/latest/creational_patterns/simple_factory.html) #### 工厂方法模式(Factory Method Pattern) ##### 定义 * 工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。 ##### 结构 * Factory:抽象工厂 * ConcreteFactory:具体工厂 * Product:抽象产品 * ConcreteProduct:具体产品 ##### 对比简单工厂 * 简单工厂将对象的创建逻辑写在工厂类里,那么当我们有新的对象加入时,就需要修改工厂类的代码。这不符合开闭原则。那如果我们定义一个抽象工厂类,然后将具体的对象创建过程交给子类去做,当我们引进新的产品时,只需要增加一个新的工厂类即可。显然这是要优于简单工厂模式的,这就是我们所说的工厂方法模式。 ##### 优点 * 隐藏产品具体实现,用户只关心工厂,无需关心细节。 * 有新产品加入时,无需修改其他工厂和产品,只需添加新的工厂和产品。扩展性良好,完全符合“开闭原则”。 * 可结合配置文件实现对象实例化时的动态指定。 ##### 缺点 * 会给系统带来更多的类,增加复杂度和额外开销。 ##### 代码 * [工厂方法模式-披萨加盟店](src/main/java/com/prik/factory/method/factory_method_pattern_pizza.md) #### 抽象工厂模式(Abstract Factory Pattern) ##### 定义 * 提供一个创建一系列相关或相互依赖对象的接口,用于创建相关或依赖对象的家族,而无须指定它们具体的类。 ##### 结构 * AbstractFactory:抽象工厂 * ConcreteFactory:具体工厂 * AbstractProduct:抽象产品 * ConcreteProduct:具体产品 ##### 对比工厂方法和简单工厂 * 工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、有效率。 * 当抽象工厂模式中每一个具体工厂只创建一个产品对象,那抽象工厂模式就退化为工厂方法模式。 * 当工厂方法模式的抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,那工厂方法模式就退化为简单工厂。 ##### 优点 * 新增加具体工厂和产品族很方便,符合“开闭”原则 * 符合“依赖倒置”原则 * 高层模块不需要关心低层模块的具体实现,只需要关心抽象,关心工厂。 * 抽象工厂可以约束产品族的特性(比如配比,芝士与奶油1:2) ##### 缺点 * 对于产品族的扩展非常困难。抽象工厂中定义了有可能会被创建的所有产品的方法。如果新增产品,就必须要修改抽象类和所有实现类。(“开闭原则”的倾斜性) ##### 使用场景 * 系统中有多个产品族,每次只使用其中某一族 * 系统需要约束属于同一产品族的产品必须在一起使用 ##### 代码 * [抽象工厂模式-披萨加盟店-食材控制](src/main/java/com/prik/factory/abs/abstract_factory_pattern_pizza.md) #### 单例模式(Singleton Pattern) ##### 定义 * 例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。 ##### 优点 * 节约系统资源,避免频繁创建销毁对象带来的开支 * 保证对象的全局唯一性 * 在类的实例化进程上有一定的灵活性 * 进行扩展之后可以允许存在可变数目的实例 ##### 缺点 * 扩展困难 * 一定程度上违反了“单一职责原则” ##### 使用场景: * 系统要求有且只有一个对象实例,比如全局唯一序列号生成器, * 系统考虑到性能,避免某对象频繁创建销毁 * 使用者除单例提供的单个访问点外,不允许通过其他途径访问实例。 ##### 代码 * [单例模式的几种写法](src/main/java/com/prik/singleton/singleton.md) #### 命令模式(Command Pattern) ##### 定义 * 将一个请求封装成一个对象,以便使用不同的请求、队列或者日志来参数化其他对象。也支持可撤销功能。 ##### 结构 * Command:抽象命令 * ConcreteCommand:具体命令 * Invoker:调用者 * Receiver:接受者 * Client:客户端 ##### 分析 命令模式的本质是对命令进行封装,将命令发出和执行的责任分割开 * 使请求本身成为一个对象,可以被储存和传递 * 请求方和接收方独立开,请求方不需要知道请求如何被接收,是否、何时被执行,以及是如何执行的 ##### 优点 * 降低耦合度 * 新命令的加入很容易 * 轻松设计一个宏命令或命令队列 * 请求可以撤销和重播 ##### 缺点 * 系统内会产生大量命令类 ##### 使用场景 * 需要将请求发出方和执行方解耦,两者间不直接交互 * 需要命令的撤销和重播功能 * 需要宏命令功能 * 需要在不同的时间指定请求,将请求排队和执行 ##### 代码: * [命令模式——遥控器](src/main/java/com/prik/command/command.md) #### 适配器模式(Adapter Pattern) ##### 定义 * 将一个接口转换成客户希望的另一个接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作 ##### 优点 * 可以让两个没有任何关系的类一起运行,让目标类和适配类实现解耦 * 增加了类的透明性 * 提高了类的复用度 * 具有非常好的灵活性 ##### 使用场景 * 系统需要使用已经投入使用的类,但这些类不符合需求 #### 门面模式(外观模式)(Facade Pattern) ##### 定义 * 要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。 门面模式提供一个高层次的接口, 使得子系统更易于使用。 ##### 结构 * Facade:门面(外观)角色,将收到的请求委派给相应的子系统 * Subsystem:子系统角色,处理请求,并不知道门面的存在,只是一个正常的客户端 ##### 优点 * 减少系统的相互依赖 * 提高灵活性。无论子系统内部如何变化,只需要修改门面一个对象就可以 * 提供安全性。子系统只能被门面对象访问 ##### 缺点 不符合“开闭原则”。门面对象有可能会被修改,有一定的风险性。 ##### 使用场景 * 为一个复杂的子系统向外界提供一个简单明了的访问接口 * 保持子系统的独立性 * 降低因个人代码质量差而对整体质量的影响,“画地为牢”,规定只能在指定子系统中开发,然后对外提供访问接口 ##### 示例 现在来模拟一个写信、寄信的过程,步骤如下: ```java /** * 写信寄信流程 */ public class LetterWriter { public void writeContext(String context) { System.out.println(context); } public void writeAddress(String address) { System.out.println(address); } public void putIntoEnvelope() { System.out.println("mua~"); } public void sendLetter() { System.out.println("send the letter..."); } } ``` 来模拟这个过程: ```java public class Test { public static void main(String[] args) { LoveLetter loveLetter = new LoveLetter(); loveLetter.writeContext("loving u..."); loveLetter.writeAddress("To my lover..."); loveLetter.putIntoEnvelope(); loveLetter.sendLetter(); } } ``` 我们发现,写信的过程很复杂,步骤顺序不能乱,也不能漏掉,这还是只有4个步骤的情况。 让我们用门面模式来改造一下:引入一个邮局类,我们只需要告诉他内容和地址,他会帮我们完成整个过程。 ```java public class PostOffice { private LetterWriter letterWriter = new LetterWriter(); public void sendLetter(String context, String address) { letterWriter.writeContext(context); letterWriter.writeAddress(address); letterWriter.putIntoEnvelope(); letterWriter.sendLetter(); } } ``` 这时我们写信就会变得很简单: ```java public class Test { public static void main(String[] args) { PostOffice postOffice = new PostOffice(); postOffice.sendLetter("love u", "xxx"); } } ``` 这样带来的一个好处就是,简化客户端的操作。至于你邮局到底做了什么,我不关心,也没必要关心。信送到了就行。比如有一天突然多了一个步骤:警察需要检查信件内容。我们只需要修改门面: ```java public class PostOffice { private LetterWriter letterWriter = new LetterWriter(); private Police police = new Police(); public void sendLetter(String context, String address) { letterWriter.writeContext(context); letterWriter.writeAddress(address); // 增加警察检查内容步骤 police.checkLetter(letterWriter); letterWriter.putIntoEnvelope(); letterWriter.sendLetter(); } } ``` 到这里还没完,细心的朋友们可能已经发现了,门面类参与了子系统的业务逻辑,这是我们非常不希望看到的,而且违反了单一职责原则。门面应该只提供访问子系统的路径。我们再来封装一下,在子系统中再封装一下 ```java /** * 寄信流程 */ public class LetterProcess { private LetterWriter letterWriter = new LetterWriter(); private Police police = new Police(); public void sendLetter(String context, String address) { letterWriter.writeContext(context); letterWriter.writeAddress(address); police.checkLetter(letterWriter); letterWriter.putIntoEnvelope(); letterWriter.sendLetter(); } } ``` 然后修改门面: ```java public class PostOffice { private LetterProcess letterProcess = new LetterProcess(); public void sendLetter(String context, String address) { letterProcess.sendLetter(context, address); } } ``` ok,大功告成。 ##### 注意事项 * 一个子系统可以有多个门面,例如: * 门面已经非常庞大,可以按功能分类拆分(增删改查) * 多个高层系统访问门面,权限不同,可访问的方法不同,根据权限创建不同的门面 * 门面不参与子系统业务逻辑 * 子系统的业务逻辑是会经常变动的,但门面在系统投入使用后应该保持稳定。 子系统不应该依赖门面才能被访问。 ##### 门面模式 vs 适配器模式 门面模式其实和上一节的适配器模式非常非常相似,他们都封装了(来自一个类或多个类的)一组接口,让客户与子系统解耦。但他们的意图完全不同: * 适配器模式:将一组接口转化为另一组不同的接口 * 门面模式:简化接口