细节之处见真章 - 请求对象 trim 最佳实践

2023-03-15 09:45:22 浏览数 (4)

一、背景

日常开发中,经常需要对前端传入的请求对象(如 StudentQueryVO)的某些属性执行 trim 操作,比如搜索的关键字、输入的名称等。 很多人会选择在用到的时候,对其中的属性执行 trim 操作然后再使用。这样做很容易出现: (1)有些用到的地方想起来执行了 trim,有些地方没有 trim,很难保持一致。 (2)不同的地方需要执行相似的 trim 操作,代码复用性不高。

二、方案

2.1 入口处 trim 后设置回去

我们可以选择在 Facade / Controller 层,取出对应的属性,执行 trim 操作,然后赋值回去。 这样不管内部执行了多少次转换,不管多少次使用这些待 trim 的属性,都不会忘记处理,也都不需要重复处理。

代码语言:javascript复制
public PageResult<Student> queryStudents(StudentQuery query){
   //1 执行 trim
    String name = query.getName();
    if (name != null && !name.isEmpty()) {
      query.setName(name.trim());
    }

    String nickname = query.getNickname();
    if (nickname != null && !nickname.isEmpty()) {
      query.setNickname(nickname.trim());
    }

   //2 执行查询
    studentService.pageQuery(query);
}

这样写虽然可以解决问题,但是不太优雅。

2.2 将 trim 逻辑封装在请求对象内部

我们可以对上述方案再一次优化。 可以在构造查询对象时自动执行 trim 方法,也可以在外部执行一次 trim 方法即可。 这样将 trim 的逻辑封装在查询对象内部,可以尽可能降低耦合,后面如果新增属性也需要 trim ,直接在 trim 方法里处理掉即可,外部也不需要感知。

代码语言:javascript复制
package org.example.demo;

// 定义一个学生查询类
public class StudentQuery {
    // 定义学生的学号、姓名、昵称属性
    private String id; // 学号
    private String name; // 姓名
    private String nickname; // 昵称

    public StudentQuery() {
    }

    // 定义构造方法,用于创建学生查询对象
    public StudentQuery(String id, String name, String nickname) {
        this.id = id;
        this.name = name;
        this.nickname = nickname;

        // 自动 trim
        trim();
    }

    // 定义获取和设置属性的方法
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

   // 定义一个 trim 方法,用于去除属性的空格
    public void trim() {
        // 调用 trim 方法赋值给 name 属性
        name = StringUtils.trimToNull(name);

        // 调用 trim 方法赋值给 nickname 属性
        nickname = StringUtils.trimToNull(nickname);
    }
}

底层建议使用 org.apache.commons.lang3.StringUtils#trimToNull 可以让 trim 更简洁。

代码语言:javascript复制
    /**
     * <p>Removes control characters (char &lt;= 32) from both
     * ends of this String returning {@code null} if the String is
     * empty ("") after the trim or if it is {@code null}.
     *
     * <p>The String is trimmed using {@link String#trim()}.
     * Trim removes start and end characters &lt;= 32.
     * To strip whitespace use {@link #stripToNull(String)}.</p>
     *
     * <pre>
     * StringUtils.trimToNull(null)          = null
     * StringUtils.trimToNull("")            = null
     * StringUtils.trimToNull("     ")       = null
     * StringUtils.trimToNull("abc")         = "abc"
     * StringUtils.trimToNull("    abc    ") = "abc"
     * </pre>
     *
     * @param str  the String to be trimmed, may be null
     * @return the trimmed String,
     *  {@code null} if only chars &lt;= 32, empty or null String input
     * @since 2.0
     */
    public static String trimToNull(final String str) {
        final String ts = trim(str);
        return isEmpty(ts) ? null : ts;
    }

下面写个简单的单测验证下:

代码语言:javascript复制
import org.junit.Test;

import static org.junit.Assert.assertEquals;


public class StudentQueryTest {

    // 直接使用全参构造方法
    @Test
    public void testStudentQueryAllArgs() {
        // 构造一个 StudentQuery 对象
        StudentQuery sq = new StudentQuery("001", " Alice ", "  Bob ");

        // 断言属性的值是否符合预期
        assertEquals("001", sq.getId());
        assertEquals("Alice", sq.getName());
        assertEquals("Bob", sq.getNickname());
    }

    // 使用午无参构造方法
    @Test
    public void testStudentQueryNoArgs() {
        // 构造一个 StudentQuery 对象
        StudentQuery sq = new StudentQuery();
        // 调用 set 方法给出一些随机值
        sq.setId("002");
        sq.setName(" Charlie");
        sq.setNickname(" David ");
        // 调用 trim 方法去除空格
        sq.trim();

        // 断言属性的值是否符合预期
        assertEquals("002", sq.getId());
        assertEquals("Charlie", sq.getName());
        assertEquals("David", sq.getNickname());
    }
}

上面的请求就可以简化为:

代码语言:javascript复制
public PageResult<Student> queryStudents(StudentQuery query){
   //1 执行 trim
    query.trim();

   //2 执行查询
    studentService.pageQuery(query);
}

如果是服务内部调用,构造 StudentQuery 时使用全参构造方法,就可以自动调用 trim 不需要显示处理,会更加清爽。

三、启发

细节之处见真章,代码的功底并不都体现在大的地方,日常开发中自己觉得“别扭”的地方,都可以停下来斟酌优化一番。

大家在思考代码优化方案时,主要遵循软件设计的常见原则,如高内聚、弱耦合、降低复杂度的原则。

重点参考设计模式的几大原则。 • 单一职责原则 (Single Responsibility Principle):一个类或者一个方法只负责一项职责,避免过多的功能耦合在一起。 • 开放-关闭原则 (Open-Closed Principle):一个软件实体应该对扩展开放,对修改关闭,即在不改变原有代码的基础上增加新的功能。 • 里氏替换原则 (Liskov Substitution Principle):子类应该能够完全替代父类,并且保持程序的正确性和稳定性,避免子类违背父类的约定。 • 依赖倒转原则 (Dependence Inversion Principle):高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象,即要针对接口编程,而不是针对实现编程12。 • 接口隔离原则 (Interface Segregation Principle):一个接口应该尽可能小,只包含客户端需要的方法,避免出现臃肿的接口。 • 迪米特法则(Law Of Demeter),又叫“最少知道法则”:一个对象应该尽可能少地与其他对象发生相互作用,只与直接相关的对象通信,降低对象之间的耦合度。 • 组合/聚合复用原则 (Composite/Aggregate Reuse Principle):在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的,优先使用组合或者聚合关系复用代码,而不是使用继承。

本文重点采用了迪米特法则来讲 trim 的逻辑封装在请求对象内部,避免 trim 的逻辑外溢,对使用者非常友好。

四、总结

本文讲解的内容虽然比较简答,但是实际开发中很多同学采用到处 trim 导致遗漏后者逻辑重复的问题,本文给出推荐的方案,希望对大家有帮助。

0 人点赞