JAVA设计模式--从入门到精通

设计模式(design pattern)是对软件设计中普遍存在的各种问题,所提出的解决方案。本文以面试题作为切入点,介绍了设计模式的常见问题。我们需要掌握各种设计模式的原理、实现、设计意图和应用场景,搞清楚能解决什么问题。

设计模式从入门到精通

推荐书籍

推荐书籍:

书籍语言难易程度
《大话设计模式》java学起来最简单
《Head First 设计模式》java自学设计模式最好的教材,学起来简单,缺点是缺乏实际工程实践
《图解设计模式》java适合入门学习
《人人都懂设计模式:从生活中领悟设计模式:Python实现》python
《设计模式:可复用面向对象软件的基础》GOF基于C++枯燥,适合理论提高
《设计模式》(刘伟,清华大学出版社)java入门教材

推荐课程:
设计模式深度解析34讲
设计模式之美


1、学习设计模式的意义?

1、应对面试中设计模式相关问题 从功利的角度
2、编写高质量代码

  • 避免在工作中写出烂代码,坑人坑己

3、提高复杂代码的设计和开发能力

  • 即便做与业务无关的框架类复杂设计,也能应对自如
  • 可以写出高拓展、易维护的代码

4、让读源码、学框架事半功倍

5、为你的职场发展做铺垫

  • 场景1:当一个项目开发完后,如果客户提出增新功能,怎么办?
  • 场景2:项目开发完后,原来程序员离职,你接手维护该项目怎么办?
  • 场景3:经验丰富后,需要指导新入职的同事写好代码及Code Review。

2、如何编写高质量代码

2.1 设计模式的目的(高内聚,松耦合)

①代码重用性 (相同功能代码,不用多次编写);
②可读性 (编程规范性, 便于其他程序员阅读和理解)
③可扩展性 (当需要增加新功能时,非常方便,称为可维护)
④可靠性 (当我们增加新功能后,对原来的功能没有影响)

2.2 如何写出高质量代码

如何编写高质量代码如图所示:
在这里插入图片描述

要写出满足这些评价标准的高质量代码,我们需要掌握一些更加细化、更加能落地的编程方法论,包括面向对象设计思想、设计原则、编码规范、重构技巧、设计模式等。而所有这些编程方法论的最终目的都是为了编写出高质量的代码

具体而言:
①面向对象中的继承、多态能让我们写出可复用的代码;
②设计原则中的单一职责、DRY、基于接口而非实现、里式替换原则等,可以让我们写出可复用、灵活、可读性好、易扩展、易维护的代码;
③编码规范能让我们写出可读性好的代码;
编码规范:可以参考这几本书《重构》《代码大全》《代码整洁之道》
④持续重构可以时刻保持代码的可维护性
重构技巧:持续重构是保持代码质量不下降的有效手段,能有效避免代码腐化到无可救药的地步
设计模式可以让我们写出易扩展的代码


3、常用的设计原则(SOLID 7大原则)

设计原则是各类设计模式的基础
对于每一种设计原则,我们需要掌握它的设计初衷,能解决哪些编程问题,有哪些应用场景。只有这样,我们才能在项目中灵活恰当地应用这些原则。
1、单一职责原则

  • 对类来说的,即一个类应该只负责一项职责。
  • 定义了类的粒度

2、接口隔离原则

  • 客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
  • 不要让子类实现不必要的接口 -----之前做日志模板时出过这样的问题

3、依赖倒转原则(面向接口编程)

  • 高层模块不应该依赖低层模块,应该依赖其抽象
  • 抽象不应该依赖细节
  • 建议:低层模块尽量都要有抽象类或接口。

4、里式替换原则

  • 教你如何正确的使用继承(引用基类的地方必须能透明地使用其子类的对象)
  • 为了满足里式替换,我们定义一个更为抽象的基类,实现类采用组合的方式 替换原有的继承关系。

5、开闭原则(最基础、最重要)

  • 对扩展开放、对修改关闭
  • 当程序变更时,尽量是通过扩展程序的行为实现变化,而不是修改已有的代码来实现变化
    见第4个标题

6、迪米特原则

  • 最少知道原则,即一个类对自己依赖的类知道的越少越好
  • 陌生的类最好不要以局部变量的形式出现在类的内部。

7、合成复用原则

  • 尽量使用组合的方式,而不是使用继承
  • 原因:继承会让两个类的耦合性增强

4、如何做到对扩展开放、对修改关闭?深入理解开闭原则

理论:越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对

添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

案例: API 接口监控告警的代码
AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。

public class Alert {
   private AlertRule rule;
   private Notification notification;
   /* 使用构造函数来初始化,可以防止npe */
   public Alert(AlertRule rule, Notification notification) {
     this.rule = rule;
     this.notification = notification;
  }

  public void check(String api, long requestCount, long errorCount, long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
       notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
       notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}

问题:业务逻辑主要集中在 check() 函数中。当接口的 TPS 超过某个预先设置的最大值时,以及当接口请求出错数大于某个最大允许值时,就会触发告警,通知接口的相关负责人或者团队。现在,如果我们需要添加一个功能,当每秒钟接口超时请求个数,超过某个预先设置的最大阈值时,我们也要触发告警发送通知。这个时候,我们该如何改动代码呢?主要的改动有两处:第一处是修改 check() 函数的入参,添加一个新的统计数据 timeoutCount,表示超时接口请求数;第二处是在 check() 函数中添加新的告警逻辑。

public class Alert {
  // ...省略AlertRule/Notification属性和构造函数...
  
  // 改动一:添加参数timeoutCount
  public void check(String api, long requestCount, long errorCount, long timeoutCount, long durationOfSeconds) {
    long tps = requestCount / durationOfSeconds;
    if (tps > rule.getMatchedRule(api).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
    if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
    // 改动二:添加接口超时处理逻辑
    long timeoutTps = timeoutCount / durationOfSeconds;
    if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
  }
}

存在的问题:

  • 一方面,我们对接口进行了修改,这就意味着调用这个接口的代码都要做相应的修改
  • 另一方面,修改了check() 函数,相应的单元测试都需要修改

我们先重构一下之前的 Alert 代码,让它的扩展性更好一些。重构的内容主要包含两部分:第一部分是将 check() 函数的多个入参封装成 ApiStatInfo 类;第二部分是引入handler的概念,将 if 判断逻辑分散在各个 handler(抽象类) 中。

具体实现逻辑如下所示

public class Alert {
  private List<AlertHandler> alertHandlers = new ArrayList<>();
  public void addAlertHandler(AlertHandler alertHandler) {
    this.alertHandlers.add(alertHandler);
  }

  public void check(ApiStatInfo apiStatInfo) {
    for (AlertHandler handler : alertHandlers) {
      handler.check(apiStatInfo);
    }
  }
}

public class ApiStatInfo {//省略constructor/getter/setter方法
  private String api;
  private long requestCount;
  private long errorCount;
  private long durationOfSeconds;
}

public abstract class AlertHandler {
  protected AlertRule rule;
  protected Notification notification;
  public AlertHandler(AlertRule rule, Notification notification) {
    this.rule = rule;
    this.notification = notification;
  }
  public abstract void check(ApiStatInfo apiStatInfo);
}

public class TpsAlertHandler extends AlertHandler {
  public TpsAlertHandler(AlertRule rule, Notification notification) {
    super(rule, notification);
  }
  @Override
  public void check(ApiStatInfo apiStatInfo) {
    long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds();
    if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) {
      notification.notify(NotificationEmergencyLevel.URGENCY, "...");
    }
  }
}

public class ErrorAlertHandler extends AlertHandler {
  public ErrorAlertHandler(AlertRule rule, Notification notification){
    super(rule, notification);
  }
  @Override
  public void check(ApiStatInfo apiStatInfo) {
    if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
      notification.notify(NotificationEmergencyLevel.SEVERE, "...");
    }
  }
}

再来看下,重构之后的 Alert 该如何使用呢?
ApplicationContext 是一个单例类,负责 Alert 的创建、组装(alertRule 和 notification 的依赖注入)、初始化(添加 handlers)工作。

public class ApplicationContext {
  private AlertRule alertRule;
  private Notification notification;
  private Alert alert;
  
  public void initializeBeans() {
    alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
    notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
    alert = new Alert();
    alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
    alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
  }
  public Alert getAlert() { return alert; }

  // 饿汉式单例
  private static final ApplicationContext instance = new ApplicationContext();
  private ApplicationContext() {
    initializeBeans();
  }
  public static ApplicationContext getInstance() {
    return instance;
  }
}

public class Demo {
  public static void main(String[] args) {
    ApiStatInfo apiStatInfo = new ApiStatInfo();
    // ...省略设置apiStatInfo数据值的代码
    ApplicationContext.getInstance().getAlert().check(apiStatInfo);
  }
}

补充:更好的方式,借助Spring IOC的依赖注入功能

@Component
public class AlertFactory {
    // 关键功能 Spring 会自动将 AlertHandler 接口的类注入到这个Map中
    @Autowired
    private Map<String, AlertHandler> alertHandleryMap;
   	public AlertRule getBy(String alertEnum) {
        return alertHandleryMap.get(alertEnum);
    }
}

现在,我们再来看下,基于重构之后的代码,如果再添加上面讲到的那个新功能,每秒钟接口超时请求个数超过某个最大阈值就告警,我们又该如何改动代码呢?
主要的改动有下面四处。

  • 第一处改动是:在 ApiStatInfo 类中添加新的属性 timeoutCount。
  • 第二处改动是:添加新的TimeoutAlertHander类。
  • 第三处改动是:在 ApplicationContext 类的 initializeBeans() 方法中,往alert对象中注册新的 timeoutAlertHandler。
  • 第四处改动是:在使用Alert类的时候,需要给check()函数的入参 apiStatInfo 对象设置 timeoutCount 的值。
public class Alert { // 代码未改动... }
public class ApiStatInfo {//省略constructor/getter/setter方法
  private String api;
  private long requestCount;
  private long errorCount;
  private long durationOfSeconds;
  private long timeoutCount; // 改动一:添加新字段
}
public abstract class AlertHandler { //代码未改动... }
public class TpsAlertHandler extends AlertHandler {//代码未改动...}
public class ErrorAlertHandler extends AlertHandler {//代码未改动...}
// 改动二:添加新的handler
public class TimeoutAlertHandler extends AlertHandler {//省略代码...}

public class ApplicationContext {
  private AlertRule alertRule;
  private Notification notification;
  private Alert alert;
  
  public void initializeBeans() {
    alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
    notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
    alert = new Alert();
    alert.addAlertHandler(new TpsAlertHandler(alertRule, notification));
    alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification));
    // 改动三:注册handler
    alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification));
  }
  //...省略其他未改动代码...
}

public class Demo {
  public static void main(String[] args) {
    ApiStatInfo apiStatInfo = new ApiStatInfo();
    // ...省略apiStatInfo的set字段代码
    apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值
    ApplicationContext.getInstance().getAlert().check(apiStatInfo);
}

重构之后的代码更加灵活和易扩展如果我们要想添加新的告警逻辑,只需要基于扩展的方式创建新的 handler 类即可,不需要改动原来的 check() 函数的逻辑。而且,我们只需要为新的 handler 类添加单元测试,老的单元测试都不会失败,也不用修改。

ACTION:
一、修改代码就意味着违背开闭原则吗?
要认识到,添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则

二、如何做到“对扩展开放、修改关闭”?
指导思想:为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要。

在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。

在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)

三、如何在项目中灵活应用开闭原则?
对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求


5、如何做到“高内聚低耦合”?

什么是高内聚?

  • 高内聚,就是指相近的功能应该放到同一个类中,不相近的功能不要放到同一个类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中,代码容易维护。
    • 单一职责原则

什么是低耦合?

  • 在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动不会或者很少导致依赖类的代码改动;
  • 依赖注入、接口隔离、基于接口而非实现编程,迪米特法则,都是为了实现代码的松耦合。

不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的
接口

案例:实现了简化版的搜索引擎爬取网页的功能。代码中包含三个主要的类。其中,
NetworkTransporter 类负责底层网络通信,根据请求获取数据;HtmlDownloader 类用来通过URL获取网页;Document 表示网页文档,后续的网页内容抽取、分词、索引都是以此为处理对象。

public class NetworkTransporter {
	// 省略属性和其他方法...
	public Byte[] send(HtmlRequest htmlRequest) {
		//...
	}
} 
public class HtmlDownloader {
	private NetworkTransporter transporter;// 通过构造函数或 IOC 注入
	public Html downloadHtml(String url) {
		Byte[] rawHtml = transporter.send(new HtmlRequest(url));
		return new Html(rawHtml);
	}
} 
public class Document {
	private Html html;
	private String url;
	public Document(String url) {
		this.url = url;
		HtmlDownloader downloader = new HtmlDownloader();
		this.html = downloader.downloadHtml(url);
	}
	//...
}

缺陷有哪些?
1、首先,我们来看NetworkTransporter类。作为一个底层网络通信类,我们希望它的功能
尽可能通用,而不只是服务于下载 HTML,所以,我们不应该直接依赖太具体的发送对象
HtmlRequest;

  • 我们应该把 address 和content 交给 NetworkTransporter,而非是直接把 HtmlRequest 交给NetworkTransporter, 代码入下所示
public class NetworkTransporter {
	// 省略属性和其他方法...
	public Byte[] send(String address, Byte[] data) {
		//...
	}
}

2、我们再来看 HtmlDownloader 类。这个类的设计没有问题。不过,我们修改了NetworkTransporter的send()函数的定义,而这个类用到了send()函数,所以我们需要对它做相应的修改;

public class HtmlDownloader {
	private NetworkTransporter transporter;// 通过构造函数或IOC注入
	// HtmlDownloader 这里也要有相应的修改
	public Html downloadHtml(String url) {
		HtmlRequest htmlRequest = new HtmlRequest(url);
		Byte[] rawHtml = transporter.send(htmlRequest.getAddress(), htmlRequest.getContent().getBytes());
		return new Html(rawHtml);
	}
}

3、我们来看下 Document 类。问题主要有三点。第一,构造函数中的 downloader.downloadHtml() 逻辑复杂,耗时长,不应该放到构造函数中,会影响代
码的可测试性。第二,HtmlDownloader 对象在构造函数中通过 new 来创建,违反了基于接口而非实现编程的设计思想,也会影响到代码的可测试性。第三,从业务含义上来讲,Document 网页文档没必要依赖 HtmlDownloader 类,违背了迪米特法则

public class Document {
	private Html html;
	private String url;
	public Document(String url, Html html) {
		this.html = html;
		this.url = url;
	}
	//...
}
// 通过一个工厂方法来创建 Document
public class DocumentFactory {
	private HtmlDownloader downloader;
	public DocumentFactory(HtmlDownloader downloader) {
		this.downloader = downloader;
	} 
	public Document createDocument(String url) {
		Html html = downloader.downloadHtml(url);
		return new Document(url, html);
	}
}

6、DDD(即领域驱动设计),充血模式和贫血模式深度对比

  • 越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。

问题背景–什么是基于贫血模型的开发模式
传统的 MVC 结构分为 Model 层、Controller 层、View 层这三层。不过,在做前后端分离之后,三层结构在后端开发中,会稍微有些调整,被分为 Controller 层、Service 层、Repository 层。Controller 层负责暴露接口给前端调用,Service 层负责核心业务逻辑,Repository 层负责数据读写。而在每一层中,我们又会定义相应的 VO(View Object)、BO(Business Object)、Entity。一般情况下,VO、BO、Entity 中只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的 Controller 类、Service 类、Repository 类中。这就是典型的面向过程的编程风格。

传统编程模型案例:

// Controller+VO(View Object) //
public class UserController {
	private UserService userService; // 通过构造函数或者 IOC 框架注入
	public UserVo getUserById(Long userId) {
		UserBo userBo = userService.getUserById(userId);
		UserVo userVo = [...convert userBo to userVo...];
		return userVo;
	}
}
public class UserVo {// 省略其他属性、get/set/construct 方法
	private Long id;
	private String name;
	private String cellphone;
}
 
// Service+BO(Business Object) //
public class UserService {
	private UserRepository userRepository; // 通过构造函数或者 IOC 框架注入
	public UserBo getUserById(Long userId) {
		UserEntity userEntity = userRepository.getUserById(userId);
		UserBo userBo = [...convert userEntity to userBo...];
		return userBo;
	}
} 
public class UserBo {// 省略其他属性、get/set/construct 方法
	private Long id;
	private String name;
	private String cellphone;
} 

// Repository+Entity //
public class UserRepository {
	public UserEntity getUserById(Long userId) { //... }
} 
public class UserEntity {// 省略其他属性、get/set/construct 方法
	private Long id;
	private String name;
	private String cellphone;
}

像 UserBo 这样,只包含数据,不包含业务逻辑的类,就叫作贫血模型(Anemic Domain Model)。同理,UserEntity、UserVo 都是基于贫血模型设计的。

什么是基于充血模型的 DDD 开发模式?
充血模型(Rich Domain Model),数据和对应的业务逻辑被封装到同一个类中
在基于贫血模型的传统开发模式中,Service 层包含 Service 类和 BO 类两部分,BO 是贫血模型,只包含数据,不包含具体的业务逻辑。业务逻辑集中在 Service 类中。在基于充血模型的 DDD 开发模式中,Service 层包含 Service 类和 Domain 类两部分。Domain就相当于贫血模型中的 BO。不过,Domain 与 BO 的区别在于它是基于充血模型开发的,既包含数据,也包含业务逻辑。而 Service 类变得非常单薄。总结就是,基于贫血模型的传统的开发模式,重 Service 轻 BO;基于充血模型的 DDD 开发模式,轻 Service 重Domain

示例代码:

@Data
public class VirtualWallet {
	private Long id;
	private Long createTime = System.currentTimeMillis();
	//余额
	private BigDecimal balance = BigDecimal.ZERO; 
	// 是否允许超支
	private boolean isAllowedOverdraft = true;
 	// 超支金额
	private BigDecimal overdraftAmount = BigDecimal.ZERO;
	// 冻结总额
	private BigDecimal frozenAmount = BigDecimal.ZERO;
	public VirtualWallet(Long preAllocatedId) {
		this.id = preAllocatedId;
	} 
	public void freeze(BigDecimal amount) { ... }
	public void unfreeze(BigDecimal amount) { ...}
	public void increaseOverdraftAmount(BigDecimal amount) { ... }
	public void decreaseOverdraftAmount(BigDecimal amount) { ... }
	public void closeOverdraft() { ... }
	public void openOverdraft() { ... }
	public BigDecimal balance() {
		return this.balance;
	} 
	// 获取余额
	public BigDecimal getAvaliableBalance() {
		BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount)
		if (isAllowedOverdraft) {
			totalAvaliableBalance += this.overdraftAmount;
		}
		return totalAvaliableBalance;
	} 
	// 借记
	public void debit(BigDecimal amount) {
		BigDecimal totalAvaliableBalance = getAvaliableBalance();
		if (totoalAvaliableBalance.compareTo(amount) < 0) {
			throw new InsufficientBalanceException(...);
		}
		this.balance.subtract(amount);
	} 
	// 信贷 
	public void credit(BigDecimal amount) {
		if (amount.compareTo(BigDecimal.ZERO) < 0) {
			throw new InvalidAmountException(...);
		}
		this.balance.add(amount);
	}
}

为什么基于贫血模型的传统开发模式如此受欢迎?
第一点原因是,大部分情况下,我们开发的系统业务可能都比较简单,简单到就是基于SQL 的 CRUD 操作,所以,我们根本不需要动脑子精心设计充血模型
第二点原因是,充血模型的设计要比贫血模型更加有难度,我们从一开始就要设计好针对数据要暴露哪些操作,定义哪些业务逻辑;
第三点原因是,思维已固化,转型有成本。基于贫血模型的传统开发模式经历了这么多年,已经深得人心、习以为常。

亮点1:将充血模型用在类目属性代码中,业务不停在拓展,如何高效兼容现有业务
问题1:值不值得变为充血模型。
demo如下所示:

问题2:后续拓展的需求,如何在DDD上补充代码呢?

DDD辩证思考与灵活应用
①要讨论的问题是:在基于充血模型的 DDD 开发模式中,将业务逻辑移动到Domain 中,Service类变得很薄,但在我们的代码设计与实现中,并没有完全将Service 类去掉,这是为什么?或者说,Service 类在这种情况下担当的职责是什么?哪些功能逻辑会放到 Service 类中?
Service 类主要有下面这样几个职责
1.Service 类负责与 Repository 交流。

  • 不是让领域模型 xxx 与 Repository 打交道,那是因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository 层的代码)或开发框架(比如 Spring、MyBatis)耦合在一起,将流程性的代码逻辑(比如从 DB 中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用;

2、Service 类负责跨领域模型的业务聚合功能
3、Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。

②在基于充血模型的 DDD 开发模式中,尽管 Service 层被改造成了充血模型,但是 Controller 层和 Repository 层还是贫血模型,是否有必要也进行充血领域建模呢?
答案是没有必要。Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多,如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。
对于Repository 层Entity,一般来讲,我们把它传递到 Service 层之后,就会转化成 BO 或者 Domain 来继续后面的业务逻辑。Entity 的生命周期到此就结束了,所以也并不会被到处任意修改;
对于Controller 层VO,它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。

我对充血模型的看法就是:
1、它可以把原来最重的service逻辑拆分并且转移一部分逻辑,可以使得代码可读性略微提高;
2、模型充血以后,基于模型的业务抽象在不断的迭代之后会越来越明确,业务的细节会越来越精准,通过阅读模型的充血行为代码,能够极快的了解系统的业务,对于开发来说能说明显的提升开发效率


7、常用的设计模式(代表了最佳实践 共23种,常用的14种)

掌握设计模式的五个层次
第一层:刚开始学编程不久,听说过什么是设计模式;
第二层:有长时间的编程经验,自己写了很多代码,其中用到了设计模式,但是自己却不知道;
第三层:学习过了设计模式,发现自己已经在使用了,并且发现一些新的模式挺好用的;✅
第四层:阅读了很多别人写的源码和框架, 在其中看到别人设计模式,并且能够领会设计模式的精妙和带来的好处
第五层: 代码写着写着,自己都没有意识到使用了设计模式,并且熟练的写了出来

总体来说设计模式分为三大类
创建型模式(对对象创建过程中各种问题和解决方案的总结)

  • 共5种:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)

结构型模式(关注于类/对象 继承/组合方式)

  • 共7种:代理模式,桥接模式,适配器模式,装饰器模式,外观模式(不常用),组合模式(不常用),享元模式(不常用)

行为型模式(是从类或对象之间交互/职责划分等角度总结的模式)

  • 共11种:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)

  • 总结

    • 设计模式要干的事情就是解耦。
      创建型模式是将创建和使用代码解耦结构型模式是将不同功能代码解耦行为型模式是将不同的行为代码解耦

8、创建型设计模式

创建型设计模式包括:单例模式,工厂方法模式(抽象工厂模式),建造者模式,原型模式(不常用)。它主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码

8.1、单例设计模式一共有几种实现方式?请分别用代码实现,并说明各个实现方式的优点和缺点?

单例模式用来创建全局唯一的对象。
定义:一个类只允许创建一个对象(或者叫实例),那这个类就是一个单例类,这种设计模式就叫作单例模式。单例有几种经典的实现方式,它们分别是:饿汉式(两种)、懒汉式(三种)、双重检测、静态内部类、枚举(最佳实践)。
使用场景:①需要频繁的进行创建和销毁的对象、②创建对象时耗时过多或耗费资源过多(即:重量级对象), 但又经常用到的对象、③工具类对象、④频繁访问数据库或文件的对象(比如数据源、 session工厂等)

Demo1、饿汉式:(关键词:静态常量)

public Class singleton{
	 // 1、私有化构造函数 
     private Singleton() {}
     // 2、内部直接创建对象 
     public static singleton instance = new singleton();
     // 3、提供公有静态方法,返回对象实例 
     public static Singleton getInstance() {
       	return instance;
     }
}

优点:写法简单,避免线程同步问题
缺点:没有懒加载,可能造成内存浪费  不推荐. 
使用场景:耗时的初始化操作,提前到程序启动时完成

Demo2、饿汉式(静态代码块)

public Class singleton{
	 // 1、私有化构造函数 
     private Singleton() {}
    //2.本类内部创建对象实例
	private static Singleton instance;
	static { 
		// 在静态代码块中,创建单例对象
		instance = new Singleton();
	}
	//3. 提供一个公有的静态方法,返回实例对象
	public static Singleton getInstance() {
		return instance;
	}
}
优点:写法简单,避免线程同步问题
缺点:没有懒加载,可能造成内存浪费   不推荐

Demo3、懒汉式(线程不安全)

class Singleton {
	private static Singleton instance;
	private Singleton() {}
	//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
优点:起到了懒加载的效果
缺点:只能在单线程环境使用, 不要使用

Demo4、懒汉式(线程安全)

class Singleton {
	private static Singleton instance;
	private Singleton() {}
	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
优点:起到了懒加载的效果,解决了线程安全问题
缺点:效率低,不推荐

Demo5、懒汉式(线程安全,同步代码块)

class Singleton {
	private static Singleton singleton;
	private Singleton() {}
	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	public static Singleton getInstance() {
		if(singleton == null) {
		synchronized (Singleton.class) {
			singleton = new Singleton();
		}
		return singleton;
	}
}
不推荐使用,并不能起到线程同步的作用

Demo6、双重锁校验 可以应用于连接池的使用中

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;
	}
}
优点:Double-Check机制保证线程安全,效率高   推荐使用

Demo7、静态内部类

class Singleton {
	private static Singleton instance;
	//构造器私有化
	private Singleton() {}
	//写一个静态内部类,该类中有一个静态属性 Singleton
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}
	//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
	public static Singleton getInstance() {
		return SingletonInstance.INSTANCE;
	}
}
优点:JVM保证了线程安全,类的静态属性只会在第一次加载类的时候初始化,效率高,推荐

Demo8、枚举 – 单例实现的最佳实践

enum Singleton {
	INSTANCE; //属性
	public void sayOK() {
		System.out.println("ok");
	}
}
// main方法来测试
public static void main(String[] args) {
	Singleton instance = Singleton.INSTANCE;
	instance.sayOK();
}
优点:使用枚举,可以实现单例,避免了多线程同步问题,还能防止反序列化重新创建新的对象,推荐

在这里插入图片描述

补充Demo9:java核心类库 Runtime 的单例实现(饿汉式)

//静态实例被声明为final,一定程度上保证了实例不被篡改
public class Runtime {
	private Runtime(){}
	private static final Runtime currentRuntime = new Runtime();
	private static Version version;
	public static Runtime getRuntime(){
	    return currentRuntime;
	}
}

Demo10: 获取 spi 单例

public class SpiProviderSelector {
    private static SpiProviderSelector instance = null;
    private SpiProviderSelector(){}
    /* 获取单例*/
    public static SpiProviderSelector getInstance() {
        if(instance == null){
            synchronized (SpiProviderSelector.class){
                if(instance == null){
                    instance = new SpiProviderSelector();
                }
            }
        }
        return instance;
    }
}

单例模式缺点
1、单例对 OOP 特性的支持不友好

  • 对继承、多态特性支持不友好

2、单例对代码的可测试性不友好

  • 硬编码方式,无法实现 mock 替换

3、单例不支持有参数的构造函数

Demo11:怎么在单例模式中给对象初始化数据?

public class Singleton {
	private static Singleton instance = null;
	private final int paramA;
	private final int paramB;
	private Singleton(int paramA, int paramB) {
		this.paramA = paramA;
		this.paramB = paramB;
	} 
	public static Singleton getInstance() {
		if (instance == null) {
			throw new RuntimeException("Run init() first.");
		}
		return instance;
	} 
	public synchronized static Singleton init(int paramA, int paramB) {
		if (instance != null){
			throw new RuntimeException("Singleton has been created!");
		}
		instance = new Singleton(paramA, paramB);
		return instance;
	}
} 

// 先init,再使用 getInstance()
Singleton.init(10, 50); 
Singleton singleton = Singleton.getInstance()

单例模式的替代方案?

  • 通过工厂模式、IOC 容器来保证全局唯一性。

Action:请问 Spring下的 bean 单例模式与设计模式(GOF)中的单例模式区别? 可以作为面试题

  • 它们关联的环境不同,单例模式是指在一个JVM进程中仅有一个实例,不管在程序中的何处获取实例,始终都返回同一个对象。
    Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。

8.2、什么时候该用工厂模式?相对于直接 new 来创建对象,用工厂模式来创建究竟有什么好处呢?

概念:工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象。

使用场景如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象就可以了,不需要使用工厂模式用到大量的创建某种、某类或者某批对象时,考虑使用工厂模式

工厂模式的作用将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系解耦。从而提高项目的扩展和维护性

Demo1:简单工厂使用示例(规则校验器)

@Component
public class RuleValidatorFactory {
    @Autowired
    private ArRuleValidator aValidator;
    @Autowired
    private BRuleValidator bValidator;
    @Autowired
    private CRuleValidator cValidator;
    @Autowired
    private DRuleValidator dValidator;

    public IBaseValidator createRuleValidator(ValidatorModeEnum validatorModeEnum) {
        switch (validatorModeEnum) {
            case NAME:
                return aValidator;
            case DETAIL:
                return bValidator;
            case MAIN_IMG:
                return cValidator;
            case CATEGORY_ATTR:
                return dValidator;
        }
        throw new ServiceException("校验模式不存在");
    }
}

//调用该工厂模式的方法
IBaseValidator baseValidator = ruleValidatorFactory.ruleValidator(mode);
List<String> result = baseValidator.validateItemRule(validatorModeEnum);

工厂方法模式

  • 1、普通工厂:建立一个工厂类,对实现了同一接口的一些类进行实例的创建
  • 2、多个工厂方法模式 :在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。
  • 3、静态工厂方法:上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
  • 4、抽象工厂模式:围绕一个超级工厂创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。
    • 防止工厂过多,对工厂类做了一层抽象

Demo2:使用是单例模式结合工厂模式
解决每次调用工厂类都会创建新对象的问题

public class RuleValidatorFactory {
	private static final Map<String, IBaseValidator> cachedParsers = new HashMap<>();

	// 定义一个静态代码块,存储对象
	static {
		cachedParsers.put(NAME, new ArRuleValidator());
		cachedParsers.put(DETAIL, new BRuleValidator());
		cachedParsers.put(MAIN_IMG, new CRuleValidator());
		cachedParsers.put(CATEGORY_ATTR, new DRuleValidator());
	}

    public IBaseValidator createRuleValidator(ValidatorModeEnum validatorModeEnum) {
           	return cachedParsers.get(validatorModeEnum); 
        }
        throw new ServiceException("校验模式不存在");
    }
}

Demo3 工厂模式在JDK-Calendar的应用
public static Calendar getInstance(){
	return createCalendar(TimeZone.getDefault(),Locale.getDefault(Locale.Category.FORMAT));
}
private static Calendar createCalendar(TimeZone zone,Locale aLocale) {
	if (provider != null) {
		try {
			return provider.getInstance(zone, aLocale);//默认方式获取
		} catch (IllegalArgumentException iae) {
			// fall back to the default instantiation
		}
	 }
	Calendar cal = null;
	if (aLocale.hasExtensions()) {
            String caltype = aLocale.getUnicodeLocaleType("ca");
            if (caltype != null) {
                switch (caltype) {
                case "buddhist":
                cal = new BuddhistCalendar(zone, aLocale);
                    break;
                case "japanese":
                    cal = new JapaneseImperialCalendar(zone, aLocale);
                    break;
                case "gregory":
                    cal = new GregorianCalendar(zone, aLocale);
                    break;
                }
            }
        }
}
public class SimpleFactory {
	public static void main(String[] args) {
		Calendar cal = Calendar.getInstance();
		// 注意月份下标从0开始,所以取月份要+1
		System.out.println("年:" + cal.get(Calendar.YEAR));
		System.out.println("月:" + (cal.get(Calendar.MONTH) + 1));
		System.out.println("日:" + cal.get(Calendar.DAY_OF_MONTH));
		System.out.println("时:" + cal.get(Calendar.HOUR_OF_DAY));
		System.out.println("分:" + cal.get(Calendar.MINUTE));
		System.out.println("秒:" + cal.get(Calendar.SECOND));
	}
}

使用了工厂模式的组件:DateFormat String

工厂模式最佳实践

都使用抽象工厂模式,按照产品族维度来建立工厂,如果只有一个产品那么工厂中就一个方法,如果有多个产品就多个方法。
重温设计模式之 Factory–阿里对工厂模式的实践

工厂模式一个非常经典的应用场景:依赖注入框架
比如 SpringIOC、Google Guice它用来集中创建、组装、管理对象,跟具体业务代码解耦让程序员聚焦在业务代码的开发上

Demo4 使用工厂模式实现 Spring BeanFactory?

工厂类:负责某个类对象或者某一组相关类对象(继承自同一抽象类或者接口的子类)的创建;
DI 容器:负责的是整个应用中所有类对象的创建。

  • 它的功能:①配置解析 ②对象创建 ③对象生命周期管理

第一步,配置解析
DI 容器来创建的类对象和创建类对象的必要信息放到配置文件中。容器读取配置文件,根据配置文件提供的信息来创建对象。
Spring 容器的配置文件。Spring 容器读取这个配置文件,解析出要创建的两个对象:rateLimiterredisCounter,并且得到两者的依赖关系:rateLimiter 依
赖 redisCounter。

public class RateLimiter {
	private RedisCounter redisCounter;
	public RateLimiter(RedisCounter redisCounter) {
		this.redisCounter = redisCounter;
	}
	public void test() {
		System.out.println("Hello World!");
	}
	//...
} 
public class RedisCounter {
	private String ipAddress;
	private int port;
	public RedisCounter(String ipAddress, int port) {
		this.ipAddress = ipAddress;
		this.port = port;
	}
	//...
}

Spring 配置文件beans.xml:

<beans>
	<bean id="rateLimiter" class="com.xzg.RateLimiter">
		<constructor-arg ref="redisCounter"/>
	</bean>
	<bean id="redisCounter" class="com.xzg.redisCounter">
		<constructor-arg type="String" value="127.0.0.1">
		<constructor-arg type="int" value=1234>
	</bean>
</beans>

对象创建
将所有类对象的创建都放到一个BeansFactory工厂类中完成就可以了,通过“反射”机制,它能在程序运行的过程中,动态地加载类、创建对象,不需要事先在代码中写死要创建哪些对象

对象的生命周期管理
简单工厂模式有两种实现方式,一种是每次都返回新创建的对象,另一种是每次都返回同一个事先创建好的对象,也就是单例对象

在 Spring 框架中,①我们可以通过配置 scope 属性,来区分这两种不同类型的对象。scope=prototype 表示返回新创建的对象,scope=singleton 表示返回单例对象
配置对象是否支持懒加载。如果 lazy-init=true,对象在真正被使用到的时候才会被创建。
③配置对象的 init-method (初始化对象) 和 destroy-method (做清理工作)方法

如何使用 BeanFactory
从 classpath 中加载 XML 格式的配置文件,然后通过 BeanConfigParser 解析为统一的 BeanDefinition 格式,然后,BeansFactory 根据 BeanDefinition 来创建对象。

public class Demo {
	public static void main(String[] args) {
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
		RateLimiter rateLimiter = (RateLimiter) applicationContext.getBean("rateLimiter");
		rateLimiter.test();
		//...
	}
}

public interface ApplicationContext {
	Object getBean(String beanId);
}

public class ClassPathXmlApplicationContext implements ApplicationContext {
	private BeansFactory beansFactory;
	private BeanConfigParser beanConfigParser;
	
	public ClassPathXmlApplicationContext(String configLocation) {
		this.beansFactory = new BeansFactory();
		this.beanConfigParser = new XmlBeanConfigParser();
		loadBeanDefinitions(configLocation);
	} 
	private void loadBeanDefinitions(String configLocation) {
		InputStream in = null;
		try {
			in = this.getClass().getResourceAsStream("/" + configLocation);
			if (in == null) {
				throw new RuntimeException("Can not find config file: " + configLocation);
			}
			// 推荐看 spring 源码
			List<BeanDefinition> beanDefinitions = beanConfigParser.parse(in);
			beansFactory.addBeanDefinitions(beanDefinitions);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					// TODO: log error
				}
			}
		}
	} 
	@Override
	public Object getBean(String beanId) {
		return beansFactory.getBean(beanId);
	}
}

public class BeanDefinition {
	private String id;
	private String className;
	private List<ConstructorArg> constructorArgs = new ArrayList<>();
	private Scope scope = Scope.SINGLETON;
	private boolean lazyInit = false;
	// 省略必要的getter/setter/constructors
	public boolean isSingleton() {
		return scope.equals(Scope.SINGLETON);
	} 
	public static enum Scope {
		SINGLETON,
		PROTOTYPE
	} 
	public static class ConstructorArg {
		private boolean isRef;
		private Class type;
		private Object arg;
		// 省略必要的getter/setter/constructors
	}
}

BeansFactory 的定义 负责根据从配置文件解析得到的 BeanDefinition 来创建对象

JVM 在启动的时候会根据代码自动地加载类、创建对象。至于都要加载哪些类、创建哪些对象,这些都是在代码中写死的,或者说提前写好的。但是,如果某个对象的创建并不是写死在代码中,而是放到配置文件中,我们需要在程序运行期间,动态地根据配置文件来加载类、创建对象(Java反射技术)

public class BeansFactory {
	// 用于保存单例对象  scope == singleton,下次直接从 Map 中取数据
	private ConcurrentHashMap<String, Object> singletonObjects = new ConcurrentHashMap<>();
	private ConcurrentHashMap<String, BeanDefinition> beanDefinitions = new ConcurrentHashMap<>();
	
	public void addBeanDefinitions(List<BeanDefinition> beanDefinitionList) {
		for (BeanDefinition beanDefinition : beanDefinitionList) {
			this.beanDefinitions.putIfAbsent(beanDefinition.getId(), beanDefinition)
		} 
		for (BeanDefinition beanDefinition : beanDefinitionList) {
			// 非懒加载 且为单例  ---》 饿汉式单例
			if (beanDefinition.isLazyInit() == false && beanDefinition.isSingleton())
			createBean(beanDefinition);
		}
	}

	public Object getBean(String beanId) {
		BeanDefinition beanDefinition = beanDefinitions.get(beanId);
		if (beanDefinition == null) {
			throw new NoSuchBeanDefinitionException("Bean is not defined: " + beanId);
		}
		return createBean(beanDefinition);
	}
	
	@VisibleForTesting
	protected Object createBean(BeanDefinition beanDefinition) {
		// 单例
		if (beanDefinition.isSingleton() && singletonObjects.containsKey(beanDefinition.getId())) {
			return singletonObjects.get(beanDefinition.getId());
		} 
		Object bean = null;
		try {
			// 非单例 or singletonObjects 不包含时,通过反射加载类,创建对象
			Class beanClass = Class.forName(beanDefinition.getClassName());
			List<BeanDefinition.ConstructorArg> args = beanDefinition.getConstructorArg();
			if (args.isEmpty()) {
				bean = beanClass.newInstance();
			} else {
				Class[] argClasses = new Class[args.size()];
				Object[] argObjects = new Object[args.size()];
				for (int i = 0; i < args.size(); ++i) {
					BeanDefinition.ConstructorArg arg = args.get(i);
					if (!arg.getIsRef()) {
						argClasses[i] = arg.getType();
						argObjects[i] = arg.getArg();
					} else {
						BeanDefinition refBeanDefinition = beanDefinitions.get(arg.getArg());
						if (refBeanDefinition == null) {
							throw new NoSuchBeanDefinitionException("Bean is not defined: " +
						}
						argClasses[i] = Class.forName(refBeanDefinition.getClassName());
						// 递归调用
						argObjects[i] = createBean(refBeanDefinition);
					}
				}
				bean = beanClass.getConstructor(argClasses).newInstance(argObjects);
			}
		} catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTarget
			throw new BeanCreationFailureException("", e);
		} 
		if (bean != null && beanDefinition.isSingleton()) {
			singletonObjects.putIfAbsent(beanDefinition.getId(), bean);
			return singletonObjects.get(beanDefinition.getId());
		}
		return bean;
	}
}

好处是:对象创建、组装、管理完全有 DI 容器来负责,跟具体业务代码解耦

Action:递归调用可能会导致了循环依赖,Spring 如何解决 A 和 B 对象的循环引用?
(1)只能处理单例的、setter 注入的循环依赖,其他的注入模式无法处理;
(2)依赖缓存处理循环依赖,关键思想是,将正在创建中的对象提前暴露一个单例工厂,让其他实例可以引用到。
可以参考这篇文章:Spring 源码学习(五)循环依赖


8.3、Builder 设计模式 将产品和产品建造过程解耦

定义Builder 模式用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。将复杂对象的建造过程抽象出来。

Builder模式的四个角色
①Product(产品角色):一个具体的产品对象
②Builder(抽象建造者):创建一个Product对象的各个部件指定的 接口/抽象类
③ConcreteBuilder(具体建造者): 实现接口,构建和装配各个部件。
④Director(指挥者): 构建一个使用Builder接口的对象。它主要是用于创建一个复杂的对象。它主要有两个作用,一是:隔离了客户与对象的生产过程,二是:负责控制产品对象的生产过程。

使用场景
之前的做法:构建对象时必填项使用有参构造函数,非必填属性使用 set() 方法
现在
1、当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数;
2、类的属性之间有一定的依赖关系或者约束条件;
3、如果我们希望创建不可变对象,也就是说,不能在类中暴露 set() 方法。

Demo1:Builder 模式如何使用

public class ResourcePoolConfig {
	private String name;
	// 最大资源数
	private int maxTotal;
	// 最大空闲资源数
	private int maxIdle;
	// 最小空闲资源数
	private int minIdle;
	private ResourcePoolConfig(Builder builder) {
		this.name = builder.name;
		this.maxTotal = builder.maxTotal;
		this.maxIdle = builder.maxIdle;
		this.minIdle = builder.minIdle;
	}
	//...省略getter方法...
	//我们将Builder类设计成了ResourcePoolConfig的内部类。
	public static class Builder {
		private static final int DEFAULT_MAX_TOTAL = 8;
		private static final int DEFAULT_MAX_IDLE = 8;
		private static final int DEFAULT_MIN_IDLE = 0;
		private String name;
		private int maxTotal = DEFAULT_MAX_TOTAL;
		private int maxIdle = DEFAULT_MAX_IDLE;
		private int minIdle = DEFAULT_MIN_IDLE;
		public ResourcePoolConfig build() {
			// 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
			if (StringUtils.isBlank(name)) {
				throw new IllegalArgumentException("...");
			}
			if (maxIdle > maxTotal) {
				throw new IllegalArgumentException("...");
			}
			if (minIdle > maxTotal || minIdle > maxIdle) {
				throw new IllegalArgumentException("...");
			} 
			return new ResourcePoolConfig(this);
			} 
			public Builder setName(String name) {
				if (StringUtils.isBlank(name)) {
					throw new IllegalArgumentException("...");
				}
				this.name = name;
				return this;
			} 
			public Builder setMaxTotal(int maxTotal) {
				if (maxTotal <= 0) {
					throw new IllegalArgumentException("...");
				}
				this.maxTotal = maxTotal;
				return this;
			} 
			public Builder setMaxIdle(int maxIdle) {
				if (maxIdle < 0) {
					throw new IllegalArgumentException("...");
				}
				this.maxIdle = maxIdle;
				return this;
			}	 
			public Builder setMinIdle(int minIdle) {
				if (minIdle < 0) {
					throw new IllegalArgumentException("...");
				}
				this.minIdle = minIdle;
				return this;
			}
		}
	} 
}
// Builder 模式的使用
// 符合面向对象的封装原则
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
		.setName("dbconnectionpool")
		.setMaxTotal(16)
		.setMaxIdle(10)
		.setMinIdle(12)
		.build();
Demo2 借助 Lombok 中的 @Builder 注解实现建造者模式
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ZcyStandardTransferLog implements Serializable {
    /** 自定义主键*/
    private Long id;
    /** 迁移相关id spuId*/
    private Long relatedId;
    /** 枚举值: */
    private Integer type;
    /**枚举值:0 offLine 下架;1 freeze 冻结;2 delete 删除 3 上架 online*/
    private Integer operateType;
    /** 日志详情*/
    private String detail;
    /** 创建人id*/
    private Long creatorId;
    /** 创建时间*/
    private Date createdAt;
}

创建了一个名为 ZcyStandardTransferLogBuilder 的静态内部类, 并且具有和实体类相同的属性(称为构建器).
1: 对于目标类中的所有的属性, 都会在构建器中创建对应的属性.
2: 创建一个无参的default构造方法.
3: 对于实体类中的每个参数, 都会对应创建类似于setter方法, 但是方法名是与该参数名是相同的, 并且返回值是构建器本身(便于链式调用).
4: 一个build方法, 调用此方法, 就会根据设置的值进行创建对象实例.
5: 同时也会生成一个toString() 方法.
6: 会创建一个builder()方法, 它的目的是用来创建构建器.

补充:builder中的常用注解

Demo3 建造者模式在JDK的应用和源码分析

java.lang.StringBuilder中的建造者模式,将append()逻辑放在抽象父类中,然后返回this

@Override
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
// StringBuilder 的父类
public AbstractStringBuilder append(String str) {
    if (str == null)
        return appendNull();
    int len = str.length();
    ensureCapacityInternal(count + len);
    str.getChars(0, len, value, count);
    count += len;
    return this;
}
// AbstractStringBuilder 的接口
// 因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象
public interface Appendable {
	Appendable append(CharSequence csq) throws IOException;
	Appendable append(CharSequence csq, int start, int end) throws IOException;
	Appendable append(char c) throws IOException;
}

// StringBuilder 的使用
stringBuilder.append("属性项【").append(entry.getKey()).append("】长度不允许超出 (").append(entry.getValue()).append(")字符,");

工厂模式和 Builder 模式的区别?

  • 工厂模式是用来创建不同但是相关类型的对象,工厂模式是用来创建不同但是相关类型的对象;
  • Builder 模式是用来创建一种类型的复杂对象,通过设置不同的可选参数,“定制化”地创建不同的对象

8.4、原型设计模式的深拷贝和浅拷贝是什么,并写出深拷贝的两种方式的源码

概念如果对象的创建成本比较大(复杂的RPC/IO计算),而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式,来创建新对象,以达到节省创建时间的目的这种基于原型来创建对象的方式就叫作原型模式

Java中:Java中Object类是所有类的根类, Object类提供了一个clone()方法,该方法可以
将一个Java对象复制一份,但是需要实现clone的Java类必须要实现一个接口Cloneable,
该接口表示该类能够复制且具有复制的能力。 (浅拷贝,不实用)

原型模式的实现方式–深拷贝和浅拷贝:浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象……而深拷贝得到的是一份完完全全独立的对象。所以,深拷贝比起浅拷贝来说,更加耗时,更加耗内存空间。
深拷贝实现方案:1、使用Spring的BeanUtils工具类(原理是Java的反射语法) 2、使用Json序列化工具(推荐)

Demo1:重写Object的clone方法实现深拷贝,使用序列化来实现深拷贝

public class DeepProtoType implements Serializable, Cloneable{
	public String name; //String 属性
	public DeepCloneableTarget deepCloneableTarget;// 引用类型
	public DeepProtoType() {
		super();
	}

	//深拷贝 - 方式 1 使用clone 方法
	@Override
	protected Object clone() throws CloneNotSupportedException {
		Object deep = null;
		//这里完成对基本数据类型(属性)和String的克隆
		deep = super.clone(); 
		//对引用类型的属性,进行单独处理
		DeepProtoType deepProtoType = (DeepProtoType)deep;
		deepProtoType.deepCloneableTarget  = (DeepCloneableTarget)deepCloneableTarget.clone();
		return deepProtoType;
	}
	
	//深拷贝 - 方式2 通过对象的序列化实现 (推荐)
	public Object deepClone() {
		//创建流对象
		ByteArrayOutputStream bos = null;
		ObjectOutputStream oos = null;
		ByteArrayInputStream bis = null;
		ObjectInputStream ois = null;
		
		try {
			//序列化
			bos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(bos);
			oos.writeObject(this); //当前这个对象以对象流的方式输出
			//反序列化
			bis = new ByteArrayInputStream(bos.toByteArray());
			ois = new ObjectInputStream(bis);
			DeepProtoType copyObj = (DeepProtoType)ois.readObject();
			return copyObj;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		} finally {
			//关闭流
			try {
				bos.close();
				oos.close();
				bis.close();
				ois.close();
			} catch (Exception e2) {
				// TODO: handle exception
				System.out.println(e2.getMessage());
			}
		}
	}	
}

//深拷贝工具1 springframework BeanUtil  原理:反射
BeanUtils.copyProperties(source, target, "id", "updatedAt", "updatedId", "updatedName");

//深拷贝工具2 Dozer工具
List<AttachmentDTO> attachmentDtos = DozerBeanUtil.convertList(xxx.getAttachments(), attachmentDTO.class);

//深拷贝工具3 AnyBeanCopy工具  原理:json序列化   推荐
Person personCopy = AnyBeanCopyUtils.convert(person, Person.class);

2、请使用UML类图画出原型模型核心角色? 原理结构图
在这里插入图片描述
1)原型类,声明一个克隆自己的接口
2) ConcretePrototype: 具体的原型类, 实现一个克隆自己的操作
3) Client: 让一个原型对象克隆自己,从而创建一个新的对象(属性一样)

Action:原型设计模式和 Spring 原型区别在哪? 面试题

区别SpringGOF
对象类型根据Bean定义来创建对象用原型实例指定创建对象类型
创建方式根据Bean定义创建对象通过拷贝原型创建对象
友好方式非侵入式侵入式

Demo2 Spring 框架哪些地方使用了原型模式,并对源码进行分析?
beans.xml

<bean id="id01" class="com.spring.bean.Monster" scope="prototype"/>
public void main (){
	ApplicationContext applicationContext = newClassPathXmlApplicationContext("beans.xml");
	//获取monster[通过id获取monster]
	Object bean = applicationContext.getBean("id01");
	System.out.println("bean" + bean);
}

// 在源码的 doGetBean 方法里面进行了判断
else if (mbd.isPrototype()) {
	// It's a prototype -> create a new instance.
	Object prototypeInstance = null;
	try {
		beforePrototypeCreation(beanName);
		// 进入了原型模式的对象创建
		prototypeInstance = createBean(beanName, mbd, args);
	}
	finally {
		afterPrototypeCreation(beanName);
	}
	bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}

原型模式使用踩坑
1、不要使用Common包里面的BeanUtils工具类
2、在日常开发中,注意对象里面的字段被修改的情况,使用深拷贝避免该问题。

创建型设计模式总结:

Action:使用双重锁校验的单例模式时,使用需要在成员变量前加上 volatile 关键字?

  • 暂时在成员变量里加上 volatile,防止指令重排,确保没有问题。

9、结构型设计模式

结构型模式主要总结了一些 类或对象组合在一起 的经典结构,这些经典的结构可以解决特定应用场景的问题。结构型模式包括:代理模式,桥接模式,适配器模式,装饰器模式,(2021-12-03) 外观模式(不常用),组合模式(不常用),享元模式(不常用)

结构型设计模式教你如何正确使用继承和组合

9.1、代理模式 Proxy

定义:为一个对象提供一个替身,以控制对这个对象的访问。 即通过代理对象访问目标对象。这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。

代理模式的使用场景:
①在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志
②RPC 框架也可以看作一种代理模式。

如何使用:
代理模式在不改变原始类接口的条件下,为原始类定义一个代理类主要目的是控制访问,而非加强功能。①如果有接口,让代理类和原始类实现同样的接口(JDK 代理);②如果原始类并没有定义接口,我们可以通过让代理类继承原始类的方法来实现代理模式(Cglib 代理)

Demo 用户登录业务

public class UserController {
	//...省略其他属性和方法...
	@Autowired
	private MetricsCollector metricsCollector;
	
	public UserVo login(String telephone, String password) {
		long startTimestamp = System.currentTimeMillis();
		// ... 省略login业务逻辑...
		long endTimeStamp = System.currentTimeMillis();
		long responseTime = endTimeStamp - startTimestamp;
		RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimes);
		metricsCollector.recordRequest(requestInfo);
		//...返回UserVo数据...
	}
}

存在的问题:非当前业务的代码嵌入到了该业务代码中,造成了代码耦合,且职责不单一

如何解决代码耦合问题呢?

方法1、静态代理:在使用时需要定义接口或者父类被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类

Demo 静态代理

// 接口
public interface IUserController {
	void login(String telephone, String password);
}

// 实现类 1
public class UserController implements IUserController {
	@Override
	public void login(String telephone, String password) {
		// 业务逻辑
	}
}

// 实现类2 代理对象,静态代理
public class UserControllerProxy implements IUserController{
	// 将目标对象组合到代理类中
	private IUserController userController; 
	private MetricsCollector metricsCollector;
	//构造器
	public UserControllerProxy(IUserController userController, MetricsCollector metricsCollector) {
		this.userController = userController;
		this.metricsCollector = metricsCollector;
	}
	@Override
	public void login() {
		//方法
		System.out.println("开始代理  完成某些操作。。。。。 ");
		
		userController.login();
		//方法
		System.out.println("提交。。。。。");
		metricsCollector.recordRequest();
	}
}

public static void main(String[] args) {
	//被代理对象
	UserController userController = new UserController();
	//创建代理对象, 同时将被代理对象传递给代理对象
	UserControllerProxy userControllerProxy = new UserControllerProxy(userController);
	//执行的是代理对象的方法,代理对象再去调用被代理对象的方法
	userControllerProxy.login();
}

优点:在不修改目标对象代码前提下, 能通过代理对象对目标功能扩展
缺点:代理对象需要与目标对象实现同样的接口,所以有很多代理类,维护很困难

如何解决代理类过多的问题?

动态代理我们不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类

分为两种:
①JDK 动态代理,目标对象需要实现接口
② Cglib 动态代理,目标对象不需要实现对象

Demo2, JDK 动态代理 MetricsCollectorProxy 作为一个动态代理类,动态地给每个需要收集接口请求信息的类创建代理类
在这里插入图片描述

// JDK 动态代理   底层依赖 Java 反射语法
public class ProxyFactory {
    // 被代理的对象
    private Object target;

    // 在构造器中对目标对象进行初始化
    public ProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        Class<?>[] interfaces = target.getClass().getInterfaces();
        DynamicProxyHandler handler = new DynamicProxyHandler(target);
        // 由 JDK 提供核心接口
        // 1、ClassLoader loader:指定当前被代理对象的类加载器   
        // 2、Class<?> interfaces: 被代理对象实现的接口类型,使用泛型方法确认类型 
        // 3、InvocationHandler h 事件处理,执行被代理对象的方法时,会去触发事件处理器方法,会把当前执行的被代理对象方法作为参数
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), interfaces, handler);
    }

    private class DynamicProxyHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 前置处理。。。
            System.out.println("jdk 代理模式--前置处理");
            // 使用反射机制调用目标对象的方法
            Object result = method.invoke(target, args);
            // 后置处理。..
            System.out.println("jdk 代理模式--后置处理");
            return result;
        }
    }
}

public interface MetricsCollector {
    /* 数据统计 */
    String recordRequest(RequestInfoVo vo);
}

public class MetricsCollectorImpl implements MetricsCollector {
    /* 数据统计 */
    @Override
    public String recordRequest(RequestInfoVo vo) {
        return "数据统计";
    }
}

public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        MetricsCollectorImpl target = new MetricsCollectorImpl();
        // 获取到代理对象,并将目标对象传递给代理对象
        MetricsCollector proxyInstance = (MetricsCollector) new ProxyFactory(target).getProxyInstance();
        // 执行代理对象的方法,触发 intercept 方法
        String res = proxyInstance.recordRequest(new RequestInfoVo());
        System.out.println("res:" + res);
    }
}
// 返回的数据:当前代理动态生成的对象,如果调用该对象的 getClass() 方法,返回的是$Proxy0

Cglib 动态代理
目标对象只有一个单独的对象,并没有实现任何的接口,这是使用被代理对象子类来实现代理。

Demo3 Cglib 动态代理
1、需要引入 cglib 的 jar 包
在这里插入图片描述
2、注意代理的类不能为 final,否则报错 java.lang.illegalArgumentException;目标对象的方法方法如果是 final/static, 那么就不会被拦截,即不会执行目标对象的额外业务方法

3、使用示例代码如下所示
在这里插入图片描述

public class ProxyFactory implements MethodInterceptor {

    // 维护目标对象
    private Object target;

    // 在构造器中对目标对象进行初始化
    public ProxyFactory(Object target) {
        this.target = target;
    }

    // 返回代理对象,是 target 的对象的代理对象
    public Object getProxyInstance() {
        // 1、设置工具类
        Enhancer enhancer = new Enhancer();
        // 2、设置父类
        enhancer.setSuperclass(target.getClass());
        // 3、设置回调函数
        enhancer.setCallback(this);
        // 4、创建子类对象,即代理对象
        return enhancer.create();
    }

    // 重写 intercept 方法,会调用目标对象的方法
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("Cglib 代理模式--前置处理");
        Object invoke = method.invoke(target, args);
        System.out.println("Cglib 代理模式--后置处理");
        return invoke;
    }

}

public class MetricsCollector {

    public String stastic() {
        return "统计信息~";
    }
}

public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        MetricsCollector target = new MetricsCollector();
        // 获取到代理对象,并将目标对象传递给代理对象
        MetricsCollector proxyInstance = (MetricsCollector) new ProxyFactory(target).getProxyInstance();
        // 执行代理对象的方法,触发 intercept 方法
        String res = proxyInstance.stastic();
        System.out.println("res:" + res);
    }

}

在Aop 编程中如何选择代理模式?
①目标对象需要实现接口,使用 JDK 代理
目标对象不需要实现接口,使用 Cglib 代理
底层原理:使用字节码处理框架 ASM 来转换字节码并生成新的类

Spring AOP 实现原理
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。Spring AOP 底层的实现原理就是基于 动态代理

Aop 动态代理原理图
在这里插入图片描述
实现切面的三种方式
①JDK proxy 如demo2
②Cglib 如demo3
③AspectJ AOP,Spring AOP 已经集成了AspectJ ,底层实现原理为 字节码操作

MyBatis Dao 层实现原理

  • 使用到了代理模式
  • 后续补充原理

9.2、桥接模式(不常用)

定义:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。

使用场景:对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统

  • 1、JDBC驱动程序
    • Driver(接口):mysql驱动、oracle驱动
    • JDBC(抽象):JDBC这套类库
  • 2、银行转账系统
    • 转账分类(接口):网上转账,柜台,ATM机器
    • 用户类型(抽象):普通用户,会员用户
  • 3、消息管理
    • 消息类型(接口):严重、紧急、普通
    • 消息分类(抽象):邮件、短信、微信、手机

难点:要求正确识别出系统中独立变化的维度(抽象、实现),使用范围有局限性。

Demo1
继续下面的案例: API 接口监控告警的代码。根据不同的告警规则,触发不同类型的告警。
Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道

首先看看最简单的实现方式:

public enum NotificationEmergencyLevel {
	SEVERE, URGENCY, NORMAL, TRIVIAL
} 
public class Notification {
	private List<String> emailAddresses;
	private List<String> telephones;
	private List<String> wechatIds;
	public Notification() {}
	public void setEmailAddress(List<String> emailAddress) {
		this.emailAddresses = emailAddress;
	} 
	public void setTelephones(List<String> telephones) {
		this.telephones = telephones;
	} 
	public void setWechatIds(List<String> wechatIds) {
		this.wechatIds = wechatIds;
	} 
	public void notify(NotificationEmergencyLevel level, String message) {
		if (level.equals(NotificationEmergencyLevel.SEVERE)) {
			//...自动语音电话
		} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
			//...发微信
		} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
			//...发邮件
		} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
			//...发邮件
		}
	}
}

//在API监控告警的例子中,我们如下方式来使用Notification类:
public class ErrorAlertHandler extends AlertHandler {
	public ErrorAlertHandler(AlertRule rule, Notification notification){
		super(rule, notification);
	}
	@Override
	public void check(ApiStatInfo apiStatInfo) {
		if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi())
			notification.notify(NotificationEmergencyLevel.SEVERE, "...");
		}
	}
}

通知类中 if-else逻辑比较复杂,所有发送通知的逻辑都堆砌在Notification类中,如果将发送消息的逻辑剥离出来,形成独立的消息发送类 MsgSender,即 Notification相当于抽象,MsgSender相当于实现,两者可以独立开发,通过组合关系 任意组合在一起

消息类型(MsgSender 接口):严重、紧急、普通
消息分类(Notification 抽象类):邮件、短信、微信、手机

重构后的代码如下所示:

public interface MsgSender {
	void send(String message);
} 
public class TelephoneMsgSender implements MsgSender {
	private List<String> telephones;
	public TelephoneMsgSender(List<String> telephones) {
		this.telephones = telephones;
	}
	@Override
	public void send(String message) {
		//...
	}
}

public class EmailMsgSender implements MsgSender {
	// 与TelephoneMsgSender代码结构类似,所以省略...
} 
public class WechatMsgSender implements MsgSender {
	// 与TelephoneMsgSender代码结构类似,所以省略...
} 

public abstract class Notification {
	protected MsgSender msgSender;
	public Notification(MsgSender msgSender) {
		this.msgSender = msgSender;
	} 
	public abstract void notify(String message);
} 

public class SevereNotification extends Notification {
	public SevereNotification(MsgSender msgSender) {
	super(msgSender);
	} 
	@Override
	public void notify(String message) {
		msgSender.send(message);
	}
} 
public class UrgencyNotification extends Notification {
	// 与SevereNotification代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
	// 与SevereNotification代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
	// 与SevereNotification代码结构类似,所以省略...
}

public static void main(String[] args) {	
	//通过电话发送紧急通知
	Notification notifi1 = new UrgencyNotification(new TelephoneMsgSender());
	notifi1.notify("电话通知紧急消息");
	System.out.println("=======================");
	
	//通过邮件发送普通消息
	Notification notifi2 = new NormalNotification(new EmailMsgSender());
	notifi1.notify("邮件通知普通消息");
}

demo2 桥接模式在 JDBC 的源码剖析
JDBC 驱动是桥接模式的经典应用,利用 JDBC 驱动来查询数据库。具体的代码如下所示

//加载及注册JDBC驱动程序,切换 oracle 使用 "oracle.jdbc.driver.OracleDriver"
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=root
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement()String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
	rs.getString(1);
	rs.getInt(2);
}

如果想将 mysql 数据库替换为 Oracle数据库,只需要修改 url 字段的数据。

JDBC 是如何优雅的切换数据库呢?

当执行 Class.forName(“com.mysql.jdbc.Driver”) 这条语句的时候,实际上是做了两件事情。
①要求 JVM 查找并加载指定的 Driver 类,
②执行该类的静态代码,也就是将 MySQL Driver 注册到 DriverManager 类中。

package com.mysql.jdbc;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
	static {
		try {
			// 将mysql driver 注册到 DriverManager 中
			DriverManager.registerDriver(new Driver());
		} catch (SQLException E) {
			throw new RuntimeException("Can't register driver!");
		}
	} 
	/**
	* Construct a new driver and register it with DriverManager
	* @throws SQLException if a database error occurs.
	*/
	public Driver() throws SQLException {
		// Required for Class.forName().newInstance()
	}
}

DriverManager 类是干什么用的。具体的代码如下所示。Driver 实现类(比如,com.mysql.jdbc.Driver)注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。而 Driver 实现类都实现了相同的接口(java.sql.Driver ),这也是可以灵活切换 Driver 的原因。

//并不是一个抽象类
public class DriverManager {
	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
	//...
	static {
		loadInitialDrivers();
		println("JDBC DriverManager initialized");
	}
	//...
	public static synchronized void registerDriver(java.sql.Driver driver) throws SQLExcepton {
		if (driver != null) {
			registeredDrivers.addIfAbsent(new DriverInfo(driver));
		} else {
			throw new NullPointerException();
		}
	} 
	public static Connection getConnection(String url, String user, String password) {
		java.util.Properties info = new java.util.Properties();
		if (user != null) {
			info.put("user", user);
		}
		if (password != null) {
			info.put("password", password);
		}
		return (getConnection(url, info, Reflection.getCallerClass()));
	}
	//...
}

JDBC中,什么是抽象?

  • JDBC 本身就相当于“抽象”,并非指“抽象类"
  • 与具体数据库无关的、被抽象出来的一套“类库”

什么是实现?

  • 具体的Driver(比如,com.mysql.jdbc.Driver)就相当于“实现”
  • 并非指“接口的实现类”,而是跟具体数据库相关的一套“类库”

JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。JDBC 的所有逻辑操作,最终都委托给 Driver 来执行。如下图所示:
在这里插入图片描述


9.3、装饰者模式 Decorator(BufferedInputStream)

装饰器模式:主要解决继承关系过于复杂的问题,通过组合来替代继承,给原始类添加增强功能。能动态地将新功能附加到对象上。

功能增强,也是判断是否该用装饰者模式的一个重要的依据

与代理模式比较

  • 代理类附加的是跟原始类无关的功能
  • 而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能

适用场景:当我们需要修改原有功能,但又不愿直接去修改原有代码时,设计一个Decorator 套在原有代码外面。

装饰者模式原理图
在这里插入图片描述
装饰者模式的特点总结
①装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类
②装饰器类的成员变量类型为父类类型
装饰器类是对功能的增强

Demo1 使用示例

// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
	void f();
}
public class A impelements IA {
	public void f() { 
		//... 
	}
}
public class ADecorator impements IA {
	private IA a;
	public ADecorator(IA a) {
		this.a = a;
	} 
	public void f() {
		// 功能增强代码
		a.f();
		// 功能增强代码
	}
}

demo2 装饰者模式在商品中心的应用- 类名上含有 Wrapper或者 Decorator。这些类基本上都是动态地给一个对象添加一些额外的职责。

例如:is里面的
ip里面的 NewFullItemWrapper
im里面的 MqMessageWrapperDto

Demo3 装饰者模式在 Java IO中的使用,Java IO 类库如下图所示:
在这里插入图片描述

InputStream 是一个抽象类,FileInputStream 是专门用来读取文件流的子类。BufferedInputStream 是一个支持带缓存功能的数据读取类,可以提高数据读取效率。

现在有一个需求,读取文件 test.txt。

InputStream in = new FileInputStream("/user/1202/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
	//...
}

如果是基于继承的设计,设计成 BufferedFileInputStream,需要附加更多的增强功能,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护

InputStream bin = new BufferedFileInputStream("/user/1202/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
	//...
}

基于装饰器模式的设计模式的 Java IO 源码核心思想:组合优于继承,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。

public abstract class InputStream {
	//...
	public int read(byte b[]) throws IOException {
		return read(b, 0, b.length);
	} 
	public int read(byte b[], int off, int len) throws IOException {
		//...
	} 
	public long skip(long n) throws IOException {
		//...
	} 
	public int available() throws IOException {
		return 0;
	} 
	public void close() throws IOException {}
	public synchronized void mark(int readlimit) {}
	public synchronized void reset() throws IOException {
		throw new IOException("mark/reset not supported");
	} 
	public boolean markSupported() {
		return false;
	}
}

public class BufferedInputStream extends InputStream {
	protected volatile InputStream in;
	protected BufferedInputStream(InputStream in) {
		this.in = in;
	} 
	//...实现基于缓存的读数据接口...
} 
public class DataInputStream extends InputStream {
	protected volatile InputStream in;
	protected DataInputStream(InputStream in) {
		this.in = in;
	} 
	//...实现读取基本类型数据的接口
}

9.4、适配器设计模式 Adapter

概念适配器模式用来将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作用于消除接口不匹配所造成的类的兼容性问题
在这里插入图片描述

代理模式、装饰器模式提供的都是跟原始类相同的接口,而适配器提供跟原始类不同的接口。

主要分为两类
1、类的适配器模式 使用继承关系来实现
2、对象的适配器模式 使用组合关系来实现

Demo1:ITarget 表示要转化成的接口定义。Adaptee 是一组不兼容 ITarget 接口定义的接口,Adaptor 将 Adaptee 转化成一组符合 ITarget 接口定义的接口

  • 类适配器: 基于继承
// 适配接口
public interface ITarget {
	void f1();
	void f2();
	void fc();
} 
//被适配者
public class Adaptee {
	public void fa() { //... }
	public void fb() { //... }
	public void fc() { //... }
} 
//适配者  继承被适配者,实现适配接口
public class Adaptor extends Adaptee implements ITarget {
	public void f1() {
		super.fa();
	} 
	public void f2() {
		//...重新实现f2()...
	} 
	// 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}

缺点:需要继承被适配者

  • 对象适配器:基于组合
// 最终需要的输出
public interface ITarget {
	void f1();
	void f2();
	void fc();
} 
//被适配者
public class Adaptee {
	public void fa() { //... }
	public void fb() { //... }
	public void fc() { //... }
} 
//适配者 组合被适配者,实现适配接口
public class Adaptor implements ITarget {
	private Adaptee adaptee;
	public Adaptor(Adaptee adaptee) {
		this.adaptee = adaptee;
	} 
	public void f1() {
		//委托给Adaptee
		adaptee.fa(); 
	} 
	public void f2() {
		//...重新实现f2()...
	} 
	public void fc() {
		adaptee.fc();
	}
}
使用组合替代继承,解决了类适配器必须继承 被是适配者的问题

在开发中,到底该如何选择使用哪一种呢?
①一个是 Adaptee 接口的个数
②另一个是 Adaptee 和 ITarget 的契合程度
怎么简便怎么来。相对而言:更加推荐使用对象适配器,因为组合结构相对于继承更加灵活

  • 符合“合成复用原则”

③ 接口适配

  • 当不需要全部实现某接口提供的方法时,可以先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求

使用场景:

  • ①封装有缺陷的接口设计, 对外部系统提供的接口进行二次封装
  • ②替换外部系统
  • 兼容老版本接口 重点

Demo2 修改配置中心SDK

public class InstanceServiceClient {
//...
	public Response<List<InstanceRO>> fetchInstanceByIndustry(InstanceSearchDTO param){ //... }
    public Response<InstanceRO> fetchInstanceByCondition(SearchCondition param){ //... }
	public void tooManyParamsFunction3(int paramA, int paramB, ...) { //... }
	//...
}

// 使用适配器模式进行重构
public class ITarget {
	void function1(InstanceSearchDTO param);
	void function2(SearchCondition param);
	void fucntion3(ParamsWrapperDefinition paramsWrapper);
	//...
}

public class InstanceServiceClientAdaptor extends InstanceServiceClient implements ITarget {
	//...
	public void function1() {
		super.fetchInstanceByIndustry(param);
	} 
	public void function2() {
		super.fetchInstanceByCondition();
	}
	public void function3(ParamsWrapperDefinition paramsWrapper) {
		super.tooManyParamsFunction3(paramsWrapper.getParamA(), ...);
	} 
}

Demo3:替换外部系统

// 外部系统A
public interface IA {
	//...
	void fa();
}
public class A implements IA {
	//...
	public void fa() { //... }
}
// 在我们的项目中,外部系统A的使用示例
public class Demo {
	private IA a;
	public Demo(IA a) {
		this.a = a;
	}
	//...
}
Demo d = new Demo(new A());

// 将外部系统A 替换成外部系统B
public class BAdaptor implemnts IA {
	private B b;
	public BAdaptor(B b) {
		this.b= b;
	}
	public void fa() {
		//...
		b.fb();
	}
}
// 借助BAdaptor,Demo的代码中,调用IA接口的地方都无需改动,
// 只需要将BAdaptor如下注入到Demo即可。
Demo d = new Demo(new BAdaptor(new B()));

Demo4 兼容升级 ☆
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且
标注为 deprecated,并将内部实现逻辑委托为新的接口实现

好处让项目有个过渡期,而不是强制进行代码修改
场景: 兼容api升级,API治理可以使用这种方案

// Emueration jdk1.0提供  jdk2.0 改为了 iterator
public class Collections {
	public static Emueration emumeration(final Collection c) {
		return new Enumeration() {
			Iterator i = c.iterator();
			public boolean hasMoreElments() {
				return i.hashNext();
			}
			public Object nextElement() {
				return i.next():
			}
		}
	}
}

Demo5:适配器模式在 Java 日志中的应用
Slf4j 这个日志框架,它相当于 JDBC 规范,提供了一套打印日志的统一接口规范。不过,它只定义了接口,并没有提供具体的实现,需要配合其他日志框架(log4j、logback……)来使用。
它不仅仅提供了统一的接口定义,还提供了针对不同日志框架的适配器。对不同日志框
架的接口进行二次封装,适配成统一的 Slf4j 接口定义。

package org.slf4j;
public interface Logger {
	public boolean isTraceEnabled();
	public void trace(String msg);
	public void trace(String format, Object arg);
	public void trace(String format, Object arg1, Object arg2);
	public void trace(String format, Object[] argArray);
	public void trace(String msg, Throwable t);
	public boolean isDebugEnabled();
	public void debug(String msg);
	public void debug(String format, Object arg);
	public void debug(String format, Object arg1, Object arg2)
	public void debug(String format, Object[] argArray)
	public void debug(String msg, Throwable t);
	//...省略info、warn、error等一堆接口
}

// log4j日志框架的适配器
// Log4jLoggerAdapter实现了LocationAwareLogger接口,
// 其中LocationAwareLogger继承自Logger接口,
// 也就相当于Log4jLoggerAdapter实现了Logger接口。
package org.slf4j.impl;
public final class Log4jLoggerAdapter extends MarkerIgnoringBase implements LocationAwareLogger, Serializable {
	final transient org.apache.log4j.Logger logger; // log4j
	public boolean isDebugEnabled() {
		return logger.isDebugEnabled();
	} 
	public void debug(String msg) {
		logger.log(FQCN, Level.DEBUG, msg, null);
	} 
	public void debug(String format, Object arg) {
		if (logger.isDebugEnabled()) {
			FormattingTuple ft = MessageFormatter.format(format, arg);
			logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
		}
	} 
	public void debug(String format, Object arg1, Object arg2) {
		if (logger.isDebugEnabled()) {
			FormattingTuple ft = MessageFormatter.format(format, arg1, arg2);
			logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
		}
	} 
	public void debug(String format, Object[] argArray) {
		if (logger.isDebugEnabled()) {
			FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);
			logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable());
		}
	} 
	public void debug(String msg, Throwable t) {
		logger.log(FQCN, Level.DEBUG, msg, t);
	}
	//...省略一堆接口的实现...
}

Demo6 适配器模式在 SpringMVC 框架应用的源码剖析

SpringMVC 处理用户请求的流程 在这里插入图片描述
SpringMVC 中的 HandlerAdapter, 就使用了适配器模式

//多种Controller实现  
public interface Controller {
}

class HttpController implements Controller {
	public void doHttpHandler() {
		System.out.println("http...");
	}
}

class SimpleController implements Controller {
	public void doSimpleHandler() {
		System.out.println("simple...");
	}
}

class AnnotationController implements Controller {
	public void doAnnotationHandler() {
		System.out.println("annotation...");
	}
}

public class DispatchServlet extends FrameworkServlet {
	public static List<HandlerAdapter> handlerAdapters = new ArrayList<HandlerAdapter>();

	public DispatchServlet() {
		handlerAdapters.add(new AnnotationHandlerAdapter());
		handlerAdapters.add(new HttpHandlerAdapter());
		handlerAdapters.add(new SimpleHandlerAdapter());
	}

	public void doDispatch() {
		// 此处模拟 SpringMVC 从 request 取 handler 的对象,适配器可以获取到想要的Controller
	 	HttpController controller = new HttpController();
		//AnnotationController controller = new AnnotationController();
		//SimpleController controller = new SimpleController();
		// 得到对应适配器
		HandlerAdapter adapter = getHandler(controller);
		// 通过适配器执行对应的controller对应方法
		adapter.handle(controller);
	}

	public HandlerAdapter getHandler(Controller controller) {
		//遍历:根据得到的controller(handler), 返回对应适配器
		for (HandlerAdapter adapter : this.handlerAdapters) {
			if (adapter.supports(controller)) {
				return adapter;
			}
		}
		return null;
	}

	public static void main(String[] args) {
		new DispatchServlet().doDispatch(); // http...
	}

}

///定义一个Adapter接口 
public interface HandlerAdapter {
	public boolean supports(Object handler);
	public void handle(Object handler);
}

// 多种适配器类

class SimpleHandlerAdapter implements HandlerAdapter {
	public boolean supports(Object handler) {
		return (handler instanceof SimpleController);
	}
	public void handle(Object handler) {
		((SimpleController) handler).doSimplerHandler();
	}
}

class HttpHandlerAdapter implements HandlerAdapter {
	public boolean supports(Object handler) {
		return (handler instanceof HttpController);
	}
	public void handle(Object handler) {
		((HttpController) handler).doHttpHandler();
	}
}

class AnnotationHandlerAdapter implements HandlerAdapter {
	public boolean supports(Object handler) {
		return (handler instanceof AnnotationController);
	}
	public void handle(Object handler) {
		((AnnotationController) handler).doAnnotationHandler();
	}
}

使用HandlerAdapter 的原因:
可以看到处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果需要直接调用Controller方法,需要调用的时候就得不断是使用if else来进行判断是哪一种子类然后执行。那么如果后面要扩展Controller,就得修改原来的代码,这样违背了OCP原则。

SpringMVC使用适配器设计模式的好处
• Spring 定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类;
• 适配器代替 Controller 执行相应的方法;
• 扩展 Controller 时,只需要增加一个适配器类就完成了SpringMVC的扩展了


代理、桥接、装饰器、适配器 4 种设计模式的区别

代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,控制访问,而非加强功能
桥接模式桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用
适配器模式:一种补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

20211203 done


9.5、你在编码时最常用的设计模式有哪些?在什么场景下用?在业务代码中,经常发现大量XXFacade,门面模式是解决什么问题?适用于什么场景?

定义:门面模式为子系统提供一组统一的接口定义一组高层接口让子系统更易用,合理的使用门面模式,可以帮我们更好的划分访问层次。

使用场景:
① 解决易用性问题 封装底层的复杂实现;
② 解决性能问题 将多个接口组装为一个大而全接口,减少网络开销;
③维护遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口,让新系统与Facade类交互, 提高复用性。

me商品中心使用广泛,但是有些 facade 并不是与于封装底层子系统复杂实现的目的编写的,这些业务逻辑并不复杂的接口去除 facade 后缀为好。

Demo1 facade模式的使用姿势
在这里插入图片描述

例如:业务方需要在一个接口中返回商品信息(配置项、类目、属性、价格、库存)

public class ItemReadFacade {
	
	//定义各个子系统对象
	private ItemConfig itemConfig;
	private Category category;
	private Attributes attributes;
	private Price price;
	private Stock stock;
	
	//构造器
	public ItemReadFacade() {
		super();
		this.itemConfig = ItemConfig.getInstance();
		this.category = Category.getInstance();
		this.attributes = Attributes.getInstance();
		this.price = Price.getInstance();
		this.stock = Stock.getInstance();
	}

	//操作分成 4 步
	public ItemWrapperDTO read() {
		itemConfig.read();
		category.read();
		attributes.read();
		price.read();
		stock.read();
	}
}

public class ItemConfig {
	//使用单例模式, 使用饿汉式
	private static ItemConfig instance = new ItemConfig();
	
	public static ItemConfig getInstanc() {
		return instance;
	}
	public ItemConfig read() {
		System.out.println(" ItemConfig ");
	}
}

...

public static void main(String[] args) {
	ItemReadFacade itemReadFacade = new ItemReadFacade();
	itemReadFacade.read();
}

Demo2:门面模式在 MyBatis 框架应用的分析
MyBatis 中的Configuration 去创建MetaObject 对象使用到门面模式

public class Configuration {
	protected ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
	protected ObjectFactory objectFactory = new DefaultObjectFactory();
	protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();
	public MetaObject newMetaObject(Object object) {
		// 封装了子系统的复杂性
		return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
	}
}
public class MetaObject{
	private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
        this.originalObject = object;
        this.objectFactory = objectFactory;
        this.objectWrapperFactory = objectWrapperFactory;
        this.reflectorFactory = reflectorFactory;
        if (object instanceof ObjectWrapper) {
            this.objectWrapper = (ObjectWrapper)object;
        } else if (objectWrapperFactory.hasWrapperFor(object)) {
            this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object);
        } else if (object instanceof Map) {
            this.objectWrapper = new MapWrapper(this, (Map)object);
        } else if (object instanceof Collection) {
            this.objectWrapper = new CollectionWrapper(this, (Collection)object);
        } else {
            this.objectWrapper = new BeanWrapper(this, object);
        }
    }
    public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {
        return object == null ? SystemMetaObject.NULL_META_OBJECT : new MetaObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
    }
}

9.6、组合模式(不常用)

概念:将对象组合成树状结构以表示 “整体-部分”的层次关系
使用场景:用来处理树状结构数据。

  • 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式;业务需求可以通过在树上的递归遍历算法来实现
  • 如果节点和叶子有很多差异性的话,比如很多方法和属性不一样, 不适合使用组合模式

组合模式的角色及职责

  1. Component:这是组合中对象声明接口,在适当情况下,实现所有类共有的接口默认行为,用于访问和管理Component 子部件, Component 可以是抽象类或者接口
  2. Leaf:在组合中表示叶子节点,叶子结点没有子节点;
    3)Composite:非叶子节点,用于存储子部件,在 Component 接口实现子部件的相关操作,比如增加、删除。

Demo1 需求:设计一个“类目”类,能方便地实现下面这些功能:

  • 动态地添加、删除某个类目下的父类目或子类目;
  • 统计指定类目下的类目个数;
  • 统计指定类目下的标签个数;

代码如下所示,把父类目和子类目统一用 CategoryNode 类来表示,并通过hasChildren 属性来区分。

public class CategoryNode {
	//类目名称
	private String name;
	//是否有叶子节点
	private boolean hasChildren;
	//子节点
	private List<CategoryNode> subNodes = new ArrayList<>();
	
	public CategoryNode(String name, boolean hasChildren) {
		this.name = name;
		this.hasChildren = hasChildren;
	}
	public int countNumOfCategories() {
		if (!hasChildren) {
			return 1;
		}
		int numOfCategories = 0;
		for (CategoryNode categoryNode : subNodes) {
			numOfCategories += categoryNode.countNumOfCategories();
		}
	}

	public String getName() {
		return name;
	}
	public void addSubNode(CategoryNode categoryNode) {
		subNodes.add(categoryNode);
	}
	public void removeSubNode(CategoryNode categoryNode) {
		int size = subNodes.size();
		int i = 0;
		for (; i < size; ++i) {
			if (subNodes.get(i).getName().equals(categoryNode.getName())){
				break;
			}
		}
		if (i < size) {
			subNodes.remove(i);
		}
	}
}
public class Demo {
	public static void main(String[] args) {
		CategoryNode pCategoryNode = new CategoryNode("名贵花木");
		CategoryNode node_1 = new CategoryNode("笔记本电脑");
		CategoryNode node_2 = new CategoryNode("台式整机");
		pCategoryNode.addSubNode(node_1);
		pCategoryNode.addSubNode(node_2);
		System.out.println("category num:" + pCategoryNode.countNumOfCategories());
	}
}

Demo2 构建整个公司的人员架构图(部门、子部门、员工的隶属关系),提供接口计算出部门的薪资成本(隶属于这个部门的所有员工的薪资和),部门包含子部门和员工,这是一种嵌套结构,可以表示成树这种数据结构。计算每个部门的薪资开支这样一个需求,也可以通过在树上的遍历算法来实现。从这个角度来看,该场景可以使用组合模式来设计和实现。

HumanResource 是部门类(Department)和员工类(Employee)抽象出来的父类,提供统一薪资的处理逻辑。

public abstract class HumanResource {
	protected long id;
	protected double salary;
	public HumanResource(long id) {
		this.id = id;
	} 
	public long getId() {
		return id;
	} 
	public abstract double calculateSalary();
} 
// 员工
public class Employee extends HumanResource {
	public Employee(long id, double salary) {
		super(id);
		this.salary = salary;
	} 
	@Override
	public double calculateSalary() {
		return salary;
	}
} 
// 部门
public class Department extends HumanResource {
	private List<HumanResource> subNodes = new ArrayList<>();
	public Department(long id) {
		super(id);
	} 
	@Override
	public double calculateSalary() {
		double totalSalary = 0;
		for (HumanResource hr : subNodes) {
			totalSalary += hr.calculateSalary();
			}
			this.salary = totalSalary;
			return totalSalary;
		}
	}
	public void addSubNode(HumanResource hr) {
		subNodes.add(hr);
	}
}
// 构建组织架构的代码
public class Demo {
	private static final long ORGANIZATION_ROOT_ID = 1001;
	// 依赖注入
	private DepartmentRepo departmentRepo; 
	// 依赖注入
	private EmployeeRepo employeeRepo; 
	public void buildOrganization() {
		Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
		buildOrganization(rootDepartment);
	} 
	private void buildOrganization(Department department) {
		List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department);
		for (Long subDepartmentId : subDepartmentIds) {
			Department subDepartment = new Department(subDepartmentId);
			department.addSubNode(subDepartment);
			buildOrganization(subDepartment);
		}
		List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
		for (Long employeeId : employeeIds) {
			double salary = employeeRepo.getEmployeeSalary(employeeId);
			department.addSubNode(new Employee(employeeId, salary));
		}
	}
}

Demo3 组合模式在 HashMap 的源码分析
职责:
①Map起 Component 的作用,提供通用的能力;
②Node 起 leaf 的作用,在组合中表示叶子节点;
②HashMap起 Composite 的作用,继承 Map接口,借助 Node 实现 Map 的功能

public interface Map<K,V> {
	V put(K key, V value);
	V remove(Object key);
	...
}

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
	 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
 	public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    // 调用Node节点的put实现功能
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                  boolean evict) {
       Node<K,V>[] tab; Node<K,V> p; int n, i;
       if ((tab = table) == null || (n = tab.length) == 0)
           n = (tab = resize()).length;
       if ((p = tab[i = (n - 1) & hash]) == null)
           tab[i] = newNode(hash, key, value, null);
       else {
           Node<K,V> e; K k;
           if (p.hash == hash &&
               ((k = p.key) == key || (key != null && key.equals(k))))
               e = p;
           else if (p instanceof TreeNode)
               e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
           else {
               for (int binCount = 0; ; ++binCount) {
                   if ((e = p.next) == null) {
                       p.next = newNode(hash, key, value, null);
                       if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                           treeifyBin(tab, hash);
                       break;
                   }
                   if (e.hash == hash &&
                       ((k = e.key) == key || (key != null && key.equals(k))))
                       break;
                   p = e;
               }
           }
           if (e != null) { // existing mapping for key
               V oldValue = e.value;
               if (!onlyIfAbsent || oldValue == null)
                   e.value = value;
               afterNodeAccess(e);
               return oldValue;
           }
       }
       ++modCount;
       if (++size > threshold)
           resize();
       afterNodeInsertion(evict);
       return null;
   }
   final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }
}


static class Node<K,V> implements Map.Entry<K,V> {
	 final int hash;
     final K key;
     V value;
     Node<K,V> next;
}

9.7、享元模式(被共享的单元)

定义:运用共享技术有效地支持大量细粒度的对象。

  • 当一个系统中存在大量重复对象的时候,我们就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。
    在这里插入图片描述
  • 享元角色:存储在享元对象内部且不会随环境而改变
  • 不可共享角色:随环境改变而改变、不可共享的状态。

使用场景:各类池技术:String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。

享元模式的缺点:,享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此不要过度使用这个模式。

Demo1:棋牌游戏,一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。
我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。

// 享元角色
public class ChessPieceUnit {
	private int id;
	private String text;
	private Color color;
	public ChessPieceUnit(int id, String text, Color color) {
		this.id = id;
		this.text = text;
		this.color = color;
	} 
	public static enum Color {
		RED, BLACK
	} 
	// ...省略其他属性和getter方法...
} 

// 享元工厂,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的
public class ChessPieceUnitFactory {
	private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();
	static {
		pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
		pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
		//...省略摆放其他棋子的代码...
	} 
	public static ChessPieceUnit getChessPiece(int chessPieceId) {
		return pieces.get(chessPieceId);
	}
}
// 不可共享的角色
public class ChessPiece {
	private ChessPieceUnit chessPieceUnit;
	private int positionX;
	private int positionY;
	public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
		this.chessPieceUnit = unit;
		this.positionX = positionX;
		this.positionY = positionY;
	}
	// 省略getter、setter方法
}


// Client
public class ChessBoard {
	private Map<Integer, ChessPiece> chessPieces = new HashMap<>();
	public ChessBoard() {
		init();
	} 
	private void init() {
		chessPieces.put(1, new ChessPiece(
		ChessPieceUnitFactory.getChessPiece(1), 0,0));
		chessPieces.put(1, new ChessPiece(
		ChessPieceUnitFactory.getChessPiece(2), 1,0));
		//...省略摆放其他棋子的代码...
	}
	public void move(int chessPieceId, int toPositionX, int toPositionY) {
		//...省略...
	}
}

Demo2 剖析一下享元模式在 Java Integer、String 中的应用。
通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象时,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。

  • JDK库中的Integer cache(-128~127)
Integer i1 = 56;   //自动装箱 Integer i = Integer.valueOf(59);
Integer i2 = 56;
Integer i3 = 129;
Integer i4 = 129;
System.out.println(i1 == i2);  // true
System.out.println(i3 == i4);  //false

public static Integer valueOf(int i) {
	if (i >= IntegerCache.low && i <= IntegerCache.high)
		return IntegerCache.cache[i + (-IntegerCache.low)];
	return new Integer(i);
}

private static class IntegerCache {
	static final int low = -128;
	static final int high;
	static final Integer cache[];
	static {
		// high value may be configured by property
		int h = 127;
		String integerCacheHighPropValue =
		sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high")
		if (integerCacheHighPropValue != null) {
			try {
				int i = parseInt(integerCacheHighPropValue);
				i = Math.max(i, 127);
				// Maximum array size is Integer.MAX_VALUE
				h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
			} catch( NumberFormatException nfe) {
				// If the property cannot be parsed into an int, ignore it.
			}
		}
		high = h;
		cache = new Integer[(high - low) + 1];
		int j = low;
		for(int k = 0; k < cache.length; k++)
			cache[k] = new Integer(j++);
		// range [-128, 127] must be interned (JLS7 5.1.7)
		assert IntegerCache.high >= 127;
	} 
	private IntegerCache() {}
}

可以用如下方式,将缓存的最大值从 127 调整到 255

//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255

使用建议:在日常开发中,对于下面这样三种创建整型对象的方式,我们优先使用后两种

Integer a = new Integer(123); // 并不会使用到 IntegerCache
Integer a = 123;
Integer a = Integer.valueOf(123);

享元模式在 Java String 中的应用
JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。对于字符串来说,没法事先知道要共享哪些字符串常量,只能在某个字符串常量第一次被用到时,存储到常量池中,当之后再用到时,直接引用常量池中已经存在的即可,就不需要再重新创建了。

String s1 = "设计模式";
String s2 = "设计模式";
String s3 = new String("设计模式");
System.out.println(s1 == s2);  //true
System.out.println(s1 == s3);  //false

10、行为型设计模式

创建型设计模式主要解决“对象的创建”问题,结构型设计模式主要解决“类或对象的组合”问题那行为型设计模式主要解决的就是“类或对象之间的交互”问题。行为型模式比较多,有 11 种,它们分别是:观察者模式,模板方法模式,策略模式,迭代器模式,责任链模式,状态模式,命令模式(不常用),备忘录模式(不常用),访问者模式(不常用),中介者模式(不常用),解释器模式(不常用)

10.1、观察者模式(也称发布订阅模式,za开发常用)

概念:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。
应用场景:非常广泛,小到代码层面的解耦,大到架构层面的系统解耦,再或者一些产品的设计思路,都有这种模式的影子,比如,邮件订阅、RSS Feeds,本质上都是观察者模式。

观察者模式的实现方式:
1、同步阻塞的实现方式:编码层次的解耦
2、观察者模式异步非阻塞实现方式:EventBus

  • 在商品中心代码中大量被应用

3、观察者模式跨进程的实现方式(不同的两系统):

  • 基于消息队列来实现

补充:EventBus原理(在商品中心大量使用)
EventBus 中两个核心函数 register() 和 post() 的实现原理。
在这里插入图片描述

在这里插入图片描述
最关键的一个数据结构是 Observer 注册表,记录了消息类型和可接收消息函数的对应关系。当调用 register() 函数注册观察者的时候,EventBus 通过解析@Subscribe 注解,生成 Observer 注册表。当调用 post() 函数发送消息的时候,EventBus 通过注册表找到相应的可接收消息的函数,然后通过 Java 的反射语法来动态地创建对象、执行函数。对于同步阻塞模式,EventBus 在一个线程内依次执行相应的函数。对于异步非阻塞模式,EventBus 通过一个线程池来执行相应的函数

demo1:



10.2、模板方法模式

模板方法模式的定义:在一个方法中定义一个算法(业务逻辑)骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。

模板模式有两大作用:复用和扩展
复用指的是,所有的子类可以复用父类中提供的模板方法的代码。
扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

回调:回调是一种双向调用关系
A 类事先注册某个函数 F 到 B 类,A类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。

结论:回调基于组合关系来实现,模板模式基于继承关系来实现。回调比模板模式更加灵活。


10.3、策略模式:就是将几个类中公共的方法提取到一个新的类中,从而使扩展更容易,保证代码的可移植性

策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。
因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。
实现方式:
a)提供公共接口或抽象类,定义需要使用的策略方法。(策略抽象类)
b)多个实现的策略抽象类的实现类。(策略实现类)
c)环境类,对多个实现类的封装,提供接口类型的成员量,可以在客户端中切换。
d)客户端 调用环境类 进行不同策略的切换。
使用场景是什么?

Demo1 使用 Spring 原生注解,快速实现策略模式 + 工厂模式?
目前做的需求中,将业务逻辑梳理后抽离出来,借助Spring IOC依赖注入特性,使用到了策略模式 + 工厂模式,向外提供统一的调用方式,有效减少了 if/else 的业务代码,使得代码更易维护、拓展。

业务场景:微服务A中协议状态的变更,会影响微服务B中商品状态的变更。我们的期望目标是,根据不同协议状态,我们能够快速找到对应的策略实现类去执行对商品的操作。
在这里插入图片描述
快速实现
Step1 现在我们先定义个ProtocolChangeHandler 接口,并实现策略类

public interface ProtocolChangeHandler {
    /**
     * @param items           商品
     * @param changeType      变更类型
     */
    void associateItemChange(List<Item> items, Byte changeType);
}

两个策略类

@Component
public class ItemShelfHandler implements ProtocolChangeHandler {
	 @Override
    public void associateItemChange(List<Item> agItems, Byte changeType) {
		// todo   业务逻辑
    }
}

@Component
public class ItemUnShelfHandler implements ProtocolChangeHandler {
	 @Override
    public void associateItemChange(List<Item> agItems, Byte changeType) {
		// todo   业务逻辑
    }
}

Step2 借助Spring 强大的依赖注入
下面的设计是消除if/else的关键代码,定义了一个StrategyHolder 来当做工厂类

@Component
public class StrategyHolder {
    // 关键功能 Spring 会自动将 EntStrategy 接口的类注入到这个Map中
    @Autowired
    private Map<String, ProtocolChangeHandler> strategyMap;

    public ProtocolChangeHandler getBy(String entNum) {
        return strategyMap.get(entNum);
    }
}

这个Map的key值就是你的 bean id,你可以用@Component(“value”)的方式设置,像我上面直接用默认的方式的话,就是首字母小写。value值则为对应的策略实现类。
在这里插入图片描述

先让用一个简单的启动类试一下。

public static void main(String[] args) {
	AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
    context.getBean(StrategyHolder.class).getBy("itemShelfHandler").associateItemChange("","");
}

Step3 别名转换
启动类里面的“itemShelfHandler”填的实际上是bean id的值。那在实际业务中肯定是不会这样的,怎么可能把一个策略编号定义的这么奇怪呢?
我们可以利用 SpringBoot 的特性,通过配置文件的方式来修改建立 name 与 bean id 间的映射关系。

@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "ent")
public class EntAlias {

    private HashMap<String, String> aliasMap;
    
    public static final String DEFAULT_STATEGY_NAME = "defaultStrategy";

    public HashMap<String, String> getAliasMap() {
        return aliasMap;
    }

    public void setAliasMap(HashMap<String, String > aliasMap) {
        this.aliasMap = aliasMap;
    }

    String of(String entNum) {
        return aliasMap.get(entNum);
    }
}

在对应配置文件application.yml中配置:

ent:
  aliasMap:
    entA: entAStrategy
    entB: entBStrategy

改写一下 ProtocolChangeHandler 类

@Component
public class ProtocolChangeHandler {
    
    @Autowired
    private EntAlias entAlias;

    // 关键功能 Spring 会自动将 EntStrategy 接口的类注入到这个Map中
    @Autowired
    private Map<String, ProtocolChangeHandler> strategyMap;

    // 找不到对应的策略类,使用默认的
    public ProtocolChangeHandler getBy(String entNum) {
        String name = entAlias.of(entNum);
        if (name == null) {
            return strategyMap.get(EntAlias.DEFAULT_STATEGY_NAME);
        }
        ProtocolChangeHandler strategy = strategyMap.get(name);
        if (strategy == null) {
            return strategyMap.get(EntAlias.DEFAULT_STATEGY_NAME);
        }
        return strategy;
    }
}

10.4、责任链模式

定义:使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系, 将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理他为止。

责任链模式的涉及初衷:是为了解耦代码,应对代码的复杂性,让代码满足开闭原则,提高代码的可扩展性

责任链模式的应用场景
最常用来开发框架的过滤器和拦截器。

案例1:对于支持 UGC(User Generated Content,用户生成内容)的应用(比如论坛)来说,用户生成的内容(比如,在论坛中发表的帖子)可能会包含一些敏感词(比如涉黄、广告、反动等词汇)。针对这个应用场景,我们就可以利用职责链模式来过滤这些敏感词。对于包含敏感词的场景,我们直接禁止发布。

public interface SensitiveWordFilter {
	boolean doFilter(Content content);
}
public class SexyWordFilter implements SensitiveWordFilter {
	@Override
	public boolean doFilter(Content content) {
		boolean legal = true;
		//...
		return legal;
	}
}

// PoliticalWordFilter、AdsWordFilter类代码结构与SexyWordFilter类似
public class SensitiveWordFilterChain {
	private List<SensitiveWordFilter> filters = new ArrayList<>();
	public void addFilter(SensitiveWordFilter filter) {
		this.filters.add(filter);
	}
	//return true if content doesn't contain sensitive words.
	public boolean filter(Content content) {
		for(SensitiveWordFilter filter : filters) {
			if(!filter.doFilter(content)){
				return false;
			}
		}
		return true;
	}
}
public class ApplicationDemo {
	public static void main(String[] args) {
		SensitiveWordFilterChain filterChain = new SensitiveWordFilterChain();
		filterChain.addFilter(new AdsWordFilter());
		filterChain.addFilter(new SexyWordFilter());
		filterChain.addFilter(new PoliticalWordFilter());
		boolean legal = filterChain.filter(new Content());
		if(!legal) {
			//不发表
		} else {
			// 发表
		}
	}
}

案例2:责任链模式在商品中心的应用

  • 在Spring初始化时,被注册了10来个规则,放在规则引擎集合里面

在发商品和更新商品/spu时调用

// 设计的不友好,破坏了规则引擎,既要保证性能又要满足审核时去掉校验规则,所以只能硬编码处理
// 只有申请变更才会走handleInboundAuditData逻辑,审核单独走自己的校验规则
// 校验数据,一旦数据不合法, 会抛出异常
//* 重要: 可以根据需求而定是否抛出异常,
//* 例如可能的需求有:
//* 1.忽略或者过滤用户提交的无效数据
//* 2. 可以处理用户提交的数据, 比如修正或者添加更多的信息
if(Objects.nonNull(auditState) && Objects.equals(GoodsAuditStateEnum.IN_UPDATE.getCode(),auditState)){
    ruleEngine.handleInboundAuditData(newFullItem, null);
}else {
    ruleEngine.handleInboundData(newFullItem, null);
}

在用户查询商品/spu, 或者进入编辑商品/spu的界面

//* 规则引擎在处理数据输出时会调用这个方法 (例如用户查询商品/spu, 或者进入编辑商品/spu的界面)
//* 处理数据, 如果数据不合法, 不会抛出异常, 而是根据规则做相应的修正
//* 目前的策略是, 根据规则本身来修正或者过滤, 或者添加信息,  也可以根据需要抛出异常
 public <T extends BaseInput> void handleOutboundData(T input, BaseOutput output) {
    for (GeneralRuleExecutor ruleExecutor : ruleExecutorPipeline.getRuleExecutors()) {
        ruleExecutor.handleOutboundData(input, output, propertyBusinessTagCode);
    }
}

责任链模式在Spring中的使用
1、servlet中的filter:定义一个Chain链,里面包含了Filter列表和servlet,达到在调用真正servlet之前进行各种filter逻辑
在这里插入图片描述
提供规范,其实现依赖于web容器,例如tomcat,ApplicationFilterChain 类就是 Tomcat 提供的 FilterChain 的实现类。

2、dubbo中的filter :把Filter封装成 Invoker的匿名类,通过链表这样的数据结构来完成责任链,调用的时候我们只知道第一个节点,每个节点包含了下一个调用的节点信息
3、亮点:mybatis中的plugin(插件):与过滤器类似,例如,我们在项目里面用到了mybatis的分页插件pagehelper,相当于执行Sql语句的时候做一些操作。
4、netty中的channelhandler和pipeline:共同构成了责任链模式
6、Spring中的Interceptor
在这里插入图片描述

可以讨论的问题? Spring AOP 是基于代理模式来实现的。在实际的项目开发中,我们可以利用 AOP 来实现访问控制功能,比如鉴权、限流、日志等。而Servlet Filter、Spring Interceptor 也可以用来实现访问控制。那在项目开发中,类似权限这样的访问控制功能,我们该选择三者(AOP、Servlet Filter、Spring Interceptor)中的哪个来实现呢?有什么参考标准吗?

Filter 可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息; Interceptor 可以拿到你请求的控制器和方法,却拿不到请求方法的参数; Aop 可以拿到方法的参数,但是却拿不到http请求和响应的对象。
要区分三者的特点,Spring AOP的使用粒度是类,是对类的一个包装;servlet filter 和 spring interceptor主要是对httpRequest、httpResponse做处理,servlet filterChain的实现依赖于具体的Web容器,而spring interceptor和spring AOP都依赖于spring框架,servlet filter在一个函数里拦截请求和响应,而spring interceptor将请求、响应的拦截分成了两个函数;其次,针对特定的应用场景,选择适合的。

demo?


10.5、状态模式

什么是状态模式?

  • 在软件开发过程中,应用程序可能会根据不同的情况作出不同的处理。最直接的解决方案是将这些所有可能发生的情况全都考虑到。然后使用if… ellse语句来做状态判断来进行不同情况的处理。但是对复杂状态的判断就显得“力不从心了”。随着增加新的状态或者修改一个状体(if else(或switch case)语句的增多或者修改)可能会引起很大的修改,而程序的可读性,扩展性也会变得很弱。维护也会很麻烦。这时就要考虑只修改自身状态的模式。

类图
在这里插入图片描述
1)环境角色(Context)
客户程序需要的接口,并且维护一个具体状态的实例,这个实例决定当前状态。
2)状态角色(State)
定义一个接口以封装与使用环境角色的一个特定状态的相关行为。
定义一个接口用以封装对象的一个特定状态所对应的行为。
3)具体状态角色(ConcreteState)
实现状态角色定义的接口,结构十分简单与策略模式相似。
一个具体的状态的实现类实现了Context对象的一个状态所对应的行为

使用场景:
《重构:改善既有代码的设计》中第一个例子
使用场景1:

  • 在日志系统中可以用起来(对于各条业务线:都有上架、下架、冻结、开启、关闭、解冻、删除等状态)。

使用场景2:

  • 我们在购物网站进行购物时,订单会产生几种状况:已下单、已付款、送货中、确定收货等状态。所以系统会判断该订单的状态,不管是哪种状态都应给出对应的操作,这就是状态

使用场景3:

  • 商品状态(上架、下架、冻结、解冻)

状态模式优缺点
优点:

  1. 每个状态都是一个子类,只要增加状态就要增加子类,修改状态,只修改一个子类即可。
  2. 结构清晰,避免了过多的switch…case或者if…else语句的使用,避免了程序的复杂性,提高可维护性。
  3. State对象可被共享 如果State对象没有实例变量—即它们表示的状态完全以它们的类型来编码—那么各Context对象可以共享一个State对象。当状态以这种方式被共享时, 它们必然是没有内部状态, 只有行为的轻量级对象。

缺点:
1) 状态模式的使用必然会增加系统类和对象的个数。
2) 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。

状态模式面试题
金融借贷平台项目:借贷平台的订单,有审核-发布-抢单 等等 步骤,随着操作的不同,会改变订单的状态, 项目中的这个模块实现就会使用到状态模式,请你使用状态模式进行设计,并完成实际代码

状态模式和策略模式比较
在状态模式中,状态的变迁是由对象的内部条件决定,外界只需关心其接口,不必关心其状态对象的创建和转换。
而策略模式里,采取何种策略由外部条件(C)决定。Strategy模式与State模式的结构形式完全一样。但它们的应用场景(目的)却不一样,State模式重在强调对象的内部状态的变化改变对象的行为,Strategy模式重在外部对策略的选择,策略的选择由外部条件决定,也就是说算法动态的切换。但由它们的结构是如此的相似。我们可以认为状态模式是完全封装且自修改的策略模式。
所以说策略和状态模式是孪生兄弟。

状态模式的适用性

  1. 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
  2. 代码中包含大量与对象状态有关的条件语句:一个操作中含有庞大的多分支的条件(if else(或switch case)语句,且这些分支依赖于该对象的状态。这个状态通常用一个或多个枚举常量表示。通常 , 有多个操作包含这一相同的条件结构。 State模式将每一个条件分支放入一个独立的类中。这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化。

JDK中用到的状态模式
java.util.Iterator
迭代器模式的角色组成

  1. 迭代器角色(Iterator):迭代器角色负责定义访问和遍历元素的接口。
  2. 具体迭代器角色(Concrete Iterator):具体迭代器角色要实现迭代器接口,并要记录遍历中的当前位置。
  3. 容器角色(Container):容器角色负责提供创建具体迭代器角色的接口。
  4. 具体容器角色(Concrete Container):具体容器角色实现创建具体迭代器角色的接口—这个具体迭代器角色与该容器的结构相关。

代码示例(错误):

public class Order {
    public static  void  main(String [] args) {
        // 关于订单状态的事情,大家有没有这样判断过
        String stateName = getStateName(1);
        System.out.println(stateName);
    }
 
    /**
     * @method  getStateName
     * @description 获取订单状态的方法
     *              订单状态1、已下单2、已付款3、已发货4、送货中5、确认收货
     * @date: 2018/12/19 22:04
     * @author: Ni Shichao
     * @param status 状态
     * @return
     */
    public  static  String getStateName (int status) {
        String stateName = "" ;
        if (status == 1) {
            stateName = "已下单";
        } else if (status == 2) {
            stateName = "已付款";
        } else if (status == 3) {
            stateName = "已发货";
        } else if (status == 4) {
            stateName = "送货中";
        } else if (status == 5) {
            stateName = "确认收货";
        }
        return stateName;
    }
}

代码示例(状态模式的使用):

// 1、定义state接口
public interface State {
    void handle();
}

// 2、定义一个环境类来维护State接口 
public class Context {
 
    private State state;
    // 无参构造
    public Context() {}
	// 有参构造
    public Context(State state) {
        this.state = state;
    }
    public void setState(State state) {
        System.out.println("订单信息已更新!");
        this.state = state;
        this.state.handle();
    }
}

// 3、具体状态角色  ConcreteState
public class Booked implements  State {
    @Override
    public void handle() {
        System.out.println("您已下单!");
    }
}

//4、测试类
public class Client {
    public static  void  main(String [] args) {
        Context context = new Context();
        context.setState(new Booked());
        context.setState(new Payed());
        context.setState(new Sended());
        context.setState(new InWay());
        context.setState(new Recieved());
    }
} 

测试结果

订单信息已更新!
您已下单!
订单信息已更新!
您已付款!
订单信息已更新!
已发货!
订单信息已更新!
送货中。。。
订单信息已更新!
已确认收货!

状态模式在商品审核中的应用

// 状态机的定义
public class ItemFSM {
    // 事件
    private enum Trigger {
        APPLY_ONSHELF,  // 申请上架
        APPLY_UNFREEZE,  // 申请解冻
        UNFREEZE,         // 解冻
        CANCEL_AUDIT,    // 撤销审核
        FREEZE    // 撤销审核
    }
    private final StateMachine<ItemStatus, Trigger> stateMachine;
    public ItemFSM(ItemStatus status) throws Exception {
        this.stateMachine = new StateMachine<>(status);
        /** * 【冻结】---(申请解冻)---> 【解冻审核中】*/
        stateMachine.Configure(ItemStatus.FROZEN)
                .Permit(Trigger.APPLY_UNFREEZE, ItemStatus.WAIT_UNFREEZE);
        /*** 【解冻审核中】---(解冻)---> 【下架】*/
        stateMachine.Configure(ItemStatus.WAIT_UNFREEZE)
                .Permit(Trigger.UNFREEZE, ItemStatus.UNDERSHELF);
        /*** 【待审核】---(取消审核)---> 【下架】*/
        stateMachine.Configure(ItemStatus.AUDITWAIT)
                .Permit(Trigger.CANCEL_AUDIT, ItemStatus.UNDERSHELF);
        /*** 【上架】---(冻结)---> 【冻结中】*/
        stateMachine.Configure(ItemStatus.ONSHELF)
                .Permit(Trigger.FREEZE, ItemStatus.FROZEN);
        /*** 【下架】---(申请上架)---> 【审核中】*/
        stateMachine.Configure(ItemStatus.UNDERSHELF)
                .Permit(Trigger.APPLY_ONSHELF, ItemStatus.AUDITWAIT);
        /** * 【审核失败】---(申请上架)---> 【审核中】*/
        stateMachine.Configure(ItemStatus.AUDITREJECT)
                .Permit(Trigger.APPLY_ONSHELF, ItemStatus.AUDITWAIT);
    }

    /*** 获取状态机当前状态*/
    public ItemStatus getCurrentState() {
        return stateMachine.getState();
    }
    /** * 申请上架*/
    public void applyOnshelf() throws StateMachineException {
        try {
            stateMachine.Fire(Trigger.APPLY_ONSHELF);
        } catch (Exception e) {
            log.warn("item audit failed", e);
            throw new StateMachineException("item audit statemachine trigger apply unfreeze.");
        }
    }
    ...
}
// 状态机的使用
ItemStatus originalStatus = ItemStatus.from(blockItem.getStatus());
ItemFSM fsm = new ItemFSM(originalStatus);
fsm.applyOnshelf();
ItemStatus newStatus = fsm.getCurrentState();
...

总结
状态模式的主要优点在于封装了转换规则,并枚举可能的状态,它将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为,还可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数;其缺点在于使用状态模式会增加系统类和对象的个数,且状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱,对于可以切换状态的状态模式不满足“开闭原则”的要求


10.6、迭代器模式

问题
1、在 Java 中,如果在使用迭代器的同时删除容器中的元素,会导致迭代器报错,这是为什
么呢?如何来解决这个问题呢?

  • 使用 foreach 或者 iterator 进行迭代删除 remove 时,容易导致 next() 检测的 modCount 不等于 expectedModCount 从而引发 ConcurrentModificationException。
  • 在通过迭代器来遍历集合元素的同时,增加或者删除集合中的元素,有可能会导致某个元素被重复遍历或遍历不到。不过,并不是所有情况下都会遍历出错,有的时候也可以正常遍历,所以,这种行为称为结果不可预期行为或者未决行为。

2、除了编程语言中基础类库提供的针对集合对象的迭代器之外,实际上,迭代器还有其他的应用场景,比如MySQL ResultSet 类提供的 first()、last()、previous() 等方法,也可以看作一种迭代器,你能分析一下它的代码实现吗?

  • ResultSet 内部通过维护一个类型为 ResultSetRows 的 rowData 变量来实现迭代,而 rowData 的迭代方法本质就是实现了标准库的 Iterator 接口。

demo1:



10.7、发布-订阅(Publish/Subscribe)模式-访问者模式(不常用)

dubbo,服务中间件 zookeeper
实现方式:
a)角色抽象类(提供对观察者的添加,删除和通知功能)。
b)角色具体类,实现a,维护一个c的集合(对角色抽象类的实现)。
c)观察者抽象类(被角色通知后实现的方法)。
d)观察者实现类,实现c(多个)。


10.9、备忘录模式(不常用)


10.8、命令模式


10.9、解释器模式(不常用)

面试题:
1、介绍解释器设计模式是什么?
2、画出解释器设计模式的UML类图,分析设计模式中的各个角色是什么?
在这里插入图片描述
3、请说明Spring的框架中,哪里使用到了解释器设计模式,并做源码级别的分析。
Spring框架中 SpelExpressionParser就使用到解释器模式。

10.10 中介模式(不常用)

定义:中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。

EventBus 框架解析


行为型设计模式总结


11、在实际项目开发中,如何避免过度设计?


12、各框架源码中,使用了哪些设计模式

12.1 JDK源码里面都有些什么让你印象深刻的设计模式使用,举例看看?

1、抽象工厂模式 (通过创造性的方法来识别工厂本身,这又可以用于创建另一个抽象/接口类型)

  • javax.xml.parsers.DocumentBuilderFactory#newInstance()
  • javax.xml.transform.TransformerFactory#newInstance()
  • javax.xml.xpath.XPathFactory#newInstance()

2、建造者模式 (通过创建方法识别返回实例本身)

  • java.lang.StringBuilder#append() (非线程安全)
  • java.lang.StringBuffer#append() (线程安全)

3、工厂模式 (可通过创建方法识别返回抽象/接口类型的实现)

  • java.text.NumberFormat#getInstance()
  • java.nio.charset.Charset#forName()

4、单例模式(通过创造性方法识别,每次返回相同的实例(通常是自己))

  • java.lang.Runtime#getRuntime()
  • java.awt.Desktop#getDesktop()

12.2 Spring源码里面都有些什么让你印象深刻的设计模式使用,举例看看?

使用了这些设计模式,具体请看这篇文章
Spring从入门到精通

12.3 Guava源码中使用了哪些设计模式

请看这篇文章

二十多岁刚进入职场时,总要工作到头昏眼花,才有做了工作的感觉,着实白费了相当多的时间。虽说年轻时体力充沛,像那样的工作方式,也算有助于心情愉快。然而说穿了,其实那也不过是自我陶醉而已,所谓的收获可能也只是了解到自己体力的极限,只有在确实产生出有意义的成果(输出)之后才能获得成长若能持续有价值的工作,并保持其质量,就算“偷工减料”也完全不成问题如果是问人就可以解决的事,那么问人就好;如果有更简单的完成工作的方法,就该换个方式处理。 --《麦肯锡教我的思考武器:从逻辑思考到真正解决问题》

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员 jet_qi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值