如何提高代码质量

news2024/11/13 4:42:49

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

如何定义“好”的代码?

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

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

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

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

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

请添加图片描述

面向对象

特性

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

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

参考文献:

《设计模式之美》

如何提高代码可读性

写好代码的建议

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/392115.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

scrpy学习-02

新浪微博[Scrapy 教程] 3. 利用 scrapy 爬取网站中的详细信息 - YouTubedef parse(self,response):soup BeautifulSoup(response.body,html.parser)tags soup.find_all(a,hrefre.compile(r"sina.*\d{4}-\d{2}-\d{2}.*shtmls"))#匹配日期for tag in tags:url tag.get(…

Android性能优化-UI优化

文章目录一.Android绘制原理View绘制过程双缓冲机制布局加载原理布局加载优化1. AsyncLayoutInflater方案2. X2C方案3. Compose方案二.布局优化三.绘制优化1. 去掉多余背景色,减少复杂shape的使用2. 自定义View使用clipRect屏蔽被遮盖View绘制3.onDraw 中不要创建新的局部对象。…

基于Citespace和vosviewer文献计量学可视化SCI论文高效写作方法

文献计量学是指用数学和统计学的方法&#xff0c;定量地分析一切知识载体的交叉科学。它是集数学、统计学、文献学为一体&#xff0c;注重量化的综合性知识体系。特别是&#xff0c;信息可视化技术手段和方法的运用&#xff0c;可直观的展示主题的研究发展历程、研究现状、研究…

磁盘分区和挂载

磁盘分区和挂载一、linux分区1.原理介绍2.分区和文件关系示意图&#xff1a;3.硬盘说明二、linux分区1.查看所有设备挂载情况三、挂载案例1.使用lsblk命令查看2. 虚拟机硬盘分区3.虚拟机硬盘分区格式化4.mount挂载 重启挂载失效4.1挂载名词解释4.2注意事项4.3挂载4.4挂载非空目…

网上订餐管理系统的设计与实现

技术&#xff1a;Java、JSP等摘要&#xff1a;随着信息技术的广泛使用&#xff0c;电子商务对于提高管理和服务水平发挥着关键的作用。越来越多的商家开始着手于电子商务建设。电子商务的发展为人们的生活提供了极大的便利&#xff0c;也成为现实社会到网络社会的真实体现。当今…

【java基础】类型擦除、桥方法、泛型代码和虚拟机

文章目录基础说明类型擦除无限定有限定转换泛型表达式方法类型擦除&#xff08;桥方法&#xff09;关于重载的一些说明总结基础说明 虚拟机没有泛型类型对象一所有对象都属于普通类。在泛型实现的早期版本中&#xff0c;甚至能够将使用泛型的程序编译为在1.0虚拟机上运行的类文…

L - Let‘s Swap(哈希 + 规律)

2023河南省赛组队训练赛&#xff08;四&#xff09; - Virtual Judge (vjudge.net) 约瑟夫最近开发了一款名为Pandote的编辑软件&#xff0c;现在他正在测试&#xff0c;以确保它能正常工作&#xff0c;否则&#xff0c;他可能会被解雇!Joseph通过实现对Pandote上字符串的复制和…

文件上传和下载(原生JS + SpringBoot实现)

目录 概述 前端编写-上传表单和图片回显 HTML表单代码 发送请求逻辑 CSS代码 后端编写-文件上传接口 后端编写-文件下载接口 概述 在现代Web应用程序中&#xff0c;文件上传和下载是常见的功能。本博客将介绍如何使用原生JS和Spring Boot实现文件上传和下载的功能。 在其…

vue移动端h5,文本溢出显示省略号,且展示‘更多’按钮

问题&#xff1a; 元素宽度100%&#xff0c;宽度会随着浏览器缩放而变化。元素内文本超过4行时显示省略号&#xff0c;同时展示‘更多’按钮&#xff0c;点击更多按钮展示全部文本。如下图所示 超出四行显示省略号(…)的代码 .content{overflow:hidden;text-overflow: elli…

【Spring】入门概述(一)

&#x1f697;Spring学习第一站~ &#x1f6a9;本文已收录至专栏&#xff1a;Spring家族学习之旅 &#x1f44d;希望您能有所收获 一.初识 Spring并不是单一的一个技术&#xff0c;而是一个大家族&#xff0c;发展到今天已经形成了一种开发的生态圈&#xff0c;Spring提供了若…

JavaScript(2)

一、事件 HTML事件是发生在hTML元素上的“事情”。比如&#xff1a;按钮被点击、鼠标移动到元素上等… 事件绑定 方式一&#xff1a;通过HTML标签中的事件属性进行绑定 <input type"button" value"点我" onclick"on()"><script>fun…

C++基础 | 从C到C++快速过渡

一、开发环境 c使用的编译器是g。 vim或者vscodeclionVS 二、C版本的Hello World /*** brief c版本helloworld示例* author Mculover666* date 2023/2/26*/#include <iostream> using namespace std;int main() {int a 1;double b 3.14;char c[] "str…

软件测试用例篇(4)

测试知识回顾: 我们想要根据需求来写测试用例&#xff0c;首先要保证的需求的合理性和正确性&#xff0c;首先要验证需求&#xff0c;需求合理&#xff0c;理解需求&#xff0c;细化需求&#xff0c;把大需求细化成小需求&#xff0c;根据每一个小需求提炼出功能点根据每一个功…

html+css 实现 熊猫样式

效果 html代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><meta http-equiv"X-UA-Compatible"…

【MySQL高级篇】第01章 Linux下MySQL的安装与使用

第01章 Linux下MySQL的安装与使用 1. 安装前说明 1.1 查看是否安装过MySQL 如果你是用rpm安装, 检查一下RPM PACKAGE&#xff1a; rpm -qa | grep -i mysql # -i 忽略大小写检查mysql service&#xff1a; systemctl status mysqld.service1.2 MySQL的卸载 1. 关闭 mysql …

工控机ARM工业边缘计算机搭建Node-Red环境

搭建Node-Red环境Node-RED是一个基于Node.js的开源可视化流程编程环境&#xff0c;可以轻松构建自定义应用程序&#xff0c;通过连接简单的节点来完成复杂的任务。Node-RED提供了一种简单的方法&#xff0c;可以快速连接到外部服务&#xff0c;从而实现物联网应用的开发。Node-…

乡村企业门户网站

技术&#xff1a;Java、JSP等摘要&#xff1a;随着时代的发展&#xff0c;电脑与Internet已经进入我们的生活。信息时代的来临&#xff0c;知识经济的扩张&#xff0c;网站已越来越靠近我们的生活。据CNNIC报告显示&#xff0c;中国上网用户有6800万。通过Internet来经营运作一…

寻找时空中的引力波:科学家控制量子运动至量子基态

据英国每日邮报报道&#xff0c;时空织布里的涟漪或可以揭示宇宙在140亿年前是如何产生的&#xff0c;然而寻找这些名为“引力波”的涟漪却一直难以捉摸。现在美国科学家们声称他们发现了改善用于检测宇宙大爆炸的引力波的探测器的方法。 ​宇宙大爆炸残留的引力波 美国加州理…

电脑文件软件搬家迁移十大工具

10 大适用于 Windows 的数据迁移软件。 数据迁移至关重要&#xff0c;几乎所有组织都依赖于此。如果您认为数据传输不是一件容易的事&#xff0c;那么数据迁移软件可以帮上忙。 1、奇客电脑迁移 将现有操作系统、软件、文件迁移到 新电脑的最佳方法之一是使用名为奇客电脑迁移…

SpringMvc快速启动

Spring快速启动 1、tomcat配置 仔细查看下图标记位置配置 添加Tomcat server时选择LocalApplcation server选择Http port与JMX port 2、Project Structure 打开FIle -> Project Structure&#xff0c; 确认WEB-INFO下是否添加了lib&#xff0c;并将.jar包加入lib包中 …