设计模式
六大设计原则
单一职责(Single Reposibility Principle)
- 定义:一个类或者模块只负责完成一个职责。
- 优点:降低类的复杂度、提高类的可读性,提高系统的可维护性、降低变更引起的风险。
- 缺点:最大的缺点就是会增加类的数量,所以单一职责的界定就尤为重要。
- 使用场景:根据实际业务和项目经验,每个类应该具有一个明确的单一职责,方法也是一样,对于类、方法、模块和包等设计同理。
里 氏 替 换 原 则(Liskov Substitution Principle)
- 定义:多用组合,少用继承。(针对继承而言)
- 代码重用:为了共享方法,那么父类的方法就因该保持不变,不能被子类重新定义,子类只能通过新添加方法来扩展功能,父类和子类都可以实例化。
- 多态:将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
- 优点:代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;提高代码的重用性;提高代码的可扩展性;提高产品或项目的开放性。
- 缺点:
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;降低代码的灵活性。
- 子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
- 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。
- 使用场景:里氏替换原则主要是为了代码的共用性,在继承共性的同时,有支持子类拥有一定的个性。但是一定要避免太个性化的子类的诞生,如果一个子类他的方法大多都是个性化的,那么就没必要继承父类,否则会让代码间的耦合关系变得混乱。
依赖倒置原则(Dependence Inversion Principle)
- 定义:下层模块引入上层模块的依赖,改变原有自上而下的依赖方向。(设计代码结构时,高层模块不应该依赖低层模块,二者都应该依赖其抽象。)面向接口编程。
- 优点:可以减少类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并且能够降低修改程序所造成的风险。
- 缺点:
- 实现难度高:依赖倒置原则的实现需要使用抽象类或接口,这会增加代码的复杂性和开发难度。同时,为了实现依赖倒置,对象的创建需要使用对象工厂,以避免对具体类的直接引用,这也会增加代码的复杂性。
- 类数量增加:为了实现依赖倒置原则,需要设计和实现大量的抽象类和接口,这将增加类数量和项目规模,也可能对性能产生一定的影响。
- 过度设计:有时候,过度设计和抽象可能使代码变得难以理解和维护。当一个具体类已经非常稳定并且不会发生变化时,客户端完全可以依赖于这个具体类型,不必为此发明一个抽象类型。
- 不适用于所有场景:尽管依赖倒置原则对于一些大型、复杂的系统来说非常有用,但并不是所有的场景都适合使用这个原则。例如,对于一些小型、简单的项目或模块来说,过度使用依赖倒置可能会使代码变得更加复杂和难以管理。
- 使用场景:依赖倒置原则的核心思想是将具体的实现依赖于抽象的接口,而非具体的类。这种思想可以在各种场景中应用,包括模拟场景和程序设计中。
接口隔离原则
-
定义:建立单一接口,不要建立臃肿庞大的接口。接口尽量细化,同时接口中的方法尽量少。接口尽量小不违反单一职责原则,高内聚提高接口有、类、模块的处理能力,减少对外的交互。通过高质量接口的组装实现服务的定制化。
例如:存在类A和类B,类B实现了接口InterfaceC,类A通过接口依赖类B,这时候应该确保接口只包括类A所依赖的方法。
-
优点:
- 降低耦合度:接口隔离原则可以减少类之间的依赖关系,使得代码更加模块化,降低耦合度。
- 提高可维护性:通过定义最小接口和拆分大型接口,可以使接口更加清晰,易于理解和维护。
- 提高可扩展性:接口隔离原则可以使得系统更加可扩展,因为新的接口可以很容易地添加到现有系统中,而不会对其他部分造成太大的影响。
- 提高代码质量:使用接口隔离原则可以减少代码中的错误和缺陷,提高代码质量。
-
缺点:
- 接口数量过多:如果过度拆分接口,可能会导致接口数量过多,使得代码更加复杂,难以理解和维护。
- 实现难度增加:拆分接口会增加代码的复杂度和实现难度,因为需要定义更多的接口和实现更多的方法。
- 可能会降低性能:由于拆分接口需要创建更多的对象和调用更多的方法,可能会对性能产生一定的影响。
- 需要更多的开发时间:由于拆分接口需要更多的开发时间和工作量,可能会增加开发成本。
-
使用场景:
- 接口隔离原则可以应用于单一职责原则的实践中
- 在备忘录模式中,可以使用接口隔离原则来定义备忘录接口和参与者接口,使得备忘录模式中的组件之间的依赖关系更加清晰,提高可维护性和可扩展性。
- 工厂模式中,可以使用接口隔离原则来定义工厂接口和产品接口,使得工厂模式中的组件之间的依赖关系更加清晰,提高可维护性和可扩展性。
- 策略模式中,可以使用接口隔离原则来定义策略接口和算法接口,使得策略模式中的组件之间的依赖关系更加清晰,提高可维护性和可扩展性。
迪米特法则 / 最少知识原则(Law of Demeter)
- 定义:一个对象应该对其他对象保持最少的了解,也就是说,一个类应该只与直接的朋友进行通信。这个原则是设计模式中的一种规范,其目的是降低类之间的耦合度,使得系统更加模块化和易于维护。
- 优点:
- 降低类之间的耦合度:通过减少一个类对其他类的依赖和了解,可以降低系统的复杂性和耦合性,使得代码更加模块化和易于维护。
- 提高模块的相对独立性:迪米特法则鼓励将模块解耦为独立的子模块,每个子模块都具有自己的职责和功能。这种相对独立性使得代码更加清晰,易于理解和维护。
- 提高代码的可复用性和可扩展性:由于类之间的耦合度降低,可以更加方便地将模块复用和扩展到新的应用场景中。这减少了代码的重复性,提高了软件的可重用性。
- 缺点:
- 造成系统的不同模块之间的通信效率降低:由于一个类只能与直接的朋友进行通信,这可能会使得系统的不同模块之间的通信效率降低。这可能会影响到系统的性能和响应速度。
- 使系统的不同模块之间不容易协调:由于每个模块只能与直接的朋友进行通信,这可能会导致不同模块之间难以协调和配合。这可能会影响到系统的整体功能和性能。
- 增加系统的复杂度:为了遵循迪米特法则,可能需要将系统划分为更多的模块和类。这可能会增加系统的复杂度和开发成本。
- 缺点应对:解决迪米特法则缺点的方式主要是通过依赖倒转原则(Dependency Inversion Principle,DIP),也就是要针对接口编程,不要针对具体编程。这可以通过使用接口或抽象类来实现,使得调用方和被调用方之间有一个抽象层,被调用方在遵循抽象层的前提下可以自由地变化。这样可以减少类之间的耦合度,提高系统的可维护性和可扩展性。
- 使用场景:
- 创建松耦合的系统:耦合是指一个类对另一个类的依赖程度。迪米特法则鼓励创建松耦合的系统,这意味着每个类都只依赖于其他类中的最少部分。通过遵循这个原则,可以降低系统的复杂性和耦合性,提高代码的可维护性和可扩展性。
- 模块化设计:迪米特法则鼓励将系统划分为独立的模块或组件,每个组件都有自己的职责和功能。通过模块化设计,可以降低系统的复杂度,提高代码的可重用性和可维护性。
- 控制反转(IoC)容器:在Java中,控制反转(Inversion of Control,IoC)容器是遵循迪米特法则的典型示例之一。IoC容器负责创建、配置和管理对象之间的关系,从而降低了类之间的耦合度,提高了代码的可扩展性和可维护性。
- 设计数据库访问层:在设计数据库访问层时,迪米特法则可以用来规范类之间的关系。例如,一个类应该只与数据库中的一个表或视图进行交互,而不应该与其他表或视图发生耦合。这可以提高代码的可维护性和可重用性。
- 分布式系统:在分布式系统中,迪米特法则可以用来规范不同组件之间的通信。每个组件应该只与其直接朋友进行通信,而不应该与其他组件发生耦合。这可以提高系统的可扩展性和可靠性。
开闭原则
- 定义:类、方法、模块应该对扩展开放,对修改关闭。(添加一个功能应该是在已有的代码基础上进行扩展,而不是修改已有的代码。)
- 优点:
- 保持软件产品的稳定性:通过保持原有代码不变,添加新代码来实现软件的变化,这样可以避免为实现新功能而改坏线上功能的情况,避免老用户的流失。
- 不影响原有测试代码的运行:软件开发规范性好的团队都会写单元测试,如果原有的某个功能发生了变化,则单元测试代码也应做相应的变更,否则就有可能导致测试出错。而开闭原则可以让单元测试充分发挥作用而又不会成为后期软件开发的累赘。
- 使代码更具模块化,易于维护:通过将不同的功能和测试代码独立存在不同的单元模块中,可以使得代码更具模块化,易于维护。一旦某个功能出现问题,可以很快地锁定代码位置做出修改,而由于模块间代码独立不相互调用,更改一个功能的代码也不会引起其他功能的崩溃。
- 缺点:
- 增加了代码复杂性和开发成本:开闭原则要求保持原有代码不变,添加新代码来实现软件的变化,因此,对于一些需要对原有代码进行修改的需求,需要额外开发新的代码来实现,这增加了代码的复杂性和开发成本。
- 需要更多的测试和验证:开闭原则要求对原有的每个功能都进行单元测试,以确保新添加的代码不会对原有的功能产生影响。因此,需要进行更多的测试和验证,以确保软件的质量和稳定性。
- 对设计的要求较高:开闭原则需要将不同的功能和测试代码独立存在不同的单元模块中,这要求设计者需要具备良好的设计能力和经验,才能够有效地实现开闭原则。
- 使用场景:
- 在开发新产品或新功能时,需要尽可能地减少对现有代码的修改,而是通过添加新代码来实现变化。这样可以避免破坏现有功能,保证软件产品的稳定性。
- 当需要对软件产品进行升级或改进时,需要尽可能地避免对原有代码的修改,而是通过添加新代码来实现升级或改进。这样可以减少对原有代码的破坏,保证软件产品的可靠性。
- 在开发可重用或可扩展的组件或框架时,需要遵循开闭原则。这样可以使得组件或框架具有良好的扩展性和灵活性,以适应不断变化的需求和技术环境。
- 在开发测试驱动的软件产品时,开闭原则可以帮助保持测试代码的独立性和可维护性。每个测试用例应该是一个独立的单元模块,只对该测试用例负责。这样可以使得测试代码更加清晰、易于理解和维护。
设计模式
创建型设计模式
- 定义:主要关注点是如何创建对象,将对象的创建和使用进行分离,从而降低系统的耦合度,使用者不必关系对象的创建细节。
- 优点:单例、原型、工厂、建造者、模式中对应优点
- 缺点:单例、原型、工厂、建造者、模式中对应缺点
- 使用场景:单例、原型、工厂、建造者、模式中对应的使用场景
1单例模式(Singleton)
-
定义:确保一个类只有一个实例,并提供一个全局访问点。
-
优点:
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
-
缺点:
1、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
2、单例类的职责过重,在一定程度上违背了“单一职责原则”。
3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而 出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。 -
使用场景:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
-
实现方式:
-
饿汉式:在类加载的时候,静态实例
instance
就已创建并初始化好了。/** *优点:单例对象的创建是线程安全的;获取单例对象时不需要加锁。 *缺点:不能延时加载。 **/ public class Singleton { private static final Singleton instance = new Singleton(); private Singleton () {} public static Singleton getInstance() { return instance; } }
-
懒汉式:支持延时加载,将对象的创建延迟到了获取对象的时候,但为了线程安全,不得不为获取对象的操作加锁,这就导致了低性能。
/** *优点:对象的创建时线程安全的,支持延迟加载。 *缺点:获取对象的操作被加上了锁,影响了并发度(如果该对象不会频繁使用则可忽略)。 **/ public class Singleton { private static final Singleton instance; private Singleton () {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
-
双重校验:懒汉式的改进,将懒汉式中的的
synchronized
方法改成了synchronized
代码块/** *优点:对象的创建是线程安全的;支持延时加载;获取对象时不需要加锁。 **/ public class Singleton { private static Singleton instance; private Singleton () {} public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { // 注意这里是类级别的锁 if (instance == null) { // 这里的检测避免多线程并发时多次创建对象 instance = new Singleton(); } } } return instance; } }
-
静态内部类:Java 加载外部类的时候,不会创建内部类的实例,只有在外部类使用到内部类的时候才会创建内部类实例。(利用了静态内部类的特性)
/** *只有当调用 getInstance() 方法时,SingletonInner 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建 *过程的线程安全性,都由 JVM 来保证。 *优点:对象的创建是线程安全的;支持延时加载;获取对象时不需要加锁。 **/ public class Singleton { private Singleton () {} private static class SingletonInner { private static final Singleton instance = new Singleton(); } public static Singleton getInstance() { return SingletonInner.instance; } }
-
枚举类:通过 Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。
public enum Singleton { INSTANCE; // 该对象全局唯一 }
-
2原型模式 (Prototype)
-
定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。也就是说,这种不通过new关键字来产生一个对象,而是通过对象复制(Java中的clone或反序列化)来实现的模式,就叫做原型模式。
-
优点:
- 通过内存二进制流的复制,在性能上比直接使用new的方式创建对象要快,可以更快速的创建对象。
- 当创建的对象实例较为复杂时,使用原型模式可以简化对象的创建过程。
- 可以在运行时动态的获取对象的类以及状态,从而创建一个对象。
-
缺点:
- 需要为每一个类都配备一个clone方法,当对已有的类进行改造的时候,需要修改源代码,这可能违背了开闭原则。
- 实现深克隆时需要编写较为复杂的代码,当对象之间存在多重嵌套引用的时候,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来比较困难。
-
使用场景:
- 类的初始化需要消耗非常多的资源。
- 通过new产生一个对象需要非常繁琐的数据准备或访问。
- 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时。
-
实现方式:
/** *定义了一个Circle类,它实现了Cloneable接口,并重写了clone()方法。在clone()方法中,我们调用了super.clone()方法,它会返 *回一个浅拷贝的对象。由于Circle类中的所有字段都是基本类型,因此浅拷贝和深拷贝的效果是一样的。如果需要实现深拷贝,我们需要 *在每一个需要被深拷贝的类中重写clone()方法,并在其中实现深拷贝的逻辑。 */ public class Circle implements Cloneable { private int x; public Circle(int x) { this.x = x; } @Override public Circle clone() { try { return (Circle) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); return null; } } }
3简单工厂(SimpleFactory)
-
定义:简单工厂其实不是一个设计模式,反而比较像是一种编程习惯。(建立一个工厂类,对实现了同一接口的一些类进行实例的创建。)
-
优点:
- 逻辑简单,可以提高系统的灵活性,使用者只需关注如何通过工厂方法获取对象并调用方法,而不用去关注对象的创建。
- 实现了对象的使用和创建的隔离,使用者只关心如何使用对象,而不关心对象的创建过程,减少了代码的耦合度。
- 在一定程度上提高了系统的可维护性,使用者只需要了解工厂类的使用方法,不需要了解具体的对象创建过程。
-
缺点:
- 由于工厂类集中了所有产品的创建逻辑,一旦出现问题,整个系统都会受到影响,职责过重。
- 增加新产品时需要修改工厂类的代码,不符合开闭原则,系统拓展困难。
- 在工厂类中使用静态方法,无法形成基于继承的等级结构,造成工厂角色无法拓展。
- 使用了静态工厂方法,造成具体工厂角色不能实现继承,也就是说工厂类无法再拓展。
- 增加了系统的复杂度和理解难度,因为需要引入新的工厂类和静态方法。
-
使用场景:
- 需要完全封装隔离具体实现,让外部只能通过接口来操作封装类。
- 需要把对外创建对象的职责集中管理和控制。
-
实现方式:
// 手机接口 public interface Phone { public void call(); } // 苹果手机实现类 public class IPhone implements Phone { @Override public void call() { System.out.println("用苹果手机打电话!"); } } // 小米手机实现类 public class MPhone implements Phone{ @Override public void call() { System.out.println("用小米手机打电话!"); } } // 生产手机的工厂类 public class PhoneFactory { public Phone create(String type){ if (type.equals("IPhone")){ return new IPhone(); }else if (type.equals("MPhone")){ return new MPhone(); }else return null; } }
4工厂方法模式(FactoryMethod)
-
定义:定义了一个创建对象的接口(类或接口中的方法),但由子类决定要实例化的类是哪一个。工厂方法把实例化推迟到子类。(对简单工厂模式的改进,使用一个工厂接口,创建多个工厂类,每个工厂创建对应的对象。)
-
优点:
- 增加新产品时,只需要创建一个对应的具体工厂类,而不需要修改客户端代码和抽象工厂类,符合开闭原则。
- 将客户端代码与具体产品类的实例化分离,降低了代码耦合度。
- 抽象工厂类可以提供更好的扩展性,当需要增加一个全新的产品族时,只需要创建一个新的抽象工厂类,而不需要修改原有代码。
-
缺点:
- 每个具体工厂类只完成单一任务,如果需要增加新产品,就需要增加对应的工厂类,可能会增加系统的复杂度和理解难度。
- 工厂方法模式需要使用多态和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,如果需要在客户端代码中使用新产品类的方法,就需要引入新的抽象层和实现层,会增加系统的实现难度。
- 如果有多个产品族需要使用工厂方法模式,就需要为每个产品族都定义一个抽象工厂类和多个具体工厂类,这会增加系统的复杂度和维护难度。
-
使用场景:
- 客户端代码需要创建多个具有相似特征的对象,这些对象的创建过程需要封装起来,使得客户端代码只关注接口而不关注具体实现。
- 需要将对象的创建和使用过程解耦,降低客户端代码与具体对象之间的依赖关系。
- 需要在增加新产品时,只需要创建一个新的具体工厂类,而不需要修改客户端代码和抽象工厂类。
-
实现方式:
// 工厂接口 public interface PhoneFactory { public Phone create(); } // 小米手机工厂 public class MPhoneFactory implements PhoneFactory{ @Override public Phone create() { return new MPhone(); } } // 苹果手机工厂 public class IPhoneFactory implements PhoneFactory{ @Override public Phone create() { return new IPhone(); } } // 使用不同的工厂类创建对应的实例
5抽象工厂模式(AbstractFactory)
-
定义:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。(围绕一个超级工厂创建其他工厂,每个工厂可以生产不同类型的产品)
-
优点:
- 抽象工厂模式隔离了具体类的生成,使得客户端并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。
- 当一个产品族中的多个对象被设计成一起工作时,抽象工厂模式能够保证客户端始终只使用同一个产品族中的对象,增加了系统的内聚性。
- 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
-
缺点:
- 增加新的产品等级结构比较麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,可能会带来较大的不便。
- 如果系统中有多于一个的产品族,而每次只使用其中某一产品族,就需要创建对应的具体工厂,增加了系统的复杂性。
-
实现方式:
// 超级工厂 public interface Factory { public Phone createPhone(); public Book createBook(); } // 苹果工厂 public class AppleFactory implements Factory{ @Override public Phone createPhone() { return new IPhone(); } @Override public Book createBook() { return new MacBook(); } } // 小米工厂 public class XiaoMiFactory implements Factory{ @Override public Phone createPhone() { return new MPhone(); } @Override public Book createBook() { return new MiBook(); } } // 电脑接口类 public interface Book { public void play(); } // 苹果电脑接口实现类 public class MacBook implements Book{ @Override public void play() { System.out.println("用苹果电脑打游戏!"); } } // 小米电脑接口实现类 public class MiBook implements Book{ @Override public void play() { System.out.println("用小米电脑打游戏!"); } }
6建造者/生成器模式(Builder)
-
定义:使用生成器模式,可以封装一个产品的构造过程,并允许按步骤构造产品。
-
优点:
- 降低代码耦合度:通过将产品的构建与表示分离,使得客户端不需要知道产品的内部实现,只需得到产品的对象。同时,使用建造者和导演者分离组装过程和组件具体构造过程,具有灵活的扩展性。
- 优秀的扩展性:具体建造者相互独立,方便扩展,符合开闭原则。如果需要增加新的具体建造者,无需修改原有类库的代码,只需增加新的具体建造者即可。
- 控制产品创建过程:将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更加方便使用程序来控制创建过程。
-
缺点:
- 使用范围限制:建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
- 系统复杂性:如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很底大。
-
使用场景:
- 多个部件或零件都可以装配到一个对象中,但是产生的运行结果又不相同时,可以采用建造者模式。
- 如果一个对象有非常复杂的内部结构,比如说很多属性等,可以考虑使用建造者模式。
- 如果在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式。
- 如果需要将产品的创建过程和表示分离,使得客户端不必知道产品内部组成的细节,可以考虑使用建造者模式。
-
实现方式:
// 产品类 public interface Product { void use(); } // 具体产品类 public class ConcreteProduct implements Product { private String partA; private String partB; private String partC; public ConcreteProduct(String partA, String partB, String partC) { this.partA = partA; this.partB = partB; this.partC = partC; } @Override public void use() { System.out.println("Using product with partA: " + partA + ", partB: " + partB + ", partC: " + partC); } } // 建造者接口 public interface Builder { void buildPartA(); void buildPartB(); void buildPartC(); Product getProduct(); } // 具体建造者类 public class ConcreteBuilder implements Builder { private String partA; private String partB; private String partC; private Product product; @Override public void buildPartA() { partA = "A"; } @Override public void buildPartB() { partB = "B"; } @Override public void buildPartC() { partC = "C"; } @Override public Product getProduct() { product = new ConcreteProduct(partA, partB, partC); return product; } } // 导演者类 public class Director { private Builder builder; public Director(Builder builder) { this.builder = builder; } public void build() { builder.buildPartA(); builder.buildPartB();
结构型设计模式
-
定义:结构型模式描述如何将类或对象按某种布局组成更大的结构。分为类结构型模式(采用继承机制来组织接口和类)和对象结构型模式(釆用组合或聚合来组合对象)
-
分类:
结构型模式分为以下 7 种:
1、代理(Proxy):为某对象提供一种代理以控制对该对象的访问。即:客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
2、 适配器(Adapter):将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
3、 桥接(Bridge):将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现的,从而降低了抽象和实现这两个可变维度的耦合度。
4、装饰(Decorator):动态地给对象增加一些职责,即:增加其额外的功能。
5、门面(Facade):为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
6、享元(Flyweight):运用共享技术来有效地支持大量细粒度对象的复用。
7、组合(Composite):将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
1代理模式(Proxy)
-
定义:代理模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。使用代理模式创建代理对象,让代理对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。
-
优点:
- 隐藏实现细节:客户端只需要与代理对象交互便可,无需了解实现细节。
- 增强安全性:代理对象可以对客户端的请求进行过滤和验证,从而确保要求的合法性。
- 提高性能:代理对象可以缓存请求结果,从而减少对实现对象的访问次数,提高系统的响应速度。
- 实现松耦合:代理对象可以在客户端和实现对象之间起到中介的作用,使得系统的结构更加灵活和可扩大。
- 职责清晰:代理模式能够将客户端和真实主题分离,使得职责更加清晰,降低了系统的复杂性。
- 高扩展性:代理模式可以方便地增加新的代理对象,从而实现系统的扩展。
-
缺点:
- 增加复杂性:代理模式需要实现代理对象和实现对象之间的交互逻辑,增加了代码的复杂性和保护成本。
- 下降响应速度:由于代理对象需要处理客户端的请求,可能会增加系统的负担和延迟。
- 增加内存占用:代理对象需要缓存请求结果,可能会占用较多的内存空间。
- 不合适复杂对象:由于代理对象需要和实现对象保持一致的接口,可能会致使接口和代码的复杂性增加。
- 需要额外的工作:实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
-
使用场景:
- 控制对真实对象的访问:代理模式可以控制对真实对象的访问,只允许代理对象访问真实对象,从而起到保护作用。例如,需要对文件进行读写操作时,可以通过代理对象来控制对文件的访问,避免直接操作文件。
- 提高性能:代理模式可以提高性能,例如,需要对数据库进行频繁的访问时,可以通过代理对象来缓存数据,避免频繁的数据库查询。
- 实现职责分离:代理模式可以实现职责分离,将客户端和真实对象分离,使得职责更加清晰,降低了系统的复杂性。
- 扩展功能:代理模式可以对真实对象进行增强操作,例如,需要对某个方法进行日志记录或性能监控时,可以通过代理对象来增强该方法的功能。
- 简化接口:代理模式可以为复杂接口或类提供一个简化接口,使得使用者可以更加方便地使用该接口或类。
- 动态代理:代理模式可以用于动态代理,即在运行时动态地生成代理对象,从而实现更加灵活和动态的代理功能。
-
实现方式:
// 定义接口 public interface Interface { void method(); } // 定义被代理类 public class Proxyee implements Interface { @Override public void method() { System.out.println("Hello, I'm Proxyee."); } } // 定义代理类 public class Proxy implements Interface { private Interface proxyee; public Proxy(Interface proxyee) { this.proxyee = proxyee; } @Override public void method() { System.out.println("Before invoking method."); proxyee.method(); System.out.println("After invoking method."); } } // 客户端代码 public class Client { public static void main(String[] args) { Interface proxyee = new Proxyee(); Interface proxy = new Proxy(proxyee); proxy.method(); // 输出 "Before invoking method.", "Hello, I'm Proxyee.", "After invoking method." } }
2适配器模式(Adapter)
-
定义: 将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。
-
优点:
- 增强可复用性:适配器模式允许两个没有关联的类一起运行,提高了类的复用性。
- 提高灵活性:适配器模式增加了类的透明度,使得客户端在使用时不需要了解适配器的具体实现,从而提高了系统的灵活性。
- 增加可扩展性:适配器模式适用于有动机修改一个正常运行的系统的接口时,它可以使原有系统的功能得到扩展。
-
缺点:
- 增加系统复杂性:由于适配器模式需要额外编写适配器代码,因此会增加系统的复杂性。
- 增加系统可读性难度:由于适配器模式的实现逻辑可能比较复杂,因此会增加系统可读性难度。
- 使用场景有限:适配器模式适用于解决两个不兼容的接口之间的适配问题,但如果目标接口必须为抽象类,则适配器的使用会受到一定限制。
- 不适合大规模使用:如果系统中过多地使用适配器,会导致系统整体变得零乱,不易把握。
-
使用场景:
- 隔离设计缺陷:当我们需要对外部系统提供的接口进行二次封装,抽象出更好的接口设计时,可以使用适配器模式来隔离设计上的缺陷。
- 统一接口设计:当某个功能的实现依赖多个外部系统,我们可以通过适配器模式将它们的接口适配为统一的接口定义,从而方便使用多态的特性来复用代码逻辑。
- 替换依赖的外部系统:在替换项目中依赖的外部系统时,利用适配器模式可以减少对代码的改动。
- 兼容老版本接口:在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并标注为 deprecated,并将内部实现逻辑委托为新的接口实现。这样做的好处是让使用它的项目有个过渡期,而不是强制进行代码修改。
- 适配不同格式的数据:适配器模式还可以用在不同格式的数据之间的适配。
-
实现方式:
// 目标接口 public interface Target { void request(); } // 适配者接口 public interface Adaptee { void specificRequest(); } // 适配器类 public class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override public void request() { adaptee.specificRequest(); } } // 适配者实现类 public class ConcreteAdaptee implements Adaptee { @Override public void specificRequest() { System.out.println("Specific request."); } } // 客户端代码 public class Client { public static void main(String[] args) { Adaptee adaptee = new ConcreteAdaptee(); Target target = new Adapter(adaptee); target.request(); // 输出 "Specific request." } }
3桥接模式(Bridge)
-
定义:将抽象部分和实现部分,分离解耦,使得两者可以独立地变化。桥接模式通过将实现和抽象放在两个不同的类层次中而使它们可以独立变化。
-
优点:
- 减少子类数量:桥接模式可以减少子类的数量,从而降低管理和维护的成本。
- 提高可扩展性:桥接模式提高了系统的可扩充性,当需要在两个变化维度中任意扩展一个维度时,都不需要修改原有系统。
- 符合开闭原则:桥接模式符合开闭原则,可以把两个变化的维度连接起来,通过桥接模式可以使它们在抽象层建立一个关联关系。
-
缺点:
- 增加理解和设计难度:由于桥接模式涉及到抽象化和实现化的分离,因此可能会增加系统的理解和设计难度。
- 识别独立变化维度不易:要使用桥接模式,需要正确地识别出系统中两个独立变化的维度,这并不是一件容易的事情。
-
使用场景:
- 当一个类存在多个维度的变化,且每个维度都可以独立地进行扩展时,可以使用桥接模式。通过桥接模式,可以避免类的爆炸性增长,将各个维度的变化分离开来,使系统更加灵活。
- 当需要将一个类的抽象部分和实现部分分离,并且让它们能够独立地变化时,可以使用桥接模式。这样可以使抽象部分和实现部分可以独立地进行扩展和修改,而不会相互影响。
-
实现方式:
// 抽象化角色 public abstract class Abstraction { protected Implementor implementor; public Abstraction(Implementor implementor) { this.implementor = implementor; } public abstract void operation(); } // 扩展抽象化角色 public class RefinedAbstraction extends Abstraction { public RefinedAbstraction(Implementor implementor) { super(implementor); } @Override public void operation() { implementor.operation(); } } // 实现化角色 public interface Implementor { void operation(); } // 具体实现化角色 public class ConcreteImplementor implements Implementor { @Override public void operation() { System.out.println("Concrete operation."); } } // 客户端代码 public class Client { public static void main(String[] args) { Implementor implementor = new ConcreteImplementor(); Abstraction abstraction = new RefinedAbstraction(implementor); abstraction.operation(); // 输出 "Concrete operation." } }
4装饰者模式(Decorator)
-
定义:动态地将责任附加到对象上。若要扩展功能,装饰者提供了比继承更弹性的替代方案。
-
优点:
- 扩展功能:装饰者模式可以动态地给一个对象添加职责或者扩展功能,这样可以实现更加灵活的功能扩展。
- 多种方式扩展:通过使用不同的装饰者类以及它们的组合,可以以多种方式扩展功能,设计出更加灵活多变的组合效果。
- 代码分离:装饰者模式可以将类的核心职责和附加功能分离,这样可以让代码更加清晰,职责明确。
- 多态:装饰者模式可以使用多态来增强代码的可读性和可维护性,同时可以动态地替换不同的具体装饰者,实现不同的效果。
-
缺点:
- 代码复杂:装饰者模式需要在原有的类上添加新的装饰者类,这样会增加代码的复杂性,同时类的数量也会增多,不利于维护和管理。
- 对象组合:在使用装饰者模式时,需要将对象组合成不同的装饰者类,这样会增加代码的复杂性和可读性的难度。
- 过度使用装饰者模式:如果过度使用装饰者模式,可能会导致代码的复杂性增加,同时也不利于调试和维护。
- 对继承的替代:虽然装饰者模式可以提供比继承更多的灵活性,但如果过度使用装饰者模式,可能会替代继承,导致代码的复杂性增加。
-
使用场景:
- 需要以动态、透明的方式给对象添加功能时,可以使用装饰者模式。
- 当需要处理一些可以撤销的职责时,可以使用装饰者模式。
- 当不能采用生成子类的方式进行扩展的时候,就可以使用装饰者模式。
- 如果有大量的扩展,如果要扩展这些功能,可能会造成太多的子类,此时可以使用装饰者模式。
- 当不允许有子类的时候,可以使用装饰者模式。
-
实现方式:
// 咖啡接口 public interface Coffee { void price(); } // 深培咖啡 public class DarkRoast implements Coffee { @Override public void price() { System.out.println("深培咖啡10元"); } } // 加料抽象类 public abstract class Decorator implements Coffee { protected Coffee coffee; public Decorator(Coffee coffee) { this.coffee = coffee; } public void price() { coffee.price(); } } // 加料子类 public class ConcreteDecorator extends Decorator { public ConcreteDecorator(Coffee coffee) { super(coffee); } @Override public void price() { super.price(); addedMilk(); } public void addedMilk() { System.out.println("添加牛奶+2元"); } } // 客户端代码 public class Client { public static void main(String[] args) { Coffee cofee = new DarkRoast(); ConcreteDecorator decorator = new ConcreteDecorator(cofee); decorator.price(); // 输出 "10元" 和 "添加牛奶2元" } }
5门面模式(Facade)
-
定义:提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。
-
优点:
- 减少客户与子系统的耦合:门面模式通过提供一个统一的接口来访问子系统,使得客户与子系统的耦合度降低,方便了客户对子系统的使用。
- 简化客户编程:门面模式使得客户只需要与一个统一接口进行交互,而不需要直接处理子系统的复杂实现细节,简化了客户的编程任务。
- 松耦合:通过引入门面模式,可以减少子系统与客户之间的依赖关系,实现子系统与客户之间的松耦合关系。这种松耦合关系使得子系统的改变不会影响到客户代码,只需要调整门面类的代码即可。
- 更好的模块化:门面模式使得子系统对外提供一个统一的接口,与具体子系统的实现相分离。这种模块化方式有利于代码的维护和扩展。
-
缺点:
- 引入额外的复杂性:门面模式的实现需要引入一个额外的门面类来处理客户请求的转发,增加了系统的复杂性。
- 对子系统限制过多:门面模式对子系统的访问进行了一定的限制,只允许通过门面接口进行访问。如果需要对子系统进行更底层的操作或访问,可能需要对门面模式进行修改或扩展。
- 增加客户与子系统之间的依赖:虽然门面模式减少了客户与子系统之间的耦合,但同时也增加了客户对门面类的依赖。如果门面类发生改变,可能会影响到客户的代码。
- 降低性能:由于门面模式的实现需要引入额外的转发层,可能会导致一定的性能损失。但在大多数情况下,这种性能损失并不明显。
-
使用场景:
- 为一个复杂的子系统或模式提供一个简洁的接口。这样,客户端可以更容易地使用子系统,而无需了解子系统内部的实现细节。
- 当需要提高子系统的独立性时。门面模式可以隔离客户端与子系统的直接交互,预防代码污染。如果子系统可能存在BUG或者性能相关问题,这种模式可以提供一个高层接口,减少客户端与子系统的直接依赖。
- 当子系统需要组织成层次结构时,门面模式可以帮助划分访问的层次。门面角色可以集中需要暴露给外部的功能,而将其他功能隐藏在内部,使得客户端只需要跟门面角色进行交互,简化客户端的使用。
-
实现方式:
// 定义三个模块 public class ModuleA { public void testA() { System.out.println("调用ModuleA中的testA方法"); } } public class ModuleB { public void testB() { System.out.println("调用ModuleB中的testB方法"); } } public class ModuleC { public void testC() { System.out.println("调用ModuleC中的testC方法"); } } // 门面角色类 public class Facade { // 示例方法,满足客户端需要的功能 public void test() { ModuleA a = new ModuleA(); a.testA(); ModuleB b = new ModuleB(); b.testB(); ModuleC c = new ModuleC(); c.testC(); } } // 客户端角色类 public class Client { public static void main(String[] args) { Facade facade = new Facade(); facade.test(); } }
6享元/蝇量模式(Flyweight)
-
定义:享元模式是池技术的重要实现方式,使用共享对象可以有效地支持大量的细粒度的对象。
-
优点:
- 减少内存消耗:享元模式通过共享内部状态来减少系统中的对象数量,从而显著降低了内存消耗。
- 提高性能:由于避免了大量重复对象的创建和销毁,享元模式可以显著提高系统的性能。
- 简化对象结构:享元模式将对象的内部状态和外部状态进行划分,使得对象的结构更加清晰,更易于理解和维护。
-
缺点:
- 增加代码复杂性:享元模式需要分离出内部状态和外部状态,并需要维护一个共享对象的池,这增加了代码的复杂性和维护成本。
- 线程安全性问题:如果多个线程同时访问共享对象并修改其外部状态,需要确保线程安全性,这可能需要进行同步处理。
-
使用场景:
- 当系统中存在大量相似对象且消耗大量内存时,可以考虑使用享元模式来减少内存消耗。
- 当需要频繁创建和销毁对象时,可以使用享元模式提升系统性能。
- 当对象的内部状态与外部状态分离,并且外部状态相对较少时,可以考虑使用享元模式。
-
实现方式:
// 抽象享元类 public abstract class Flyweight { public abstract void operation(String extrinsicstate); } // 具体享元类 public class ConcreteFlyweight extends Flyweight { private String intrinsicstate; public ConcreteFlyweight(String intrinsicstate) { this.intrinsicstate = intrinsicstate; } @Override public void operation(String extrinsicstate) { // 实现具体的操作逻辑 } } // 享元工厂类 public class FlyweightFactory { private static Map<String, Flyweight> flyweights = new HashMap<>(); public static Flyweight getFlyweight(String key) { Flyweight flyweight = flyweights.get(key); if (flyweight == null) { flyweight = new ConcreteFlyweight(key); flyweights.put(key, flyweight); } return flyweight; } } // 客户端使用 public class Client { public static void main(String[] args) { Flyweight flyweight1 = FlyweightFactory.getFlyweight("A"); Flyweight flyweight2 = FlyweightFactory.getFlyweight("B"); // 使用享元对象进行操作 } }
7组合模式(Composite)
-
定义:允许你将对象组合成树形结构来表现“整体/部分”层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。
-
优点:
- 结构清晰:组合模式采用树形结构来表示对象之间的组合关系,使得对象之间的关系更加清晰,易于理解和维护。
- 代码简洁:使用组合模式,客户端可以一致地处理单个对象和组合结构,无需编写额外的代码来处理对象之间的关系,简化了代码。
- 灵活性高:组合模式支持在运行时动态地添加和删除对象,客户端可以根据需要灵活地操作组合结构中的对象。
- 扩展性强:通过组合模式,可以很容易地扩展组合结构中的对象类型和数量,具有较强的扩展性。
-
缺点:
- 容易产生大量的对象:当组合结构中的对象数量较大时,会产生大量的对象,增加内存开销和垃圾回收的负担。
- 对树形结构的理解难度较大:组合模式需要理解树形结构的性质和特点,对客户端开发人员的素质要求较高。
- 对对象的遍历和查询效率较低:组合模式中的对象数量较多时,对树的遍历和查询操作的效率会有所降低。
-
使用场景:
- 表示层次结构:组合模式适用于表示整体和部分的层次结构,如文件系统、组织结构等。在这些场景中,通过组合模式可以将不同层次的对象统一处理,简化客户端的代码。
- 表示递归结构:组合模式适用于表示递归结构,如二叉树、多叉树等。在这些场景中,通过组合模式可以将不同层次的对象统一处理,同时支持在运行时动态地添加和删除对象。
- 实现动态结构:组合模式适用于实现动态结构,如动态菜单、动态列表等。在这些场景中,可以根据需要灵活地添加、删除和修改对象,实现动态的可视化界面。
-
实现方式:
// 组件接口 public interface Component { void operation(); } // 叶子类 public class Leaf implements Component { private String name; public Leaf(String name) { this.name = name; } @Override public void operation() { System.out.println("Leaf " + name + ": operation executed."); } } // 容器类 public class Composite implements Component { private List<Component> children = new ArrayList<>(); public void add(Component component) { children.add(component); } public void remove(Component component) { children.remove(component); } @Override public void operation() { for (Component child : children) { child.operation(); } } } // 客户端代码 public class Client { public static void main(String[] args) { Composite root = new Composite(); Leaf leaf1 = new Leaf("1"); Leaf leaf2 = new Leaf("2"); Leaf leaf3 = new Leaf("3"); root.add(leaf1); root.add(leaf2); root.add(leaf3); root.operation(); // 执行操作,输出:Leaf 1: operation executed.Leaf 2: operation executed.Leaf 3: operation executed. } }
行为型设计模式
- 定义:
- 行为型模式用于描述程序在运行时复杂的流程控制,即:描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配;
- 行为型模式分为类行为模式(采用继承机制来在类间分派行为)和对象行为模式(采用组合或聚合在对象间分配行为)。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
- 分类:
- 模板方法(Template Method):定义一个操作中的算法骨架,将算法的一些步骤延迟到子类中,使得子类在可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
- 策略(Strategy):定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
- 命令(Command):将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
- 职责链(Chain of Responsibility):把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
- 状态(State):允许一个对象在其内部状态发生改变时改变其行为能力。
- 观察者(Observer):多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
- 中介者(Mediator):定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
- 迭代器(Iterator):提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
- 访问者(Visitor):在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
- 备忘录(Memento):在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
- 解释器(Interpreter):提供如何定义语言的文法,以及对语言句子的解释方法,即解释器
1模板方法模式(TemplateMethod)
-
定义:在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
-
优点:
- 提高代码复用性:通过将不变的算法封装在父类中,子类可以通过继承和扩展来实现可变的算法,从而提高代码的复用性。
- 提取公共部分代码:模板方法模式可以将公共的代码提取到父类中,方便维护和管理。
- 行为由父类控制,子类实现:子类可以通过实现父类中的抽象方法来增加相应的功能,符合开闭原则。
-
缺点:
- 类的个数增加:由于需要在父类中定义模板方法和抽象方法,子类需要扩展这些方法,因此会增加类的个数,使设计更加抽象,间接地增加了系统实现的复杂度。
- 对代码阅读和新手不适感:由于父类中的抽象方法由子类实现,子类的执行结果会影响父类的结果,这导致一种反向的控制结构,可能会降低代码阅读的流畅性,并且可能会让新手感到不适。
- 对继承关系的依赖:模板方法模式对继承关系的依赖较大,如果父类添加新的抽象方法,则所有子类都需要修改。
-
使用场景:
- 算法的骨架确定,但某些步骤的具体实现未知或与环境相关。在这种情况下,可以将易变的部分抽象出来,供子类实现,从而在不改变算法结构的情况下,重新定义算法的某些步骤。
- 各个子类中有公共的行为,这些行为可以被提取出来并集中到一个公共的父类中,从而避免代码重复。
-
实现方式:
// 定义抽象类 public abstract class Game { // 定义抽象方法 public abstract void initialize(); public abstract void startPlay(); public abstract void endPlay(); // 定义模板方法 public final void play() { initialize(); startPlay(); endPlay(); } } // 扩展抽象类 public class Cricket extends Game { // 实现抽象方法 @Override public void initialize() { System.out.println("Cricket Game Initialized! Start playing."); } @Override public void startPlay() { System.out.println("Cricket Game Started. Enjoy the game!"); } @Override public void endPlay() { System.out.println("Cricket Game Finished!"); } } // 使用模板方法 public class TemplatePatternDemo { public static void main(String[] args) { Game game = new Cricket(); // 创建子类对象 game.play(); // 使用模板方法 } }
2策略模式(Strategy)
-
定义:定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
-
优点:
- 扩展性:可以在不修改对象结构的情况下,为新的算法进行添加新的类进行实现。
- 灵活性:可以对算法进行自由切换。
- 可以避免使用多重条件判断:在需要根据不同条件选择不同算法的情况下,使用策略模式可以避免使用多重条件判断语句,提高代码的可读性和可维护性。
-
缺点:
- 策略类会增多:随着算法的增多,需要创建更多的策略类,增加系统的复杂度。
- 所有策略类都需要对外暴露:客户端必须知道所有的策略类才能进行调用。
-
使用场景:
- 多个类只区别在表现行为不同:在这些情况下,可以使用Strategy模式,在运行时动态选择具体要执行的行为。
- 需要在不同情况下使用不同的策略(算法):如果一个系统需要动态地在几种算法中选择一种,可以使用策略模式。
- 一个对象有很多的行为:如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
-
实现方式:
// 首先我们定义一个策略接口 public interface Strategy { public int doOperation(int num1, int num2); } // 我们定义两个具体策略 public class OperationAdd implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 + num2; } } public class OperationSubtract implements Strategy { @Override public int doOperation(int num1, int num2) { return num1 - num2; } } // 我们定义一个Context类,它会根据具体的策略变化 public class Context { private Strategy strategy; public Context(Strategy strategy) { this.strategy = strategy; } public int executeStrategy(int num1, int num2) { return strategy.doOperation(num1, num2); } } // 主程序使用Context对象执行不同的策略 public class StrategyPatternDemo { public static void main(String[] args) { Context context = new Context(new OperationAdd()); System.out.println("10 + 5 = " + context.executeStrategy(10, 5)); context = new Context(new OperationSubtract()); System.out.println("10 - 5 = " + context.executeStrategy(10, 5)); } }
3命令模式(Command)
-
定义:将“请求”封装成命令对象,以便使用不同的请求、队列或日志来参数化其他对象。
-
优点:
- 降低系统的耦合度:命令模式可以将请求发送者与接收者解耦,发送者不需要知道接收者的具体实现细节,只需通过命令对象来发送请求。
- 可扩展性:通过添加新的具体命令类,可以很容易地扩展命令模式,而无需修改已有的代码,符合开闭原则。
- 撤销和重做操作:命令模式可以记录命令的执行历史,从而支持撤销和重做操作。
- 事务性操作:命令模式可以将多个命令组合成一个复合命令,实现事务性操作,确保多个命令的执行和撤销都成功。
- 日志和审计功能:由于命令模式记录了命令的执行历史,可以用于实现日志和审计功能。
-
缺点:
- 代码复杂性增加:引入命令对象和调用者对象,会增加代码的复杂性和额外的类。
- 类爆炸:如果系统中有大量的命令类,可能会导致类的数量爆炸,增加系统的复杂性。
- 执行效率降低:由于命令模式需要将请求封装成对象,可能会导致执行效率降低。
-
使用场景:命令模式在需要将请求发送者和接收者解耦、支持撤销和重做、实现事务性操作等场景下非常有用。但在一些简单的场景下,引入命令模式可能会增加代码复杂性,因此需要根据具体情况进行权衡和选择。
-
实现方式:
// 声明一个命令接口 public interface Command { void execute(); } // 创建一个接收者类,它实现了命令接口 public class Receiver implements Command { @Override public void execute() { System.out.println("Receiver: Action executed."); } } // 创建一个请求者类,它持有一个命令对象 public class Invoker { private Command command; public Invoker(Command command) { this.command = command; } public void setCommand(Command command) { this.command = command; } public void executeCommand() { command.execute(); } } // 主程序 public class Main { public static void main(String[] args) { // 创建接收者对象 Receiver receiver = new Receiver(); // 创建请求者对象,并设置命令对象 Invoker invoker = new Invoker(receiver); // 执行命令 invoker.executeCommand(); // 更改命令 Command newCommand = new NewCommand(); // 假设NewCommand是另一个实现了Command接口的类 invoker.setCommand(newCommand); // 再次执行命令 invoker.executeCommand(); } }
4责任链模式(ChainofResponsibility)
-
定义:当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式。使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
-
优点:
- 降低耦合度:责任链模式减少了请求的发送者和接收者之间的耦合度,使两者可以独立地操作。
- 增强灵活性:可以动态地增加或删除处理器,改变请求的处理顺序和方式,增强了系统的灵活性。
- 简化了对象:责任链模式使得对象不需要知道链的结构,减少了需要处理的对象数量。
- 方便添加新请求处理类:当有新的请求需要处理时,只需要添加新的处理器类即可,不需要修改原有的代码。
-
缺点:
- 不能保证请求一定被接收:由于责任链中可能包含大量的处理器,因此可能会影响系统的性能。
- 系统性能可能受影响:由于责任链中可能包含大量的处理器,因此可能会影响系统的性能。
- 不能保证请求一定被处理:如果没有任何一个处理器能够处理该请求,则该请求将被忽略。
- 不容易观察运行时的特征:责任链模式可能不容易观察运行时的特征,有碍于除错。
-
使用场景:
- 处理多个对象协作:如果有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定,可以使用责任链模式。
- 不明确指定接收者的情况:如果不明确指定接收者的情况下,向多个对象中的一个或多个提交一个请求,可以使用责任链模式。
- 动态指定处理器集合:可以在运行时动态指定一组对象处理请求,而不需要改变原有的代码,增强了系统的灵活性。
-
实现方式:
// 抽象的处理器类 public abstract class AbstractHandler { protected AbstractHandler nextHandler; public void setNextHandler(AbstractHandler nextHandler) { this.nextHandler = nextHandler; } public abstract void handleRequest(String request); } // 定义两个具体的处理器类 public class ConcreteHandlerA extends AbstractHandler { @Override public void handleRequest(String request) { if (request.startsWith("A")) { System.out.println("HandlerA handled request: " + request); } else { if (nextHandler != null) { nextHandler.handleRequest(request); } else { System.out.println("No handler to handle request: " + request); } } } } public class ConcreteHandlerB extends AbstractHandler { @Override public void handleRequest(String request) { if (request.startsWith("B")) { System.out.println("HandlerB handled request: " + request); } else { if (nextHandler != null) { nextHandler.handleRequest(request); } else { System.out.println("No handler to handle request: " + request); } } } } // 创建客户端发送请求 public class Client { public static void main(String[] args) { AbstractHandler handlerA = new ConcreteHandlerA(); AbstractHandler handlerB = new ConcreteHandlerB(); handlerA.setNextHandler(handlerB); handlerB.setNextHandler(null); // B是链的最后一个处理器,设置其nextHandler为null String[] requests = {"A1", "B1", "C1", "A2", "B2", "C2"}; // 假设有6个请求需要处理 for (String request : requests) { handlerA.handleRequest(request); // 每个请求都先由handlerA处理,如果handlerA不能处理,则由handlerA的nextHandler处理,依此类推。 } } }
5状态模式(State)
-
定义:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
-
优点:
- 结构清晰:状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
- 将状态转换显示化:减少对象间的相互依赖。
- 状态类职责明确:有利于程序的扩展。
- 通过定义新的子类很容易地增加新的状态和转换。
-
缺点:
- 状态模式的使用必然会增加系统的类与对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
- 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码。
- 修改某个状态类的行为也需要修改对应类的源码。
-
使用场景:
- 对象的行为依赖于它的状态(如某些属性值),状态的改变将导致行为的变化。
- 在代码中包含大量与对象状态有关的条件语句,这些条件语句的出现,会导致代码的可维护性和灵活性变差,不能方便地增加和删除状态,并且导致客户类与类库之间的耦合增强。
-
实现方式:
// 首先,我们定义一个接口,表示状态 public interface State { void handle(Context context); } // 我们定义两个具体状态,比如,我们有一个灯泡,可以打开或关闭 public class StateOn implements State { @Override public void handle(Context context) { System.out.println("灯泡是开着的"); context.setState(new StateOff()); // 灯泡可以从开状态转换到关状态 } } public class StateOff implements State { @Override public void handle(Context context) { System.out.println("灯泡是关着的"); context.setState(new StateOn()); // 灯泡可以从关状态转换到开状态 } } // 接着,我们定义一个上下文类,这个类会维护一个具体状态对象,并有一个行为依赖于当前状态的方法 public class Context { private State state; public Context(State state) { this.state = state; } public void setState(State state) { this.state = state; } public void request() { state.handle(this); // 上下文对象将请求委托给当前的状态对象处理 } } // 最后,我们在客户端中使用这些类 public class Client { public static void main(String[] args) { Context context = new Context(new StateOff()); // 创建一个初始状态为关着的灯泡 context.request(); // 输出 "灯泡是关着的",然后切换到开状态 context.request(); // 输出 "灯泡是开着的",然后切换到关状态 } }
6观察者模式(Observer)
-
定义:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。
-
优点:
- 观察者模式实现了观察者和目标之间的抽象耦合,使得观察者和被观察者的代码解耦,可以独立演化。
- 实现了动态联动,当一个对象状态改变时,依赖于它的对象都能自动更新。
- 支持广播通信,当一个对象状态改变时,所有依赖于它的对象都会得到通知。
-
缺点:
- 在某些情况下,可能会引起无谓的操作,比如一些观察者并不需要知道目标对象的状态变化。
- 依赖过多:观察者 之间 细节依赖 过多 , 会增加 时间消耗 和 程序的复杂程度 ;这里的 细节依赖 指的是 触发机制 , 触发链条 ; 如果 观察者设置过多 , 每次触发都要花很长时间去处理通知 ;
- 循环调用:避免 循环调用 , 观察者 与 被观察者 之间 绝对不允许循环依赖 , 否则会触发 二者 之间的循环调用 , 导致系统崩溃 ;
-
使用场景:
- 关联行为场景:关联行为是可拆分的,而不是“组合”关系,这种场景下可以使用观察者模式。
- 事件多级触发场景:一级事件发生时,需要同时触发多级事件,这种场景下可以使用观察者模式。
- 跨系统的消息交换场景:比如消息队列、事件总线的处理机制,这种场景下可以使用观察者模式。
-
实现方式:
// 观察者接口 public interface Observer { void update(String message); } // 被观察者接口 public interface Observable { void registerObserver(Observer observer); void removeObserver(Observer observer); void notifyObservers(String message); } // 实现观察者接口 import java.util.ArrayList; import java.util.List; public class ConcreteObservable implements Observable { private List<Observer> observers = new ArrayList<>(); @Override public void registerObserver(Observer observer) { observers.add(observer); } @Override public void removeObserver(Observer observer) { observers.remove(observer); } @Override public void notifyObservers(String message) { for (Observer observer : observers) { observer.update(message); } } } // 实现观察者接口 public class ConcreteObserver implements Observer { private String name; public ConcreteObserver(String name) { this.name = name; } @Override public void update(String message) { System.out.println(name + " received: " + message); } } // 测试 public class Main { public static void main(String[] args) { ConcreteObservable observable = new ConcreteObservable(); Observer observer1 = new ConcreteObserver("Observer 1"); Observer observer2 = new ConcreteObserver("Observer 2"); Observer observer3 = new ConcreteObserver("Observer 3"); observable.registerObserver(observer1); observable.registerObserver(observer2); observable.registerObserver(observer3); observable.notifyObservers("Hello, world!"); observable.removeObserver(observer2); observable.notifyObservers("Goodbye, world!"); } }
7中介者模式(Mediator)
-
定义:使用中介者模式来集中相关对象之间复杂的沟通和控制方式。
-
优点:
- 降低类的复杂度:通过中介者模式,可以将多个对象之间的交互封装在一个中介者对象中,从而避免每个对象都需要了解其他对象的细节,降低了类的复杂度。
- 提高类的可复用性:由于中介者模式将多个对象之间的交互封装在一个对象中,因此可以更容易地复用该中介者对象,而不需要修改其他对象的代码。
- 增加系统的可扩展性:通过中介者模式,可以很容易地增加新的中介者对象来扩展系统的功能,而不需要修改原有对象的代码。
- 符合迪米特原则:中介者模式符合迪米特原则,即一个对象应该对其他对象有最少的了解,使得系统更加模块化、可维护。
-
缺点:
- 中介者对象可能会变得过于复杂:由于中介者对象需要处理多个对象之间的交互,因此可能会变得非常复杂,维护起来比较困难。
- 中介者对象可能会引入新的问题:中介者对象可能会出现自己的状态和行为,这可能会影响到其他对象的性能和正确性。
- 中介者模式的使用可能会违反开闭原则:如果要增加新的中介者对象,可能需要修改已有的代码,这可能会违反开闭原则。
-
使用场景:
- 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
-
实现方式:
// 中介者接口 public interface Mediator { void register(Colleague colleague); void remove(Colleague colleague); void notify(String message); } // 同事类接口 public interface Colleague { void receive(String message); } // 实现同事类接口 public class ConcreteColleague1 implements Colleague { @Override public void receive(String message) { System.out.println("ConcreteColleague1 received: " + message); } } public class ConcreteColleague2 implements Colleague { @Override public void receive(String message) { System.out.println("ConcreteColleague2 received: " + message); } } // 实现中介者接口 import java.util.ArrayList; import java.util.List; public class ConcreteMediator implements Mediator { private List<Colleague> colleagues = new ArrayList<>(); @Override public void register(Colleague colleague) { colleagues.add(colleague); } @Override public void remove(Colleague colleague) { colleagues.remove(colleague); } @Override public void notify(String message) { for (Colleague colleague : colleagues) { colleague.receive(message); } } } // 测试 public class Main { public static void main(String[] args) { ConcreteMediator mediator = new ConcreteMediator(); Colleague colleague1 = new ConcreteColleague1(); Colleague colleague2 = new ConcreteColleague2(); mediator.register(colleague1); mediator.register(colleague2); mediator.notify("Hello, world!"); mediator.remove(colleague2); mediator.notify("Goodbye, world!"); } }
8迭代器模式(Iterator)
-
定义:提供一个方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
-
优点:
- 支持以不同的方式遍历一个聚合对象:迭代器模式提供了一种统一的接口,可以用于遍历不同的集合结构,使得外部的代码可以透明地访问集合内部的数据。
- 简化聚合类:通过将集合对象的遍历行为分离出来,迭代器模式简化了聚合类的设计,使得聚合类可以将存储和遍历的职责分离。
- 支持多个遍历:迭代器模式可以在同一个聚合上支持多个遍历,使得对聚合对象的访问更加灵活和多样化。
-
缺点:
- 类的个数成对增加:由于迭代器模式将存储数据和遍历数据的职责分离,因此在增加新的聚合类时,需要对应增加新的迭代器类,导致类的个数成对增加,在一定程度上增加了系统的复杂性。
- 不够灵活:迭代器模式的使用场景相对固定,如果需要在遍历过程中进行更复杂的操作,或者需要修改现有的遍历方式,可能会比较困难,因为迭代器模式的接口是固定的。
-
使用场景:
- 访问一个聚合对象的内容而无须暴露它的内部表示:这是迭代器模式最常用的场景,例如在数据访问层中,我们可能需要遍历一个数据集,而不希望暴露该数据集的内部实现细节。
- 需要为聚合对象提供多种遍历方式:例如在数据库访问中,我们可能需要按照不同的排序方式来遍历数据集。
- 为遍历不同的聚合结构提供一个统一的接口:例如在处理不同的业务逻辑时,我们可能需要遍历不同的业务对象集合,为了简化代码,我们可以为这些不同的集合结构提供统一的遍历接口。
-
实现方式:
// 聚合接口 public interface Aggregate { Iterator createIterator(); } // 迭代器接口 public interface Iterator { boolean hasNext(); Object next(); } // 实现聚合接口 public class ConcreteAggregate implements Aggregate { private List<Object> list = new ArrayList<>(); public void add(Object obj) { list.add(obj); } @Override public Iterator createIterator() { return new ConcreteIterator(list); } } // 实现迭代器接口 public class ConcreteIterator implements Iterator { private List<Object> list; private int index = 0; public ConcreteIterator(List<Object> list) { this.list = list; } @Override public boolean hasNext() { return index < list.size(); } @Override public Object next() { if (hasNext()) { return list.get(index++); } else { throw new NoSuchElementException(); } } } // 测试 public class Client { public static void main(String[] args) { ConcreteAggregate aggregate = new ConcreteAggregate(); aggregate.add("A"); aggregate.add("B"); aggregate.add("C"); Iterator iterator = aggregate.createIterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } } }
9访问者模式(Visitor)
-
定义:表示一个作用于某个对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
-
优点:
- 增加新的操作:访问者模式使得易于增加新的操作,因为只需要增加一个新的访问者即可在一个对象结构上定义一个新的操作。
- 集中相关操作:访问者模式可以将相关的操作集中在一个访问者中,简化类的设计,也能简化在这些访问者中定义的算法。
- 统一操作接口:对功能进行统一,可以用访问者模式做报表、UI、拦截器与过滤器等,适用于数据结构相对稳定的系统。
-
缺点:
- 增加新的元素类困难:访问者模式使得难以增加新的元素子类,因为每添加一个新的具体元素,就需要在访问者中添加一个新的抽象操作,并在每一个具体访问者类中实现相应的操作。
- 破坏封装性:访问者模式假定具体元素类具有足够强的接口,才能让访问者进行它们的工作,这可能会破坏元素的封装性,导致迪米特法则的违背。
-
使用场景:需要在一个对象结构中定义许多不同种类的操作,同时需要避免让这些操作污染这些对象的类时,可以考虑使用访问者模式。
-
实现方式:
// 元素接口 public interface Element { void accept(Visitor visitor); } // 实现元素接口 public class ConcreteElement implements Element { @Override public void accept(Visitor visitor) { visitor.visit(this); } } // 访问者接口 public interface Visitor { void visit(Element element); } // 实现访问者接口 public class ConcreteVisitor implements Visitor { @Override public void visit(Element element) { // 访问元素的具体操作 } } // 对象结构类 public class ObjectStructure { private List<Element> elements = new ArrayList<>(); public void addElement(Element element) { elements.add(element); } public void accept(Visitor visitor) { for (Element element : elements) { element.accept(visitor); } } } // 测试 ObjectStructure objectStructure = new ObjectStructure(); objectStructure.addElement(new ConcreteElement()); objectStructure.addElement(new ConcreteElement()); objectStructure.accept(new ConcreteVisitor());
10备忘录模式(Memento)
-
定义:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态。当你需要让对象返回之前的状态时(例如:你的用户请求“撤销”),就使用备忘录模式。
-
优点:
- 保存状态:备忘录模式可以保存对象的状态,并在需要时恢复对象到之前的状态。
- 封装状态:备忘录模式将对象的状态封装在备忘录对象中,保护了对象的封装性,不会暴露内部状态给外部对象。
- 可撤销和重做:备忘录模式可以实现撤销和重做操作,这是通过保存一系列的状态来实现的。
-
缺点:
- 资源消耗:如果需要保存的对象状态较多或者状态变化频繁,备忘录模式会消耗较多的内存资源。
- 复杂性:如果需要保存的对象状态较复杂,备忘录模式可能会增加代码的复杂性和维护成本。
- 破坏封装性:如果对象的状态需要保密或者访问权限受限,备忘录模式可能会破坏对象的封装性。
-
使用场景:
- 需要保存和恢复数据的相关状态场景。例如,需要实现撤销、重做、历史记录等功能时,可以使用备忘录模式。
- 需要避免暴露对象内部状态给外部对象的场景。例如,如果一个对象的内部状态不应该被外部对象直接访问,可以使用备忘录模式来保护对象的封装性。
- 需要进行事务处理的场景。例如,在进行数据库操作时,可以使用备忘录模式来保存和恢复数据状态,以确保事务的安全性和一致性。
-
实现方式:
// 备忘录接口 public interface Memento { void setState(String state); String getState(); } // 实现备忘录接口 public class ConcreteMemento implements Memento { private String state; @Override public void setState(String state) { this.state = state; } @Override public String getState() { return state; } } // 发起人接口 public interface Originator { Memento createMemento(); void restoreMemento(Memento memento); } // 实现发起人接口 public class ConcreteOriginator implements Originator { private String state; @Override public Memento createMemento() { ConcreteMemento memento = new ConcreteMemento(); memento.setState(state); return memento; } @Override public void restoreMemento(Memento memento) { this.state = memento.getState(); } } // 负责人类 public class Caretaker { private Memento memento; public void setMemento(Memento memento) { this.memento = memento; } public Memento getMemento() { return memento; } } // 创建一个发起人对象和一个负责人对象,然后通过负责人对象保存和恢复发起人对象的状态: Originator originator = new ConcreteOriginator(); Caretaker caretaker = new Caretaker(); originator.setState("State 1"); // 设置发起人状态为 State 1 caretaker.setMemento(originator.createMemento()); // 将发起人状态保存到负责人对象中
本文来自博客园,作者:喵师傅,转载请注明原文链接:https://www.cnblogs.com/wywblogs/articles/17743169.html