CodeGym /Java 博客 /随机的 /编码规则:从创建系统到使用对象
John Squirrels
第 41 级
San Francisco

编码规则:从创建系统到使用对象

已在 随机的 群组中发布
大家好!今天我们想和你谈谈写出好的代码。当然,并不是每个人都想立即阅读像 Clean Code 这样的书,因为它们包含大量信息,但一开始并不清楚。当你读完时,你可能会扼杀所有编码的欲望。考虑到所有这些,今天我想为您提供一个小指南(一小组建议)以帮助您编写更好的代码。在本文中,让我们回顾一下与创建系统和使用接口、类和对象相关的基本规则和概念。阅读本文不会花费太多时间,我希望不会让您感到厌烦。我将从上到下按照我的方式进行,即从应用程序的一般结构到更具体的细节。 编码规则:从创建系统到使用对象 - 1

系统

以下是系统通常需要的特征:
  • 最小的复杂性。必须避免过于复杂的项目。最重要的是简单明了(更简单 = 更好)。
  • 易于维护。创建应用程序时,您必须记住它需要维护(即使您个人不负责维护它)。这意味着代码必须清晰明了。
  • 松耦合。这意味着我们最大限度地减少了程序不同部分之间的依赖性(最大限度地遵守 OOP 原则)。
  • 可重用性。我们设计的系统具有在其他应用程序中重用组件的能力。
  • 可移植性。使一个系统适应另一个环境应该很容易。
  • 统一风格。我们在其各个组件中使用统一的样式来设计我们的系统。
  • 可扩展性(scalability)。我们可以在不违反其基本结构的情况下增强系统(添加或更改组件不应影响所有其他组件)。
构建不需要修改或新功能的应用程序几乎是不可能的。我们将不断需要添加新部件,以帮助我们的创意与时俱进。这就是可扩展性发挥作用的地方。可扩展性本质上是扩展应用程序、添加新功能以及使用更多资源(或者换句话说,使用更大的负载)。换句话说,为了更容易添加新的逻辑,我们坚持一些规则,比如通过增加模块化来降低系统的耦合度。编码规则:从创建系统到使用对象 - 2

图片来源

系统设计阶段

  1. 软件系统。整体设计应用程序。
  2. 划分为子系统/包。定义逻辑上不同的部分并定义它们之间交互的规则。
  3. 将子系统划分为类。将系统的各个部分划分为具体的类和接口,并定义它们之间的交互。
  4. 将类划分为方法。根据分配的职责,为类创建必要方法的完整定义。
  5. 方法设计。创建各个方法功能的详细定义。
通常普通开发人员处理这种设计,而应用程序的架构师处理上述几点。

系统设计的一般原则和概念

延迟初始化。在这个编程习惯中,应用程序不会浪费时间创建一个对象,直到它被实际使用。这加快了初始化过程并减少了垃圾收集器的负载。也就是说,您不应该做得太过火,因为那样会违反模块化原则。也许值得将所有构造实例移动到某个特定部分,例如,main 方法或工厂类。好的代码的一个特征是没有重复的样板代码。通常,此类代码放在单独的类中,以便在需要时调用。

面向对象编程

我还要注意面向方面的编程。这种编程范式就是要引入透明逻辑。即,将重复的代码放入类(方面)中,并在满足某些条件时调用。例如,调用具有特定名称的方法或访问特定类型的变量时。有时方面可能会令人困惑,因为无法立即清楚代码从何处调用,但这仍然是非常有用的功能。特别是在缓存或日志记录时。我们在不向普通类添加额外逻辑的情况下添加此功能。 肯特·贝克 (Kent Beck) 的简单架构四项规则:
  1. 表现力——一堂课的意图应该清楚地表达出来。这是通过适当的命名、小尺寸和遵守单一职责原则(我们将在下面更详细地考虑)来实现的。
  2. 最少数量的类和方法——如果你希望让类尽可能小且范围狭窄,你可能会走得太远(导致霰弹枪手术反模式)。这个原则要求保持系统紧凑,不要走得太远,为每个可能的动作创建一个单独的类。
  3. 无重复——重复的代码会造成混乱,并且表明系统设计欠佳,将被提取并移动到一个单独的位置。
  4. 运行所有测试——通过所有测试的系统是可管理的。任何更改都可能导致测试失败,向我们揭示我们对方法内部逻辑的更改也以意想不到的方式改变了系统的行为。

坚硬的

在设计系统时,著名的 SOLID 原则值得考虑:

S(单一职责)、 O(开闭)、 L(里氏代换)、 I(接口隔离)、 D(依赖倒置)。

我们不会详述每个单独的原则。这会超出本文的范围,但您可以在此处阅读更多内容。

界面

也许创建一个设计良好的类的最重要步骤之一是创建一个设计良好的接口来表示一个良好的抽象,隐藏类的实现细节并同时呈现一组明显彼此一致的方法。让我们仔细看看 SOLID 原则之一——接口隔离:客户端(类)不应该实现他们不会使用的不必要的方法。换句话说,如果我们谈论的是创建一个具有最少数量方法的接口,旨在执行接口的唯一工作(我认为这与单一责任原则非常相似),那么最好创建几个较小的方法来代替一个臃肿的界面。幸运的是,一个类可以实现多个接口。请记住正确命名您的接口:名称应尽可能准确地反映分配的任务。当然,它越短,引起的混乱就越少。文档注释通常写在界面级别。这些注释提供了关于每个方法应该做什么、它需要什么参数以及它将返回什么的详细信息。

班级

编码规则:从创建系统到使用对象 - 3

图片来源

我们来看看类在内部是如何安排的。或者更确切地说,编写类时应该遵循的一些观点和规则。通常,一个类应该以特定顺序的变量列表开始:
  1. 公共静态常量;
  2. 私有静态常量;
  3. 私有实例变量。
接下来是各种构造函数,从参数最少的到参数最多的顺序排列。它们之后是从最公共到最私有的方法。一般来说,隐藏一些我们想要限制的功能实现的私有方法在最底层。

班级规模

现在我想谈谈班级规模。让我们回顾一下 SOLID 原则之一——单一责任原则。它声明每个对象只有一个目的(职责),其所有方法的逻辑旨在完成它。这告诉我们要避免大的、臃肿的类(这实际上是上帝对象的反模式),如果我们有很多方法,各种不同的逻辑塞进一个类,我们需要考虑把它拆成一个几个逻辑部分(类)。这反过来又会增加代码的可读性,因为如果我们知道任何给定类的大致目的,就不会花很长时间来理解每个方法的目的。另外,请注意类名,它应该反映它包含的逻辑。例如,如果我们有一个名称中有 20 多个单词的类,我们需要考虑重构。任何自重的类都不应该有那么多内部变量。事实上,每个方法都与其中的一个或几个一起工作,从而在类中产生了很多内聚力(这正是它应该的,因为类应该是一个统一的整体)。结果,增加班级的凝聚力会导致班级规模的减少,当然,班级的数量也会增加。这对某些人来说很烦人,因为您需要更多地仔细阅读类文件才能了解特定的大型任务是如何工作的。最重要的是,每个类都是一个小模块,应该与其他模块的相关性最低。这种隔离减少了我们在向类中添加额外逻辑时需要进行的更改数量。每个方法都与其中的一个或几个一起工作,从而在类中产生很多凝聚力(这正是它应该的,因为类应该是一个统一的整体)。结果,增加班级的凝聚力会导致班级规模的减少,当然,班级的数量也会增加。这对某些人来说很烦人,因为您需要更多地仔细阅读类文件才能了解特定的大型任务是如何工作的。最重要的是,每个类都是一个小模块,应该与其他模块的相关性最低。这种隔离减少了我们在向类中添加额外逻辑时需要进行的更改数量。每个方法都与其中的一个或几个一起工作,从而在类中产生很多凝聚力(这正是它应该的,因为类应该是一个统一的整体)。结果,增加班级的凝聚力会导致班级规模的减少,当然,班级的数量也会增加。这对某些人来说很烦人,因为您需要更多地仔细阅读类文件才能了解特定的大型任务是如何工作的。最重要的是,每个类都是一个小模块,应该与其他模块的相关性最低。这种隔离减少了我们在向类中添加额外逻辑时需要进行的更改数量。的凝聚力导致班级规模的减少,当然,班级的数量也会增加。这对某些人来说很烦人,因为您需要更多地仔细阅读类文件才能了解特定的大型任务是如何工作的。最重要的是,每个类都是一个小模块,应该与其他模块的相关性最低。这种隔离减少了我们在向类中添加额外逻辑时需要进行的更改数量。的凝聚力导致班级规模的减少,当然,班级的数量也会增加。这对某些人来说很烦人,因为您需要更多地仔细阅读类文件才能了解特定的大型任务是如何工作的。最重要的是,每个类都是一个小模块,应该与其他模块的相关性最低。这种隔离减少了我们在向类中添加额外逻辑时需要进行的更改数量。

对象

封装

这里先说一个OOP的原则:封装。隐藏实现并不等于创建一个方法来隔离变量(通过单独的方法、getter 和 setter 不加考虑地限制访问,这并不好,因为整个封装点都丢失了)。隐藏访问旨在形成抽象,即该类提供我们用来处理数据的共享具体方法。用户不需要确切地知道我们是如何处理这些数据的——它有效,这就足够了。

得墨忒耳法则

我们还可以考虑 Demeter 法则:它是一小组规则,有助于在类和方法级别管理复杂性。假设我们有一个Car对象,它有一个move(Object arg1, Object arg2)方法。根据 Demeter 法则,此方法仅限于调用:
  • Car对象本身的方法(换句话说,“this”对象);
  • 在move方法中创建的对象的方法;
  • 作为参数传递的对象的方法(arg1arg2);
  • 内部Car对象的方法(同样是“this”)。
换句话说,得墨忒耳法则就像父母对孩子说的:“你可以和你的朋友交谈,但不能和陌生人交谈”。

数据结构

数据结构是相关元素的集合。将对象视为数据结构时,有一组数据元素可供方法操作。这些方法的存在是隐含的假设。也就是说,数据结构是一个对象,其目的是存储和处理(处理)存储的数据。它与常规对象的主要区别在于,普通对象是对隐式假定存在的数据元素进行操作的方法集合。你明白吗?普通对象的主要方面是方法。内部变量有助于它们的正确操作。但是在数据结构中,方法是用来支持你使用存储的数据元素的,这在这里是最重要的。一种类型的数据结构是数据传输对象 (DTO)。这是一个具有公共变量且没有方法(或只有读/写方法)的类,用于在处理数据库、解析来自套接字的消息等时传输数据。数据通常不会长期存储在此类对象中。它几乎立即转换为我们的应用程序工作的实体类型。反过来,实体也是一种数据结构,但其目的是参与应用程序各个级别的业务逻辑。DTO 的目的是将数据传输到应用程序或从应用程序传输数据。DTO 示例:也是一种数据结构,但其目的是参与应用程序各个级别的业务逻辑。DTO 的目的是将数据传输到应用程序或从应用程序传输数据。DTO 示例:也是一种数据结构,但其目的是参与应用程序各个级别的业务逻辑。DTO 的目的是将数据传输到应用程序或从应用程序传输数据。DTO 示例:

@Setter
@Getter
@NoArgsConstructor
public class UserDto {
    private long id;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}
一切似乎都很清楚,但在这里我们了解了混合动力车的存在。混合对象是具有处理重要逻辑、存储内部元素以及访问器(获取/设置)方法的方法的对象。这样的对象很杂乱,很难添加新方法。您应该避免使用它们,因为不清楚它们的用途是什么——存储元素还是执行逻辑?

创建变量的原则

让我们思考一下变量。更具体地说,让我们考虑一下创建它们时适用的原则:
  1. 理想情况下,您应该在使用变量之前声明并初始化它(不要创建一个变量然后忘记它)。
  2. 尽可能将变量声明为 final 以防止其值在初始化后更改。
  3. 不要忘记我们通常在某种for循环中使用的计数器变量。也就是说,不要忘记将它们归零。否则,我们所有的逻辑都可能崩溃。
  4. 您应该尝试在构造函数中初始化变量。
  5. 如果可以选择使用带引用或不带引用的对象(new SomeObject()),请选择不带引用,因为在使用该对象后,它将在下一个垃圾收集周期中被删除,并且不会浪费其资源。
  6. 保持变量的生命周期(变量的创建和最后一次引用之间的距离)尽可能短。
  7. 在循环之前初始化循环中使用的变量,而不是在包含循环的方法的开头。
  8. 始终从最有限的范围开始,仅在必要时扩展(您应该尝试使变量尽可能本地化)。
  9. 每个变量仅用于一个目的。
  10. 避免具有隐藏目的的变量,例如在两个任务之间拆分的变量——这意味着它的类型不适合解决其中一个任务。

方法

编码规则:从创建系统到使用对象 - 4

来自电影“星球大战前传 III - 西斯的复仇”(2005 年)

让我们直接进行逻辑的实现,即方法。
  1. 规则#1——紧凑。理想情况下,一个方法不应超过 20 行。这意味着,如果公共方法显着“膨胀”,您需要考虑拆分逻辑并将其移动到单独的私有方法中。

  2. 规则 #2 — ifelsewhile和其他语句不应包含大量嵌套块:大量嵌套会显着降低代码的可读性。理想情况下,嵌套的{}块不应超过两个。

    并且还希望保持这些块中的代码紧凑和简单。

  3. 规则#3——一个方法应该只执行一个操作。也就是说,如果一个方法执行各种复杂的逻辑,我们将其分解为子方法。因此,该方法本身将是一个外观,其目的是以正确的顺序调用所有其他操作。

    但是,如果该操作看起来太简单而无法放入单独的方法中怎么办?诚然,有时感觉就像向麻雀开炮,但小方法有很多好处:

    • 更好的代码理解;
    • 随着开发的进行,方法往往会变得更加复杂。如果一个方法开始时很简单,那么将其功能复杂化会容易一些;
    • 实现细节被隐藏;
    • 更容易的代码重用;
    • 更可靠的代码。

  4. 递减规则——代码应该从上到下阅读:你阅读的越低,你对逻辑的研究就越深入。反之亦然,你走得越高,方法就越抽象。例如,switch 语句相当不紧凑且不受欢迎,但如果您无法避免使用 switch,则应尝试将其尽可能低地移动到最低级别的方法。

  5. 方法参数——理想的数字是多少?理想情况下,完全没有 :) 但这真的发生了吗?也就是说,您应该尝试使用尽可能少的参数,因为参数越少,方法越容易使用,也越容易测试。如有疑问,请尝试预测使用具有大量输入参数的方法的所有场景。

  6. 此外,最好将具有布尔标志的方法分开作为输入参数,因为这一切本身就意味着该方法执行多个操作(如果为真,则做一件事;如果为假,则做另一件事)。正如我在上面所写的,这不好,应该尽可能避免。

  7. 如果一个方法有大量的输入参数(一个极端是 7 个,但你真的应该在 2-3 之后开始思考),一些参数应该被分组到一个单独的对象中。

  8. 如果有几个相似的(重载的)方法,那么相似的参数必须以相同的顺序传递:这提高了可读性和可用性。

  9. 当你给一个方法传递参数时,你必须确保它们都被使用了,否则你为什么需要它们?从界面中删除任何未使用的参数并完成它。

  10. try/catch本质上看起来不太好,所以将它移到一个单独的中间方法(一种处理异常的方法)中是个好主意:

    
    public void exceptionHandling(SomeObject obj) {
        try {  
            someMethod(obj);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    

我在上面谈到了重复的代码,但让我再重复一遍:如果我们有几个方法有重复的代码,我们需要将它们移到一个单独的方法中。这将使方法和类都更加紧凑。不要忘记管理名称的规则:有关如何正确命名类、接口、方法和变量的详细信息将在本文的下一部分讨论。但这就是我今天要给你的全部内容。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION