👍一、创建者模式
🔖1.1、单例模式
单例模式(Singleton Pattern),用于确保一个类只有一个实例,并提供全局访问点。
在某些情况下,我们需要确保一个类只能有一个实例,比如数据库连接、线程池等。单例模式可以解决这个问题,它通过限制类的实例化过程,使得一个类只能创建一个对象,并提供一个静态方法来获取该对象。
单例模式的关键是将类的构造函数私有化,这样外部就无法直接实例化该类。然后,我们可以在类内部定义一个静态变量来保存类的唯一实例,并提供一个静态方法来获取该实例。这个静态方法会检查实例是否已经存在,如果存在则返回现有实例,如果不存在则创建一个新实例并返回。
单例设计模式分为两种:
- 饿汉式:类加载就会导致该实例对象被创建
- 懒汉式:类加载不会导致该实例对象被创建,而是首次使用该对象的时候才会创建
①饿汉式-方式1(静态变量方式)
/**
* @author 小白程序员
* @date 2023/8/15 10:14
*/
public class Singleton2 {
private Singleton2(){}//私有构造方法
//在成员位置创建该类的对象
private static Singleton2 instance = new Singleton2();
//对外提供静态方法获取对象
public static Singleton2 getInstance(){
return instance;
}
}
说明:该方式在成员位置声明Singleton2类型的静态变量,并创建Singleton2类的对象instance。instance对象是随着类的加载而创建的,如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
②饿汉式-方式2(静态代码块方式)
/**
* @author 小白程序员
* @date 2023/8/15 10:14
*/
public class Singleton2 {
private Singleton2(){}//私有构造方法
//在成员位置创建该类的对象
private static Singleton2 instance;
static{
instance = new Singleton2();
}
//对外提供静态方法获取对象
public static Singleton2 getInstance(){
return instance;
}
}
说明:该方式和方式1一样,一样会造成内存上的浪费。
③懒汉式-方式1(线程不安全)
/**
* @author 小白程序员
* @date 2023/8/15 10:23
*/
public class Role {
private Role(){}//私有构造函数
//在成员位置创建该类的对象
private static Role instance;
//对外提供静态方法获取对象
public static Role getInstance(){
if(instance == null){
instance = new Role();
}
return instance;
}
}
说明:该方式在成员位置声明Role类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值呢?当调用getInstance()方法获取Role类的对象的时候才创建Role对象,这样就实现了懒加载的效果,但是,如果是多线程环境下,会出现线程安全问题。
④懒汉式-方式2(线程安全(双重检查锁))
/**
* @author 小白程序员
* @date 2023/8/14 17:19
*/
public class Student {
private static Student instance; //私有静态变量,保存唯一实例
private Student(){} //私有构造函数,防止外部实例化
public static Student getInstance(){
//第一次判断,如果instance不为null,不进入枪锁阶段,直接返回实例
if(instance == null){
//使用双重检查锁定来确保线程安全
synchronized (Student.class){
if(instance == null){
instance = new Student();
}
}
}
return instance;
}
public void doSomething(){
System.out.println("单例对象做一些事情!");
}
public static void main(String[] args) {
Student singleton1 = Student.getInstance();
Student singleton2 = Student.getInstance();
System.out.println(singleton1==singleton2);
singleton1.doSomething();
}
}
说明:上面的双重检查锁模式看上去去完美无缺,其实存在问题,在多线程情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁带来的空指针问题,只需要使用volatile关键字,可以保证可见性和有序性。
/**
* @author 小白程序员
* @date 2023/8/14 17:19
*/
public class Student {
private static volatile Student instance; //私有静态变量,保存唯一实例
private Student(){} //私有构造函数,防止外部实例化
public static Student getInstance(){
if(instance == null){
//使用双重检查锁定来确保线程安全
synchronized (Student.class){
if(instance == null){
instance = new Student();
}
}
}
return instance;
}
说明:添加volatile关键字之后的双重检查锁是一种较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
🔖1.2、工厂模式
①简单工厂模式(不属于23种模式的一种)
简单工厂包含如下角色:
-
抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
-
具体产品 :实现或者继承抽象产品的子类
-
具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。
在现实生活种,我们常常会遇到需要根据不同的条件来创建不同类型的对象的情况。比如,我们要创建一个图形绘制程序,根据用户选择的图形类型来创建对应的图形对象。这时候,简单工厂模式就可以派上用场了。
简单工厂模式的核心思想是将对象的创建封装在一个工厂类中。这个工厂类负责根据客户端的请求来创建具体的对象,并将创建的对象返回给客户端使用。
假设我们要创建一个图形绘制程序,其中包含不同类型的图形,如圆形、矩形和三角形。首先,我们定义一个抽象的图形接口:
/**
* @author 小白程序员
* @Classname Shape
* @Description 抽象图形接口
* @date 2023/8/16 15:32
*/
public interface Shape {
void draw();
}
定义三个具体的图形类,分别实现了抽象图形接口。
/**
* @author 小白程序员
* @Classname Circle
* @Description 圆形类
* @date 2023/8/16 15:33
*/
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("画圆形");
}
}
/**
* @author 小白程序员
* @Classname Rectangle
* @Description 矩形类
* @date 2023/8/16 15:34
*/
public class Rectangle implements Shape{
@Override
public void draw() {
System.out.println("矩形");
}
}
/**
* @author 小白程序员
* @Classname Triangle
* @Description 三角形
* @date 2023/8/16 15:34
*/
public class Triangle implements Shape{
@Override
public void draw() {
System.out.println("三角形");
}
}
定义简单工厂类,用于客户端的请求来创建具体的图形对象。
/**
* @author 小白程序员
* @Classname ShapeFactory
* @Description 简单工厂类
* @date 2023/8/16 15:36
*/
public class ShapeFactory {
public static Shape createShape(String shapeType){
if(shapeType.equalsIgnoreCase("CIRCLE")){
return new Circle();
}else if(shapeType.equalsIgnoreCase("RECTANGLE")){
return new Rectangle();
}else if(shapeType.equalsIgnoreCase("TRIANGLE")){
return new Triangle();
}else{
System.out.println("没有找到对应的形状");
return null;
}
}
public static void main(String[] args) {
//创建图形对象
Shape circle = ShapeFactory.createShape("CIRCLE");
if (circle != null) {
circle.draw();
}
Shape rectangle = ShapeFactory.createShape("RECTANGLE");
if (rectangle != null) {
rectangle.draw();
}
Shape triangle = ShapeFactory.createShape("TRIANGLE");
if (triangle != null) {
triangle.draw();
}
}
}
优点:
封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。
缺点:
增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。
扩展:静态工厂,实际很简单,就是简单工厂的工厂方法设置成static。
②工厂方法模式
工厂方法模式的主要角色:
-
抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
-
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
-
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
-
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
工厂方法模式用于定义一个创建对象的接口,但由子类决定实例化哪个类。简单来说,就是将对象的创建延迟到子类中进行。
工厂方法模式的核心思想是将对象的创建封装在一个工厂接口中,每个具体的工厂类负责创建特定类型的对象。客户端通过调用工厂接口的方法来获取所需的对象,而无需关心具体的类是什么。
假设我们要创建一个电脑制造工厂,其中包含不同品牌的电脑,如苹果电脑、华为电脑和联想电脑。首先,我们定义一个抽象的电脑接口:
/**
* @author 小白程序员
* @Classname Computer
* @Description 抽象电脑接口
* @date 2023/8/16 15:58
*/
public interface Computer {
void playGame();
}
定义具体的电脑类,分别实现了抽象电脑接口
/**
* @author 小白程序员
* @Classname AppleComputer
* @Description 苹果电脑
* @date 2023/8/16 15:59
*/
public class AppleComputer implements Computer{
@Override
public void playGame() {
System.out.println("苹果电脑玩游戏");
}
}
/**
* @author 小白程序员
* @Classname HuaWeiComputer
* @Description 华为电脑
* @date 2023/8/16 16:00
*/
public class HuaWeiComputer implements Computer{
@Override
public void playGame() {
System.out.println("华为手机玩游戏");
}
}
/**
* @author 小白程序员
* @Classname LenovoComputer
* @Description 联想电脑
* @date 2023/8/16 16:01
*/
public class LenovoComputer implements Computer{
@Override
public void playGame() {
System.out.println("联想游戏");
}
}
接下来定义工厂接口,用户创建电脑的对象
/**
* @author 小白程序员
* @Classname ComputerFactory
* @Description 工厂接口
* @date 2023/8/16 16:02
*/
public interface ComputerFactory {
Computer createComputer();
}
然后,我们分别实现具体的工厂类,每个工厂类负责创建特点品牌的电脑对象
/**
* @author 小白程序员
* @Classname AppleComputerFactory
* @Description 苹果电脑工厂
* @date 2023/8/16 16:04
*/
public class AppleComputerFactory implements ComputerFactory{
@Override
public Computer createComputer() {
return new AppleComputer();
}
}
/**
* @author 小白程序员
* @Classname HuaWeiComputerFactory
* @Description 华为电脑工厂
* @date 2023/8/16 16:05
*/
public class HuaWeiComputerFactory implements ComputerFactory{
@Override
public Computer createComputer() {
return new HuaWeiComputer();
}
}
/**
* @author 小白程序员
* @Classname LenovoComputerFactory
* @Description 联想电脑工厂
* @date 2023/8/16 16:06
*/
public class LenovoComputerFactory implements ComputerFactory{
@Override
public Computer createComputer() {
return new LenovoComputer();
}
}
/**
* @author 小白程序员
* @Classname Main
* @Description 测试
* @date 2023/8/16 16:06
*/
public class Main {
public static void main(String[] args) {
//创建苹果电脑对象
AppleComputerFactory appleComputerFactory = new AppleComputerFactory();
Computer appleComputer = appleComputerFactory.createComputer();
appleComputer.playGame();
//创建华为电脑对象
HuaWeiComputerFactory huaWeiComputerFactory = new HuaWeiComputerFactory();
Computer huaweiComputer = huaWeiComputerFactory.createComputer();
huaweiComputer.playGame();
//创建联想电脑对象
LenovoComputerFactory lenovoComputerFactory = new LenovoComputerFactory();
Computer lenovoComputer = lenovoComputerFactory.createComputer();
lenovoComputer.playGame();
}
}
通过工厂方法模式,我们可以根据客户端的请求来创建不同品牌的电脑对象,而无需关心具体的类是什么。这样,我们可以方便地扩展和修改对象的创建逻辑,同时也符合开闭原则。、
优点:
-
用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
-
在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;
缺点:
-
每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
自我理解:定义一个抽象的工厂接口,其中包含创建对象的接口,然后我们可以创建多个具体的对象类,每个类去实现抽象工厂的接口负责创建特定对象。
③抽象工厂模式
抽象工厂模式(Abstract Factory Pattern)用于提供一个接口,用于创建相关或依赖对象的家族,而不需要指定具体的类。
前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机、传智播客只培养计算机软件专业的学生等。
这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。
本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。
抽象工厂模式的主要角色如下:
-
抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
-
具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
-
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
-
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
在现实生活中,我们经常会遇到需要创建一组相关的对象的情况。比如,我们要创建一个家具工厂,包括创建椅子、桌子和柜子等家具。这时候,抽象工厂模式就可以派上用场。
抽象工厂模式的核心思想是将一组相关的对象的创建封装在一个抽象工厂接口中,每个具体的工厂类负责创建特定类型的对象。客户端通过调用抽象工厂接口的方法来获取所需的对象,而无需关心具体的类是什么。
假设我们要创建一个家具工厂,其中包含不同类型的家具,如现代风格的椅子、桌子和柜子,以及传统风格的椅子、桌子和柜子。首先,我们定义一个抽象的家具接口:
/**
* @author 小白程序员
* @Classname Chair
* @Description 抽象椅子接口
* @date 2023/8/16 16:38
*/
public interface Chair {
void sit();
}
/**
* @author 小白程序员
* @Classname Cabinet
* @Description 抽象柜子接口
* @date 2023/8/16 16:39
*/
public interface Cabinet {
void open();
}
/**
* @author 小白程序员
* @Classname Table
* @Description 抽象桌子接口
* @date 2023/8/16 16:38
*/
public interface Table {
void use();
}
然后,我们定义了具体的家具系列,分别实现了抽象家具接口:
/**
* @author 小白程序员
* @Classname ModernChair
* @Description 现代风格椅子类
* @date 2023/8/16 16:42
*/
public class ModernChair implements Chair{
@Override
public void sit() {
System.out.println("现代椅子可以坐下");
}
}
/**
* @author 小白程序员
* @Classname ModernTable
* @Description 现代风格桌子类
* @date 2023/8/16 16:43
*/
public class ModernTable implements Table{
@Override
public void use() {
System.out.println("使用现代 TABLE");
}
}
/**
* @author 小白程序员
* @Classname ModernCabinet
* @Description 现代风格柜子类
* @date 2023/8/16 16:44
*/
public class ModernCabinet implements Cabinet{
@Override
public void open() {
System.out.println("modernCabinet 打开");
}
}
接下来,我们定义一个抽象工厂接口,用于创建家具对象:
/**
* @author 小白程序员
* @Classname FurnitureFactory
* @Description 抽象家具工厂接口
* @date 2023/8/16 16:47
*/
public interface FurnitureFactory {
Chair createChair();
Table createTable();
Cabinet createCabinet();
}
然后,我们分别实现具体的工厂类,每个工厂类负责创建特定风格的家具对象:
/**
* @author 小白程序员
* @Classname ModernFurnitureFactory
* @Description 家具工厂类
* @date 2023/8/16 16:52
*/
public class ModernFurnitureFactory implements FurnitureFactory{
@Override
public Chair createChair() {
return new ModernChair();
}
@Override
public Table createTable() {
return new ModernTable();
}
@Override
public Cabinet createCabinet() {
return new ModernCabinet();
}
}
现在,我们可以使用抽象工厂模式来创建不同风格的家具对象了:
/**
* @author 小白程序员
* @Classname Main
* @Description 测试
* @date 2023/8/16 16:54
*/
public class Main {
public static void main(String[] args) {
// 创建现代风格家具对象
FurnitureFactory modernFactory = new ModernFurnitureFactory();
Chair modernChair = modernFactory.createChair();
Table modernTable = modernFactory.createTable();
Cabinet modernCabinet = modernFactory.createCabinet();
modernChair.sit();
modernTable.use();
modernCabinet.open();
}
}
通过抽象工厂模式,我们可以根据客户端的需求来创建不同风格的家具对象,而无需关心具体的类是什么。这样,我们可以方便地扩展和修改家具的创建逻辑,同时也符合开闭原则。
优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
使用场景:
-
当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
-
系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
-
系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。
自我理解:抽象工厂模式是把创建对象的方法封装到了一个接口中,在实现中把这些对象都实现。
🔖1.3、原型模式
原型模式用于通过复制现有对象来创建新对象。
想象一下,你正在建造一个城市,需要创建许多相似的建筑物,比如房屋、商店和办公楼。为了避免每次都从头开始构建这些建筑物,你可以使用原型模式。
具体来说,你可以创建一个原型接口或抽象类,其中包含一个克隆方法。然后,你可以创建多个具体的原型类,每个类都实现了克隆方法,并且能够复制自己。
当你需要创建新的建筑物时,只需从适当的原型对象进行克隆即可,而无需重新构建整个对象。
原型模式包含如下角色:
-
抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
-
具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
-
访问类:使用具体原型类中的 clone() 方法来复制新的对象。
原型模式的克隆分为浅克隆和深克隆
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
Java中的Object类中提供了
clone()
方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。
浅拷贝——方式一
/**
* @author 小白程序员
* @Classname BuildingPrototype
* @Description 建筑物原型接口
* @date 2023/8/16 20:14
*/
public interface BuildingPrototype {
BuildingPrototype clone();
String getType();
}
/**
* @author 小白程序员
* @Classname House
* @Description 房屋类
* @date 2023/8/16 20:16
*/
public class House implements BuildingPrototype{
private String type;
public House(String type) {
this.type = type;
}
@Override
public BuildingPrototype clone() {
return new House(this.type);
}
public void setType(String type) {
this.type = type;
}
@Override
public String getType() {
return type;
}
}
/**
* @author 小白程序员
* @Classname Shop
* @Description 商店类
* @date 2023/8/16 20:19
*/
public class Shop implements BuildingPrototype{
private String type;
public Shop(String type) {
this.type = type;
}
public void setType(String type){
this.type = type;
}
@Override
public String getType(){
return type;
}
@Override
public BuildingPrototype clone() {
return new Shop(this.type);
}
}
/**
* @author 小白程序员
* @Classname Main
* @Description 测试
* @date 2023/8/16 20:21
*/
public class Main {
public static void main(String[] args) {
BuildingPrototype housePrototype = new House("单层别墅");
BuildingPrototype clonedHouse = housePrototype.clone();
System.out.println(clonedHouse.getType());
BuildingPrototype shopPrototype = new Shop("二层商铺");
BuildingPrototype clonedShop = shopPrototype.clone();
System.out.println(clonedShop.getType());
}
}
通过原型模式,你可以复制现有的建筑物对象来创建新的对象,而无需重新构建整个对象。这样,你可以节省时间和资源,并且方便地定制每个新对象的属性。
需要注意的是,克隆方法可以实现浅拷贝或深拷贝,具体取决于你的需求。浅拷贝只复制对象的基本属性,而深拷贝会递归复制对象的所有引用类型属性。
深拷贝——方式二
//奖状类
public class Citation implements Cloneable {
private Student stu;
public Student getStu() {
return stu;
}
public void setStu(Student stu) {
this.stu = stu;
}
void show() {
System.out.println(stu.getName() + "同学:在2020学年第一学期中表现优秀,被评为三好学生。特发此状!");
}
@Override
public Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}
//学生类
public class Student {
private String name;
private String address;
public Student(String name, String address) {
this.name = name;
this.address = address;
}
public Student() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
//测试类
public class CitationTest {
public static void main(String[] args) throws CloneNotSupportedException {
Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);
//复制奖状
Citation c2 = c1.clone();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");
//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));
c1.show();
c2.show();
}
}
说明:stu对象和stu1对象是同一个对象,就会产生将stu1对象中name属性值改为“李四”,两个Citation对象中显示的都是李四,这就是浅克隆的效果,对具体原型类(Citation)中的引用类型的属性进行引用复制。这种情况需要使用深克隆,而进行深克隆需要使用对象流。
public class CitationTest1 {
public static void main(String[] args) throws Exception {
Citation c1 = new Citation();
Student stu = new Student("张三", "西安");
c1.setStu(stu);
//创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\Think\\Desktop\\b.txt"));
//将c1对象写出到文件中
oos.writeObject(c1);
oos.close();
//创建对象出入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\Think\\Desktop\\b.txt"));
//读取对象
Citation c2 = (Citation) ois.readObject();
//获取c2奖状所属学生对象
Student stu1 = c2.getStu();
stu1.setName("李四");
//判断stu对象和stu1对象是否是同一个对象
System.out.println("stu和stu1是同一个对象?" + (stu == stu1));
c1.show();
c2.show();
}
}
注意:Citation类和Student类必须实现Serializable接口,否则会抛NotSerializableException异常。
自我理解:在我们需要创建一些许多相似对象的时候,可以创建一个原型接口或者抽象类,其中需要有克隆的方法,之后可以创建多个具体的对象类,每个类实现克隆的方法,实现复制效果。
🔖1.4、建造者模式
将一个复杂对象的构建与表示分离。
-
分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
-
由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
-
建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。
建造者(Builder)模式包含如下角色:
-
抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。
-
具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
-
产品类(Product):要创建的复杂对象。
-
指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
想象一下,你正在建造一座房子。建造一个房子涉及到多个步骤,比如地基的建设、墙壁的搭建、屋顶的安装等。而且,不同的房子可能有不同的建造流程和组件。
使用建造者模式,你可以将每个构建步骤封装在单独的建造者类中,并通过一个指导者类来协调这些步骤的执行。最终,你可以根据需要选择不同的建造者来构建出不同类型的房子。
/**
* @author 小白程序员
* @Classname House
* @Description 房屋类
* @date 2023/8/16 21:46
*/
@Data
public class House {
private String foundation;
private String walls;
private String roof;
}
/**
* @author 小白程序员
* @Classname HouseBuilder
* @Description 房屋建造者
* @date 2023/8/16 21:49
*/
@Getter
public abstract class HouseBuilder {
protected House house;
public void createNewHouse(){
house = new House();
}
public abstract void buildWalls();
public abstract void buildRoof();
public abstract void buildFoundation();
}
/**
* @author 小白程序员
* @Classname VillaBuilder
* @Description 别墅建造者
* @date 2023/8/16 22:06
*/
public class VillaBuilder extends HouseBuilder{
@Override
public void buildWalls() {
house.setWalls("大理石墙");
}
@Override
public void buildRoof() {
house.setRoof("玻璃屋顶");
}
@Override
public void buildFoundation() {
house.setFoundation("钢筋混凝土地基");
}
}
/**
* @author 小白程序员
* @Classname Director
* @Description 指导者
* @date 2023/8/16 22:12
*/
public class Director {
private HouseBuilder houseBuilder;
public void setHouseBuilder(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
public House getHouse(){
return houseBuilder.getHouse();
}
public void constructHouse(){
houseBuilder.createNewHouse();
houseBuilder.buildWalls();
houseBuilder.buildRoof();
houseBuilder.buildFoundation();
}
}
/**
* @author 小白程序员
* @Classname Main
* @Description 使用建造者模式构建房子
* @date 2023/8/16 22:21
*/
public class Main {
public static void main(String[] args) {
Director director = new Director();
VillaBuilder villaBuilder = new VillaBuilder();
director.setHouseBuilder(villaBuilder);
director.constructHouse();
House villa = director.getHouse();
System.out.println(villa);
}
}
在这个示例中,House
类表示房子对象,HouseBuilder
类是建造者类。你可以通过调用HouseBuilder
类的方法来设置房子的属性,然后通过调用getHouse()
方法获取最终构建好的房子对象。
使用建造者模式,你可以根据需要选择性地设置房子的属性,而不必关心构建过程的细节。这样可以使得代码更加清晰、易于维护,并且能够应对复杂对象的创建需求。
优点:
-
建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
-
在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
-
可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
-
建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
缺点:
造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
使用场景:
建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。
-
创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
-
创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。
扩展:
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。
重构前代码如下:
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Phone(String cpu, String screen, String memory, String mainboard) {
this.cpu = cpu;
this.screen = screen;
this.memory = memory;
this.mainboard = mainboard;
}
public String getCpu() {
return cpu;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public String getScreen() {
return screen;
}
public void setScreen(String screen) {
this.screen = screen;
}
public String getMemory() {
return memory;
}
public void setMemory(String memory) {
this.memory = memory;
}
public String getMainboard() {
return mainboard;
}
public void setMainboard(String mainboard) {
this.mainboard = mainboard;
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
//构建Phone对象
Phone phone = new Phone("intel","三星屏幕","金士顿","华硕");
System.out.println(phone);
}
}
上面在客户端代码中构建Phone对象,传递了四个参数,如果参数更多呢?代码的可读性及使用的成本就是比较高。
重构后代码:
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;
private Phone(Builder builder) {
cpu = builder.cpu;
screen = builder.screen;
memory = builder.memory;
mainboard = builder.mainboard;
}
public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String mainboard;
public Builder() {}
public Builder cpu(String val) {
cpu = val;
return this;
}
public Builder screen(String val) {
screen = val;
return this;
}
public Builder memory(String val) {
memory = val;
return this;
}
public Builder mainboard(String val) {
mainboard = val;
return this;
}
public Phone build() {
return new Phone(this);}
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}
public class Client {
public static void main(String[] args) {
Phone phone = new Phone.Builder()
.cpu("intel")
.mainboard("华硕")
.memory("金士顿")
.screen("三星")
.build();
System.out.println(phone);
}
}
重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。
🔖1.5创建者模式对比
📕1.5.1工厂方法模式VS建造者模式
-
工厂方法模式:
- 目的:定义一个用于创建对象的接口,但将具体的对象创建延迟到子类中进行。
- 应用场景:适用于需要根据不同的情况或条件创建不同类型的对象,并且这些对象都实现了同一接口或继承了同一基类。
- 示例:假设有一个抽象产品接口,有多个具体产品实现类,每个具体产品都有对应的工厂类来创建该产品对象。
-
建造者模式:
- 目的:将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
- 应用场景:适用于创建复杂对象的情况,其中构建过程包含多个步骤,并且不同的构建顺序和配置可以生成不同的对象。
- 示例:假设有一个汽车建造者接口,有多个具体建造者实现类,每个具体建造者都能按照一定的步骤和配置构建汽车对象。
区别总结:
- 工厂方法模式关注的是通过不同的工厂类来创建不同类型的对象,每个工厂类只负责创建一种产品;
- 建造者模式关注的是通过一个统一的建造者接口或类来构建复杂对象,可以按照不同的步骤和配置生成不同的对象表示。
简而言之,工厂方法模式适用于需要根据情况创建不同类型的对象,而建造者模式适用于创建复杂对象的场景,并且可以灵活地配置对象的构建过程。
📕1.5.2抽象工厂模式VS建造者模式
-
抽象工厂模式(Abstract Factory Pattern):
- 目的:提供一个接口,用于创建一系列相关或相互依赖的对象,而无需指定具体的类。
- 应用场景:适用于需要创建一组相关对象的情况,这些对象通常具有共同的接口或基类,并且可以通过切换具体工厂来改变整个产品族的创建过程。
- 示例:假设有一个汽车工厂接口,有两个具体工厂实现类分别生产轿车和SUV,每个具体工厂都能创建对应类型的汽车对象。
-
建造者模式(Builder Pattern):
- 目的:将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
- 应用场景:适用于创建复杂对象的情况,其中构建过程包含多个步骤,并且不同的构建顺序和配置可以生成不同的对象。
- 示例:假设有一个汽车建造者接口,有两个具体建造者实现类分别用于构建轿车和SUV,每个具体建造者都能按照一定的步骤和配置构建对应类型的汽车对象。
总结区别:
- 抽象工厂模式关注的是创建一系列相关对象,通过切换具体工厂来改变整个产品族的创建过程;
- 建造者模式关注的是创建复杂对象,通过不同的构建顺序和配置可以生成不同的对象表示。
简而言之,抽象工厂模式着重于创建一组相关对象,而建造者模式则专注于构建复杂对象。
👍二、结构型模式
🔖2.1代理模式
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。
想象一下,你要买一本书,但你不想亲自去书店。于是你找了一个代购,他会帮你去书店购买并送到你手上。在这个例子中,代购就是一个代理对象,他代表你去执行购买书籍的任务。
在软件开发中,代理模式也是类似的。它可以为其他对象提供一个替代品或占位符,以控制对这些对象的访问。代理对象和被代理对象实现相同的接口,使得代理对象可以在不改变客户端代码的情况下,对被代理对象进行控制和增强。
代理(Proxy)模式分为三种角色:
-
抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
-
真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
-
代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
①静态代理案例
/**
* @author 小白程序员
* @Classname Image
* @Description Image接口
* @date 2023/8/17 10:58
*/
public interface Image {
void display();
}
/**
* @author 小白程序员
* @Classname RealImage
* @Description image的具体实现类
* @date 2023/8/17 11:02
*/
public class RealImage implements Image{
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("从磁盘加载"+fileName+"图片"+fileName);
}
@Override
public void display() {
System.out.println("显示"+fileName+"图片"+fileName);
}
}
/**
* @author 小白程序员
* @Classname ImageProxy
* @Description 代理类
* @date 2023/8/17 11:04
*/
public class ImageProxy implements Image{
private String fileName;
private RealImage realImage;
public ImageProxy(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
if(realImage == null) {
realImage = new RealImage(fileName);
}
realImage.display();
}
}
/**
* @author 小白程序员
* @Classname Client
* @Description 测试
* @date 2023/8/17 11:07
*/
public class Client {
public static void main(String[] args) {
Image image = new ImageProxy("image.jpg");
//第一次调用displag()方法时,会创建并加载真实的图片对象
image.display();
//第二次调用display()方法时,会直接使用已经创建的真实图片对象
image.display();
}
}
解释一下:当第一次调用display()方法的时候,会调用loadFromDisk()方法,所以输出了第一句话,在第二次调用时判断了realImage是否为null,所以就不会再次创建了。ImageProxy
就是代理类,它实现了 Image
接口,并持有一个 RealImage
对象的引用。当调用 display()
方法时,代理类会通过调用真实图片对象的 display()
方法来完成显示操作。
②动态代理
📕JDK动态代理
/**
* @author 小白程序员
* @Classname Image
* @Description image接口
* @date 2023/8/17 11:19
*/
public interface Image {
void display();
}
/**
* @author 小白程序员
* @Classname RealImage
* @Description Image接口的实现类
* @date 2023/8/17 11:20
*/
public class RealImage implements Image{
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("从磁盘加载图片");
}
@Override
public void display() {
System.out.println("显示图片");
}
}
/**
* @author 小白程序员
* @Classname ImageProxyHandler
* @Description 工厂
* @date 2023/8/17 11:33
*/
public class ProxyFactory implements InvocationHandler{
private Object realObject;
public ProxyFactory(Object realObject){
this.realObject = realObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代理开始");
Object result = method.invoke(realObject, args);
System.out.println("代理结束");
return result;
}
}
/**
* @author 小白程序员
* @Classname Client
* @Description 测试
* @date 2023/8/17 11:44
*/
public class Client {
public static void main(String[] args) {
Image realImage = new RealImage("image.jpg");
Image proxyImage =(Image) Proxy.newProxyInstance(
realImage.getClass().getClassLoader(),
realImage.getClass().getInterfaces(),
new ProxyFactory(realImage)
);
proxyImage.display();
}
}
📕CGLIB动态代理(要引入CGLIB依赖)
/**
* @author 小白程序员
* @Classname Image1
* @Description Image1
* @date 2023/8/17 13:12
*/
public class Image1 {
public void display(){
System.out.println("Image1");
}
}
/**
* @author 小白程序员
* @Classname ImageMethodInterceptor
* @Description 代理类
* @date 2023/8/17 12:47
*/
public class ImageMethodInterceptor implements MethodInterceptor{
/*
intercept方法参数说明:
obj : 代理对象
method : 真实对象中的方法的Method实例
args : 实际参数
proxy :代理对象中的方法的method实例
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
//在调用代理对象的方法之前执行一些额外的操作
System.out.println("代理开始执行了");
//调用真实对象的方法
Object result = proxy.invokeSuper(obj, args);
//在调用代理对象的方法之后执行一些额外的操作
System.out.println("代理执行结束了");
return result;
}
}
/**
* @author 小白程序员
* @Classname CglibProxyExample
* @Description 测试
* @date 2023/8/17 12:52
*/
public class CglibProxyExample {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Image1.class);
enhancer.setCallback(new ImageMethodInterceptor());
Image1 proxyImage = (Image1)enhancer.create();
proxyImage.display();
}
}
在这两个示例中,我们分别使用了JDK动态代理和CGLIB动态代理来创建代理对象。JDK动态代理要求被代理类实现一个接口,而CGLIB动态代理则可以直接代理普通的类。
❗JDK动态代理、CGLIB动态代理、静态代理对比
JDK动态代理是基于接口的代理。它通过
java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口来实现。JDK动态代理要求目标对象必须实现一个或多个接口。代理对象会实现这些接口,并将方法调用委托给InvocationHandler处理器进行处理。因为基于接口的代理,所以只能代理接口中定义的方法。CGLIB动态代理是基于继承的代理。它通过继承目标对象创建一个子类,并重写目标对象的方法来实现代理。CGLIB动态代理不需要目标对象实现接口,它可以代理普通的类。CGLIB动态代理通过生成字节码来创建代理类,因此在运行时性能较JDK动态代理略低。
JDK动态代理是Java标准库的一部分,无需引入额外的依赖。而CGLIB动态代理则需要引入CGLIB库作为依赖。
JDK动态代理适用于对接口进行代理的场景,比如Spring AOP中的切面编程。而CGLIB动态代理适用于对类进行代理的场景,比如在没有接口的情况下对类进行代理。
总体来说,如果你需要代理接口中的方法,可以选择JDK动态代理;如果你需要代理普通类中的方法,或者目标对象没有实现接口,可以选择CGLIB动态代理。
-
jdk代理和CGLIB代理
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。
-
动态代理和静态代理
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
优点:
-
代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
-
代理对象可以扩展目标对象的功能;
-
代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
-
增加了系统的复杂度;
使用场景:
-
远程(Remote)代理
本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
-
防火墙(Firewall)代理
当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
-
保护(Protect or Access)代理
控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
🔖2.2适配器模式
适配器模式是一种结构型设计模式,它用于将一个类的接口转换成客户端所期望的另一个接口。通俗地说,适配器模式就像是一个插头转换器,它可以让你在不改变原有插座的情况下,使用不同类型的插头。
举个例子来说明适配器模式。假设你有一个音乐播放器,它只能播放MP3格式的音乐文件。但是你现在有一些其他格式的音乐文件,比如WAV和FLAC,你希望能够在音乐播放器上播放这些文件。这时候,你可以使用适配器模式。
适配器模式的核心思想就是通过一个适配器类来将不兼容的接口转换为兼容的接口,从而让不同的类能够协同工作。它可以帮助你在不改变已有代码的情况下,实现接口之间的互相适配。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
适配器模式(Adapter)包含以下主要角色:
-
目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
-
适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
-
适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
①类适配器模式
它通过多重继承来实现适配器功能。在类适配器模式中,适配器类同时继承了目标接口和被适配者类,从而可以将被适配者的方法转换为目标接口所期望的方法。
/**
* @author 小白程序员
* @Classname Target
* @Description 目标接口
* @date 2023/8/17 16:08
*/
public interface Target {
void request();
}
/**
* @author 小白程序员
* @Classname Adaptee
* @Description 被适配者类
* @date 2023/8/17 16:09
*/
public class Adaptee {
public void specificRequest(){
System.out.println("被适配者方法");
}
}
/**
* @author 小白程序员
* @Classname Adapter
* @Description 适配器类
* @date 2023/8/17 16:10
*/
public class Adapter extends Adaptee implements Target{
@Override
public void request() {
specificRequest();//调用被适配者类的方法
}
}
/**
* @author 小白程序员
* @Classname Main
* @Description 测试
* @date 2023/8/17 16:12
*/
public class Main {
public static void main(String[] args) {
Target target = new Adapter();
target.request();
}
}
在这个示例中,Adapter
类继承了Adaptee
类,并实现了Target
接口。当客户端调用request
方法时,实际上是通过适配器类间接调用了被适配者类的specificRequest
方法。
类适配器模式的优点是可以重用已有的代码,而不需要修改目标接口和被适配者类。但它也有一个限制,即适配器类只能适配一个被适配者类。
②对象适配器模式
它通过组合关系将一个类的接口转换成客户端所希望的另一个接口,在对象适配器模式中,适配器类持有一个被适配者类的实例,并实现了目标接口。
/**
* @author 小白程序员
* @Classname Target
* @Description 目标接口
* @date 2023/8/17 16:08
*/
public interface Target {
void request();
}
/**
* @author 小白程序员
* @Classname Adaptee
* @Description 被适配者类
* @date 2023/8/17 16:09
*/
public class Adaptee {
public void specificRequest(){
System.out.println("被适配者方法");
}
}
/**
* @author 小白程序员
* @Classname Adapter
* @Description 适配器类
* @date 2023/8/17 16:10
*/
public class Adapter implements Target {
private Adaptee adaptee;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
@Override
public void request() {
adaptee.specificRequest();
}
}
/**
* @author 小白程序员
* @Classname Main
* @Description 测试
* @date 2023/8/17 16:37
*/
public class Main {
public static void main(String[] args) {
Adaptee adaptee = new Adaptee();
Target target = new Adapter(adaptee);
target.request();
}
}
在这个示例中,Adapter
类持有一个Adaptee
类的实例,并实现了Target
接口。当客户端调用request
方法时,实际上是通过适配器类间接调用了被适配者类的specificRequest
方法。
对象适配器模式的优点是可以适配多个被适配者类,并且不需要修改目标接口和被适配者类。但它也有一个限制,即适配器类只能适配与目标接口兼容的被适配者类。
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
应用场景:
-
以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
-
使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
🔖2.3装饰者模式
装饰者模式是一种结构型设计模式,它允许你在不改变现有对象结构的情况下,动态地将新功能添加到对象上。装饰者模式通过创建一个包装对象来实现,在保持接口的同时,增加了额外的行为。
装饰(Decorator)模式中的角色:
-
抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
-
具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
-
抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
-
具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
/**
* @author 小白程序员
* @Classname Component
* @Description 基础组件库
* @date 2023/8/17 17:21
*/
public interface Component {
void operation();
}
/**
* @author 小白程序员
* @Classname ConcreteComponent
* @Description 基础组件库的实现
* @date 2023/8/17 17:27
*/
public class ConcreteComponent implements Component{
@Override
public void operation() {
System.out.println("基础组件操作");
}
}
/**
* @author 小白程序员
* @Classname Decorator
* @Description 装饰者抽象类
* @date 2023/8/17 17:28
*/
public abstract class Decorator implements Component{
protected Component component;
public Decorator(Component component){
this.component = component;
}
@Override
public void operation() {
component.operation();
}
}
/**
* @author 小白程序员
* @Classname ConcreteDecoratorA
* @Description 具体的装饰者类A
* @date 2023/8/17 17:32
*/
public class ConcreteDecoratorA extends Decorator{
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public void operation(){
System.out.println("添加额外行为A");
super.operation();
System.out.println("添加额外行为A");
}
}
/**
* @author 小白程序员
* @Classname ConcreteDecoratorB
* @Description 具体的装饰者类B
* @date 2023/8/17 17:34
*/
public class ConcreteDecoratorB extends Decorator{
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public void operation(){
System.out.println("添加额外行为B");
super.operation();
System.out.println("添加额外行为B");
}
}
/**
* @author 小白程序员
* @Classname Main
* @Description 测试
* @date 2023/8/17 17:36
*/
public class Main {
public static void main(String[] args) {
Component component = new ConcreteComponent();
component.operation();
System.out.println("------------------------");
Component concreteDecoratorA = new ConcreteDecoratorA(component);
concreteDecoratorA.operation();
System.out.println("------------------------");
Component concreteDecoratorB = new ConcreteDecoratorB(component);
concreteDecoratorB.operation();
System.out.println("------------------------");
Component concreteDecoratorAB = new ConcreteDecoratorB(new ConcreteDecoratorA(component));
concreteDecoratorAB.operation();
}
}
在这个示例中,ConcreteComponent
是基础组件类,Decorator
是装饰者抽象类,ConcreteDecoratorA
和ConcreteDecoratorB
是具体的装饰者类。我们可以通过创建不同的装饰者对象来动态地给基础组件添加额外的行为。
使用场景:
-
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况主要有两类:
-
第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
-
第二类是因为类定义不能继承(如final类)
-
-
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
-
当对象的功能要求可以动态地添加,也可以再动态地撤销时。
❗静态代理和装饰者的区别
静态代理和装饰者模式的区别:
-
相同点:
-
都要实现与目标类相同的业务接口
-
在两个类中都要声明目标对象
-
都可以在不修改目标类的前提下增强目标方法
-
-
不同点:
-
目的不同 装饰者是为了增强目标对象 静态代理是为了保护和隐藏目标对象
-
获取目标对象构建的地方不同 装饰者是由外界传递进来,可以通过构造方法传递 静态代理是在代理类内部创建,以此来隐藏目标对象
-
🔖2.4桥接模式
它用于将抽象部分和实现部分分离,使它们可以独立地变化。通俗来说,桥接模式就像是搭建一座桥,连接两个独立的部分。
桥接(Bridge)模式包含以下主要角色:
-
抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
-
扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
-
实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
-
具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
假设有两个维度的类别,比如形状(Shape)和颜色(Color),每个类别都有多个具体的实现类。在没有桥接模式的情况下,我们可能需要为每个形状和颜色的组合创建一个具体的类,这样会导致类的爆炸性增长。
而通过桥接模式,我们可以将形状和颜色这两个维度进行解耦,将它们分别作为独立的类层级结构。然后使用一个桥接接口(Bridge)来连接形状和颜色,实现它们之间的关联。这样,当我们需要新增一种形状或者颜色时,只需要扩展对应的类层级即可,不需要修改已有的代码。
举个例子,假设我们有形状接口 Shape 和颜色接口 Color,它们分别有多个具体的实现类,比如圆形、正方形、红色、蓝色等。通过桥接模式,我们可以定义一个桥接接口 DrawAPI,它有一个方法 drawShape,然后在具体的形状类中调用 DrawAPI 的 drawShape 方法来绘制形状。这样,我们可以根据需要随意组合形状和颜色,而不需要为每一种组合都创建一个具体的类。
/**
* @author 小白程序员
* @Classname Shape
* @Description 形状接口
* @date 2023/8/18 15:38
*/
public interface Shape {
void draw();
}
/**
* @author 小白程序员
* @Classname Color
* @Description 颜色接口
* @date 2023/8/18 15:40
*/
public interface Color {
void fill();
}
/**
* @author 小白程序员
* @Classname Circle
* @Description 形状接口具体实现类
* @date 2023/8/18 15:41
*/
public class Circle implements Shape{
private Color color;
public Circle(Color color){
this.color = color;
}
@Override
public void draw() {
System.out.println("绘制圆形!");
color.fill();
}
}
/**
* @author 小白程序员
* @Classname Red
* @Description 颜色具体实现类
* @date 2023/8/18 15:43
*/
public class Red implements Color{
@Override
public void fill() {
System.out.println("填充红色!");
}
}
/**
* @author 小白程序员
* @Classname BridgePatternDemo
* @Description 测试
* @date 2023/8/18 15:44
*/
public class BridgePatternDemo {
public static void main(String[] args) {
Shape redCricle = new Circle(new Red());
redCricle.draw();
}
}
通过桥接模式,我们可以灵活地组合不同的形状和颜色,而不需要为每一种组合都创建一个具体的类。
好处:
-
桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
-
实现细节对客户透明
使用场景:
-
当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
-
当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
-
当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
🔖2.5外观模式
它提供了一个简单的接口,隐藏了底层复杂系统的复杂性,使得使用者可以更方便地与系统进行交互。通俗来说,外观模式就像是一个门面,将系统的复杂性隐藏在背后,对外提供一个简单的接口。
外观(Facade)模式包含以下主要角色:
-
外观(Facade)角色:为多个子系统对外提供一个共同的接口。
-
子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
假设你要使用一个复杂的系统,这个系统由多个子系统组成,每个子系统都有很多复杂的类和方法。如果直接与每个子系统进行交互,那么会变得非常繁琐和复杂。而通过外观模式,我们可以定义一个外观类(Facade),它封装了底层子系统的复杂操作,并提供一个简单的接口给使用者。
举个例子,假设你要使用一个电脑,电脑内部有CPU、内存和硬盘等多个子系统。如果直接与每个子系统进行交互,需要了解各个子系统的细节,操作起来会很麻烦。而通过外观模式,我们可以定义一个电脑外观类(ComputerFacade),它封装了启动电脑、关闭电脑等复杂操作,并提供一个简单的接口给使用者,比如 startComputer()
和 shutdownComputer()
。
/**
* @author 小白程序员
* @Classname CPU
* @Description 子系统类
* @date 2023/8/18 16:05
*/
public class CPU {
public void start(){
System.out.println("CPU启动!");
}
public void shutdown(){
System.out.println("CPU关闭!");
}
}
/**
* @author 小白程序员
* @Classname Memory
* @Description 子系统类
* @date 2023/8/18 16:06
*/
public class Memory {
public void start(){
System.out.println("内存启动!");
}
public void shutdown(){
System.out.println("内存关闭!");
}
}
/**
* @author 小白程序员
* @Classname HardDrive
* @Description 子系统类
* @date 2023/8/18 16:07
*/
public class HardDrive {
public void start(){
System.out.println("硬盘启动!");
}
public void shutdown(){
System.out.println("硬盘关闭!");
}
}
/**
* @author 小白程序员
* @Classname ComputerFacade
* @Description 外观类
* @date 2023/8/18 16:08
*/
public class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade(){
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void startComputer(){
System.out.println("启动电脑!");
cpu.start();
memory.start();
hardDrive.start();
System.out.println("电脑启动完成!");
}
public void shutdownComputer(){
System.out.println("关闭电脑!");
cpu.shutdown();
memory.shutdown();
hardDrive.shutdown();
System.out.println("电脑关闭完成!");
}
}
/**
* @author 小白程序员
* @Classname FacadeOatternDemo
* @Description 测试
* @date 2023/8/18 16:13
*/
public class FacadeOatternDemo {
public static void main(String[] args) {
ComputerFacade computer = new ComputerFacade();
computer.startComputer();
System.out.println("=====================================================");
computer.shutdownComputer();
}
}
通过外观模式,我们可以使用简单的接口 startComputer()
和 shutdownComputer()
来启动和关闭电脑,而不需要了解底层子系统的复杂性。
好处:
-
降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
-
对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
-
不符合开闭原则,修改很麻烦
使用场景:
-
对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
-
当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
-
当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
🔖2.6组合模式
组合模式,允许我们将对象组合成树形结构来表示部分-整体的层次关系。通俗来讲,组合模式就像是一个文件夹里面可以包含文件和其他文件夹,形成了一个树状结构。
组合模式主要包含三种角色:
-
抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
-
树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
-
叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
举个例子,假设你要处理一个文件系统,文件系统由文件和文件夹组成。文件夹可以包含文件和其他文件夹,而文件不能再包含其他文件或文件夹。如果直接操作每个文件和文件夹,会变得非常繁琐和复杂。而通过组合模式,我们可以使用统一的方式来处理文件和文件夹,将它们组织成一个树形结构。
在组合模式中,我们定义一个抽象类或接口(比如 Component
),它既可以代表文件,也可以代表文件夹。然后我们实现具体的文件类(比如 File
)和文件夹类(比如 Folder
)。文件夹类中可以包含多个文件和文件夹,从而形成了一个树状结构。
/**
* @author 小白程序员
* @Classname File
* @Description 文件类
* @date 2023/8/18 16:42
*/
public class File extends Component{
public File(String name) {
super(name);
}
@Override
public void display() {
System.out.println("文件:"+name);
}
}
/**
* @author 小白程序员
* @Classname Folder
* @Description 文件夹类
* @date 2023/8/18 16:44
*/
public class Folder extends Component{
private final List<Component> components;
public Folder(String name) {
super(name);
components = new ArrayList<>();
}
@Override
public void display() {
System.out.println("文件夹:"+name);
for (Component component : components) {
component.display();
}
}
public void add(Component component){
components.add(component);
}
public void remove(Component component){
components.remove(component);
}
}
/**
* @author 小白程序员
* @Classname Component
* @Description 抽象类
* @date 2023/8/18 16:39
*/
public abstract class Component {
protected String name;
public Component(String name){
this.name = name;
}
public abstract void display();
public void add(Component component){
throw new UnsupportedOperationException();
}
public void remove(Component component){
throw new UnsupportedOperationException();
}
}
/**
* @author 小白程序员
* @Classname CompositePatternDemo
* @Description 测试
* @date 2023/8/18 16:50
*/
public class CompositePatternDemo {
public static void main(String[] args) {
Component file = new File("file1.txt");
Component file1 = new File("file2.txt");
Component folder1 = new Folder("folder1");
Component folder2 = new Folder("folder2");
folder1.add(file);
folder2.add(file1);
folder2.add(folder1);
folder2.display();
}
}
通过组合模式,我们可以使用统一的方式来处理文件和文件夹,无论是操作单个文件还是整个文件夹,都可以使用相同的方式进行处理。
📕组合模式的分类
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。
-
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中
Component
声明了add
、remove
方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
-
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Folder类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
优点:
-
组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
-
客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
-
在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
-
组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
应用场景:
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。
🔖2.7享元模式
享元模式,它的目标是通过共享对象来减少内存使用和提高性能。在享元模式中,将对象分为可共享的内部状态和不可共享的外部状态。
享元(Flyweight )模式中存在以下两种状态:
内部状态,即不会随着环境的改变而改变的可共享部分。
外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要有以下角色:
-
抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
-
具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
-
非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
-
享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
通俗地说,想象一下你正在玩一个多人游戏,每个玩家都有自己的角色。这些角色可能具有相同的外观、技能或者其他属性。如果每个角色都创建一个新的对象,那么会占用大量的内存。而使用享元模式,我们可以将相同的属性提取出来作为内部状态,并在需要时共享这些内部状态,从而节省内存。
/**
* @author 小白程序员
* @Classname Character
* @Description 游戏角色类
* @date 2023/8/18 20:45
* 在这个例子中,name 和 weapon 是角色的外部状态,因为它们会随着每个角色的不同而变化。
* 如果我们有很多角色,但是有些角色具有相同的武器,那么我们可以将武器作为内部状态进行共享。
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Character {
private String name;
private String weapon;
}
/**
* @author 小白程序员
* @Classname CharacterFactory
* @Description 享元工厂类
* @date 2023/8/18 20:47
* 在上述代码中,CharacterFactory 是享元工厂类,负责创建和管理角色对象。
* 当需要一个角色时,首先检查是否已经有该武器的角色存在,如果存在则直接返回共享的角色对象,否则创建一个新的角色对象并进行缓存。
* 通过使用享元模式,我们可以节省内存,并且在需要时重复使用相同的属性。这对于拥有大量相似对象的场景非常有用,例如游戏中的角色、图形编辑器中的图形等。
*/
public class CharacterFactory {
private Map<String,Character> characterMap = new HashMap<>();
public Character getCharacter(String weapon){
if(!characterMap.containsKey(weapon)){
Character character = new Character("Default",weapon);
characterMap.put(weapon,character);
}
return characterMap.get(weapon);
}
}
/**
* @author 小白程序员
* @Classname FlyweightPatternTest
* @Description 测试
* @date 2023/8/18 20:55
*/
public class FlyweightPatternTest {
public static void main(String[] args) {
CharacterFactory characterFactory = new CharacterFactory();
//创建两个角色,武器相同
Character character1 = characterFactory.getCharacter("Sword");
Character character2 = characterFactory.getCharacter("Sword");
//创建另一个角色,武器不同
Character character3 = characterFactory.getCharacter("Bow");
//检查是否共享了相同的角色对象
System.out.println(character1 == character2);
System.out.println(character1 == character3);
}
}
在上述测试类中,我们首先创建了一个 CharacterFactory
对象,然后通过该工厂获取了几个角色对象。其中,character1
和 character2
具有相同的武器 "Sword",而 character3
的武器是 "Bow"。
最后,我们使用 ==
运算符检查了这些角色对象的引用是否相同。根据享元模式的定义,character1
和 character2
应该是同一个对象,而 character1
和 character3
是不同的对象。
优点:
-
极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
-
享元模式中的外部状态相对独立,且不影响内部状态
缺点:
为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
使用场景:
-
一个系统有大量相同或者相似的对象,造成内存的大量耗费。
-
对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
-
在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
👍 三、行为性模式
🔖3.1模板方法模式
模板方法模式,它定义了一个算法的骨架,将某些步骤的实现延迟到子类中。通俗地说,模板方法模式就像是制定了一个操作流程的模板,其中一些具体的步骤由子类来实现。
举个例子,假设我们要编写一个制作咖啡和茶的程序。这两种饮料的制作过程有一些共同的步骤,例如煮水、冲泡、倒入杯子等,但是具体的步骤和顺序可能会有所不同。
在模板方法模式中,我们可以定义一个抽象类,其中包含一个模板方法,该方法定义了整个制作过程的骨架,并调用了一系列的抽象方法,这些抽象方法由子类来实现。
模板方法(Template Method)模式包含以下主要角色:
-
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
-
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
-
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
-
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
-
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
-
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
-
-
-
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
/**
* @author 小白程序员
* @Classname Beverage
* @Description TODO干啥呢
* @date 2023/8/18 21:17
* Beverage 是抽象类,其中 prepareBeverage() 方法是模板方法,定义了制作饮料的整个流程。
* brew() 和 addCondiments() 是抽象方法,由子类来实现具体的步骤。
*/
public abstract class Beverage {
public final void prepareRecipe(){
boilWater();
brew();
pourInCup();
addCondiments();
}
protected abstract void addCondiments();
private void pourInCup() {
System.out.println("Pouring into cup");
}
protected abstract void brew();
private void boilWater() {
System.out.println("Boiling water");
}
}
/**
* @author 小白程序员
* @Classname Coffee
* @Description Coffee类
* @date 2023/8/18 21:22
*/
public class Coffee extends Beverage{
@Override
protected void addCondiments() {
System.out.println("加糖");
}
@Override
protected void brew() {
System.out.println("冲泡咖啡");
}
}
/**
* @author 小白程序员
* @Classname BeverageTest
* @Description 测试
* @date 2023/8/18 21:24
*/
public class BeverageTest {
public static void main(String[] args) {
Beverage coffee = new Coffee();
coffee.prepareRecipe();
}
}
注意:为防止恶意操作,一般模板方法都加上 final 关键词。
优点:
-
提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
-
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
-
对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
-
父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
使用场景:
-
算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
-
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
🔖3.2策略模式
策略模式,它允许在运行时根据不同的情况选择不同的算法或行为。通俗地说,策略模式就像是在一个问题上有多个可选的解决方案,我们可以根据具体的需求来选择使用哪种解决方案。
策略模式的主要角色如下:
-
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
-
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
-
环境(Context)类:持有一个策略类的引用,最终给客户端调用。
举个例子,假设我们正在开发一个游戏,其中有多种角色,每个角色都有不同的攻击方式。我们可以使用策略模式来实现这个功能。
/**
* @author 小白程序员
* @Classname Character
* @Description 角色类
* @date 2023/8/18 21:42
*/
public abstract class Character {
public abstract void attack();
}
/**
* @author 小白程序员
* @Classname Knight
* @Description 角色具体实现类
* @date 2023/8/18 21:43
*/
public class Knight extends Character{
@Override
public void attack() {
System.out.println("Knight attacks with a sword");
}
}
/**
* @author 小白程序员
* @Classname Mage
* @Description 角色具体实现类
* @date 2023/8/18 21:47
*/
public class Mage extends Character{
@Override
public void attack() {
System.out.println("法师释放大法术");
}
}
/**
* @author 小白程序员
* @Classname Game
* @Description Game 类通过 setCharacter() 方法设置角色对象,并通过 performAttack() 方法执行角色的攻击动作。
* @date 2023/8/18 21:48
* Game 类通过 setCharacter() 方法设置角色对象,并通过 performAttack() 方法执行角色的攻击动作。
*/
public class Game {
private Character character;
public void setCharacter(Character character) {
this.character = character;
}
public void performAttack(){
character.attack();
}
}
/**
* @author 小白程序员
* @Classname StrategyPatternTest
* @Description 测试
* @date 2023/8/18 21:59
*/
public class StrategyPatternTest {
public static void main(String[] args) {
Game game = new Game();
Character knight = new Knight();
game.setCharacter(knight);
game.performAttack();
Character mage = new Mage();
game.setCharacter(mage);
game.performAttack();
}
}
优点:
-
策略类之间可以自由切换
由于策略类都实现同一个接口,所以使它们之间可以自由切换。
-
易于扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
-
避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
缺点:
-
客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
-
策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
使用场景:
-
一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
-
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
-
系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
-
系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
-
多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
🔖3.3命令模式
命令模式,它允许将请求封装成一个对象,从而使得可以用不同的请求对客户端进行参数化。这样可以将请求的发送者和接收者解耦,使得系统更加灵活。
通俗来说,命令模式就像是我们在餐厅点餐一样。顾客(客户端)通过服务员(调用者)来下订单(命令),然后服务员将订单交给厨师(接收者),厨师根据订单进行烹饪(执行命令)。这样,顾客不需要直接与厨师交流,而是通过服务员来传达自己的需求。
在命令模式中,有以下几个关键角色:
- 命令(Command):定义了执行操作的接口,包含了执行命令的方法。
- 具体命令(Concrete Command):实现了命令接口,持有接收者,并将具体的操作委托给接收者执行。
- 接收者(Receiver):负责具体执行命令所要求的操作。
- 调用者(Invoker):负责调用命令对象并触发命令执行。
- 客户端(Client):创建命令对象,并设置其接收者,将命令对象传递给调用者进行调用。
通过使用命令模式,我们可以将请求的发送者和接收者解耦,使得系统更加灵活。例如,我们可以轻松地添加新的命令,而无需修改现有的代码。同时,命令模式还支持撤销、重做等操作,提供了更多的灵活性和可扩展性。
/**
* @author 小白程序员
* @Classname Command
* @Description 命令接口
* @date 2023/8/20 14:33
*/
public interface Command {
void execute();
}
/**
* @author 小白程序员
* @Classname Light
* @Description 接收类
* @date 2023/8/20 14:35
*/
public class Light {
public void turnOn(){
System.out.println("Light is on");
}
public void turnOff(){
System.out.println("Light is off");
}
}
/**
* @author 小白程序员
* @Classname LightOnCommand
* @Description 命令类
* @date 2023/8/20 14:34
*/
public class LightOnCommand implements Command{
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
/**
* @author 小白程序员
* @Classname LightOffCommand
* @Description 命令类
* @date 2023/8/20 14:37
*/
public class LightOffCommand implements Command{
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
/**
* @author 小白程序员
* @Classname RemoteControl
* @Description 调用者类
* @date 2023/8/20 14:38
*/
public class RemoteControl {
private Command command;
public void setCommand(Command command){
this.command = command;
}
public void pressButton(){
command.execute();
}
}
/**
* @author 小白程序员
* @Classname Client
* @Description 测试
* @date 2023/8/20 14:39
*/
public class Client {
public static void main(String[] args) {
Light light = new Light();
LightOnCommand lightOnCommand = new LightOnCommand(light);
LightOffCommand lightOffCommand = new LightOffCommand(light);
RemoteControl remoteControl = new RemoteControl();
remoteControl.setCommand(lightOnCommand);
remoteControl.pressButton();
remoteControl.setCommand(lightOffCommand);
remoteControl.pressButton();
}
}
通过这个例子,我们可以看到命令模式的使用方式。客户端创建具体的命令对象,并将其传递给调用者。调用者负责调用命令对象的 execute()
方法来触发命令的执行,而不需要直接与接收者交互。
优点:
-
降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
-
增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
-
可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
-
方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点:
-
使用命令模式可能会导致某些系统有过多的具体命令类。
-
系统结构更加复杂。
使用场景:
-
系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
-
系统需要在不同的时间指定请求、将请求排队和执行请求。
-
系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
🔖3.4责任链模式
责任链模式,它允许将请求沿着处理链进行传递,直到有一个处理者能够处理该请求为止。每个处理者都可以决定是否将请求传递给下一个处理者。
通俗来说,责任链模式就像是在工厂生产线上的流水线作业一样。产品从流水线的第一个工人开始处理,如果他能够完成工作,则将产品交给下一个工人继续处理;如果他无法完成工作,则将产品交给下一个工人处理。这样,产品会依次经过多个工人的处理,直到最后一个工人完成工作。
在责任链模式中,有以下几个关键角色:
- 抽象处理者(Handler):定义了处理请求的接口,并持有下一个处理者的引用。
- 具体处理者(Concrete Handler):实现了处理请求的方法,并决定是否将请求传递给下一个处理者。
-
客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
首先,我们定义抽象处理者接口 Handler
:
public interface Handler {
void setNextHandler(Handler nextHandler);
void handleRequest(Request request);
}
然后,创建具体的处理者类 ConcreteHandlerA
和 ConcreteHandlerB
,它们实现了 Handler
接口:
public class ConcreteHandlerA implements Handler {
private Handler nextHandler;
@Override
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public void handleRequest(Request request) {
if (request.getType().equals("TypeA")) {
System.out.println("ConcreteHandlerA 处理了请求");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
} else {
System.out.println("没有合适的处理者");
}
}
}
public class ConcreteHandlerB implements Handler {
private Handler nextHandler;
@Override
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
@Override
public void handleRequest(Request request) {
if (request.getType().equals("TypeB")) {
System.out.println("ConcreteHandlerB 处理了请求");
} else if (nextHandler != null) {
nextHandler.handleRequest(request);
} else {
System.out.println("没有合适的处理者");
}
}
}
接着,我们创建请求类 Request
,用于封装请求的信息:
public class Request {
private String type;
public Request(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
最后,我们可以在客户端中使用这些类来测试责任链模式:
public class Client {
public static void main(String[] args) {
Handler handlerA = new ConcreteHandlerA();
Handler handlerB = new ConcreteHandlerB();
handlerA.setNextHandler(handlerB);
Request request1 = new Request("TypeA");
handlerA.handleRequest(request1);
Request request2 = new Request("TypeB");
handlerA.handleRequest(request2);
Request request3 = new Request("TypeC");
handlerA.handleRequest(request3);
}
}
运行上述代码,输出结果为:
ConcreteHandlerA 处理了请求
ConcreteHandlerB 处理了请求
没有合适的处理者
通过这个例子,我们可以看到责任链模式的使用方式。客户端创建具体的处理者对象,并设置它们的下一个处理者。当请求发生时,首先由第一个处理者进行处理,如果无法处理,则将请求传递给下一个处理者,直到找到能够处理该请求的处理者为止。
优点:
-
降低了对象之间的耦合度
该模式降低了请求发送者和接收者的耦合度。
-
增强了系统的可扩展性
可以根据需要增加新的请求处理类,满足开闭原则。
-
增强了给对象指派职责的灵活性
当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
-
责任链简化了对象之间的连接
一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
-
责任分担
每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
缺点:
-
不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
-
对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
-
职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
🔖3.5状态模式
状态模式,它允许对象在内部状态发生改变时改变其行为。换句话说,状态模式可以让一个对象在不同的状态下表现出不同的行为。
想象一下,你正在玩一个游戏,你的角色有不同的状态,比如正常状态、受伤状态和死亡状态。在不同的状态下,你的角色可能会有不同的行为,比如移动、攻击或者无法进行任何操作。
在状态模式中,有以下几个关键角色:
- 环境类(Context):维护一个对具体状态对象的引用,并将客户端的请求委托给当前状态对象来处理。
- 抽象状态类(State):定义了一个接口,用于封装与环境类的特定状态相关的行为。
- 具体状态类(Concrete State):实现了抽象状态类的接口,具体实现了与该状态相关的行为。
假设我们有一个电梯系统,其中包含电梯和多个楼层。电梯有不同的状态,比如停止状态、运行状态和故障状态。在不同的状态下,电梯可能会有不同的行为,比如开门、关门、上升或下降。
首先,我们定义一个抽象状态类 State
,它包含了电梯可能的行为:
public interface State {
void open();
void close();
void move();
}
然后,我们创建具体的状态类 StopState
、RunState
和 FaultState
,它们分别实现了 State
接口:
public class StopState implements State {
@Override
public void open() {
System.out.println("电梯门已打开");
}
@Override
public void close() {
System.out.println("电梯门已关闭");
}
@Override
public void move() {
System.out.println("电梯开始运行");
}
}
public class RunState implements State {
@Override
public void open() {
System.out.println("电梯正在运行中,无法打开门");
}
@Override
public void close() {
System.out.println("电梯门已关闭");
}
@Override
public void move() {
System.out.println("电梯继续运行");
}
}
public class FaultState implements State {
@Override
public void open() {
System.out.println("电梯发生故障,无法打开门");
}
@Override
public void close() {
System.out.println("电梯发生故障,无法关闭门");
}
@Override
public void move() {
System.out.println("电梯发生故障,无法移动");
}
}
接下来,我们创建环境类 Elevator
,它维护了当前的状态,并将请求委托给当前状态对象来处理:
public class Elevator {
private State currentState;
public Elevator() {
// 初始状态为停止状态
currentState = new StopState();
}
public void setState(State state) {
currentState = state;
}
public void open() {
currentState.open();
}
public void close() {
currentState.close();
}
public void move() {
currentState.move();
}
}
最后,我们可以在客户端中使用这些类来测试状态模式:
public class Client {
public static void main(String[] args) {
Elevator elevator = new Elevator();
elevator.open(); // 输出:电梯门已打开
elevator.close(); // 输出:电梯门已关闭
elevator.move(); // 输出:电梯开始运行
elevator.setState(new FaultState());
elevator.open(); // 输出:电梯发生故障,无法打开门
elevator.close(); // 输出:电梯发生故障,无法关闭门
elevator.move(); // 输出:电梯发生故障,无法移动
}
}
通过这个例子,我们可以看到状态模式的使用方式。在环境类中维护一个对具体状态对象的引用,并将客户端的请求委托给当前状态对象来处理。不同的状态类实现了相应的行为,从而使得环境类的行为随着状态的改变而改变。
优点:
-
将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
-
允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
缺点:
-
状态模式的使用必然会增加系统类和对象的个数。
-
状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
-
状态模式对"开闭原则"的支持并不太好。
使用场景:
-
当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
-
一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
🔖3.6观察者模式
观察者模式是一种行为设计模式,它定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都会得到通知并自动更新。
想象一下,你正在订阅一个新闻发布系统,你和其他人都是这个系统的观察者。当有新的新闻发布时,系统会自动通知所有的观察者,并更新他们的新闻内容。
在观察者模式中,有以下几个关键角色:
- 主题(Subject):也称为被观察者或可观察对象,它维护了一个观察者列表,并提供了添加、删除和通知观察者的方法。
- 观察者(Observer):定义了一个接口,用于接收主题的通知,并进行相应的更新操作。
- 具体主题(Concrete Subject):实现了主题接口,维护了观察者列表,并在状态发生改变时通知观察者。
- 具体观察者(Concrete Observer):实现了观察者接口,接收到主题的通知后进行相应的更新操作。
假设我们有一个气象站,它可以实时获取天气信息,并将天气信息通知给订阅了该气象站的观察者。观察者可以是手机应用程序、网站或者其他设备。
首先,我们定义一个主题接口 Subject
,其中包含了添加、删除和通知观察者的方法:
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
然后,我们创建一个具体主题类 WeatherStation
,它实现了主题接口,并维护了观察者列表:
public class WeatherStation implements Subject {
private List<Observer> observers;
private String weather;
public WeatherStation() {
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() {
for (Observer observer : observers) {
observer.update(weather);
}
}
public void setWeather(String weather) {
this.weather = weather;
notifyObservers();
}
}
接下来,我们定义一个观察者接口 Observer
,其中包含了更新操作的方法:
public interface Observer {
void update(String weather);
}
然后,我们创建一个具体观察者类 PhoneApp
,它实现了观察者接口,并在收到通知后进行相应的更新操作:
public class PhoneApp implements Observer {
@Override
public void update(String weather) {
System.out.println("手机应用程序收到天气更新:" + weather);
}
}
最后,我们可以在客户端中使用这些类来测试观察者模式:
public class Client {
public static void main(String[] args) {
WeatherStation weatherStation = new WeatherStation();
PhoneApp phoneApp = new PhoneApp();
weatherStation.registerObserver(phoneApp);
weatherStation.setWeather("晴天"); // 输出:手机应用程序收到天气更新:晴天
weatherStation.removeObserver(phoneApp);
weatherStation.setWeather("下雨"); // 无输出
}
}
通过这个例子,我们可以看到观察者模式的使用方式。主题维护了一个观察者列表,并在状态发生改变时通知观察者。观察者实现了相应的接口,在收到通知后进行相应的更新操作。
优点:
-
降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
-
被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】
缺点:
-
如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
-
如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃
使用场景:
-
对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
-
当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。
🔖3.7中介者模式
中介者模式,它通过引入一个中介者对象来解耦多个对象之间的交互。在中介者模式中,多个对象不再直接相互通信,而是通过中介者进行通信。
想象一下,你和你的朋友们正在参加一个团队项目。在开始项目之前,你们需要进行沟通和协调。传统的方式是每个人都与其他人直接交流,但这样会导致沟通混乱和冲突。于是,你们决定任命一个项目经理作为中介者,他负责协调大家的工作、解决问题并传达信息。
在中介者模式中,有以下几个关键角色:
- 中介者(Mediator):定义了一个接口,用于各个同事对象之间的通信。
- 具体中介者(Concrete Mediator):实现了中介者接口,负责协调各个同事对象之间的通信。
- 同事对象(Colleague):定义了一个接口,用于与中介者进行通信。
- 具体同事对象(Concrete Colleague):实现了同事接口,与其他同事对象通过中介者进行通信。
假设我们有一个聊天室应用程序,用户可以在聊天室中发送消息,并与其他用户进行交流。在这个应用程序中,每个用户都是一个同事对象,而聊天室就是中介者。
首先,我们定义一个中介者接口 ChatRoom
,其中包含了发送消息的方法:
public interface ChatRoom {
void sendMessage(String message, User user);
}
然后,我们创建一个具体中介者类 ChatRoomImpl
,它实现了中介者接口,并负责协调各个用户之间的通信:
public class ChatRoomImpl implements ChatRoom {
@Override
public void sendMessage(String message, User user) {
System.out.println(new Date().toString() + " [" + user.getName() + "] : " + message);
}
}
接下来,我们定义一个同事接口 User
,其中包含了发送消息和接收消息的方法:
public interface User {
void sendMessage(String message);
void receiveMessage(String message);
String getName();
}
然后,我们创建一个具体同事类 UserImpl
,它实现了同事接口,并在发送消息时通过中介者发送消息给其他用户,在接收消息时将消息打印出来:
public class UserImpl implements User {
private String name;
private ChatRoom chatRoom;
public UserImpl(String name, ChatRoom chatRoom) {
this.name = name;
this.chatRoom = chatRoom;
}
@Override
public void sendMessage(String message) {
chatRoom.sendMessage(message, this);
}
@Override
public void receiveMessage(String message) {
System.out.println(name + " received message: " + message);
}
@Override
public String getName() {
return name;
}
}
最后,我们可以在客户端中使用这些类来测试中介者模式:
public class Client {
public static void main(String[] args) {
ChatRoom chatRoom = new ChatRoomImpl();
User user1 = new UserImpl("User1", chatRoom);
User user2 = new UserImpl("User2", chatRoom);
user1.sendMessage("Hello, User2!"); // 输出:[User1] : Hello, User2!
user2.sendMessage("Hi, User1!"); // 输出:[User2] : Hi, User1!
}
}
通过这个例子,我们可以看到中介者模式的使用方式。同事对象之间不再直接通信,而是通过中介者进行通信。中介者负责协调各个同事对象之间的交互,并将消息传递给目标用户。
优点:
①松散耦合
中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。
②集中控制交互
多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。
③一对多关联转变为一对一的关联
没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。
缺点:
当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
使用场景:
-
系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
-
当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
🔖3.8迭代器模式
迭代器模式,它提供了一种顺序访问聚合对象中各个元素的方法,而不需要暴露其内部结构。
想象一下你去购物超市买东西。超市里有很多货架,每个货架上都摆放着不同的商品。你想要遍历这些商品并逐个查看它们的信息,但你不想知道货架是如何组织和排列的。
在这种情况下,超市可以被视为一个聚合对象,每个货架上的商品可以被视为聚合对象中的元素。而你则可以使用迭代器来遍历这些商品,无需了解超市内部的具体实现。
在迭代器模式中,有以下几个关键角色:
- 迭代器(Iterator):定义了访问和遍历聚合对象元素的接口。
- 具体迭代器(Concrete Iterator):实现了迭代器接口,负责具体的遍历操作。
- 聚合对象(Aggregate):定义了创建迭代器对象的接口。
- 具体聚合对象(Concrete Aggregate):实现了聚合对象接口,负责创建具体迭代器对象。
假设我们有一个名为 Book
的类,表示一本书。我们还有一个 BookShelf
类,它表示一个书架,可以存放多本书。
首先,我们定义一个迭代器接口 Iterator
,其中包含了访问和遍历书架上书籍的方法:
public interface Iterator {
boolean hasNext();
Object next();
}
然后,我们创建一个具体迭代器类 BookShelfIterator
,它实现了迭代器接口,并负责具体的遍历操作:
public class BookShelfIterator implements Iterator {
private BookShelf bookShelf;
private int index;
public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0;
}
@Override
public boolean hasNext() {
return index < bookShelf.getLength();
}
@Override
public Object next() {
Book book = bookShelf.getBookAt(index);
index++;
return book;
}
}
接下来,我们定义一个聚合对象接口 Aggregate
,其中包含了创建迭代器对象的方法:
public interface Aggregate {
Iterator createIterator();
}
然后,我们创建一个具体聚合对象类 BookShelf
,它实现了聚合对象接口,并负责创建具体迭代器对象:
public class BookShelf implements Aggregate {
private Book[] books;
private int last = 0;
public BookShelf(int maxSize) {
this.books = new Book[maxSize];
}
public Book getBookAt(int index) {
return books[index];
}
public void addBook(Book book) {
if (last < books.length) {
books[last] = book;
last++;
}
}
public int getLength() {
return last;
}
@Override
public Iterator createIterator() {
return new BookShelfIterator(this);
}
}
最后,我们可以在客户端中使用这些类来测试迭代器模式:
public class Client {
public static void main(String[] args) {
BookShelf bookShelf = new BookShelf(3);
bookShelf.addBook(new Book("Design Patterns"));
bookShelf.addBook(new Book("Clean Code"));
bookShelf.addBook(new Book("Refactoring"));
Iterator iterator = bookShelf.createIterator();
while (iterator.hasNext()) {
Book book = (Book) iterator.next();
System.out.println(book.getName());
}
}
}
通过这个例子,我们可以看到迭代器模式的使用方式。迭代器提供了一种简单而统一的方式来遍历聚合对象中的元素,无需了解其内部结构。
优点:
-
它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
-
迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
-
在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足 “开闭原则” 的要求。
缺点:
增加了类的个数,这在一定程度上增加了系统的复杂性。
使用场景:
-
当需要为聚合对象提供多种遍历方式时。
-
当需要为遍历不同的聚合结构提供一个统一的接口时。
-
当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
注意:
当我们在使用JAVA开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现
java.util.Iterable
并实现其中的iterator()方法使其返回一个java.util.Iterator
的实现类就可以了。
🔖3.9访问者模式
访问者模式,它允许你在不改变已有对象结构的前提下,定义新的操作。
想象一下你去参观一个博物馆。博物馆里有很多展品,每个展品都有不同的特点和价值。你可以自由地在博物馆中游览,并对每个展品进行不同的观察和评价,比如拍照、记录信息等。
在这种情况下,博物馆可以被视为一个对象结构,每个展品可以被视为该对象结构中的元素。而你则可以作为一个访问者,在不改变博物馆内部结构的情况下,对每个展品进行不同的操作。
在访问者模式中,有以下几个关键角色:
- 访问者(Visitor):定义了对每个元素进行访问的方法,可以根据需要定义多个不同的访问者。
- 具体访问者(Concrete Visitor):实现了访问者接口,负责具体的访问操作。
- 元素(Element):定义了接受访问者访问的方法。
- 具体元素(Concrete Element):实现了元素接口,负责具体的接受访问者访问的操作。
- 对象结构(Object Structure):包含了一组元素,并提供了遍历这些元素的方法。
假设我们有一个名为 Element
的接口,其中包含了接受访问者访问的方法:
public interface Element {
void accept(Visitor visitor);
}
然后,我们创建一个具体元素类 ConcreteElementA
和 ConcreteElementB
,它们实现了元素接口,并负责具体的接受访问者访问的操作:
public class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationA() {
return "具体元素A的操作";
}
}
public class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String operationB() {
return "具体元素B的操作";
}
}
接下来,我们定义一个访问者接口 Visitor
,其中包含了对每个元素进行访问的方法:
public interface Visitor {
void visit(ConcreteElementA elementA);
void visit(ConcreteElementB elementB);
}
然后,我们创建一个具体访问者类 ConcreteVisitor
,它实现了访问者接口,并负责具体的访问操作:
public class ConcreteVisitor implements Visitor {
@Override
public void visit(ConcreteElementA elementA) {
System.out.println(elementA.operationA());
}
@Override
public void visit(ConcreteElementB elementB) {
System.out.println(elementB.operationB());
}
}
最后,我们可以在客户端中使用这些类来测试访问者模式:
public class Client {
public static void main(String[] args) {
Element elementA = new ConcreteElementA();
Element elementB = new ConcreteElementB();
Visitor visitor = new ConcreteVisitor();
elementA.accept(visitor);
elementB.accept(visitor);
}
}
通过这个例子,我们可以看到访问者模式的使用方式。访问者模式允许你定义新的操作,而无需改变已有对象结构的代码。
优点:
-
扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
-
复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
-
分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
缺点:
-
对象结构变化很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
-
违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
使用场景:
-
对象结构相对稳定,但其操作算法经常变化的程序。
-
对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
扩展:
访问者模式用到了一种双分派的技术。
1,分派:
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap()
,map变量的静态类型是 Map
,实际类型是 HashMap
。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。
2,动态分派:
通过方法的重写支持动态分派。
public class Animal {
public void execute() {
System.out.println("Animal");
}
}
public class Dog extends Animal {
@Override
public void execute() {
System.out.println("dog");
}
}
public class Cat extends Animal {
@Override
public void execute() {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Dog();
a.execute();
Animal a1 = new Cat();
a1.execute();
}
}
上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。
Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
3,静态分派:
通过方法重载支持静态分派。
public class Animal {
}
public class Dog extends Animal {
}
public class Cat extends Animal {
}
public class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();
Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}
运行结果:
这个结果可能出乎一些人的意料了,为什么呢?
重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
4,双分派:
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
public class Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Dog extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Cat extends Animal {
public void accept(Execute exe) {
exe.execute(this);
}
}
public class Execute {
public void execute(Animal a) {
System.out.println("animal");
}
public void execute(Dog d) {
System.out.println("dog");
}
public void execute(Cat c) {
System.out.println("cat");
}
}
public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();
Execute exe = new Execute();
a.accept(exe);
d.accept(exe);
c.accept(exe);
}
}
在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派
,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。
说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。
运行结果如下:
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
🔖3.10备忘录模式
备忘录模式,它允许你在不破坏封装性的前提下,捕获一个对象的内部状态,并在需要时恢复到之前的状态。
想象一下你在写一个文档编辑器时,你可以随时对文档进行修改和保存。但是有时候你可能会误操作或者需要撤销之前的修改。这时候备忘录模式就能派上用场了。
在备忘录模式中,有以下几个关键角色:
- 发起人(Originator):负责创建一个备忘录,并记录当前状态。
- 备忘录(Memento):存储发起人的内部状态。
- 管理者(Caretaker):负责保存备忘录,并在需要时将其恢复给发起人。
备忘录有两个等效的接口:
窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。
要实现上述案例,有两种方式:
-
“白箱”备忘录模式
-
“黑箱”备忘录模式
📕“白箱”备忘录模式
//游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力
//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
//保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(vit, atk, def);
}
//回复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
//游戏状态存储类(备忘录类)
public class RoleStateMemento {
private int vit;
private int atk;
private int def;
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
//角色状态管理者类
public class RoleStateCaretaker {
private RoleStateMemento roleStateMemento;
public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}
public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
}
}
//测试类
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();
//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());
System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.stateDisplay();
}
}
分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。
📕“黑箱”备忘录模式
备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。
将 RoleStateMemento
设为 GameRole
的内部类,从而将 RoleStateMemento
对象封装在 GameRole
里面;在外面提供一个标识接口 Memento
给 RoleStateCaretaker
及其他对象使用。这样 GameRole
类看到的是 RoleStateMemento
所有的接口,而RoleStateCaretaker
及其他对象看到的仅仅是标识接口 Memento
所暴露出来的接口,从而维护了封装型。
窄接口Memento
,这是一个标识接口,因此没有定义出任何的方法:
public interface Memento {
}
定义发起人类 GameRole
,并在内部定义备忘录内部类 RoleStateMemento
(该内部类设置为私有的)
/游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力
//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
//保存角色状态
public Memento saveState() {
return new RoleStateMemento(vit, atk, def);
}
//回复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
private class RoleStateMemento implements Memento {
private int vit;
private int atk;
private int def;
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
}
负责人角色类 RoleStateCaretaker
能够得到的备忘录对象是以 Memento
为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容
//角色状态管理者类
public class RoleStateCaretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();
//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());
System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}
}
优点:
-
提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
-
实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
-
简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
缺点:
-
资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
使用场景
-
需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
-
需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。
🔖3.11解释器模式
解释器模式,它用于解决特定问题领域中的语言解释和表达式求值问题。它将一个语言或表达式表示为一个抽象语法树,并定义了一组规则来解释和执行这个语言或表达式。
通俗地说,就像我们平时使用计算器一样,我们输入一个数学表达式,然后计算器会解析并计算出结果。这里的计算器就是一个解释器,它能够理解并执行我们输入的表达式。
解释器模式包含以下几个角色:
-
抽象表达式(Abstract Expression):定义了一个抽象的解释操作,所有具体表达式都要实现这个接口。
-
终结符表达式(Terminal Expression):表示语言中的终结符,也就是不再进行解释的最小单位。
-
非终结符表达式(Nonterminal Expression):表示语言中的非终结符,可以由终结符和其他非终结符组成复杂的表达式。
-
环境类(Context):保存解释器需要的上下文信息,并提供解释器执行的方法。
下面举个例子来说明解释器模式的应用场景和工作原理:
假设我们有一个简单的语言,其中有两个操作符:加法和乘法。我们希望能够解析并计算这个语言中的表达式。
首先,我们定义抽象表达式接口:
interface Expression {
int interpret(Context context);
}
然后,我们实现具体的终结符表达式和非终结符表达式:
class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
public int interpret(Context context) {
return number;
}
}
class AddExpression implements Expression {
private Expression left;
private Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
}
class MultiplyExpression implements Expression {
private Expression left;
private Expression right;
public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
public int interpret(Context context) {
return left.interpret(context) * right.interpret(context);
}
}
最后,我们定义一个环境类来保存上下文信息,并提供解释器执行的方法:
class Context {
private Map<String, Integer> variables = new HashMap<>();
public void setVariable(String name, int value) {
variables.put(name, value);
}
public int getVariable(String name) {
return variables.get(name);
}
}
现在,我们可以使用解释器模式来解析和计算表达式了:
public class Client {
public static void main(String[] args) {
// 创建上下文对象
Context context = new Context();
// 设置变量的值
context.setVariable("a", 5);
context.setVariable("b", 3);
// 创建表达式
Expression expression = new AddExpression(
new NumberExpression(context.getVariable("a")),
new MultiplyExpression(
new NumberExpression(2),
new NumberExpression(context.getVariable("b"))
)
);
// 解释并计算表达式
int result = expression.interpret(context);
System.out.println("Result: " + result); // 输出:Result: 11
}
}
这个例子中,我们定义了一个简单的语言,可以解析和计算加法和乘法表达式。通过使用解释器模式,我们可以灵活地扩展和修改语言的语法规则。
优点:
-
易于改变和扩展文法。
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
-
实现文法较为容易。
在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
-
增加新的解释表达式较为方便。
如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 "开闭原则"。
缺点:
-
对于复杂文法难以维护。
在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
-
执行效率较低。
由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
使用场景
-
当语言的文法较为简单,且执行效率不是关键问题时。
-
当问题重复出现,且可以用一种简单的语言来进行表达时。
-
当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。
🏆撒花完结!有不足的地方,希望大家海涵!有需要其他资料的宝子们加QQ群:526519295