设计模式之美学习笔记

一、好代码的标准

1,可维护性:在不破坏原有代码设计,不引入新的bug情况下,能够快速的修改或者添加代码。

2,可读性:代码是否符合编码规范,命名是否答意,注释是否详尽,函数是否长短合适,模块划分是否清晰,是否符合高内聚低耦合等等

3,可扩展性:在不修改或者少量修改原有代码的情况下,通过扩展点的方式添加新的功能

4,可复用性:尽量减少重复代码的编写,复用已有的代码

面向对象中的继承,多态能让我们写出可复用的代码;变成规范能让我们写出可复用的代码;设计模式中的单一职责,DIY,基于接口而非实现,里氏替换原则等可以让我们写出可复用,灵活,可读性好,易扩展,易维护的代码;设计模式可以让我们写出易扩展的代码;持续重构可以时刻保持代码的可维护性等等。

二、设计原则与思想:面向对象

什么是面向对象

它以类和对象作为组织代码的基本单元,并将封装、继承、抽象、多态作为代码设计和实现的基石。

①封装:信息隐藏或者数据访问保护

②继承:主要解决代码复用问题

③抽象:一方面提高代码的可扩展性、维护性;另一方面也是处理复杂系统的有效手段,能有效地过滤掉不必要的关注信息

④多态:多态可以提高代码的扩展性和复用性,是很多设计模式、设计原则、编程技巧的代码实现基础;

面向对象与面向过程

什么是面向过程编程与面向对象编程

面向过程代码





struct User {
  char name[64];
  int age;
  char gender[16];
};





struct User parse_to_user(char* text) {
  // 将text(“小王&28&男”)解析成结构体struct User
}










char* format_to_text(struct User user) {
  // 将结构体struct User格式化成文本("小王\t28\t男")
}





void sort_users_by_age(struct User users[]) {
  // 按照年龄从小到大排序users
}









void format_user_file(char* origin_file_path, char* new_file_path) {
  // open files...
  struct User users[1024]; // 假设最大1024个用户
  int count = 0;
  while(1) { // read until the file is empty
    struct User user = parse_to_user(line);
    users[count++] = user;
  }

  

  sort_users_by_age(users);
  
  for (int i = 0; i < count; ++i) {
    char* formatted_user_text = format_to_text(users[i]);
    // write to new file...
  }
  // close files...
}








int main(char** args, int argv) {
  format_user_file("/home/zheng/user.txt", "/home/zheng/formatted_users.txt");
}







面向对象代码

public class User {
  private String name;
  private int age;
  private String gender;
  


  public User(String name, int age, String gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }
  
  public static User praseFrom(String userInfoText) {
    // 将text(“小王&28&男”)解析成类User
  }



  
  public String formatToText() {
    // 将类User格式化成文本("小王\t28\t男")
  }


}







public class UserFileFormatter {
  public void format(String userFile, String formattedUserFile) {
    // Open files...
    List users = new ArrayList<>();
    while (1) { // read until file is empty 
      // read from file into userText...
      User user = User.parseFrom(userText);
      users.add(user);
    }

    // sort users by age...
    for (int i = 0; i < users.size(); ++i) {
      String formattedUserText = user.formatToText();
      // write to new file...
    }



    // close files...
  }

}





public class MainApplication {
  public static void main(String[] args) {
    UserFileFormatter userFileFormatter = new UserFileFormatter();
    userFileFormatter.format("/home/zheng/users.txt", "/home/zheng/formatted_users.txt");
  }


}





面向过程风格的代码被组织成了一组方法集合及其数据结构(struct User),方法和数据结构是分开的 。

面向对象风格的代码被组织成一组类 ,方法和数据结构被绑定在一起,定义在类中。

②面向对象编程比面向过程编程有哪些优势

OOP更加能够应复杂业务应用的开发

OOP风格的代码更加易复用、易扩展、易维护

OOP语言更加人性化、更加高级、更加智能

③三种违反面向对象编程风格的典型代码设计

滥用getter、setter方法:违反了面向对象封装特效

Constants、Utils类的设计问题 :对于这两个类的设计,我们尽量能做到职责单一,比如:RedisConstants、FileUtils,而不是这几一个大而全的类

基于贫血模型的开发模式:数据和操作分开定义在VO/BO/Entity和Controller/Service/Repository中。

接口与抽象类

①定义

抽象类:不能被实例化、只能被继承。可以包含属性和方法,方法可以包含实现,也可以不包含实现(抽象方法),子类继承抽象类必须实现所有抽象方法

接口:不能被实例化、只能声明方法,方法不能包含实现

②设计思想

抽象类:是一种自下而上的设计思路,现有子类的代码重复,而后抽象成上层的父类(也就是抽象类)

接口:是一种自上而下的设计思路,定义协议或者契约

③使用场景

抽象类:是一种is-a关系,为了解决代码复用问题。

接口:是一种has-a关系,为了解决解耦问题、隔离接口和具体实现,提高代码的扩展性。

基于接口而非实现编程

①做开发的时候需要有抽象意识、封装意识、接口意识。设计越抽象,越能提高代码的灵活性、扩展性、可维护性

②设计指导

函数命名不能暴露任何实现细节。比如,前面提到的uploadToAliyun()就不符合要求,应该去掉aliyun这样的字眼,改为更加抽象的命名方式upload()

封装具体的实现细节。比如跟阿里云特殊的上传流程不应该暴露给调用者。

public interface ImageStore {
  String upload(Image image, String bucketName);
  Image download(String url);
}








public class AliyunImageStore implements ImageStore {
  //...省略属性、构造函数等...






  public String upload(Image image, String bucketName) {
    createBucketIfNotExisting(bucketName);
    String accessToken = generateAccessToken();
    //...上传图片到阿里云...
    //...返回图片在阿里云上的地址(url)...
  }







  public Image download(String url) {
    String accessToken = generateAccessToken();
    //...从阿里云下载图片...
  }





  private void createBucketIfNotExisting(String bucketName) {
    // ...创建bucket...
    // ...失败会抛出异常..
  }





  private String generateAccessToken() {
    // ...根据accesskey/secrectkey等生成access token
  }

}





// 上传下载流程改变:私有云不需要支持access token
public class PrivateImageStore implements ImageStore  {
  public String upload(Image image, String bucketName) {
    createBucketIfNotExisting(bucketName);
    //...上传图片到私有云...
    //...返回图片的url...
  }






  public Image download(String url) {
    //...从私有云下载图片...
  }







  private void createBucketIfNotExisting(String bucketName) {
    // ...创建bucket...
    // ...失败会抛出异常..
  }


}






// ImageStore的使用举例
public class ImageProcessingJob {
  private static final String BUCKET_NAME = "ai_images_bucket";
  //...省略其他无关代码...
  
  public void process() {
    Image image = ...;//处理图片,并封装为Image对象
    ImageStore imageStore = new PrivateImageStore(...);
    imagestore.upload(image, BUCKET_NAME);
  }
}


为什么推荐多用组合少用继承

继承是is-a的关系,可以解决代码复用问题,但是继承层次过深、过复杂,影响代码的可维护性

DDD充血模型与贫血模型

基于充血模型的DDD设计模式跟基于贫血模型的传统开发模式相比,主要区别在Service层。充血模式下让部分原来在Service类中的业务逻辑移动到了一个充血的Domain领域模型中,让Service类的实现依赖这个Domain类

在基于充血模型的DDD开发模式下,Service类并不会移除,而是负责一些不适合放在Domain类中的功能。比如,负责与Repository层打交道、跨领域模型业务聚合功能,幂等事务等工作

基于充血模型的DDD设计模式跟基于贫血模型的传统开发模式相比,Controller层和repsitory层的代码基本上相同。因为,Repository层的Entity生命周期有限。Contrller层的VO只是单纯作为一个DTO,两部分的业务都不会太复杂。业务逻辑主要集中在Service层。所以,Repository层和controller层继续使用贫血模型的设计思路是没有问题的.

①基于贫血的开发模式





public class VirtualWalletController {
  // 通过构造函数或者IOC框架注入
  private VirtualWalletService virtualWalletService;
  


  public BigDecimal getBalance(Long walletId) { ... } //查询余额
  public void debit(Long walletId, BigDecimal amount) { ... } //出账
  public void credit(Long walletId, BigDecimal amount) { ... } //入账
  public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { ...} //转账
}










public class VirtualWalletBo {//省略getter/setter/constructor方法
  private Long id;
  private Long createTime;
  private BigDecimal balance;
}










public class VirtualWalletService {
  // 通过构造函数或者IOC框架注入
  private VirtualWalletRepository walletRepo;
  private VirtualWalletTransactionRepository transactionRepo;
  
  public VirtualWalletBo getVirtualWallet(Long walletId) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    VirtualWalletBo walletBo = convert(walletEntity);
    return walletBo;
  }

  

  public BigDecimal getBalance(Long walletId) {
    return walletRepo.getBalance(walletId);
  }

  
  public void debit(Long walletId, BigDecimal amount) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    BigDecimal balance = walletEntity.getBalance();
    if (balance.compareTo(amount) < 0) {
      throw new NoSufficientBalanceException(...);
    }

    walletRepo.updateBalance(walletId, balance.subtract(amount));
  }
  
  public void credit(Long walletId, BigDecimal amount) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    BigDecimal balance = walletEntity.getBalance();
    walletRepo.updateBalance(walletId, balance.add(amount));
  }


  
  public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
    VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity();
    transactionEntity.setAmount(amount);
    transactionEntity.setCreateTime(System.currentTimeMillis());
    transactionEntity.setFromWalletId(fromWalletId);
    transactionEntity.setToWalletId(toWalletId);
    transactionEntity.setStatus(Status.TO_BE_EXECUTED);
    Long transactionId = transactionRepo.saveTransaction(transactionEntity);
    try {
      debit(fromWalletId, amount);
      credit(toWalletId, amount);
    } catch (InsufficientBalanceException e) {
      transactionRepo.updateStatus(transactionId, Status.CLOSED);
      ...rethrow exception e...
    } catch (Exception e) {
      transactionRepo.updateStatus(transactionId, Status.FAILED);
      ...rethrow exception e...
    }
    transactionRepo.updateStatus(transactionId, Status.EXECUTED);
  }

}

②基于充血模型的DDD开发模式

public class VirtualWallet { // Domain领域模型(充血模型)
  private Long id;
  private Long createTime = System.currentTimeMillis();;
  private BigDecimal balance = BigDecimal.ZERO;
  


  public VirtualWallet(Long preAllocatedId) {
    this.id = preAllocatedId;
  }


  

  public BigDecimal balance() {
    return this.balance;
  }

  

  public void debit(BigDecimal amount) {
    if (this.balance.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);
  }

}





public class VirtualWalletService {
  // 通过构造函数或者IOC框架注入
  private VirtualWalletRepository walletRepo;
  private VirtualWalletTransactionRepository transactionRepo;
  

  public VirtualWallet getVirtualWallet(Long walletId) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    VirtualWallet wallet = convert(walletEntity);
    return wallet;
  }


  
  public BigDecimal getBalance(Long walletId) {
    return walletRepo.getBalance(walletId);
  }
  
  public void debit(Long walletId, BigDecimal amount) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    VirtualWallet wallet = convert(walletEntity);
    wallet.debit(amount);
    walletRepo.updateBalance(walletId, wallet.balance());
  }


  
  public void credit(Long walletId, BigDecimal amount) {
    VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId);
    VirtualWallet wallet = convert(walletEntity);
    wallet.credit(amount);
    walletRepo.updateBalance(walletId, wallet.balance());
  }


  
  public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) {
    //...跟基于贫血模型的传统开发模式的代码一样...
  }
}

设计原则

单一职责原则(SRP Single Responsibility Principle)

①理解SRP

一个类只负责完成一个职责或功能。不要设计大而全的类,要设计粒度小、功能单一的类。

单一职责原则是为了实现代码的高内聚、低耦合,提高代码的复用性、可读性、可维护性。

②指导设计

类中的代码行数、函数或属性过多(代码行数不易超过200行,函数和属性不易超过10个),会影响代码的可读性与可维护性,我们需要考虑对类进行拆分。

类依赖的其他类过多,不符合高内聚、低耦合的设计思想,我们需要考虑对类进行拆分。

私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为public方法,供更多的类使用,从而提高代码的复用性

比较难给类起一个名字,很难用一个业务名词概括,或者只能用一个笼统的Manager、Context之类的词语来命名,这说明类的职责定义可能不够清晰

类中的大量方法都是集中操作类中的某几个属性,比如UserInfo例子中,如果一半的方法都是在操作address信息,那就可以考虑将这几个属性和对应的方法拆出来。

public class UserInfo {
  private long userId;
  private String username;
  private String email;
  private String telephone;
  private long createTime;
  private long lastLoginTime;
  private String avatarUrl;
  private String provinceOfAddress; // 省
  private String cityOfAddress; // 市
  private String regionOfAddress; // 区 
  private String detailedAddress; // 详细地址
  // ...省略其他属性和方法...
}

开闭原则(OCP Open Closed Principle)

①理解OCP

对扩展开放、对修改关闭,提高代码的扩展性

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

开闭原则并不是完全杜绝修改,而是以最小的修改代码的代价来完成新的功能开发。

同样的代码改动,在粗代码粒度下,可能被认为是“修改”;在细代码粒度下,可能被任务是“扩展”

②如何做到“对扩展开放,对修改关闭”

时刻具备扩展、抽象、封装意识。我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上

很多设计原则、设计思想、设计模式,都是以提高代码的扩展性为最终目的的

个人理解一句话总结:尽量小的改动,完成新的功能模块的开发。高扩展性.

里氏替换原则(LSP Liskov Substitution Principle)

里氏替换原则是用来指导,继承关系中子类该如何设计

核心理解就是按照协议来设计子类

父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。

①与多态的区别

多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路

里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。

个人理解一句话总结:指导设计继承类,约定实现规则.

接口隔离原则(ISP Interface Segregation Principle)

①理解ISP

把“接口”理解为一组接口集合(微服务或者是类库的接口):如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口

把“接口”理解为单个 API 接口或函数:部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数

把“接口”理解为 OOP 中的接口:也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数

个人理解一句话总结:隔离出不相干的接口,保留客户端关心的接口.

依赖反转原则(DIP Dependency Inversion Principle)

高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象

这条原则主要还是用来指导框架层面的设计

个人理解一句话总结:消除类与类直接的依赖。DIP原则用于指导抽象出接口或者抽象类.

LOD原则(迪米法特原则)

高类聚:相近功能放在一个类中

松耦合:类之间的依赖清晰,一个类的改动不会影响另一个类

迪米法特原则:不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口

个人理解一句话总结:减少类之间的依赖关系,侧重低耦合

规范与重构

重构的目的:为什么重构(Why)

对于项目来言,重构可以保持代码质量持续处于一个可控状态,不至于腐化到无可救药的地步。对于个人而言,重构非常锻炼一个人的代码能力,并且是一件非常有成就感的事情。它是我们学习的经典设计思想、原则、模式、编程规范等理论知识的练兵场

重构的对象:重构什么(What)

大规模高层次重构:代码分层、模块化、解耦、梳理类与类之间的交互、抽象复用组件等等。这部分工作利用的更多的是比较抽象、比较顶层的设计思想、原则、模式

小规模低层次重构:规范命名、注释、修正函数参数过多、消除超大类、提取重复代码等等编程细节问题,主要是针对类、函数级别的重构。更多的是利用编码规范这一理论知识

重构的时机:什么时候重构(When)

建立持续重构意识,把重构作为开发必不可少的部分,融入开发中。而不是等代码出现很大的问题的时候,再大刀阔斧的重构

重构的方法:如何重构(How)

大规模高层次的重构难度比较大,需要组织、有计划地进行,分阶段地小步快跑,时刻让代码处于一个可运行的状态。而小规模低层次的重构,因为影响范围小,改动耗时短,所以,只要你愿意并且有时间,随时随地都可以去做。我们还可以借助很多成熟的静态代码分析工具(比如 CheckStyle、FindBugs、PMD),来自动发现代码中的问题,然后针对性地进行重构优化。

采用单元测试 保证重构不出错

单元测试:单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试

集成测试:集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试

①如何编写单元测试:

写单元测试就是针对代码设计覆盖各种输入、异常、边界条件的测试用例,并将这些测试用例翻译成代码的过程。

②如何编写可测试性代码

依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试的时候,可以通过 mock 的方法解依赖外部服务,这也是我们在编写单元测试的过程中最有技术挑战的地方

如何“解耦”?

①封装与抽象

封装和抽象可以有效地隐藏实现的复杂性,隔离实现的易变性,给依赖的模块提供稳定且易用的抽象接口

②中间层

引入中间层能简化模块或类之间的依赖关系

第一阶段:引入一个中间层,包裹老的接口,提供新的接口定义。

第二阶段:新开发的代码依赖中间层提供的新接口。

第三阶段:将依赖老接口的代码改为调用新接口。

第四阶段:确保所有的代码都调用新接口之后,删除掉老的接口

③模块化

模块化是构建复杂系统常用的手段

模块化的思想无处不在,像 SOA、微服务、lib 库、系统内模块划分,甚至是类、函数的设计,都体现了模块化思想。如果追本溯源,模块化思想更加本质的东西就是分而治之

④其他设计思想和原则

单一职责原则、基于接口而非实现编程、依赖注入、多用组合少用继承、迪米特法则

命名

①利用上下文简化命名

  • 借助对象简化命名
  • 借助函数这个上下文来简化命名

②命名要可读、可搜索

我们在命名的时候,最好能符合整个项目的命名习惯。大家都用“selectXXX”表示查询,你就不要用“queryXXX”;大家都用“insertXXX”表示插入一条数据,你就要不用“addXXX”,统一规约是很重要的,能减少很多不必要的麻烦

③如何命名接口和抽象类

接口有两种命名方式:一种是在接口中带前缀“I”;另一种是在接口的实现类中带后缀“Impl”。对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,一种是不带前缀。这两种命名方式都可以,关键是要在项目中统一.

注释

注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。

注释本身有一定的维护成本,所以并非越多越好。类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码可读性。

如何发现质量问题

①常规checklist

目录设置是否齐全、模块划分是否清晰、代码结构是否满足“高内聚、松耦合”?

是否遵循经典的设计原则和设计思想(SLOID、DRY、KISS、YAGIN、LOD等)?

设计模式是否应用得当?是否有过度设计?

代码是否容易扩展?如果要添加新的功能,是否容易实现?

代码是否可以复用?是否可以复用已有的项目或类库?是否有重复造轮子?

代码是否容易测试?单元测试是否全面覆盖各种正常和异常的情况?

代码是否易读?是否符合编程规范(比如命名和注释是否恰当、代码风格是否一致等)?

②业务需求checklist

代码是否实现了预期的业务需求?

逻辑是否正确?是否处理了各种异常情况?

日志打印是否得当?是否方面debug排查问题?

接口是否易用?是否支持幂等、事务等?

代码是够存在并发问题?是否线程安全?

性能是否有优化空间,比如,SQL、算法是否可以优化?

是否有安全漏洞?比如,输入输出校验是否全面?

设计模式与范式:创建型

创建型模式主要解决对象的创建问题,封装复杂的创建过程,解耦对象的创建代码和使用代码;
创建型模式是将创建和使用代码解耦;
单例模式用来创建全局唯一的对象;
工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象;
建造者模式是用来创建复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。
原型模式针对创建成本比较大的对象,利用对已有对象进行复制的方式进行创建,以达到节省创建时间的目的。

单例模式

为什么要使用单例?

单例设计模式理解起来非常简单。一个类只允许创建一个对象,那这个类就是一个单例类,这种设计模式就叫做单例设计模式,简称单例模式。

案例一:处理资源访问冲突

public class Logger {







    private FileWriter fileWriter;











    public Logger() {
        File file = new File("D:/log.txt");
        try {
            fileWriter = new FileWriter(file, true);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }











    public void log(String message) {
        try {
            fileWriter.write(message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}








// Logger类应用实例
public class UserController {
    private Logger logger = new Logger();




    public void login(String username, String password) {
        // ... 业务逻辑
        logger.log(username + "login success!");
    }
}



public class OrderController {
    private Logger logger = new Logger();




    public void create(OrderVo order) {
        // ... 业务逻辑
        logger.log("created an order:" + order);
    }

}







这段代码存在一个问题,那就是两个类都写入同一个文件,那么两个线程同时执行login()和create()方法时必定存在资源竞争,那就有可能存在日志信息相互覆盖的情况。我们可以将Logger类设计成单例类,程序只允许创建一个Logger对象,所有的线程共享使用的这一个Logger对象,避免多线程覆盖的问题。

public class SingletonLogger {







    private FileWriter fileWriter;

    
    private static final SingletonLogger INSTANCE = new SingletonLogger();








    private SingletonLogger() {
        File file = new File("D:/log.txt");
        try {
            fileWriter = new FileWriter(file, true);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }



    
    public static SingletonLogger getInstance() {
        return INSTANCE;
    }








    public void log(String message) {
        try {
            fileWriter.write(message);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}





案例二:全局唯一类

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。
比如,配置信息类。在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。
再比如,唯一递增ID号码生成器,如果程序中有两个对象,那就回存在生成重复ID的情况。

public class IdGenerator {







    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator INSTANCE = new IdGenerator();
    
    private IdGenerator() {
        
    }



    
    public static IdGenerator getInstance() {
        return INSTANCE;
    }











    public long getId() {
        return id.incrementAndGet();
    }

}





如何实现一个单例?

  • 构造函数需要是private访问权限的,这样才能避免外部通过new创建实例;
  • 考虑对象创建时的线程安全问题;
  • 考虑是否支持延迟加载;
  • 考虑getInstance()性能是否高。

1. 饿汉式

饿汉式的实现方式比较简单。在类加载的时候,instance静态实例就已经创建并初始化好了,所以,instance实例的创建过程是线程安全的。不过,这样的实现方式不支持延迟加载,从名字中我们也可以看出这一点。代码示例如下:

public class DateFormatter {










    private static final DateFormatter INSTANCE = new DateFormatter();










    private DateFormatter() {}








    public static DateFormatter getInstance() {
        return INSTANCE;
    }




    public String format(Date date, String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(date);
    }



}







2. 懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载。具体的代码实现如下所示:

public class DateFormatter {



    private static DateFormatter INSTANCE;








    private DateFormatter() {}







    public static synchronized DateFormatter getInstance() {
        if (INSTANCE == null) {

            INSTANCE  = new DateFormatter();
        }
        return INSTANCE;
    }











    public String format(Date date, String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(date);
    }

}





不过懒汉式的缺点也很明显,我们给getInstance()这个方法加了一个锁,导致这个函数的并发度很低。

3. 双重检测

饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。双重检测是一种既支持延迟加载、又支持高并发的单例实现方式。示例代码如下:

public class DateFormatter {



    private static DateFormatter INSTANCE;








    private DateFormatter() {}







    public static DateFormatter getInstance() {
        if (INSTANCE == null) {

            synchronized (DateFormatter.class) {
                INSTANCE  = new DateFormatter();
            }
        }

        return INSTANCE;
    }





    public String format(Date date, String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(date);
    }


}



网上有人说,这种实现方式有些问题。因为指令重排序,可能会导致DateFormatter对象被new出来,并且赋值给instance之后,还没来得及初始化,就被另一个线程使用了。要解决这个问题,我们需要给instance成员变量加上volatile关键字,禁止指令重排序才行。实际上,只有很低版本的Java才会有这个问题。我们现在用的高版本的Java已经在JDK内部实现中解决了这个问题。

4. 静态内部类

示例代码如下:

public class DateFormatter {



    private static class SingletonHolder {
        private static final DateFormatter INSTANCE = new DateFormatter();
    }






    private DateFormatter() {}





    public static DateFormatter getInstance() {
        return SingletonHolder.INSTANCE;
    }








    public String format(Date date, String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(date);
    }
}










SingletonHolder是一个静态内部类,当外部类DateFormatter被加载的时候,并不会创建SingletonHolder实例对象。只有当调用getInstance()方法时,SingletonHolder才会被加载,这个时候才会创建instance。instance的唯一性、创建过程的线程安全性,都由JVM来保证。所以这种实现方法既保证了线程安全,又能做到延迟加载。

5. 枚举

最后,我们介绍一种最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过JAVA枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

public enum DateFormatter {
    
    INSTANCE;










    public String format(Date date, String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(date);
    }



}


单例存在哪些问题?

1. 单例对OOP特性的支持不友好

public class UserController {

    private Logger logger = new Logger();







    public void login(String username, String password) {
        // ... 业务逻辑
        long id = IdGenerator.getInstance().getId();
        logger.log(username + "login success!");
    }



}


public class OrderController {
    private Logger logger = new Logger();









    public void create(OrderVo order) {
        // ... 业务逻辑
        long id = IdGenerator.getInstance().getId();
        logger.log("created an order:" + order);
    }


}









IdGenerator的使用方式违背了基于接口而非实现的设计原则,也就违背了广义上理解的OOP的抽象特性。如果未来有一天需要针对不同业务使用不同的ID生成方式,那就意味着为了应对这个变化,需要将IdGenerator类用到的地方全部修改。
除此之外,单例对继承、多态性的支持也不友好。这里我之所以会用“不友好”这个词,而非“完全不支持”,是因为从理论上来讲,单例类也可以被继承、也可以实现多态,只是实现起来会非常奇怪,会导致代码的可读性变差。不明白设计意图的人,看到这样的设计,会觉得莫名其妙。所以,一旦你选择将某个类设计成单例类,也就意味着放弃了继承和多态这两个强有力的面向对象特性,也就相当于损失了可以应对未来需求变化的扩展性。

2. 单例会隐藏类之间的依赖关系

但是,单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以了。如果代码比较复杂,这种调用关系就会非常隐蔽。

3. 单例对代码的扩展性不友好

我们知道,单例类只能有一个对象实例。如果某一天,我们需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动。

4. 单例对代码的可测试性不友好

单元测试影响代码的可测试性,如果单例类依赖比较中的外部资源,比如DB,我们在写单元测试的时候,希望能通过mock的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现mock替换。
除此之外,如果单例类持有的成员变量,那它实际上相当于一种全局变量,被所有的代码共享。如果这个全局变量是一个可变全局变量,也就是说,他的成员变量是可以被修改的,那我们在编写单元测试的时候,还需要注意不同测试用例之间,修改了单例类中的同一个成员变量的值,从而导致测试结果互相影响的问题。

5. 单例不支持有参数的构造函数

解决方案一:先执行init()方法加载参数;
解决方案二:将参数放到getInstance()方法中。

如何理解单例模式的唯一性?

单例类中的对象的唯一性的作用范围是“进程唯一的”。“进程唯一”指的是进程内唯一,进程间不唯一;“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一 ”就意味着线程内、线程间都唯一,这也是“进程唯一”和“线程唯一” 的区别之处。“集群唯一”指的是进程内唯一、进程间也唯一。

如何实现线程唯一的单例?

我们通过一个HashMap来存储对象,其中key是线程ID,value是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java语言本身提供了ThreadLocal并发工具类,可以更加轻松地实现线程唯一单例。

如何实现集群环境下的单例?

我们需要把这个单例对象序列化并存储到外部共享存储区。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储会外部共享存储区。为了保证任何时刻在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,需要显式地将对象从内存中删除,并且释放对对象的加锁。

工厂模式

大部分工厂类都是以“Factory”这个单词结尾的,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()、createInstance()、newInstance()

①简单工厂





public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
    IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
    if (parser == null) {
      throw new InvalidRuleConfigException(
              "Rule config file format is not supported: " + ruleConfigFilePath);
    }




    String configText = "";

    //从ruleConfigFilePath文件中读取配置文本到configText中

    RuleConfig ruleConfig = parser.parse(configText);

    return ruleConfig;

  }











  private String getFileExtension(String filePath) {

    //...解析文件名获取扩展名,比如rule.json,返回json

    return "json";

  }


}








public class RuleConfigParserFactory {
  public static IRuleConfigParser createParser(String configFormat) {
    IRuleConfigParser parser = null;
    if ("json".equalsIgnoreCase(configFormat)) {
      parser = new JsonRuleConfigParser();
    } else if ("xml".equalsIgnoreCase(configFormat)) {
      parser = new XmlRuleConfigParser();
    } else if ("yaml".equalsIgnoreCase(configFormat)) {
      parser = new YamlRuleConfigParser();
    } else if ("properties".equalsIgnoreCase(configFormat)) {
      parser = new PropertiesRuleConfigParser();
    }



    return parser;
  }

}







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




  static {
    cachedParsers.put("json", new JsonRuleConfigParser());
    cachedParsers.put("xml", new XmlRuleConfigParser());
    cachedParsers.put("yaml", new YamlRuleConfigParser());
    cachedParsers.put("properties", new PropertiesRuleConfigParser());
  }



  public static IRuleConfigParser createParser(String configFormat) {
    if (configFormat == null || configFormat.isEmpty()) {
      return null;//返回null还是IllegalArgumentException全凭你自己说了算
    }
    IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
    return parser;
  }


}



简单工厂第二种实现


在 RuleConfigParserFactory 的第一种代码实现中,有一组 if 分支判断逻辑,是不是应该用多态或其他设计模式来替代呢?实际上,如果 if 分支并不是很多,代码中有 if 分支也是完全可以接受的

②工厂方法

工厂方法模式比起简单工厂模式更加符合开闭原则

public interface IRuleConfigParserFactory {
  IRuleConfigParser createParser();
}












public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override


  public IRuleConfigParser createParser() {
    return new JsonRuleConfigParser();
  }


}










public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override



  public IRuleConfigParser createParser() {
    return new XmlRuleConfigParser();
  }



}











public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override


  public IRuleConfigParser createParser() {
    return new YamlRuleConfigParser();
  }


}






public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
  @Override

  public IRuleConfigParser createParser() {
    return new PropertiesRuleConfigParser();
  }

}



工厂方法

工厂方法讲工厂类对象的创建逻辑又耦合进了 load() 函数中。我们可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象

public class RuleConfigSource {
  public RuleConfig load(String ruleConfigFilePath) {
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);










    IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
    if (parserFactory == null) {
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
    }



    IRuleConfigParser parser = parserFactory.createParser();



    String configText = "";

    //从ruleConfigFilePath文件中读取配置文本到configText中

    RuleConfig ruleConfig = parser.parse(configText);

    return ruleConfig;

  }











  private String getFileExtension(String filePath) {

    //...解析文件名获取扩展名,比如rule.json,返回json

    return "json";

  }


}








//因为工厂类只包含方法,不包含成员变量,完全可以复用,
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
public class RuleConfigParserFactoryMap { //工厂的工厂
  private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();




  static {
    cachedFactories.put("json", new JsonRuleConfigParserFactory());
    cachedFactories.put("xml", new XmlRuleConfigParserFactory());
    cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
    cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
  }







  public static IRuleConfigParserFactory getParserFactory(String type) {
    if (type == null || type.isEmpty()) {
      return null;
    }

    IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
    return parserFactory;
  }



}


③什么时候使用简单工厂,什么时候使用工厂方法

之所以将某个代码块剥离出来,独立为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。但是,如果代码块本身并不复杂,就几行代码而已,我们完全没必要将它拆分成单独的函数或者类

当每个对象的创建逻辑都比较简单的时候,我推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中

对象的创建比较复杂,需要组合其他类对象,做各种初始化操作的时候,我们推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,让每个工厂类都不至于过于复杂。

④何为创建逻辑比较复杂?

第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。

还有一种情况,尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下,我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。

⑤抽象工厂

抽象工厂就是针对这种非常特殊的场景而诞生的。我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 parser 对象。这样就可以有效地减少工厂类的个数

public interface IConfigParserFactory {
  IRuleConfigParser createRuleParser();
  ISystemConfigParser createSystemParser();
  //此处可以扩展新的parser类型,比如IBizConfigParser
}









public class JsonConfigParserFactory implements IConfigParserFactory {
  @Override
  public IRuleConfigParser createRuleParser() {
    return new JsonRuleConfigParser();
  }













  @Override



  public ISystemConfigParser createSystemParser() {
    return new JsonSystemConfigParser();
  }



}











public class XmlConfigParserFactory implements IConfigParserFactory {
  @Override


  public IRuleConfigParser createRuleParser() {
    return new XmlRuleConfigParser();
  }



  @Override
  public ISystemConfigParser createSystemParser() {
    return new XmlSystemConfigParser();
  }

}





// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码

建造者模式

为什么需要建造者模式?

假设有这样一道设计面试题:我们需要定义一个资源池配置类ResourcePoolConfig。这里的资源池,你可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。

image.png

通过构造函数方式代码如下:

public class ResourcePoolConfig {








    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(String name, Integer maxTotal, Integer maxIdle, Integer minIdle) {
        if (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException("name should not be empty.");
        }
        this.name = name;
        if (maxTotal != null) {
            if (maxTotal <= 0) {
                throw new IllegalArgumentException("maxTotal should be positive.");
            }
            this.maxTotal = maxTotal;
        }
        if (maxIdle != null) {
            if (maxIdle < 0) {
                throw new IllegalArgumentException("maxIdle should not be negative.");
            }
            this.maxIdle = maxIdle;
        }
        if (minIdle != null) {
            if (minIdle < 0) {
                throw new IllegalArgumentException("minIdle should not be negative.");
            }
            this.minIdle = minIdle;
        }
    }

    
    // ...
}

现在,ResourcePoolConfig只有4个可配置项,对应到构造函数中,也只有4个参数,参数的个数不多。但是,如果可配置项组件增多,变成8个、10个,甚至更多,那继续沿用现在的设计思路,构造函数的参数列表会变得很长,代码在可读性和易用性上都会变差。
我们可以把校验逻辑放置到 Builder 类中,先创建建造者,并且通过 set() 方法设置建造者 的变量值,然后在使用 build() 方法真正创建对象之前,做集中的校验,校验通过之后才会 创建对象。除此之外,我们把 ResourcePoolConfig 的构造函数改为 private 私有权限。 这样我们就只能通过建造者来创建 ResourcePoolConfig 类对象。并且, ResourcePoolConfig 没有提供任何 set() 方法,这样我们创建出来的对象就是不可变对象 了。

public class ResourcePoolConfig {

    private String name;
    private int maxTotal;
    private int maxIdle;
    private int minIdle;








    public ResourcePoolConfig(Builder builder) {
        this.name = builder.name;
        this.maxTotal = builder.maxTotal;
        this.maxIdle = builder.maxIdle;
        this.minIdle = builder.minIdle;
    }











    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;
        }
    }
}

原型模式

原型模式的原理与应用

如果对象的创建成本比较大,而同一个类的不同对象之间差别不大,在这种情况下,我们可以利用对已有对象进行复制的方式来创建对象,已达到节省创建时间的目的。这种基于原型来创建对象的方式就叫做原型设计模式。

何为“对象的创建成本比较大”?

实际上,创建对象包含的申请内存、给成员变量赋值这一过程,本身并不会花费太多时间,或者对于大部分业务系统来说,这点时间完全可以忽略。应用一个复杂的模式,只得到一点点的性能提升,这就是 所谓的过度设计,得不偿失。
但是,如果对象中的数据需要经过复杂的计算才能得到(比如排序、计算哈希值),或者需要从RPC、网络、数据库、文件系统等非常慢速的IO中读取,这种情况下,我们就可以利用原型模式,从其他已有对象中直接拷贝得到,而不用每次在创建新对象的时候,都重复执行这些耗时的操作。

原型模式的实现方式:深拷贝和浅拷贝

image.png
散列表索引中,每个结点存储的key是搜索关键词,value是SearchWord对象的内存地址。SearchWord对象本身存储在散列表之外的内存空间中。
深拷贝和浅拷贝的区别在于,浅拷贝只会复制图中的索引,不会复制数据本身(SearchWord)。而深拷贝不仅会复制索引,还会复制数据本身。
image.pngimage.png

设计模式与范式:结构型

结构型设计模式主要解决“类或对象的组合或组装”问题
结构型模式是将不同功能代码解耦

代理模式

代理模式的即在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。我们开发一个MetricsCollector类,用来收集接口请求的原始数据,比如访问时间、处理时长等。在业务系统中,我们采用如下方式来使用这个MetricsCollector类:

public class UserController {

    private MetricsCollector metricsCollector;







    public void login(String telephone, String password) {
        // ...业务逻辑
        long startTimestamp = System.currentTimeMillis();
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp, endTimeStamp);
        metricsCollector.recordRequest(requestInfo);
    }











    public void register(String telephone, String password) {
        // ...业务逻辑
        long startTimestamp = System.currentTimeMillis();
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp, endTimeStamp);
        metricsCollector.recordRequest(requestInfo);
    }

}



很明显,上面的写法有两个问题。第一,性能计数器框架代码侵入到业务代码中,跟业务代 码高度耦合。如果未来需要替换这个框架,那替换的成本会比较大。第二,收集接口请求的代码跟业务代码无关,本就不应该放到一个类中。业务类最好职责更加单一,只聚焦业务处理。
为了将框架代码和业务代码解耦,代理模式就派上用场了。代理类 UserControllerProxy 和原始类 UserController 实现相同的接口 IUserController。UserController 类只负责业 务功能。代理类 UserControllerProxy 负责在业务代码执行前后附加其他逻辑代码,并通 过委托的方式调用原始类来执行业务代码。具体的代码实现如下所示:

public interface IUserController {







    void login(String telephone, String password);










    void register(String telephone, String password) ;
}





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











    @Override
    public void register(String telephone, String password) {
        // ...业务逻辑
    }


}









public class UserControllerProxy implements IUserController {




    private UserController userController;



    private MetricsCollector metricsCollector;




    @Override
    public void login(String telephone, String password) {
        long startTimestamp = System.currentTimeMillis();
        // 业务处理
        userController.login(telephone, password);
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp, endTimeStamp);
        metricsCollector.recordRequest(requestInfo);
    }


    @Override
    public void register(String telephone, String password) {
        long startTimestamp = System.currentTimeMillis();
        // 业务处理
        userController.register(telephone, password);
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp, endTimeStamp);
        metricsCollector.recordRequest(requestInfo);
    }

}


动态代理

不过,刚刚的代码实现还是有点问题。一方面,我们需要在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。另一方面,如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。这会导致项目中类的个数成倍增加,增加了代码维护成本。并且每个代理类中的代码都有点像模板式的“重复代码”,也增加了不必要的开发成本。那这个问题怎么解决呢?
我们可以使用动态代理来解决这个问题。所谓动态代理,就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后再系统中用代理类替换掉原始类。

public class MetricsCollectorProxy {







    private MetricsCollector metricsCollector;










    public MetricsCollectorProxy(MetricsCollector metricsCollector) {
        this.metricsCollector = metricsCollector;
    }






    public Object createProxy(Object proxiedObject) {
        Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
        DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
        return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
    }





    private class DynamicProxyHandler implements InvocationHandler {





        private Object proxiedObject;






        public DynamicProxyHandler(Object proxiedObject) {
            this.proxiedObject = proxiedObject;
        }





        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long startTimestamp = System.currentTimeMillis();
            Object result = method.invoke(proxiedObject, args);
            long endTimeStamp = System.currentTimeMillis();
            long responseTime = endTimeStamp - startTimestamp;
            String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
            RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp, endTimeStamp);
            metricsCollector.recordRequest(requestInfo);
            return result;
        }
    }



}

代理模式的应用场景

业务系统的非功能性需求开发

代理模式最常用的一个应用场景就是,在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志等。我们将这些附加功能与 业务功能解耦,放到代理类中统一处理,只关注业务开发。

代理模式在RPC中的应用

实际上,RPC框架也可以看作一种代理模式,GoF的《设计模式》一书中把它称作远程代理。通过远程代理,将网络通信、数据编解码等细节隐藏起来。客户端在使用RPC服务的时候,就像本地函数一样,无需了解跟服务器交互的细节。

桥接模式

对于这个模式有两种不同的理解方式。在GoF的《设计模式》一书中,桥接模式被定义为:“将抽象和实现解耦,让它们可以独立变化。”在其他资料和书籍中,还有另外一种更加简单的理解方式:“一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。”

对于第一种GoF的理解方式,弄懂定义中“抽象”和“实现”两个概念,是理解它的关键。定义中的“抽象”,指的并非“抽象类”或“接口”,而是被抽象出来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起。





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 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;
  }


  
  //...实现读取基本类型数据的接口
}




①装饰模式与简单的组合关系

第一个比较特殊的地方:装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类

比如,下面这样一段代码,我们对 FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。

image.png

InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();






第二个比较特殊的地方:装饰器类是对功能的增强,这也是装饰器类应用场景的一个重要特点
代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能

// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {

  void f();
}


public class A impelements IA {
  public void f() { //... }
}
public class AProxy impements IA {
  private IA a;
  public AProxy(IA a) {
    this.a = a;
  }

  

  public void f() {
    // 新添加的代理逻辑
    a.f();
    // 新添加的代理逻辑
  }


}







// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
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();
    // 功能增强代码
  }


}





适配器模式的原理与实现

适配器模式(Adpater Design Pattern)。这个模式是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。对于这个模式,有一个经常被拿来解释它的例子,就是USB转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。

封装有缺陷的接口设计

假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量的静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。

// 这个类来自外部SDK,我们无权修改它的代码
public class CD {







    public static void staticFunction1() {
        // ...
    }





    public void uglyNamingFunction2() {
        // ...
    }








    public void tooManyParamsFunction3(int paramA, int paramB, int paramC) {
        // ...
    }







    public void lowPerformanceFunction4() {
        // ...
    }


}



// 使用适配器模式进行重构
public interface ITarget {
    void function1();
    void function2();
    void function3(ParamsWrapperDefinition paramsWrapper);
    void function4();
}





public class CDAdapter extends CD implements ITarget {

    @Override
    public void function1() {
        // ....
        staticFunction1();
    }







    @Override
    public void function2() {
        super.uglyNamingFunction2();
    }






    @Override
    public void function3(ParamsWrapperDefinition paramsWrapper) {
        super.tooManyParamsFunction3(paramsWrapper.getParamA(), paramsWrapper.getParamB(), paramsWrapper.getParamC());
    }




    @Override
    public void function4() {
        // 重写
    }
}


统一多个类的接口设计

某个功能的实现依赖多个外部系统。通过适配器模式,将他们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
假设我们的系统要对用户输入的文本内容做敏感过滤,为了提高过滤的召回率,我们引入了多款第三方敏感词过滤系统,一次对用户输入的内容进行过滤,过滤掉尽可能多的敏感词。但是,每个系统提供的过滤接口都是不同的。这就意味着我们没法复用一套逻辑来调用各个系统。这个时候,我们就可以使用适配器模式,将所有系统的接口适配为统一的接口定义,这样我们就可以复用调用敏感词过滤的代码。

替换依赖的外部系统

当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。

// 外部系统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()));

设计模式与范式:行为型

行为型设计模式主要解决的就是“类或对象之间的交互”问题;
行为型模式是将不同的行为代码解耦

观察者模式

在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知

根据应用场景的不同,观察者模式会对应不同的代码实现方式:

同步阻塞是最经典的实现方式,主要是为了代码解耦

异步非阻塞除了能实现代码解耦之外,还能提高代码的执行效率

进程间的观察者模式解耦更加彻底,一般是基于消息队列来实现,用来实现不同进程间的被观察者和观察者之间的交互

一般情况下,被依赖的对象叫作被观察者(Observable),依赖的对象叫作观察者(Observer)。不过,在实际的项目开发中,这两种对象的称呼是比较灵活的,有各种不同的叫法,比如:Subject-Observer、Publisher-Subscriber、Producer-Consumer、EventEmitter-EventListener、Dispatcher-Listener。不管怎么称呼,只要应用场景符合刚刚给出的定义,都可以看作观察者模式.

模板方法

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

public abstract class AbstractClass {
  public final void templateMethod() {
    //...
    method1();
    //...
    method2();
    //...
  }


  

  protected abstract void method1();
  protected abstract void method2();
}







public class ConcreteClass1 extends AbstractClass {
  @Override
  protected void method1() {
    //...
  }


  
  @Override


  protected void method2() {
    //...

  }


}






public class ConcreteClass2 extends AbstractClass {
  @Override

  protected void method1() {
    //...
  }

  
  @Override
  protected void method2() {
    //...
  }
}








AbstractClass demo = ConcreteClass1();
demo.templateMethod();





模板模式主要是用来解决复用和扩展两个问题

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

策略模式

定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)

①策略的定义

包含一个策略接口和一组实现这个接口的策略类

public interface Strategy {
  void algorithmInterface();
}












public class ConcreteStrategyA implements Strategy {
  @Override


  public void  algorithmInterface() {
    //具体的算法...
  }


}










public class ConcreteStrategyB implements Strategy {
  @Override



  public void  algorithmInterface() {
    //具体的算法...
  }



}











②策略的创建

因为策略模式会包含一组策略,在使用它们的时候,一般会通过类型(type)来判断创建哪个策略来使用。为了封装创建逻辑,我们需要对客户端代码屏蔽创建细节。我们可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。如果策略类是无状态的,不包含成员变量,只是纯粹的算法实现,这样的策略对象是可以被共享使用的,不需要在每次调用 getStrategy() 的时候,都创建一个新的策略对象。针对这种情况,我们可以使用上面这种工厂类的实现方式,事先创建好每个策略对象,缓存到工厂类中,用的时候直接返回;

public class StrategyFactory {

  private static final Map<String, Strategy> strategies = new HashMap<>();







  static {
    strategies.put("A", new ConcreteStrategyA());
    strategies.put("B", new ConcreteStrategyB());
  }






  public static Strategy getStrategy(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }





    return strategies.get(type);
  }



}


如果策略类是有状态的,根据业务场景的需要,我们希望每次从工厂方法中,获得的都是新创建的策略对象,而不是缓存好可共享的策略对象,那我们就需要按照如下方式来实现策略工厂类

public class StrategyFactory {

  public static Strategy getStrategy(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }








    if (type.equals("A")) {
      return new ConcreteStrategyA();
    } else if (type.equals("B")) {
      return new ConcreteStrategyB();
    }











    return null;
  }



}


职责链模式

在职责链模式中,多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。

在 GoF 的定义中,一旦某个处理器能处理这个请求,就不会继续将请求传递给后续的处理器了。当然,在实际的开发中,也存在对这个模式的变体,那就是请求不会中途终止传递,而是会被所有的处理器都处理一遍

①职责链模式有两种常用的实现。一种是使用链表来存储处理器,另一种是使用数组来存储处理器,后面一种实现方式更加简单

public abstract class Handler {
  protected Handler successor = null;







  public void setSuccessor(Handler successor) {
    this.successor = successor;
  }





  public final void handle() {
    boolean handled = doHandle();
    if (successor != null && !handled) {
      successor.handle();
    }





  }





  protected abstract boolean doHandle();
}










public class HandlerA extends Handler {
  @Override
  protected boolean doHandle() {
    boolean handled = false;
    //...

    return handled;
  }

}




public class HandlerB extends Handler {
  @Override

  protected boolean doHandle() {
    boolean handled = false;
    //...
    return handled;
  }



}




// HandlerChain和Application代码不变






public interface IHandler {
  boolean handle();
}







public class HandlerA implements IHandler {
  @Override

  public boolean handle() {
    boolean handled = false;
    //...
    return handled;
  }

}


public class HandlerB implements IHandler {
  @Override
  public boolean handle() {
    boolean handled = false;
    //...
    return handled;
  }

}


public class HandlerChain {
  private List<IHandler> handlers = new ArrayList<>();



  public void addHandler(IHandler handler) {
    this.handlers.add(handler);
  }

  public void handle() {
    for (IHandler handler : handlers) {
      boolean handled = handler.handle();
      if (handled) {
        break;
      }
    }
  }
}

// 使用举例
public class Application {
  public static void main(String[] args) {
    HandlerChain chain = new HandlerChain();
    chain.addHandler(new HandlerA());
    chain.addHandler(new HandlerB());
    chain.handle();
  }
}



②使用职责链模式给敏感词打马赛克

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 {
      // 发表
    }

  }


}


状态模式

状态机又叫有限状态机,它有 3 个部分组成:状态、事件、动作。其中,事件也称为转移条件。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作;

image.png

状态:小马里奥(Small Mario)、超级马里奥(Super Mario)、火焰马里奥(Fire Mario)、斗篷马里奥(Cape Mario)

事件:吃了蘑菇、获得斗篷、获得火焰、遇到怪物

动作:增加/扣减积分

骨架代码

public enum State {
  SMALL(0),
  SUPER(1),
  FIRE(2),
  CAPE(3);








  private int value;







  private State(int value) {
    this.value = value;

  }













  public int getValue() {

    return this.value;

  }






}










public class MarioStateMachine {

  private int score;

  private State currentState;





  public MarioStateMachine() {
    this.score = 0;
    this.currentState = State.SMALL;
  }






  public void obtainMushRoom() {
    //TODO
  }







  public void obtainCape() {
    //TODO
  }







  public void obtainFireFlower() {

    //TODO
  }






  public void meetMonster() {
    //TODO
  }







  public int getScore() {
    return this.score;
  }

  public State getCurrentState() {
    return this.currentState;
  }


}




public class ApplicationDemo {
  public static void main(String[] args) {
    MarioStateMachine mario = new MarioStateMachine();
    mario.obtainMushRoom();
    int score = mario.getScore();
    State state = mario.getCurrentState();
    System.out.println("mario score: " + score + "; state: " + state);
  }
}




①三种实现方式-分支逻辑法

public class MarioStateMachine {
  private int score;
  private State currentState;










  public MarioStateMachine() {
    this.score = 0;
    this.currentState = State.SMALL;
  }


//获取蘑菇
  public void obtainMushRoom() {
    if (currentState.equals(State.SMALL)) {
      this.currentState = State.SUPER;
      this.score += 100;
    }



  }






//获得斗篷
  public void obtainCape() {
    if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
      this.currentState = State.CAPE;
      this.score += 200;
    }
  }
//获取火焰
  public void obtainFireFlower() {
    if (currentState.equals(State.SMALL) || currentState.equals(State.SUPER) ) {
      this.currentState = State.FIRE;
      this.score += 300;
    }
  }



//遇到怪物
  public void meetMonster() {
    if (currentState.equals(State.SUPER)) {
      this.currentState = State.SMALL;
      this.score -= 100;
      return;
    }






    if (currentState.equals(State.CAPE)) {
      this.currentState = State.SMALL;
      this.score -= 200;
      return;
    }


    if (currentState.equals(State.FIRE)) {
      this.currentState = State.SMALL;
      this.score -= 300;
      return;
    }
  }




  public int getScore() {
    return this.score;
  }


  public State getCurrentState() {
    return this.currentState;
  }

}

②三种实现方式-查表法

image.png

public enum Event {
  GOT_MUSHROOM(0),
  GOT_CAPE(1),
  GOT_FIRE(2),
  MET_MONSTER(3);








  private int value;







  private Event(int value) {
    this.value = value;

  }













  public int getValue() {

    return this.value;

  }






}










public class MarioStateMachine {

  private int score;

  private State currentState;





  private static final State[][] transitionTable = {
          {SUPER, CAPE, FIRE, SMALL},
          {SUPER, CAPE, FIRE, SMALL},
          {CAPE, CAPE, CAPE, SMALL},
          {FIRE, FIRE, FIRE, SMALL}
  };



  private static final int[][] actionTable = {
          {+100, +200, +300, +0},
          {+0, +200, +300, -100},
          {+0, +0, +0, -200},
          {+0, +0, +0, -300}
  };




  public MarioStateMachine() {
    this.score = 0;
    this.currentState = State.SMALL;
  }





  public void obtainMushRoom() {
    executeEvent(Event.GOT_MUSHROOM);
  }




  public void obtainCape() {
    executeEvent(Event.GOT_CAPE);
  }






  public void obtainFireFlower() {
    executeEvent(Event.GOT_FIRE);
  }



  public void meetMonster() {
    executeEvent(Event.MET_MONSTER);
  }

  private void executeEvent(Event event) {
    int stateValue = currentState.getValue();
    int eventValue = event.getValue();
    this.currentState = transitionTable[stateValue][eventValue];
    this.score = actionTable[stateValue][eventValue];
  }


  public int getScore() {
    return this.score;
  }

  public State getCurrentState() {
    return this.currentState;
  }

}


三种实现方式-状态模式

public interface IMario { //所有状态类的接口
  State getName();
  //以下是定义的事件
  void obtainMushRoom();
  void obtainCape();
  void obtainFireFlower();
  void meetMonster();
}

public class SmallMario implements IMario {
  private MarioStateMachine stateMachine;









  public SmallMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }











  @Override
  public State getName() {
    return State.SMALL;
  }






  @Override
  public void obtainMushRoom() {
    stateMachine.setCurrentState(new SuperMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 100);
  }





  @Override

  public void obtainCape() {
    stateMachine.setCurrentState(new CapeMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 200);
  }



  @Override
  public void obtainFireFlower() {

    stateMachine.setCurrentState(new FireMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 300);
  }




  @Override
  public void meetMonster() {
    // do nothing...
  }


}





public class SuperMario implements IMario {
  private MarioStateMachine stateMachine;




  public SuperMario(MarioStateMachine stateMachine) {
    this.stateMachine = stateMachine;
  }



  @Override
  public State getName() {
    return State.SUPER;
  }



  @Override
  public void obtainMushRoom() {
    // do nothing...
  }



  @Override
  public void obtainCape() {
    stateMachine.setCurrentState(new CapeMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 200);
  }


  @Override
  public void obtainFireFlower() {
    stateMachine.setCurrentState(new FireMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() + 300);
  }

  @Override
  public void meetMonster() {
    stateMachine.setCurrentState(new SmallMario(stateMachine));
    stateMachine.setScore(stateMachine.getScore() - 100);
  }
}

// 省略CapeMario、FireMario类...

public class MarioStateMachine {
  private int score;
  private IMario currentState; // 不再使用枚举来表示状态


  public MarioStateMachine() {
    this.score = 0;
    this.currentState = new SmallMario(this);
  }

  public void obtainMushRoom() {
    this.currentState.obtainMushRoom();
  }

  public void obtainCape() {
    this.currentState.obtainCape();
  }

  public void obtainFireFlower() {
    this.currentState.obtainFireFlower();
  }

  public void meetMonster() {
    this.currentState.meetMonster();
  }

  public int getScore() {
    return this.score;
  }

  public State getCurrentState() {
    return this.currentState.getName();
  }

  public void setScore(int score) {
    this.score = score;
  }

  public void setCurrentState(IMario currentState) {
    this.currentState = currentState;
  }
}

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYKXwG92' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片