如何提高代码质量

   我们要写出好的代码,其前提是要知道“好”和“烂”定义的标准是什么,然后才能在写代码的时候,去设计一份好的代码。

如何定义“好”的代码?

   好和坏是一个比较笼统的概率,代码质量高低是一个综合各种因素得到的结论,并不能通过单一的维度去评价一段代码写的好坏。对一段代码的质量评价,常常有很强的主观性。比如,怎么样的代码才算可读性好,每个人的评判标准都不大一样。

列举一些常见的代码评价标准:

定义解释
可维护性、可扩展性能够在不修改或少量修改原有代码的情况下,通过扩展的方式添加新的功能代码。(对修改关闭,对扩展开放)
可读性、简洁性思从深而行从简,目的是让他人可以轻松读懂你的代码。列如命名是否规范、注释是否详尽、函数长度是否合适等
可复用性构建可复用的代码,减少重复代码的产出
可测试性代码的单测编写时较为方便,能从侧面上反应代码质量的好坏
灵活性、模块化、健壮性…………

如何让代码符合“好”的标准?

​    要写出高质量代码,需要掌握一些能落地、够细化的编程方法论,这其中就包含编码规范、重构技巧、面向对象设计思想、设计原则、设计模式等等内容,这是平常的工作和学习中,需要去积累的。

请添加图片描述

面向对象

特性

   面向对象的四大特性:封装、继承、多态[、抽象]

封装(Encapsulation):

   作用:类通过暴露有限的访问接口,授权外部仅能通过指定的方式来访问内部信息或者数据,实现隐藏信息、保护数据的目的。其实现的基础是访问权限的控制。

继承(Inheritance):

   实现代码复用的目的。但子类和父类高度耦合,修改父类的代码,会直接影响到子类。

多态(Polymorphism)

   提高代码的可扩展性和复用性。

​    子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态的实现,在 Java 中可以利用“extends + Override ”方式,或者可以利用 “implements + Override”方式,在Python 中 可以利用 duck-typing来实现多态。

eg:

class Logger:
    def record(self):
        print(“I write a log into file.)
        
class DB:
    def record(self):
        print(“I insert data into db.)
        
def test(recorder):
    recorder.record()

def demo():
    logger = Logger()
    db = DB()
    test(logger)
    test(db)

抽象(Abstraction):

   隐藏方法的具体实现,让调用者只关心提供的功能,不需要知道实现细节。如 interface 与 abstract

待改进的面向过程设计思维

1. getter、setter方法需要按情况添加

public class ShoppingCart {
  private int itemsCount;
  private double totalPrice;
  private List<ShoppingCartItem> items = new ArrayList<>();
  
  public int getItemsCount() {
    return this.itemsCount;
  }
  
//  public void setItemsCount(int itemsCount) {
//    this.itemsCount = itemsCount;
//  }
//  
  public double getTotalPrice() {
    return this.totalPrice;
  }
//  public void setTotalPrice(double totalPrice) {
//    this.totalPrice = totalPrice;
//  }
  
	public List<ShoppingCartItem> getUnmodifiableItems() {
    return Collections.unmodifiableList(this.items);
  }
  public List<ShoppingCartItem> getItems() {
    return this.items;
  }
  
  public void addItem(ShoppingCartItem item) {
    items.add(item);
    itemsCount++;
    totalPrice += item.getPrice();
  }
  
  public void clear() {
    items.clear();
    itemsCount = 0;
    totalPrice = 0.0;
  }
  // ...省略其他方法...
}
  1. itemsCount 和 totalPrice 不对外提供设值的方法,避免跟 items 属性的值不一致
  2. ShoppingCart 中提供 clear 方法,同步修改 itemsCount 和 totalPrice

2. Util 工具类,是否可以定义到父类中

设计原则

6大原则(SOLID)、DRY

1. S-SRP(Single Responsibility Principle)

   一个类或者模块只负责完成一个职责(或者功能)。不写出大而全的类,这样不利于对后面进行扩展和阅读。

如何判断类的职责是否足够单一?

   评价一个类的职责是否足够单一,并没有一个非常明确的、可以量化的标准,每个人都有自己的理解。所以,在设计类时,可以先写一个符合当前业务需求的类。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。

2. I-ISP(Interface Segregation Principle)

接口隔离”有三种不同的理解:

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

    eg:当注销行为非用户可操作时,可将注销行为隔离到其他接口

    public interface UserService {
      boolean register(String cellphone, String password);
      boolean login(String cellphone, String password);
      UserInfo getUserInfoById(long id);
      UserInfo getUserInfoByCellphone(String cellphone);
    }
    
    public interface RestrictedUserService {
      boolean deleteUserByCellphone(String cellphone);
      boolean deleteUserById(long id);
    }
    
    public class UserServiceImpl implements UserService, RestrictedUserService {
      // ...省略实现代码...
    }
    
  2. 如果把“接口”理解为单个API接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。

    eg:打印日志时需要脱敏,将脱敏能力继续细化为一个函数

    public class LogUtils {
        public static void info(Logger logger, Object... objs) {	      
            LogUtil.info(logger, ScanAndDesensUtil.scanAndDesensText());
        }
    }
    
    public class ScanAndDesensUtil {
        public static StringBuilder scanAndDesensText(Object... text) {
          //...实现
        }
      	public static String scanAndDesensText(String text) {
          //...实现
        }
    }
    
    
  3. 如果把“接口”理解为OOP中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

    eg:定时更新配置 ScheduledUpdater 只依赖 Updater 接口,配置信息监控 SimpleHttpServer 只依赖 Viewer 接口

    //配置热更新接口
    public interface Updater {
      void update();
    }
    //配置热更新任务
    public class ScheduledUpdater {
    	public ScheduleUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) {
        this.updater = updater; 
        //...
      }
    	public void run() {
            executor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    updater.update();
                }
            }, this.initialDelayInSeconds, this.periodInSeconds, TimeUnit.SECONDS);
       }
    }
    
    //配置信息浏览接口
    public interface Viewer {
    
      String outputInPlainText();
      Map<String, String> output();
    }
    //配置信息的添加与发送
    public class SimpleHttpServer {
      private String host;
      private int port;
      private Map<String, List<Viewer>> viewers = new HashMap<>();
      
      public SimpleHttpServer(String host, int port) {//...}
      
      public void addViewers(String urlDirectory, Viewer viewer) {
        if (!viewers.containsKey(urlDirectory)) {
          viewers.put(urlDirectory, new ArrayList<Viewer>());
        }
        this.viewers.get(urlDirectory).add(viewer);
      }
      
      public void run() { //... }
    }
    
    
    public class RedisConfig implemets Updater, Viewer{
        private ConfigSource configSource; //配置中心(比如zookeeper)
        private String address;
        //...省略其他配置
    
        public RedisConfig(ConfigSource configSource) {
            this.configSource = configSource;
        }
    
        public String getAddress() {
            return this.address;
        }
        //...省略其他get()、init()方法...
    		@Override
        public void update() {
          //从configSource加载配置到address/timeout/maxTotal...
        }
    }
    public class KafkaConfig  implements Updater { //...省略... }
    public class MysqlConfig implements Viewer { //...省略... }
    

与单一职责原则相似,接口隔离原则更加细化,而单一职责原则,提出的是接口的设计思想。

3. O-OCP(Open Closed Principle)

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

4. L-LSP(Liskov Substitution Principle)

请添加图片描述

​ ​  ​  派生的子类对象,可以替换任何基类出现的地方,并且保证子类和父类中的逻辑行为不变及正确性不被破坏

eg:

public class Transporter {
  private HttpClient httpClient;
  
  public Transporter(HttpClient httpClient) {
    this.httpClient = httpClient;
  }

  public Response sendRequest(Request request) {
    // ...use httpClient to send request
  }
}

public class SecurityTransporter extends Transporter {
  private String appId;
  private String appToken;

  public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
    super(httpClient);
    this.appId = appId;
    this.appToken = appToken;
  }

  @Override
  public Response sendRequest(Request request) {
  	//此处新增的逻辑,应当在Transporter或SecurityTransporter调用该方法时,具有一致的业务功能
    if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
      request.addPayload("app-id", appId);
      request.addPayload("app-token", appToken);
    }
    return super.sendRequest(request);
  }
}

​ ​  从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性、是一种语法、是一种代码实现的思路,而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的。

5. D-DIP(Dependency Inversion Principle)

IOC(Inversion Of Control)

​ 通过框架,将程序中的流程“控制”权利,“反转”到框架(某个抽象类)中,实现通过框架统一代码流程。例如模板模式

DI(Dependency Injection)

​ 不通过 new 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

​ Spring 的控制反转容器是通过 DI 来实现的。

DIP

​ 简单点:可以理解为IOC的不同表达方式。

6. KISS(Keep it sample and stupid)

提倡用最简单的方法解决问题。如何保证代码符合 KISS 原则?

  1. 尽量使用低理解成本方式实现业务逻辑。如不使用复杂的正则表达式。
  2. 善于使用已有的工具类库,降低出 bug 的概率。
  3. 不过度优化,降低代码的可读性。

7. DRY(Don’t Repeat Yourself)

三种典型的代码重复情况:

  1. 实现逻辑重复:在保证 “SRP” 等原则的前提下,可以将重复逻辑进行重构。但如果函数的语义不同,但实际代码相同时,不能将其合并。

  2. 功能语义重复:函数名不同,但功能语义相同的函数,应当做合并重构。

  3. 代码执行重复:同一函数在流程中被不合理的多次执行。

    eg:优化 login 函数。

public class UserService {
  private UserRepo userRepo;//通过依赖注入或者IOC框架注入

  public User login(String email, String password) {
    boolean existed = userRepo.checkIfUserExisted(email, password);
    if (!existed) {
      // ... throw AuthenticationFailureException...
    }
    User user = userRepo.getUserByEmail(email);
    return usera;
  }
}

public class UserRepo {
  public boolean checkIfUserExisted(String email, String password) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }

    if (!PasswordValidation.validate(password)) {
      // ... throw InvalidPasswordException...
    }

    //...query db to check if email&password exists...
  }

  public User getUserByEmail(String email) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }
    //...query db to get user by email...
  }
}

优化后:

public class UserService {
  private UserRepo userRepo;//通过依赖注入或者IOC框架注入

  public User login(String email, String password) {
    if (!EmailValidation.validate(email)) {
      // ... throw InvalidEmailException...
    }
    User user = userRepo.getUserByEmail(email);
     if (user == null) {
      // ... throw AuthenticationFailureException...
    }
    return usera;
  }
}

public class UserRepo {
  public User getUserByEmail(String email) {
    //...query db to get user by email...
  }
}

重构技巧

为什么要重构?
  1. 保证在业务不断迭代过程中的代码质量
重构什么?

重构大致分为大规模高层次的重构和小规模低层次的重构:

  1. 大规模主要针对代码结构、模块化、解耦、抽象复用组件等等;
  2. 小规模主要是针对类、函数级别的重构。

解耦

​ 解耦将各个模块间的依赖简化,是控制代码复杂度的有效手段。

​ 给代码解耦的方法有:封装与抽象、中间层、模块化,以及一些设计原则,比如:单一职责原则、基于接口而非实现编程、依赖注入、少用继承(继承是一种强依赖关系)等

什么时候重构?

​ 持续重构,将重构当做开发的一部分,避免因技术、业务、需求变动带来的代码质量持续下降问题,避免开发初期过度设计的问题。

如何重构?
  1. 控制好重构影响到的代码范围,考虑好如何兼容老的代码逻辑,必要的时候写一些兼容过渡代码。
  2. 保证每一阶段的重构都不至于耗时太长(最好一天就能完成),不至于与新的功能开发相冲突。
重构的质量如何保证?

单测用例 + 集成测试用例

单测用例的作用:

  1. 写单元测试的过程就是Code Review和重构的过程,在单测中覆盖各种输入、异常、边界情况,能有效地发现代码中的bug和代码设计上的问题
  2. 单元测试是对集成测试的补充,是 TDD 可落地执行的改进方案

Q: 如何定义日常中的设计与过度设计?

编码规范

命名:

  • 一致性:团队内,对于关键业务名称的命名需要统一,不同团队尽量使整个链路命名保持一致
  • 贴合业务含义:保证命名有意义。例如:方法名需要明确方法的职责,可以比较长,但如果方法名太长,则表示方法中的内容较多,基于"职责单一"原则,考虑是否需要继续拆分
  • 命名规范:
    • 所有编程相关的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
    • 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
    • 类名使用UpperCamelCase风格,以下情形例外:DO / BO / DTO / VO / AO / UID等。
    • 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格。
    • 常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
    • POJO类中的任何布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。
  • 借鉴优秀的代码:通过阅读优秀的开源代码,尝试学习理解作者的命名思维、设计模式等。

合理使用 Optional:

  • 优化判空能力
  • orElse 与 orElseGet 的区别:
    • orElse:每次都会执行其中的函数。
    • orElseGet,只有Optional中的值为空时,它才会执行其中对应的函数。这样做的好处是可以避免提前计算结果的风险。
    • 故orElse中常常指定一个固定的变量值返回,orElseGet中常用于指定一个函数来执行。
  • 正确的换行:
    • 方法调用的点符号与下文一起换行;
    • 方法调用中的多个参数需要换行时,在逗号后进行。

合理使用 Lambda:

  • 简化 for 循环
  • 避免过长的匿名函数
  • 隐式设置的局部 final 变量
  • 抛出异常的影响:
    • Lambda 中调用的方法如果抛出 CheckedException,那么必须捕获 exception,这会导致表达式特别累赘。所以,在业务场景允许的情况下,被 Lambda 调用的方法最好抛出 UncheckedException。
    • 如果是外部接口申明了 CheckedException,可以考虑将外部接口封装一次,抛出 Runtime Exception。
    • Exception 分类请添加图片描述

讨论:

 ​  如何将代码方法论,更好的融入日常开发中?

设计模式

请添加图片描述

 ​  设计模式的本质就是解耦。创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦。

​ 我们需要掌握不同设计模式能解决的问题、应用场景。


创建型 - 工厂模式

定义:

​ 将复杂的类创建和业务使用进行解耦。在需要使用时,动态地根据不同的类型,从 factory 中选取一个创建完成的类。

解决的问题:

  • 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
  • 代码复用:创建代码抽离到独立的工厂类之后可以复用。
  • 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
  • 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。

工厂模式分为三种类型:简单工厂、工厂方法和抽象工厂。

简单工厂: 在 factory 中完成类的创建与选择

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

工厂方法: 相比于简单工厂,当类的创建、准备工作比较复杂时,可以考虑将创建的能力抽象到新的接口中。

/**
类创建接口
*/
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 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;
  }
}

//使用用例
IRuleConfigParserFactory parserFactory = ruleConfigParserFactoryMap.getParserFactory("xxx");
IRuleConfigParser parser = parserFactory.createParser();

抽象工厂:相比于工厂方法,抽象工厂让一个 factory 负责创建多个不同类型的对象

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

创建型 - 建造者模式

定义

​ 建造者模式是用来创建属性较多的复杂对象,通过设置不同的可选参数,“定制化”地创建对象。

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

解决的问题

如果一个类中有很多属性,为了避免构造函数的参数列表过长,影响代码的可读性和易用性,我们可以通过构造函数配合set()方法来解决。但是,如果存在下面情况中的任意一种,我们就要考虑使用建造者模式了。

  • 我们把类的必填属性放到构造函数中,强制创建对象的时候就设置。如果必填的属性有很多,把这些必填属性都放到构造函数中设置,那构造函数就会出现参数列表很长的问题,不易读,易出错。
  • 如果类的属性之间有一定的依赖关系或者约束条件,我们继续使用构造函数配合set()方法的设计思路,那这些依赖关系或约束条件的校验逻辑对使用方来讲,是不方便的。
  • 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,我们就不能在类中暴露set()方法。构造函数配合set()方法来设置属性值的方式就不适用了。

用例:

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方法,不能有setter方法...

  //我们将Builder类设计成了ResourcePoolConfig的内部类。
  //我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
  public static class Builder {
    //这些默认的值,放在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;
    }
  }
}


// 使用用例:这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
        .setName("dbconnectionpool")
        .setMaxTotal(16)
        .setMaxIdle(10)
        .setMinIdle(12)
        .build();

结构型 - 代理模式

定义:

​ 在不改变原始类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

解决的问题:

​ 用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志、RPC、缓存。我们将这些附加功能与业务功能解耦,放到代理类统一处理,让程序员只需要关注业务方面的开发。

eg:

**组合模式下的代理实现:**基于接口与实现类

public interface IUserController {
  UserVo login(String telephone, String password);
}

/**
需要对系统中历史的 UserController 进行能力扩展
*/
public class UserController implements IUserController {
  //...省略其他属性和方法...
  @Override
  public UserVo login(String telephone, String password) {
    //...省略login逻辑...
    //...返回UserVo数据...
  }
}

public class UserControllerProxy implements IUserController {
  private MetricsCollector metricsCollector;
  private UserController userController;

  public UserControllerProxy(UserController userController) {
    this.userController = userController;
    this.metricsCollector = new MetricsCollector();
  }

  @Override
  public UserVo login(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();

    // 代理
    UserVo userVo = userController.login(telephone, password);

    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);

    return userVo;
  }
}

//UserControllerProxy使用举例
//因为原始类和代理类实现相同的接口,是基于接口而非实现编程,外部使用时只感知到接口层面,不必关心实现层
//将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userController = new UserControllerProxy(new UserController());

**继承方式下的代理:**继承与重写实现,适用于 UserController 无实现接口的场景

public class UserControllerProxy extends UserController {
  private MetricsCollector metricsCollector;

  public UserControllerProxy() {
    this.metricsCollector = new MetricsCollector();
  }
  
  @Override
  public UserVo login(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();

    UserVo userVo = super.login(telephone, password);

    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);

    return userVo;
  }
}

//UserControllerProxy使用举例
UserController userController = new UserControllerProxy();

动态代理:

在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。


行为型 - 观察者模式

​ 有同步阻塞的实现方式,也有异步非阻塞的实现方式;有进程内的实现方式,也有跨进程的实现方式。

定义:

​ 在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,通过接口中的抽象方法,通知到所有依赖这个接口的对象。

​ 通常,我们将观察者命名为Observer,被观察者命名为Observable

解决的问题:

​ 将不同的行为逻辑,解耦到不同的类中去,让代码更加符合简单、单一职责原则。

eg:用户完成注册后,需要对该账户做一系列的额外操作:

请添加图片描述

扩展:

​ Google Guava EventBus 是观察者模式的一个通用框架,框架内通过 @Subscribe 标识 Observer 中的抽象能力,通过 EventBus#post(Object) 通知 Observer。

行为型 - 模板模式

定义:

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

​ 这里的“算法”,可以理解为广义上的“业务逻辑”;算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。

​ 在模板模式经典的实现中,模板方法定义为final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。

解决的问题:

1. 公共逻辑复用

​ 模板模式把一个算法中不变的流程抽象到父类的模板方法(abstract method)中,将可变的部分留给子类实现。所有的子类都可以复用父类中模板方法定义的流程代码。eg:

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);
    boolean modified = false;
    for (E e : c) {
        add(index++, e);
        modified = true;
    }
    return modified;
}

public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

//AbstractList类中,addAll()函数可以看作模板方法,add()是子类需要重写的方法,尽管没有声明为abstract的,但函数实现直接抛出了UnsupportedOperationException异常。
//意味着如果子类不重写add()方法,是不能使用的。

2. 方便扩展

​ 指的是框架的扩展性,可以在不修改框架源码的情况下,定制化框架的功能。

与同步回调的对比:

​ 从应用场景上来看,二者几乎一致,都可以实现代码复用和扩展;

​ 从代码实现上来看,回调和模板模式完全不同。回调于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

​ 基于组合优于继承的原则,在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点:

  • 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  • 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
  • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们将不同模板需要的抽象方法放到定义的接口中,使用时只需要往用到的模板方法中注入接口的实现类即可。

请添加图片描述

行为型 - 策略模式

定义:

​ 定义一族算法类,将每个算法分别封装起来,使算法的变化独立于使用它们的客户端。

解决的问题:

1. 将对策略(算法)的定义、创建、使用这三部分进行解耦。

  • 定义(interface):包含一个策略接口和一组实现这个接口的策略类。
  • 创建(factory):由工厂类来完成,封装策略创建的细节。
  • 使用(service):包含一组策略可选,一般场景下,客户端在“运行时动态确定”选择使用哪个策略。

2. 控制代码的复杂度,让每个算法部分独立,且不至于过于复杂、代码量过多

eg:实现对一个文件进行排序的功能。文件中只包含整型数,并且,相邻的数字通过逗号来区隔。(重点在代码结构设计,不实现具体的算法)

/**
排序业务功能提供类
  */
public class SorterService {
  private static final long GB = 1000 * 1000 * 1000;
  private static final List<AlgRange> algs = new ArrayList<>();
  static {
    algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
    algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
    algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
    algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
  }

  //排序能力
  public void sortFile(String filePath) {
    // 省略校验逻辑
    File file = new File(filePath);
    long fileSize = file.length();
    ISortAlg sortAlg = null;
    for (AlgRange algRange : algs) {
      if (algRange.inRange(fileSize)) {
        sortAlg = algRange.getAlg();
        break;
      }
    }
    sortAlg.sort(filePath);
  }
	
  //静态内部类,文件大小与对应排序算法的模型
  private static class AlgRange {
    private long start;
    private long end;
    private ISortAlg alg;

    public AlgRange(long start, long end, ISortAlg alg) {
      this.start = start;
      this.end = end;
      this.alg = alg;
    }

    public ISortAlg getAlg() {
      return alg;
    }

    public boolean inRange(long size) {
      return size >= start && size < end;
    }
  }
}


/**
工厂类,完成对象的创建与封装。
*/
public class SortAlgFactory {
  private static final Map<String, ISortAlg> algs = new HashMap<>();

  static {
    algs.put("QuickSort", new QuickSort());
    algs.put("ExternalSort", new ExternalSort());
    algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
    algs.put("MapReduceSort", new MapReduceSort());
  }

  public static ISortAlg getSortAlg(String type) {
    if (type == null || type.isEmpty()) {
      throw new IllegalArgumentException("type should not be empty.");
    }
    return algs.get(type);
  }
}


/**
排序能力抽象接口
*/
public interface ISortAlg {
  void sort(String filePath);
}

public class QuickSort implements ISortAlg {
  @Override
  public void sort(String filePath) {
    //...
  }
}

行为型 - 职责链模式

定义:

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

解决的问题

​ 将多个同类型、不同功能的处理器实现拆分开,但通过数组或链表的方式将他们串在一起执行,保留了扩展能力,符合开闭原则,且让各个处理器中的代码变的更加简单。

链表实现方式:

//Handler及子类

public abstract class Handler {
  protected Handler successor = null;

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

  public final void handle() {
    //当有nextHandle且返回为false时,
    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 构建
public class HandlerChain {
  private Handler head = null;
  private Handler tail = null;

  public void addHandler(Handler handler) {
    handler.setSuccessor(null);

    if (head == null) {
      head = handler;
      tail = handler;
      return;
    }

    tail.setSuccessor(handler);
    tail = handler;
  }

  public void handle() {
    if (head != null) {
      head.handle();
    }
  }
}

// 使用举例
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 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) {
        //当处理器返回true时,链执行中断。
        break;
      }
    }
  }
}

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

参考文献:

《设计模式之美》

如何提高代码可读性

写好代码的建议

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值