编码规则:正确名称、好的和坏的评论的力量 - 1您有多少次不得不深入研究别人的代码?你可能会花两天时间来简单地理解正在发生的事情的逻辑,而不是两个小时。有趣的是,对于编写代码的人来说,一切都是清晰且完全透明的。这并不奇怪:毕竟,完美代码是一个非常模糊的概念,因为每个开发人员对世界和代码都有自己的看法。我不止一次遇到这样的情况,我和一位同事查看相同的代码,但对其正确性和整洁性有不同的看法。编码规则:正确名称的力量,好的和坏的评论 - 2听起来很熟悉,不是吗?尽管如此,还是有一些经过时间考验的原则需要遵守。最终,它们会对您有利,因为如果您将代码保持在您自己希望接收它的状态,那么世界会变得更快乐、更清洁。在我们之前的文章中(或者更确切地说,小指南)关于编码规则,我们对编写一个系统作为一个整体及其组成部分,如对象、接口、类、方法和变量,有了一些建议。在同一篇文章中,我不经意地提到了某些元素的正确命名。我今天想谈谈这个,因为正确的名称使代码更容易阅读。我们将通过一些反思、代码注释的小示例以及对这是否好的考虑来结束正确代码的主题。好吧,让我们开始吧。

正确的名字

正确的名称可以提高代码的可读性,从而减少熟悉代码所需的时间,因为当方法的名称大致描述了它的功能时,使用方法会容易得多。代码中的一切都由名称(变量、方法、类、对象、文件等)组成,因此在创建正确、干净的代码时这一点变得非常重要。基于以上所述,名称应该传达含义,例如变量存在的原因、作用、用途等。我会不止一次地指出,对变量最好的注释是给它起一个好名字。编码规则:正确名称的力量,好的和坏的评论 - 3

来自电视剧“夏洛克”(2010-2017)

命名接口

接口的名称通常以大写字母开头,并以 CamelCase 书写。在编写接口时,添加前缀“I”以将其指定为接口(例如,IUserService)曾经被认为是一种很好的做法,但这看起来很丑陋并且让人分心。在这种情况下,最好省略前缀 (UserService) 并将“Impl”作为后缀添加到其实现的名称中(例如 UserServiceImpl)。或者,作为最后的手段,在实现的名称中添加一个“C”前缀(例如 CUserService)。

类名

就像接口一样,类名也是大写的并且使用 CamelCase。我们是否面临僵尸末日并不重要,末日是否临近也无关紧要 - 永远,永远,永远不要让类名成为动词!类名和对象名必须是名词或复合名词(UserController、UserDetails、UserAccount 等)。您不应将应用程序的缩写附加到每个类名称的末尾,因为那样只会增加不必要的复杂性。例如,如果我们有一个用户数据迁移应用程序,那么请不要在每个类中添加“UDM”,即 UDMUserDetails、UDMUserAccount、UDMUserController。

方法名称

通常,方法名以小写字母开头,但它们也使用驼峰式(camelCase)。上面,我们说过类名永远不应该是动词。这里的情况正好相反:方法的名称应该是动词或动词短语:findUserById、findAllUsers、createUser 等等。在创建方法(以及变量和类)时,请使用一致的命名约定以避免混淆。例如,要查找用户,可以将方法命名为 getUserById 或 findUserById。还有一件事:不要在方法名称中使用幽默,因为其他人可能无法理解这个笑话。结果,他们可能无法理解该方法的作用。

变量名

在大多数情况下,变量名以小写字母开头,也使用驼峰式命名,除非变量是全局常量。在这种情况下,名称的所有字母都以大写形式书写,单词之间用下划线(“_”)分隔。为方便起见,您可以在命名变量时使用有意义的上下文。换句话说,当变量作为更大的事物的一部分存在时,例如 firstName、lastName 或 status。在这种情况下,您可以添加一个前缀来指示该变量所属的对象。例如:userFirstName、userLastName、userStatus。当变量具有完全不同的含义时,您还应该避免使用相似的名称。以下是一些常用于变量名的反义词:
  • 开始/结束
  • 第一个/最后一个
  • 锁定/解锁
  • 最小/最大
  • 下一个/上一个
  • 老新
  • 打开/关闭
  • 可见/不可见
  • 来源/目标
  • 来源/目的地
  • 上/下

短变量名

当我们有 x 或 n 之类的变量时,我们不会立即看到编写代码的人的意图。n 做什么并不明显。弄清楚这一点需要更仔细的思考(这意味着时间、时间、时间)。例如,假设我们有一个字段代表负责用户的 ID。我们将此变量命名为“responsibleUserId”,而不是像 x 或简单的 id 这样的变量名称,这会立即提高可读性和信息内容。也就是说,像 n 这样的短名称在小方法中作为局部变量占有一席之地,其中涉及该变量的代码块只有几行长,而方法名称完美地描述了那里发生的事情。看到这样的变量,开发人员明白它是次要的,并且范围非常有限。因此,范围对变量名的长度有一定的依赖性:名称越长,变量越全局,反之亦然。例如,这是一种按日期查找最后保存的用户的方法:

public User findLastUser() {
   return findAllUsers().stream()
           .sorted((x, y) -> -x.getCreatedDate().compareTo(y.getCreatedDate()))
           .findFirst()
           .orElseThrow(() -> new ResourceNotFoundException("No user exists"));
}
这里我们使用短命名变量 x 和 y 对流进行排序,然后我们忘记它们。

最佳长度

让我们继续名称长度的话题。最佳名称长度介于 n 和 maximumNumberOfUsersInTheCurrentGroup 之间。换句话说,短的名字会缺乏意义,而太长的名字会拉长程序而不会增加可读性,而且我们也懒得每次都写。除了上面描述的具有短名称(如 n)的变量的情况外,您应该坚持大约 8-16 个字符的长度。这不是一个严格的规则,只是一个指导方针。

小差异

我不能不提一下名字上的细微差别。这也是一种不好的做法,因为这些差异可能只会让人感到困惑,或者需要花费大量额外时间才能注意到它们。例如,InvalidDataAccessApiUsageException 和 InvalidDataAccessResourceUsageException 之间的区别很难一目了然。使用小写字母 L 和 O 时也经常会出现混淆,因为它们很容易被误认为是 1 和 0。在某些字体中,差异更明显,而在某些字体中则更小。

意义

我们需要使名称有意义,但不能通过同义词产生歧义,因为例如 UserData 和 UserInfo 实际上具有相同的含义。在这种情况下,我们将不得不深入研究代码以了解我们需要哪个特定对象。避免使用不能传达有用信息的词语。例如,在 firstNameString 中,为什么我们需要 String 这个词?这真的是 Date 对象吗?当然不是。所以,我们简单地使用 firstName。我还想提一下布尔变量。例如,使用一个名为 flagDeleted 的布尔值。旗帜这个词没有任何意义。称其为 isDeleted 更为合理。

虚假信息

我还想谈谈不正确的命名约定。假设我们有一个名为 userActivityList 的变量,但这个对象不是 List,而是一些其他容器类型或自定义存储对象。这可能会使普通程序员感到困惑:最好将其称为 userActivityGroup 或 userActivities。

搜索

简短名称的缺点之一是它们很难在大量代码中找到——“name”或“NAME_FOR_DEFAULT_USER”哪个更容易找到?当然是第二种选择。我们应该避免在名称中经常遇到的单词(字母),因为它们只会增加搜索时匹配文件的数量,这是不好的。我想提醒您,程序员花在阅读代码上的时间多于编写代码,因此在命名应用程序的元素时要聪明。但是,如果找不到好名字怎么办?如果一个方法的名称没有很好地描述它的功能怎么办?这是评论进入舞台的地方。

评论

编码规则:正确名称的力量,好的和坏的评论 - 4没有什么比中肯的评论更好的了,但是没有什么比空洞的、过时的或错误的评论更能使模块混乱的了。它们可以是一把双刃剑,不是吗?不过,您不应将评论视为明确的好事,而应将其视为次要的罪恶。毕竟,注释本质上是一种补偿代码中未明确表达的想法的方法。例如,如果方法本身太令人困惑,我们会使用它们以某种方式传达方法的本质。在这种情况下,正确地重构代码比编写描述性注释更好。注释越旧,注释越差,因为代码往往会增长和演变,但注释可能保持不变。自评论创建以来经过的时间越长,它就越有问题。不准确的评论比根本没有评论要糟糕得多,因为它们具有混淆性和欺骗性,给人以错误的期望。即使我们有非常棘手的代码,我们也应该重写它而不是注释它。

评论类型

  • 法律注释——出于法律原因在每个源文件开头的注释,例如:

    
    * Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
    * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
    

  • 信息性注释——代表代码解释的注释(提供附加信息或阐述给定代码部分的意图)。

    例如:

    
    /*
    * Combines the user from the database with the one passed for updating
    * When a field in requestUser is empty, it is filled with old data from foundUser
    */
    private User mergeUser(User requestUser, User foundUser) {
           return new User(
           foundUser.getId(),
           requestUser.getFirstName() == null ? requestUser.getFirstName() : foundUser.getFirstName(),
           requestUser.getMiddleName() == null ? requestUser.getMiddleName() : foundUser.getMiddleName(),
           requestUser.getLastName() == null ? requestUser.getLastName() : foundUser.getLastName(),
           requestUser.getAge() == null ? requestUser.getAge() : foundUser.getAge()
           );
           }
    

    在这种情况下,您可以不加注释,因为方法名称及其参数以及非常透明的功能很好地描述了它们自己。

  • 警告注释——注释旨在警告其他开发人员有关操作的不良后果(例如,警告他们为什么测试被标记为@Ignore):

    
    // Takes too long to run
    // Don't run if you don't have a lot of time
    @Ignore
    @Test
    public void someIntegrationTest() {
           ……
           }
    

  • TODO — 注释是关于将来需要完成但由于某种原因现在无法完成的事情的注释。这是一个很好的做法,但应定期审查此类评论,以删除不相关的评论并避免混乱。

    一个例子是:

    
    // TODO: Add a check for the current user ID (when the security context is created)
    
    @Override
    public Resource downloadFile(File file) {
           return fileManager.download(file);
           }
    

    这里我们注意到一个事实,即我们需要添加执行下载操作的用户(我们将从安全上下文中提取其 ID)与执行保存操作的用户的比较。

  • 强化评论——强调某个情况的重要性的评论,乍一看似乎微不足道。

    例如,考虑一个用一些脚本填充测试数据库的方法:

    
    Stream.of(IOUtils.resourceToString("/fill-scripts/" + x, StandardCharsets.UTF_8)
           .trim()
           .split(";"))
           .forEach(jdbcTemplate::update);
    // The trim() call is very important. It removes possible spaces at the end of the script
    // so that when we read and split into separate requests, we don't end up with empty ones
    

  • Javadoc 注释— 描述特定功能的 API 的注释。可能有最有用的评论,因为文档化的 API 更容易使用。也就是说,它们也可能像任何其他类型的评论一样过时。所以,永远不要忘记,对文档的主要贡献不是通过注释,而是通过好的代码。

    下面是一个相当常见的更新用户方法的示例:

    
    /**
    * Updates the passed fields for a user based on its id.
         *
    * @param id id of the user to be updated
    * @param user user with populated fields for updating
    * @return updated user
    */
           User update(Long id, User user);
    

差评

  • 喃喃自语的评论——通常是匆忙写下的评论,其含义只有编写它们的开发人员才能理解,因为只有他或她才能感知评论所指的微妙情况。

    考虑这个例子:

    
    public void configureSomeSystem() {
           try{
           String configPath = filesLocation.concat("/").concat(CONFIGURATION_FILE);
           FileInputStream stream = new FileInputStream(configPath);
           } catch (FileNotFoundException e) {
           // If there is no configuration file, the default configuration is loaded 
          }
    }
    

    谁加载这些设置?他们已经加载了吗?此方法是否应该捕获异常并加载默认设置?出现的问题太多,只能通过深入研究系统的其他部分来回答。

  • 冗余注释——不带有任何语义负载的注释,因为代码的给定部分中发生的事情非常清楚。换句话说,注释并不比代码更容易阅读。

    让我们看一个例子:

    
    public class JdbcConnection{
    public class JdbcConnection{
       /**
        * The logger associated with the current class
        */
       private Logger log = Logger.getLogger(JdbcConnection.class.getName());
    
       /**
        * Creates and returns a connection using the input parameters
        */
       public static Connection buildConnection(String url, String login, String password, String driver) throws Exception {
           Class.forName(driver);
           connection = DriverManager.getConnection(url, login, password);
           log.info("Created connection with db");
           return connection;
       }
    

    这样的评论有什么意义?他们解释的一切都已经很清楚了。

  • 不可靠的评论——不真实且仅具有误导性的评论(虚假信息)。例如,这里有一个。

    
    /**
    * Helper method. Closes the connection with the scanner if isNotUsing is true
    */
    private void scanClose(Scanner scan, boolean isNotUsing) throws Exception {
       if (!isNotUsing) {
           throw new Exception("The scanner is still in use");
       } scan.close();
    }
    

    这个评论有什么问题?事实上,它对我们有点谎言,因为如果 isNotUsing 为假,连接将关闭,反之亦然,正如评论告诉我们的那样。

  • 强制性注释——被认为是强制性的注释(例如 Javadoc 注释),但实际上有时会过度堆积并且不可靠且不必要(您需要考虑是否真的需要这些注释)。

  • 例子:

    
    /**
    * Create a user based on the parameters
    * @param firstName first name of the created user
    * @param middleName middle name of the created user
    * @param lastName last name of the created user
    * @param age age of the created user
    * @param address address of the created user
    * @return user that was created
    */
    User createNewUser(String firstName, String middleName, String lastName, String age, String address);
    

    如果没有这些注释,您是否能够理解该方法的作用?很可能,是的,所以评论在这里变得毫无意义。

  • 日志评论——有时在每次编辑模块时添加到模块开头的评论(类似于更改日志)。

    
    /**
    * Records kept since January 9, 2020;
    **********************************************************************
    * 9 Jan 2020: Providing a database connection using JDBC Connection;
    * 15 Jan 2020: Adding DAO-level interfaces for working with the database;
    * 23 Jan 2020: Adding integration tests for the database;
    * 28 Jan 2020: Implementation of DAO-level interfaces;
    * 1 Feb 2020: Development of interfaces for services,
    * in accordance with the requirements specified in user stories;
    * 16 Feb 2020: Implementation of service interfaces
    * (implementation of business logic related to the work of the database);
    * 25 Feb 2020: Adding tests for services;
    * 8 Mar 2020: Celebration of International Women's Day (Terry is drunk again);
    * 21 Mar 2020: Refactoring the service layer;
    */
    

    这种方法曾经是合理的,但随着版本控制系统(例如 Git)的出现,它变成了不必要的代码混乱和复杂化。

  • 作者评论——旨在表明编写代码的人的评论,以便您可以联系他/她并讨论如何、什么和为什么,例如:

    
    * @author Bender Bending
    

    再一次,版本控制系统准确地记住了谁在什么时候添加了任何代码,所以这种方法是多余的。

  • 注释掉的代码——出于某种原因被注释掉的代码。这是最糟糕的习惯之一,因为发生的事情是你注释掉了一些东西然后忘了它,然后其他开发人员就没有勇气删除它(毕竟,如果它是有价值的东西怎么办?)。

    
    //    public void someMethod(SomeObject obj) {
    //    .....
    //    }
    

    结果,注释掉的代码像垃圾一样堆积起来。在任何情况下你都不应该留下这样的代码。如果你真的需要它,不要忘记版本控制系统。

  • 不明显的评论——以过于复杂的方式描述某事的评论。

    
    /*
        * Start with an array large enough to store
        * all the data bytes (plus filter bytes) with a cushion, plus 300 bytes
        * for header data
        */
    this.dataBytes = new byte[(this.size * (this.deep + 1) * 2)+300];
    

    评论应该解释代码。它本身不应该需要解释。那么这里出了什么问题?什么是“过滤字节”?“+ 1”是什么意思?为什么正好是 300?

如果您已经决定写评论,这里有一些提示:
  1. 使用易于维护的样式:维护过于花哨和异国情调的样式既烦人又耗时。
  2. 不要使用引用单行的行尾注释:结果是一大堆注释。更重要的是,很难为每一行想出一个有意义的注释。
  3. 撰写评论时,请尝试回答“为什么”的问题,而不是“如何”的问题。
  4. 避免删节信息。正如我上面所说,我们不需要对评论进行解释:评论本身就是解释。
  5. 您可以使用注释来记录单位和值范围。
  6. 将注释放在它们描述的代码附近。
最后,我还是要提醒你,最好的注释不是注释,而是在整个应用程序中使用巧妙的命名。通常,大部分时间我们将使用现有代码,对其进行维护和扩展。当这段代码易于阅读和理解时会方便得多,因为糟糕的代码是一个障碍。这就像在工作中投掷一把扳手,匆忙是它的忠实伙伴。糟糕的代码越多,性能下降的越多。这意味着我们需要时不时地进行重构。但是,如果从一开始您就尝试编写不会导致下一个开发人员想要找到并杀死您的代码,那么您就不需要经常重构它。但这仍然是必要的,因为产品的条件和要求会随着新的依赖项和连接的添加而不断变化。好吧,我想这就是我今天的全部内容。感谢所有读到这里的人 :)