如何写好代码

我经常会问自己一个问题"如何写好代码",源于我真的想写出"好的","易于阅读、理解和维护"的代码。但是写好代码是不容易的,那么我们能不能反过来思考,如何写出不好的代码。想到这里,我问了文心大模型一个问题在软件开发领域,如何写出不好阅读,不易于维护的代码呢, 它的回答如下图所示。

像上面所说的不好的代码,在日常开发中基本上是随处可见到。

看一看,说一说

个人见解

在现实世界中有很多"美"的标准,每个人的标准不太一样,下面,我就按我的理解来给大家分享几点。

  1. 在日常企业应用开发中,我们常见的是基于SpringBoot实现的三层或四层代码结构,常见的是使用贫血模型, 即POJO中有数据,没有行为。本该属于实体模型的行为散落在服务层,即没有实现"高内聚"。

比如下面这段代码,从Dao层获取到文章的附件列表后,开始循环处理附件的完整访问路径(因为我们经常在数据库中只存储资源的相对路径,在实际使用前再构造完整的绝对路径)和 替换逻辑。类似这样的代码,我觉得应该内聚在实体模型中,尽量复用,减少重复。

比如这样写:根据配置及实体信息,构造完整的访问路径。

  1. 将代码段合理拆分成语义更强的方法 比如下面这段代码,for循环在干啥?是不是不方便一下子看出来?
ini 复制代码
@Override
int saveSubData(List<ArticleAttachmentVo> subDataList) {
    if (CollUtil.isEmpty(subDataList)) {
        return 0;
    }
    int successCount = 0;
    for (var item : subDataList) {
        String rowGuid = item.getId().toString();
        DigitalHouseArticleAttachment digitalHouseArticleAttachment = selectById(rowGuid);
        if (Objects.isNull(digitalHouseArticleAttachment)) {
            digitalHouseArticleAttachment = DigitalHouseArticleAttachmentConverter.INSTANCE.toEntity(item);
            insert(digitalHouseArticleAttachment);
        } else {
            DigitalHouseArticleAttachmentConverter.INSTANCE.updateEntity(item, digitalHouseArticleAttachment);
            updateByPrimaryKey(digitalHouseArticleAttachment);
        }
        successCount++;
    }
    return successCount;
}

下面这段好理解吗?

ini 复制代码
@Override
int saveSubData(List<ArticleAttachmentVo> subDataList) {
    if (CollUtil.isEmpty(subDataList)) {
        return 0;
    }
    int successCount = 0;
    for (var item : subDataList) {
        try {
            saveSubData(item);
            successCount++;
        } catch (Exception ex) {
            String bizException = String.format("同步文章-附件错误, articleId:%d, id: %d", item.getArticleId(), item.getId());
            throw new RuntimeException(bizException, ex);
        }
    }
    return successCount;
}

@Override
void saveSubData(ArticleAttachmentVo item) {
    String rowGuid = item.getId().toString();
    DigitalHouseArticleAttachment digitalHouseArticleAttachment = digitalhouseArticleAttachmentDao.selectById(rowGuid);
    if (Objects.isNull(digitalHouseArticleAttachment)) {
        digitalHouseArticleAttachment = DigitalHouseArticleAttachmentConverter.INSTANCE.toEntity(item);
        digitalhouseArticleAttachmentDao.insert(digitalHouseArticleAttachment);
    } else {
        DigitalHouseArticleAttachmentConverter.INSTANCE.updateEntity(item, digitalHouseArticleAttachment);
        digitalhouseArticleAttachmentDao.updateByPrimaryKey(digitalHouseArticleAttachment);
    }
}

是不是感觉好理解了一点?在日常编码过程中,可以尝试把代码段拆分成语义更强的方法,让主体方法结构更简单,充分表现业务语义结构,增强代码的可读性,提高代码的可维护性。

  1. 减少接口中暴露的方法

左侧接口中的insert/selectById/updatePrimaryKey等方法,在同步场景之外暂无用武之地,那么是不是可以考虑先不要暴露出去,后续有业务场景了再暴露?基于这个考虑,我们将代码重构成右边这样子,只暴露一个sync方法。

4.减少代码的副作用

在程序开发中,什么是副作用?

  1. 在程序开发中,副作用(Side Effect)指的是一个函数或表达式在执行过程中对外部环境产生的除了其返回值之外的任何可观察的影响。
  2. 具体来说,副作用可能影响全局状态、文件系统、外部设备等。例如,函数可能会修改全局变量或静态变量,可能会对文件系统进行读写操作,或者执行网络操作,甚至可能包括控制台输出,例如打印日志。
  3. 副作用是函数式编程语言和面向对象/过程式编程语言之间的一个主要区别。在函数式编程中,函数的主要目的是返回一个值,而不是产生副作用。而在面向对象/过程式编程中,副作用通常被视为编程中的重要部分。
  4. 了解副作用的优点和缺点是很重要的。尽管副作用在某些情况下可以使代码更简洁、更易于理解,但它们也可能导致一些问题,例如使代码更难维护、测试和重用。因此,在编写代码时,应尽量减少不必要的副作用,并只在必要时使用它们。

像下面这段代码,它的本意是基于相对地址构造完整的访问地址。我们后来突然领悟到这段代码是存在副作用的,它改变了downlodUrlWord及downloadUrlPdf的值。虽然它做了特殊处理,可以保证"幂等性",但是依然不推荐这样写。应该将这个方法中的两段代码放在实体模型中,写成两个getter,逻辑内聚在实体中,增加了复用的可能性和方便性。

less 复制代码
/**
 * 处理自动生成的word和pdf的链接地址为绝对地址
 */
public void processWordAndPdf(SiteConfig site) {
    var article = this;
    if (StringUtils.isNotBlank(article.getDownloadUrlWord())
            && !article.getDownloadUrlWord().startsWith("http")) {
        article.setDownloadUrlWord(site.getMohurdHost() + article.getDownloadUrlWord());
    }
    if (StringUtils.isNotBlank(article.getDownloadUrlPdf())
            && !article.getDownloadUrlPdf().startsWith("http")) {
        article.setDownloadUrlPdf(site.getMohurdHost() + article.getDownloadUrlPdf());
    }
}
  1. 注意为类成员或函数中的代码行留白

在排版上,也需要注意,不要将代码行堆叠在一起,应该合理引入变量、增加空行、增加注释,让代码看起来整洁,便于阅读。

比如下面这段代码,就存在2个问题:

  • 缺少空行,代码密密麻麻的,不便于阅读。
  • setResult/setName中的表达式有点复杂,不便于理解语义。
ini 复制代码
@Mapper
public interface SourceMapper {
    SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
    @Mapping(source = "totalCount", target = "count")
    @Mapping(source = "subSource", target = "subTarget")
    Target source2target(Source source);
    default SubTarget subSource2subTarget(SubSource subSource) {
        if (subSource == null) {
            return null;
        }
        SubTarget subTarget = new SubTarget();
        subTarget.setResult(!subSource.getDeleted().equals(0));
        subTarget.setName(subSource.getName()==null?"":subSource.getName()+subSource.getName());
        return subTarget;
    }
}
  1. 注重类的设计,尤其是单一职责原则/接口隔离原则/最少知识原则。 比如下面这个类,明明是一个HttpHelper,结果其中确掺杂了记录日志的代码逻辑。

如何才能写出更好的程序呢?

我觉得首先需要我们有"想写好代码"的意识和"什么是好代码"的审美。然后在编程任务中,开始注重类的设计、方法的设计等等。

在面向对象程序设计领域,比较推崇的还是七大设计原则和23种设计模式。

面向对象设计原则

在面向对象设计中,有以下七大设计原则:

  1. 单一职责原则(Single Responsibility Principle):一个类只做一件事,只有一个引起它变化的原因。这个原则有助于降低类之间的耦合度,提高代码的可读性和可维护性。
  2. 开闭原则(Open/Closed Principle):软件实体应当对修改关闭,对扩展开放。这意味着一个软件实体应该通过扩展(而不是修改)来增加新的功能,从而提高代码的可维护性和可重用性。
  3. 依赖倒置原则(Dependency Inversion Principle):依赖于抽象,而不要依赖于具体,因为抽象相对稳定。这个原则有助于降低类之间的依赖关系,提高代码的可维护性和可重用性。
  4. 接口隔离原则(Interface Segregation Principle):尽量应用专门的接口,而不是单一的总接口,接口应该面向用户,将依赖建立在最小得接口上。这个原则有助于减少类之间的耦合度,提高代码的可维护性和可重用性。
  5. 里氏替换原则(Liskov Substitution Principle):子类必须能够替换其基类。这个原则有助于确保子类与基类之间的继承关系符合开闭原则,从而提高代码的可重用性和可维护性。
  6. 合成/聚合复用原则(Composition/Aggregation Reuse Principle):在新对象中聚合已有对象,使之成为新对象的成员,从而通过操作这些对象达到复用的目的。合成方式较继承方式耦合更松散,所以应该少继承,多聚合。这个原则有助于减少类之间的耦合度,提高代码的可重用性和可维护性。
  7. 迪米特法则(最少知识原则)(Law of Demeter/Least Knowledge Principle):软件实体应该尽可能少的和其他软件实体发生相互作用。这个原则有助于降低类之间的耦合度,提高代码的可重用性和可维护性。

23种设计模式

  • 创建型模式:主要解决对象创建问题
  • 结构型模式:主要解决对象组合问题
  • 行为型模式:主要解决对象之间的交互问题
创建型模式

Java中的创建型设计模式主要用于对象的创建和组装。这些模式通过抽象化和解耦对象的创建过程,可以使系统更加灵活和可扩展。下面是Java中的5种创建型设计模式:

  • 单例模式:确保一个类只有一个实例,并提供一个全局访问点。
  • 简单工厂模式:在不暴露创建对象的逻辑的前提下,使用工厂方法来创建对象。
  • 工厂方法模式:定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。
  • 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的系列,而不需要指定实际实现类。
  • 建造者模式:将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。
  • 原型模式 : 通过克隆来创建对象,避免了通过new关键字显式调用构造函数的开销。
结构型模式

Java中的结构型设计模式主要用于描述对象之间的关系,包括类和对象的组合、接口和继承等方面。这些模式可以帮助我们更好地组织和管理代码,提高代码的重用性和可维护性。下面是Java中的7种结构型设计模式:

  • 适配器模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而无法一起工作的类可以一起工作。

类适配器模式

对象适配器模式

  • 桥接模式:将抽象部分与它的实现部分分离,以便它们可以独立地变化。
  • 组合模式 :将对象组合成树形结构以表示"部分-整体"的层次结构,使得客户端使用单个对象或者组合对象具有一致性。
  • 装饰器模式:动态地给一个对象添加一些额外的职责,就增加功能而言,装饰器模式比生成子类方式更为灵活。
  • 外观模式:为子系统中的一组接口提供一个一致的界面,使得子系统更容易使用。
  • 享元模式:运用共享技术来有效地支持大量细粒度对象的复用。
  • 代理模式:为其他对象提供一种代理以控制对这个对象的访问。
行为型模式

Java中的行为型设计模式主要用于描述对象之间的通信和协作方式,包括算法、责任链、状态等方面。这些模式可以帮助我们更好地组织和管理代码,提高代码的可维护性和可扩展性。下面是Java中的11种行为型设计模式:

  • 责任链模式:为解除请求的发送者和接收者之间的耦合,而将请求的处理对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
  • 命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
  • 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子。
  • 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示。
  • 中介者模式:用一个中介对象封装一系列的对象交互,使得这些对象不需要显示地相互引用,从而降低耦合度。
  • 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
  • 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。
  • 状态模式:允许一个对象在其内部状态发生改变时改变其行为,对象看起来似乎修改了它的类。
  • 策略模式:定义一系列的算法,将每个算法封装起来,并使它们之间可以互换。
  • 模板方法模式:定义一个操作中的算法骨架,将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤。
  • 访问者设计模式:表示一个作用于某对象结构中的各个元素的操作。访问者模式可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

借助代码质量检查工具

  • Alibaba Java Coding Guidelines 集成了阿里巴巴的代码规范和最佳实践,可以帮助开发者保持代码规范和一致性。

  • CheckStyle 代码规范检查,代码质量改进。可以自定义规则。

  • FindBugs

    • 静态代码分析:Bug IDEA插件可以执行静态代码分析,以检测潜在的编码错误、代码风格问题和性能问题。它可以识别诸如未使用的变量、空指针异常、不一致的缩进、未处理的异常等问题。
    • 代码质量评估:该插件可以生成代码质量报告,帮助开发人员了解代码的质量水平。这些报告通常包括代码复杂性、代码覆盖率、代码重复率等指标。
    • 检查代码规范:Bug IDEA插件可以根据您选择的编码标准或规范检查代码,确保团队的代码风格一致性,并提供建议以改进代码。
    • 自动修复问题:插件通常提供了自动修复选项,可以帮助开发人员快速修复代码中的问题,提高代码的质量和可维护性。
    • 安全漏洞检测:有些Bug IDEA插件还可以检测潜在的安全漏洞,如SQL注入、跨站脚本攻击等,以提高应用程序的安全性。
    • 代码重构建议:插件还可以提供有关代码重构的建议,帮助改进代码的结构和性能。
  • SonarLint

    SonarLint提供了更广泛的代码质量和安全性检查,让你在编写代码的过程中就能发现和解决问题。相比之下,FindBugs更专注于静态代码分析,帮助开发人员发现潜在的bug模式。

其他建议

  • 多读读别人的代码:建议阅读java,spring,mybatis的一些源码,多学学人家的编码风格和技巧。
  • 团队内部多多交流

书籍推荐

  • 《重构》
  • 《代码整洁之道》
  • 《架构整洁之道》

参考资料

相关推荐
黄俊懿6 分钟前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
2401_8574396933 分钟前
“衣依”服装销售平台:Spring Boot技术应用与优化
spring boot·后端·mfc
Jerry.ZZZ1 小时前
系统设计,如何设计一个秒杀功能
后端
九圣残炎3 小时前
【springboot】简易模块化开发项目整合Redis
spring boot·redis·后端
.生产的驴3 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛3 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
爱学的小涛3 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪3 小时前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
爱码少年3 小时前
springboot工程中使用tcp协议
spring boot·后端·tcp/ip
2401_8576226611 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php