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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
全局流量管理 GTM,标准版 1个月
简介: JAVA设计模式--从入门到精通

推荐书籍:

书籍 语言 难易程度
《大话设计模式》 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 原型区别在哪?面试题

区别 Spring GOF
对象类型 根据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,防止指令重排,确保没有问题。
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
25天前
|
存储 安全 Java
从入门到精通:Java Map全攻略,一篇文章就够了!
【10月更文挑战第17天】本文详细介绍了Java编程中Map的使用,涵盖Map的基本概念、创建、访问与修改、遍历方法、常用实现类(如HashMap、TreeMap、LinkedHashMap)及其特点,以及Map在多线程环境下的并发处理和性能优化技巧,适合初学者和进阶者学习。
39 3
|
1月前
|
设计模式 Java
【设计模式】工厂模式(定义 | 特点 | Demo入门讲解)
【设计模式】工厂模式(定义 | 特点 | Demo入门讲解)
51 2
|
1月前
|
设计模式 XML Java
【设计模式】装饰器模式(定义 | 特点 | Demo入门讲解)
【设计模式】装饰器模式(定义 | 特点 | Demo入门讲解)
29 0
|
1月前
|
设计模式 传感器
【设计模式】观察者模式(定义 | 特点 | Demo入门讲解)
【设计模式】观察者模式(定义 | 特点 | Demo入门讲解)
39 0
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
10天前
|
Java 大数据 API
14天Java基础学习——第1天:Java入门和环境搭建
本文介绍了Java的基础知识,包括Java的简介、历史和应用领域。详细讲解了如何安装JDK并配置环境变量,以及如何使用IntelliJ IDEA创建和运行Java项目。通过示例代码“HelloWorld.java”,展示了从编写到运行的全过程。适合初学者快速入门Java编程。
|
16天前
|
存储 安全 Java
🌟Java零基础-反序列化:从入门到精通
【10月更文挑战第21天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
54 5
|
14天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
29 1
|
20天前
|
Java 数据处理 开发者
Java多线程编程的艺术:从入门到精通####
【10月更文挑战第21天】 本文将深入探讨Java多线程编程的核心概念,通过生动实例和实用技巧,引导读者从基础认知迈向高效并发编程的殿堂。我们将一起揭开线程管理的神秘面纱,掌握同步机制的精髓,并学习如何在实际项目中灵活运用这些知识,以提升应用性能与响应速度。 ####
43 3
|
22天前
|
Java
Java中的多线程编程:从入门到精通
本文将带你深入了解Java中的多线程编程。我们将从基础概念开始,逐步深入探讨线程的创建、启动、同步和通信等关键知识点。通过阅读本文,你将能够掌握Java多线程编程的基本技能,为进一步学习和应用打下坚实的基础。

热门文章

最新文章

  • 1
    C++一分钟之-设计模式:工厂模式与抽象工厂
    42
  • 2
    《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
    46
  • 3
    C++一分钟之-C++中的设计模式:单例模式
    54
  • 4
    《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
    38
  • 5
    《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
    62
  • 6
    Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
    57
  • 7
    Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
    41
  • 8
    Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
    50
  • 9
    Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
    106
  • 10
    Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
    78