常见设计模式八股笔记
-1.设计原则
1.单一职责原则
单一职责原则:一个类或者模块只负责完成一个职责(或者功能)
一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以, 我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构(后面的章节中我们会讲到)。
2.开闭原则
“对扩展开放、对修改关闭”。添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
怎么实现“对扩展开放、对修改关闭”?
在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。
我们还要善于识别代码中的可变部分和不可变部分,把可变部分封装
0.设计模式的划分
1.1 根据模式的目的划分
根据模式是用来完成什么样的工作来划分,这种方法可分为创建型模式、结构型模式、行为型模式3种。
1.1.1 创建型设计模式
用于描述“怎么创建对象”。它的主要特点是“将对象的创建与使用分离”。如,单例、原型、工厂方法、抽象工厂、建造者等5种创建型模式。
创建型设计模式主要解决对象的创建问题,封装复杂的创建过程,以及解耦对象的创建代码和使用代码。其中,单例模式用来创建全局唯一的对象;工厂模式用来创建类型不同但相关的对象(继承同一父类或接口的一组子类),由给定的参数来决定创建哪种类型的对象:建造者模式用来创建复杂的对象,可以通过设置不同的可选参数,定制化地创建不同的对象;
1.1.2 结构型模式
用于描述“如何将类或对象按某种布局组成更大的结构”。如,代理、适配器、桥接、装饰、外观、享元、组合等7种结构型模式。
1.1.3 行为型模式
用于描述“类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责”。如,模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录模式、解释器等11中行为模式。
1.2 根据模式的作用划分
根据模式的主要用于类上还是主要用户对象上来分,这种方式可分为类模式和对象模式两种。
1.2.1 类模式
用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时便确定下来了。如,工厂方法、(类)适配器、模板方法、解释器等4种类模式。
1.2.2 对象模式
用户处理对象之间关系的,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
类模式 |
工厂方法 |
(类)适配器 |
模板方法 解释器 |
对象模式 |
单例 原型 抽象工厂 建造者 |
代理 (对象)适配器 桥接 装饰 外观 享元 组合 |
策略 命令 职责链 状态 观察者 中介者 迭代器 访问者 备忘录 |
1. 工厂方法模式
1.什么是工厂模式?
工厂模式是一种创建型设计模式,它通过定义一个创建对象的接口来封装实例化对象的行为,让子类决定实例化哪一个类。
工厂模式的概念来源于传统的工厂生产流程,其主要目的是将对象的创建和使用分离,使得客户端不需要知道具体的产品类,只需通过工厂请求所需的产品即可。这样做的好处在于增加了系统的灵活性和可维护性,同时能够降低代码的耦合度。
2.工厂模式的具体作用
具体到工厂模式的作用,主要体现在以下几个方面:
- 解耦对象创建和使用过程:客户端代码不直接调用构造函数来创建对象,而是通过工厂类的接口来获取所需的对象,这样可以减少客户端与具体类之间的依赖关系。
- 降低重复代码:如果多个地方需要创建同一个复杂对象,可以通过工厂方法统一管理,减少重复的代码,并便于后期维护。
- 增强系统的扩展性:当系统需要新增产品时,只需要扩展新的工厂类而不用修改原有代码,符合开闭原则。
- 隐藏具体实现:客户端只关心产品的接口而不是具体的实现,使得在不改变客户端代码的情况下更换或者升级产品成为可能。
- 提高代码的可管理性和灵活性:通过工厂模式,可以方便地管理和切换不同的产品实现,适应业务需求变化。
总之,工厂模式不仅有助于构建清晰、易于维护和扩展的代码结构,而且能够在多产品或变体间提供灵活的切换机制,是面向对象设计中常用的一种高效设计模式。
将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。封装复杂的创建逻辑,调用者无需了解如何创建对象。
3.工厂模式的优缺点
工厂模式,特别是在软件工程中,主要目的是创建对象,同时将对象的创建过程和使用过程解耦。它包括几种不同的变体,如简单工厂、工厂方法、抽象工厂等。
工厂模式的优点主要包括:
- 封装性:客户端代码不需要知道如何创建所需的对象,只需传递正确的参数即可获取需要的对象。
- 灵活性和可维护性:当添加新产品时,只需扩展工厂类而不必修改客户端代码,这符合开闭原则,即对扩展开放,对修改封闭。
- 隔离变化:工厂模式把对象创建过程中易变的部分隔离起来,有助于控制变化。
然而,工厂模式也有一些缺点:
- 复杂性增加:由于引入了工厂类(在简单工厂模式中),系统的复杂性有所增加。尤其是在简单工厂模式中,如果产品类层次结构很复杂,那么工厂类的职责会变得沉重,因为它需要包含所有产品的创建逻辑。
- 违反单一职责原则:简单工厂模式可能会违反单一职责原则,因为工厂类既要负责创建对象,又要包含业务逻辑判断。
- 扩展困难:每次新增或者删除产品时,可能需要修改工厂类的代码,尤其是在简单工厂模式中,这一点尤为明显。
综上所述,工厂模式通过封装和隔离对象创建的细节,提供了一种灵活且易于维护的方式来管理对象的生命周期。但同时,它也增加了系统的复杂性并可能限制了其扩展性。在应用工厂模式时,应根据具体场景仔细考量这些优缺点,以实现最佳的设计决策。
单一职责原则
单一职责原则(Single Responsibility Principle,简称SRP)是面向对象编程中的一个重要设计原则。它的核心思想是:一个类或者模块应该只负责一项职责,如果有多个职责,就应该拆分成多个类或模块。
这个原则的主要目的是为了降低代码的复杂性,提高代码的可读性和可维护性。如果一个类承担了过多的职责,那么当其中一个职责发生变化时,可能会影响到其他的职责,这样就增加了代码的耦合度,降低了代码的可维护性。
例如,假设有一个类叫做“Employee”,它既负责处理员工的工资计算,又负责处理员工的考勤记录。那么当工资计算的规则发生变化时,可能会影响到考勤记录的功能,反之亦然。如果我们按照单一职责原则来设计,就应该将“Employee”类拆分为两个类,一个负责工资计算,一个负责考勤记录,这样就可以降低代码的耦合度,提高代码的可维护性。
开闭原则
开闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
4.3种工厂模式
三种工厂
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
1.2 简单工厂模式
简单工厂不是一种设计模式,反而比较像是一种编程习惯。把对象的
1.2.1 结构创建交给工厂类
简单工厂包含如下角色:
- 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品 :实现或者继承抽象产品的子类
- 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。
1.2.2 实现
现在使用简单工厂对上面案例进行改进,类图如下:
interface AbstractProduct{ public void dosomething(); } class ConcretProduct1 implements AbstractProduct{ @Override public void dosomething() { System.out.println("1"); } } class ConcretProduct2 implements AbstractProduct{ @Override public void dosomething() { System.out.println("2"); } } //具体工厂 class Factory{ public AbstractProduct createProduct(String type){ AbstractProduct product = null; if("concretProduct1".equals(type)){ product = new ConcretProduct1(); }else if("concretProduct2".equals(type)){ product = new ConcretProduct2(); } return product; } } public class Test { //get方法必须要是static修饰的 public static AbstractProduct getProduct(String type){ Factory factory = new Factory(); AbstractProduct product = factory.createProduct(type); return product; } public static void main(String[] args) { AbstractProduct product = getProduct("concretProduct2"); product.dosomething(); } }
工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactory,CoffeeStore类中的orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类(具体产品)的耦合,同时又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。
后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。
1.2.3 优缺点
优点:
封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。
缺点:
增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。
1.3 工厂方法模式
针对上例中的缺点,使用工厂方法模式就可以完美的解决,完全遵循开闭原则。
1.3.1 概念
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。具体工厂只用来创建具体产品
1.3.2 结构
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
1.3.3 实现
使用工厂方法模式对上例进行改进,类图如下:
流程:
代码如下:
interface AbstractProduct{ public void dosomething(); } class ConcretProduct1 implements AbstractProduct{ @Override public void dosomething() { System.out.println("1"); } } class ConcretProduct2 implements AbstractProduct{ @Override public void dosomething() { System.out.println("2"); } } interface AbstractFactory{ public AbstractProduct createProduct(); } //对应的具体工厂 class ConcretProduct1Factory implements AbstractFactory{ @Override public AbstractProduct createProduct() { return new ConcretProduct1(); } } class ConcretProduct2Factory implements AbstractFactory{ @Override public AbstractProduct createProduct() { return new ConcretProduct2(); } } public class Test { private static AbstractFactory factory; //构造函数指定某个工厂 public Test(AbstractFactory factory){ this.factory = factory; } //get方法必须要是static修饰的 public static AbstractProduct getProduct(String type){ AbstractProduct product = factory.createProduct(); return product; } public static void main(String[] args) { Test test = new Test(new ConcretProduct1Factory()); AbstractProduct product = getProduct("concretProduct2"); product.dosomething(); } }
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
1.3.4 优缺点
优点:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
- 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;
缺点:
- 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
1.4 抽象工厂模式
本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示
- 产品族:一个品牌下面的所有产品;例如华为下面的电脑、手机称为华为的产品族;
- 产品等级:多个品牌下面的同种产品;例如华为和小米都有手机电脑为一个产品等级;
1.4.1 概念
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂
1.4.2 结构
抽象工厂模式的主要角色如下:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
1.4.4 优缺点
优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
1.5 Spring中使用到的工厂设计模式
Spring使用工厂模式可以通过 BeanFactory
或 ApplicationContext
创建 bean 对象。
spring中的BeanFactory用的是工厂模式里面的简单工厂模式(由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类),根据传入一个唯一的标识来获得bean对象
两者对比:
BeanFactory
:延迟注入(使用到某个/ bean 的时候才会注入),相比于ApplicationContext
来说会占用更少的内存,程序启动速度更快。ApplicationContext
:容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory
仅提供了最基本的依赖注入支持,ApplicationContext
扩展了BeanFactory
,除了有BeanFactory
的功能之外还有额外更多功能,所以一般开发人员使用ApplicationContext
会更多。
2 策略模式
见:设计模式之策略模式 | Java学习&面试指南-程序员大彬 (topjavaer.cn)
1.什么是策略模式?
策略模式是一种行为型设计模式,用于在运行时选择不同的算法或者策略。
策略模式的核心思想是将一系列的算法或行为封装起来,使得它们可以相互替换。这种模式主要有三个组成部分:抽象策略类(Strategy),具体策略类(Concrete Strategy),以及环境类(Context)。抽象策略类定义了一个公共接口,让所有具体策略类实现这个接口,确保它们可以互换使用。具体策略类实现了抽象策略中定义的接口,提供具体的算法实现。环境类则用来维护对某个具体策略对象的引用,并在需要的时候调用其算法。
2.策略模式的作用
策略模式的主要用途包括封装多种算法、动态切换算法、避免使用多重条件转移语句、提供管理相关的操作等。具体内容如下:
- 封装多种算法:当系统需要支持多种算法或行为,且需要在运行时根据不同情况选择合适的算法时,可以使用策略模式来避免硬编码这些选择逻辑。
- 动态切换算法:策略模式允许客户端在运行时根据不同的情况动态地切换算法或行为。这一点特别适用于那些在不同条件下行为差异很大的场合。
- 避免使用多重条件转移语句:通过策略模式,可以将一系列相关的条件分支逻辑封装在不同的策略类中,从而减少原有业务代码中复杂的条件判断。
- 提供管理相关的操作:策略模式不仅分离了算法的定义和使用,还提供了一种途径来管理相关的操作,比如获取当前正在使用的策略或切换到另一种策略。
2.策略模式角色组成
策略模式主要由这三个角色组成,环境角色(Context)、抽象策略角色(Strategy)和具体策略角色(ConcreteStrategy)。
- 环境角色(Context):也叫做上下文角色, 起承上启下封装作用, 屏蔽高层模块对策略、 算法的直接访问,封装可能存在的变化。
- 抽象策略角色(Strategy):这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略角色(ConcreteStrategy):包装了相关的算法或行为。
3.具体代码
//**抽象策略角色**(Strategy) public interface Strategy { // 策略模式的运算法则 public void doSomething(); }
//**具体策略角色**(ConcreteStrategy) public class ConcreteStrategy1 implements Strategy{ @Override public void doSomething() { System.out.println("ConcreteStrategy1"); } } public class ConcreteStrategy2 implements Strategy{ @Override public void doSomething() { System.out.println("ConcreteStrategy2"); } }
//上下文角色 public class Context { // 抽象策略 private Strategy strategy = null; // 构造函数设置具体策略 public Context(Strategy strategy) { this.strategy = strategy; } // 封装后的策略方法 public void doAnything(){ this.strategy.doSomething(); } }
测试:
public class StrategyClient { public static void main(String[] args) { // 声明一个具体的策略 Strategy strategy = new ConcreteStrategy1(); // 声明上下文对象 Context context = new Context(strategy); // 执行封装后的方法 context.doAnything(); } }
4.使用策略模式简化多重if-else场景
假设我们要处理一个office文件,分为三种类型 docx、xlsx、pptx,分别表示Word文件、Excel文件、PPT文件,根据文件后缀分别解析。
常规写法:
public class OfficeHandler { public void handleFile(String filePath){ if(filePath == null){ return; } String fileExtension = getFileExtension(filePath); if(("docx").equals(fileExtension)){ handlerDocx(filePath); }else if(("xlsx").equals(fileExtension)){ handlerXlsx(filePath); }else if(("pptx").equals(fileExtension)){ handlerPptx(filePath); } } public void handlerDocx(String filePath){ System.out.println("处理docx文件"); } public void handlerXlsx(String filePath){ System.out.println("处理xlsx文件"); } public void handlerPptx(String filePath){ System.out.println("处理pptx文件"); } private static String getFileExtension(String filePath){ // 解析文件名获取文件扩展名,比如 文档.docx,返回 docx String fileExtension = filePath.substring(filePath.lastIndexOf(".")+1); return fileExtension; } }
处理逻辑全部放在一个类中,会导致整个类特别庞大,假设我们要新增一种类型处理,比如对于2007版之前的office文件,后缀分别是 doc/xls/ppt,那我们得增加 else if 逻辑,违反了开闭原则,如何解决这种问题呢,答案就是通过策略模式。
public interface OfficeHandlerStrategy { void handlerOffice(String filePath); } public class OfficeHandlerDocxStrategy implements OfficeHandlerStrategy { @Override public void handlerOffice(String filePath) { System.out.println("处理docx"); } } // 省略 OfficeHandlerXlsxStrategy/OfficeHandlerPptxStrategy 类 public class OfficeHandlerStrategyFactory { private static final Map<String,OfficeHandlerStrategy> map = new HashMap<>(); static { map.put("docx",new OfficeHandlerDocxStrategy()); map.put("xlsx",new OfficeHandlerXlsxStrategy()); map.put("pptx",new OfficeHandlerPptxStrategy()); } public static OfficeHandlerStrategy getStrategy(String type){ return map.get(type); } } //测试 public class OfficeHandlerStrategyClient { public static void main(String[] args) { String filePath = "C://file/123.xlsx"; String type = getFileExtension(filePath); OfficeHandlerStrategy strategy = OfficeHandlerStrategyFactory.getStrategy(type); strategy.handlerOffice(filePath); } private static String getFileExtension(String filePath){ // 解析文件名获取文件扩展名,比如 文档.docx,返回 docx String fileExtension = filePath.substring(filePath.lastIndexOf(".")+1); return fileExtension; } }
4.策略模式优缺点
策略模式作为一种设计模式,它的优势和局限性都是相对明显的。以下是策略模式的优缺点:
优点:
- 开闭原则:策略模式支持开闭原则,意味着系统可以在不修改原有代码的基础上引入新的策略类,实现新的行为或算法。这有助于系统维护和扩展。
- 避免条件语句:通过策略模式,可以将多重条件语句替换为对策略对象的调用,这样可以减少条件判断,使系统更加清晰易读。
- 自由切换算法:策略模式允许客户端在运行时根据不同情况选择或切换算法,提高了系统的灵活性。
- 良好的扩展性:由于策略模式的结构清晰,新策略的添加通常不会影响其他代码,这使得系统具有良好的扩展性。
- 管理算法族:如果系统中存在多个相似算法,策略模式可以通过继承机制来共享代码,减少重复代码。
缺点:
- 策略类数量增加:每增加一个策略,就需要增加一个具体的策略类,可能导致类的数量增多。
- 对外暴露策略:所有的策略类都需要对外暴露,这可能会增加系统的复杂性。
在实际使用策略模式时,需要权衡其优势和局限性,确保它适用于当前的应用场景。例如,在有多算法或行为需要在不同场景下互换使用的情况下,策略模式是一个不错的选择。同时,也要注意控制策略类的数量,避免系统过于复杂难以维护。
5.策略模式应用场景
策略模式用于实现算法的互换,使得算法可以独立于客户端独立变化。以下是该模式的具体作用:
- 行为切换:当系统存在多个类,且它们的主要区别在于行为时,策略模式允许客户端在运行时动态选择需要的行为。这有助于避免使用多重继承来处理不同的行为,简化了类的设计与维护。
- 算法选择:策略模式适用于那些需要从多种算法中动态选择一种来执行的情况。例如,根据用户的不同需求,可能需要选择不同的排序或支付算法。这种模式使得算法可以独立于客户端进行变更和扩展,符合开闭原则。
3 责任链设计模式
1.什么是责任链模式?
责任链模式是一种行为型设计模式,允许多个对象按照顺序处理请求,每个对象可以自行决定是否处理或将请求传递给链上的下一个对象。
责任链模式通过创建一系列处理对象,每个对象都有机会处理请求。这些对象连成
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
我的笔记专栏,内有自己整理的八股知识笔记和算法刷题笔记,我会不断通过他人和自己的面经来更新和完善自己的八股笔记。专栏每增加一篇文章费用就会上涨一点,如果你喜欢的话建议你尽早订阅。内有超详细苍穹外卖话术!后续还会更新其他项目和我的实习经历的话术!敬请期待!