代理模式(Proxy)

代理模式的原理非常简单,它在不改变原始类的情况下,通过引入代理类来给原始类附加功能。它最常用的场景就是为业务系统开发非功能性需求,比如:监控,统计,鉴权,限流,事务,日志等。
假设我们有如下所示的一个接口统计模块。

  1. Metrics metrics = new Metrics();
  2. long startTimestamp=System.currentTimeMillis();
  3. //执行业务
  4. long endTimestamp=System.currentTimeMillis();
  5. RequestInfo info=new RequestInfo();
  6. info.setApiName("api");
  7. info.setTimestamp(startTimestamp);
  8. info.setResponseTime(endTimestamp-startTimestamp);
  9. metrics.collect(info);

如果在接口中直接使用,统计模块的代码会侵入到业务代码中与业务代码高度耦合,如果未来需要替换接口统计模块,那么替换成本会比较大。而且统计模块与业务无关,本身就不应该放到一个类中,业务类应当只聚焦业务处理。
为了将模块代码与业务代码解耦,代理模式就派上用场了,如下所示。

  1. class UserVo{}
  2. interface IUserController{
  3. UserVo login(String telephone,String password);
  4. UserVo register(String telephone,String password);
  5. }
  6. class UserController implements IUserController{
  7. @Override
  8. public UserVo login(String telephone, String password) {
  9. UserVo userVo=new UserVo();
  10. //业务逻辑
  11. return userVo;
  12. }
  13. @Override
  14. public UserVo register(String telephone, String password) {
  15. UserVo userVo=new UserVo();
  16. //业务逻辑
  17. return userVo;
  18. }
  19. }
  20. class UserControllerProxy implements IUserController{
  21. //依赖注入
  22. private Metrics metrics;
  23. private UserController userController;
  24. @Override
  25. public UserVo login(String telephone, String password) {
  26. //统计模块代码
  27. UserVo userVo=userController.login(telephone,password);
  28. //统计模块代码
  29. return userVo;
  30. }
  31. @Override
  32. public UserVo register(String telephone, String password) {
  33. //统计模块代码
  34. UserVo userVo=userController.register(telephone,password);
  35. //统计模块代码
  36. return userVo;
  37. }
  38. }

这时候只需要调用UserControllerProxy中的login()方法和register()方法即可。当业务逻辑发生变化时只需要修改UserController即可,统计模块更换时只需要修改UserControllerProxy。
上面的代码采用基于接口而非实现的编程思想,如果我不想加一个接口,或者原始类代码并不是我们自己维护的,这时候就没法再使用基于接口的思想了。我们可以通过继承原始类来对原始类进行扩展,如下所示。

  1. class UserVo{}
  2. class UserController{
  3. public UserVo login(String telephone, String password) {
  4. UserVo userVo=new UserVo();
  5. //业务逻辑
  6. return userVo;
  7. }
  8. public UserVo register(String telephone, String password) {
  9. UserVo userVo=new UserVo();
  10. //业务逻辑
  11. return userVo;
  12. }
  13. }
  14. class UserControllerProxy extends UserController{
  15. //依赖注入
  16. private Metrics metrics;
  17. private UserController userController;
  18. @Override
  19. public UserVo login(String telephone, String password) {
  20. //统计模块代码
  21. UserVo userVo=userController.login(telephone,password);
  22. //统计模块代码
  23. return userVo;
  24. }
  25. @Override
  26. public UserVo register(String telephone, String password) {
  27. //统计模块代码
  28. UserVo userVo=userController.register(telephone,password);
  29. //统计模块代码
  30. return userVo;
  31. }
  32. }

通过代理模式我们将第三方模块与业务解耦了,但是同样还有一些问题。一方面我们要将原始类中所有的方法都重新实现一遍,另一方面每一个原始类我们都需要为其创建一个代理类。对于Java,我们可以使用动态代理来解决这个问题。

  1. class MetricsCollectorProxy{
  2. private final Metrics metrics;
  3. //依赖注入
  4. public MetricsCollectorProxy(Metrics metrics){
  5. this.metrics=metrics;
  6. }
  7. public Object createProxy(Object proxiedObject){
  8. return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(),
  9. proxiedObject.getClass().getInterfaces(),
  10. new DynamicProxyHandler(proxiedObject));
  11. }
  12. private static class DynamicProxyHandler implements InvocationHandler{
  13. private final Object proxiedObject;
  14. public DynamicProxyHandler(Object proxiedObject){
  15. this.proxiedObject=proxiedObject;
  16. }
  17. @Override
  18. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  19. long startTimestamp=System.currentTimeMillis();
  20. Object result=method.invoke(proxiedObject,args);//执行业务代码
  21. long endTimestamp=System.currentTimeMillis();
  22. String apiName=proxiedObject.getClass().getName()+":"+method.getName();
  23. RequestInfo info=new RequestInfo();
  24. info.setApiName(apiName);
  25. info.setTimestamp(startTimestamp);
  26. info.setResponseTime(endTimestamp-startTimestamp);
  27. metrics.collect(info);
  28. return result;//返回业务代码的返回值
  29. }
  30. }
  31. }

需要注意的是,动态代理的原始类是一定要实现接口的,并且代理实例转型时只能转型为接口,否则会抛出ClassCastException。
一个简单的代理类如下所示。

  1. class AnyProxy{
  2. public Object createProxy(Object proxiedObject){
  3. return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(),
  4. proxiedObject.getClass().getInterfaces(),
  5. new DynamicProxyHandler(proxiedObject));
  6. }
  7. private static class DynamicProxyHandler implements InvocationHandler{
  8. private final Object proxiedObject;
  9. public DynamicProxyHandler(Object proxiedObject){
  10. this.proxiedObject=proxiedObject;
  11. }
  12. @Override
  13. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  14. System.out.println("before");
  15. Object result=method.invoke(proxiedObject,args);
  16. System.out.println("after");
  17. return result;
  18. }
  19. }
  20. }

同时定义了如下所示的原始类和接口。

  1. interface IUserController{
  2. Object login(String tel,String password);
  3. }
  4. class UserController implements IUserController{
  5. @Override
  6. public Object login(String telephone,String password) {
  7. System.out.println("execute task: telephone="+telephone+",password="+password);
  8. return "ok";
  9. }
  10. }

使用方式如下。

  1. IUserController controller= (IUserController) new AnyProxy().createProxy(new UserController());
  2. System.out.println(controller.login("110","password"));
  3. /**
  4. * 下面为输出
  5. */
  6. before
  7. execute task: telephone=110,password=password
  8. after
  9. ok

桥接模式(Bridge)

在《设计模式》一书中对桥接模式的解释是“将抽象和实现解耦,让它们可以独立变化。”这句话一听会让人感到疑惑,在很多书籍中对桥接模式还有另一种理解,即“一个类存在两个(或多个)独立变化的维度,我们可以通过组合的方式让它们可以独立进行扩展。”通过组合来代替进程关系,避免继承层次爆炸。我们来看一个例子:根据不同的告警规则,触发不同类型的告警。告警支持多种通知渠道,包括:邮件,短信等。通知的紧急程度有多个级别,包括:严重,普通等。
理解了需求之后得到如下代码。其中,Notification类相当于抽象,MsgSender相当于实现,两者可以独立开发,通过组合关系(即桥梁)可以任意组合在一起。MsgSender的变化不会影响到Notification,并且MsgSender和Notification之间的组合关系可以通过配置文件动态地去指定。

  1. interface MsgSender{
  2. boolean send(String msg);
  3. }
  4. class TelephoneMsgSender implements MsgSender{
  5. private final List<String> telephones;
  6. public TelephoneMsgSender(List<String> telephones){
  7. this.telephones=telephones;
  8. }
  9. @Override
  10. public boolean send(String msg) {
  11. //发送消息代码
  12. System.out.println("send msg:"+msg+" by telephone");
  13. return true;
  14. }
  15. }
  16. class EmailMsgSender implements MsgSender{
  17. private final List<String> emails;
  18. public EmailMsgSender(List<String> emails){
  19. this.emails=emails;
  20. }
  21. @Override
  22. public boolean send(String msg) {
  23. //发送邮件代码
  24. System.out.println("send msg:"+msg+" by email");
  25. return true;
  26. }
  27. }
  28. abstract class Notification{
  29. protected final MsgSender sender;
  30. public Notification(MsgSender sender){
  31. this.sender=sender;
  32. }
  33. public abstract boolean notify(String msg);
  34. }
  35. class SevereNotification extends Notification{
  36. public SevereNotification(MsgSender sender) {
  37. super(sender);
  38. }
  39. @Override
  40. public boolean notify(String msg) {
  41. System.out.println("severe notification");
  42. return sender.send(msg);
  43. }
  44. }
  45. class NormalNotification extends Notification{
  46. public NormalNotification(MsgSender sender) {
  47. super(sender);
  48. }
  49. @Override
  50. public boolean notify(String msg) {
  51. System.out.println("normal notification");
  52. return sender.send(msg);
  53. }
  54. }

装饰器模式(Decorator)

装饰器模式的作用是对原始类进行增强,为原始类附加更多的功能,听起来与代理模式很像。与代理模式不同的是,代理模式是为原始类附加无关的功能,而装饰器模式附加的功能与原始类是相关的。我们以一个灯的功能增强作为示例。
首先我们有一个普通的灯,没有任何特性,它就是一颗纯粹的灯。现在按照灯的发光原理分类,可以分为氙气灯和LED;按照射程分类可以分为远光灯和近光灯。我们希望将它们组合起来得到一颗LED的远光灯,LED的近光灯,氙气的远光灯······或则我们只要一颗LED,不要它区分远光灯还是近光灯。这时候我们可以使用装饰器模式来实现,代码如下所示。

  1. class Light{
  2. void illuminate(){
  3. System.out.println("illuminate");
  4. }
  5. }
  6. class LowBeam extends Light{
  7. private final Light light;
  8. public LowBeam(Light light){
  9. this.light=light;
  10. }
  11. @Override
  12. public void illuminate() {
  13. System.out.println("low beam");
  14. light.illuminate();
  15. }
  16. }
  17. class HighBeam extends Light{
  18. private final Light light;
  19. public HighBeam(Light light){
  20. this.light=light;
  21. }
  22. @Override
  23. void illuminate() {
  24. System.out.println("high beam");
  25. light.illuminate();
  26. }
  27. }
  28. class XenonLamp extends Light{
  29. private final Light light;
  30. public XenonLamp(Light light){
  31. this.light=light;
  32. }
  33. @Override
  34. void illuminate() {
  35. System.out.println("xenon lamp");
  36. light.illuminate();
  37. }
  38. }
  39. class LED extends Light{
  40. private final Light light;
  41. public LED(Light light){
  42. this.light=light;
  43. }
  44. @Override
  45. void illuminate() {
  46. System.out.println("LED");
  47. light.illuminate();
  48. }
  49. }

如果我们想要得到一颗LED的远光灯可以这样组合。

  1. new HighBeam(new LED(new Light())).illuminate();
  2. /**
  3. * 输出如下
  4. */
  5. high beam
  6. xenon lamp
  7. illuminate

实际上Java IO库就使用了装饰器模式。

  1. File file=new File("map.data");
  2. ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(file));

适配器模式(Adapter)

顾名思义,适配器模式的作用就是用来做适配的。适配器模式可以用来补救设计上的缺陷,通过封装使得原本不兼容的接口兼容。或者通过封装使得接口更加优雅,具体例子如下所示。

  1. class Any{
  2. public static void staticFunction(){}
  3. public void uglyNameFunction(){}
  4. }
  5. interface ITarget{
  6. void fixStaticFunction();
  7. void fixUglyNameFunction();
  8. }
  9. class AnyAdapter implements ITarget{
  10. private final Any any;
  11. public AnyAdapter(Any any){
  12. this.any=any;
  13. }
  14. @Override
  15. public void fixStaticFunction() {
  16. Any.staticFunction();
  17. }
  18. @Override
  19. public void fixUglyNameFunction() {
  20. any.uglyNameFunction();
  21. }
  22. }

通过AnyAdapter封装后,抽象出了更好的接口设计。将原本会影响到代码可测试性的静态方法封装到了成员方法中,同时原本命名不合理的函数也得到了重新封装。

外观模式(Facade)

外观模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。举一个例子,假设系统提供了a,b,c,d四个接口,客户端需要调用系统的a,b,d接口。为了调用更加方便,我们可以提供一个包裹a,b,d接口的外观接口x给客户端使用。

组合模式(Composite)

组合模式就是将一组对象组织成树形结构,以表示一种“部分-整体”的层次结构。

享元模式(Flyweight)

享元,顾名思义就是被共享的单元,使用享元模式的目的就是复用对象,节约内存,当然前提是享元对象是不可变对象。因为对象是不可变的,所以一旦通过构造函数初始化完成之后,他的成员变量或属性就不会再被修改了,所以我们不能暴露setter()方法。
如下所示是一个简易的文本编辑器。

  1. class Character{
  2. private char c;
  3. private Font font;
  4. private int size;
  5. private int colorRGB;
  6. public Character(char c,Font font,int size,int colorRGB){
  7. this.c=c;
  8. this.font=font;
  9. this.size=size;
  10. this.colorRGB=colorRGB;
  11. }
  12. public enum Font{
  13. 微软雅黑
  14. }
  15. }
  16. class Editor{
  17. private List<Character> chars=new ArrayList<>();
  18. public void appendCharacter(char c, Character.Font font,int size,int colorRGB){
  19. chars.add(new Character(c,font,size,colorRGB));
  20. }
  21. }

每次输入文字都会向Editor中添加一个Character对象,如果一个文本中有上万甚至几十万字,那么我们就需要在内存中存储这么多Character对象。假设有十万个字,那么在64位JVM上存储这么多对象需要的空间大小为((8+4+(1+(8+4+4)+4+4)+3)100000)/(10242024)=3.81GB,可见内存消耗非常之大,那么有没有什么办法可以减少内存消耗呢?我们可以通过享元模式共享不变的相同单元以达到节省内存的目的。

  1. class CharacterStyle{
  2. private Font font;
  3. private int size;
  4. private int colorRGB;
  5. public enum Font{
  6. yahei;
  7. }
  8. public CharacterStyle(Font font,int size,int colorRGB){
  9. this.font=font;
  10. this.size=size;
  11. this.colorRGB=colorRGB;
  12. }
  13. @Override
  14. public boolean equals(Object o) {
  15. if (this == o) return true;
  16. if (o == null || getClass() != o.getClass()) return false;
  17. CharacterStyle that = (CharacterStyle) o;
  18. return size == that.size &&
  19. colorRGB == that.colorRGB &&
  20. font == that.font;
  21. }
  22. @Override
  23. public int hashCode() {
  24. return Objects.hash(font, size, colorRGB);
  25. }
  26. }
  27. class CharacterStyleFactory{
  28. private static final Map<Integer,CharacterStyle> styles=new HashMap<>();
  29. public static CharacterStyle getStyle(CharacterStyle.Font font,int size,int colorRGB){
  30. CharacterStyle newStyle=new CharacterStyle(font,size,colorRGB);
  31. int hash=newStyle.hashCode();
  32. styles.putIfAbsent(hash,newStyle);
  33. return styles.get(hash);
  34. }
  35. }
  36. class Character{
  37. private char c;
  38. CharacterStyle style;
  39. public Character(char c,CharacterStyle style){
  40. this.c=c;
  41. this.style=style;
  42. }
  43. }

在一个文本文件中,用到的字体格式并不会太多,所以我们可将字体格式设计为享元,让不同的文字共享使用,这样一来便节省了内存。