系统
该系统的一般理想特征是:- 最小的复杂性- 应避免过于复杂的项目。最主要的是简单和清晰(最好=简单);
- 易于维护- 创建应用程序时,您必须记住它将需要支持(即使不是您),因此代码应该清晰明了;
- 弱耦合是程序不同部分之间的连接数量最少(最大限度地利用OOP原则);
- 可重用性——设计一个能够在其他应用程序中重用其片段的系统;
- 可移植性——系统必须能够轻松适应另一个环境;
- 单一风格——在不同的片段中以单一风格设计一个系统;
- 可扩展性(scalability) ——改进系统而不扰乱其基本结构(如果添加或更改一个片段,这不应该影响其余部分)。
系统设计阶段
- 软件系统- 设计通用形式的应用程序。
- 分离为子系统/包- 定义逻辑上可分离的部分并定义它们之间的交互规则。
- 将子系统划分为类- 将系统的各个部分划分为特定的类和接口,并定义它们之间的交互。
- 将类划分为方法是根据该类的任务,对类的必要方法进行完整的定义。方法设计 - 各个方法功能的详细定义。
系统设计的主要原则和概念
延迟初始化习惯 应用程序在使用对象之前不会花时间创建对象,这加快了初始化过程并减少了垃圾收集器负载。但你不应该做得太过分,因为这可能会导致违反模块化。将所有设计步骤转移到特定部分(例如 main )或像工厂一样工作的类可能是值得的。好的代码的特点之一是没有频繁重复的样板代码。通常,此类代码放置在单独的类中,以便可以在正确的时间调用它。 AOP 另外,我想提一下面向方面的编程。这是通过引入端到端逻辑进行编程,即将重复的代码放入类(方面)中,并在达到某些条件时调用。例如,访问某个名称的方法或访问某个类型的变量时。有时方面可能会令人困惑,因为不能立即清楚从哪里调用代码,但尽管如此,这是一个非常有用的功能。特别是在缓存或日志记录时:我们添加此功能而不向常规类添加额外的逻辑。您可以在此处阅读有关 OAP 的更多信息。 Kent Beck 提出的设计简单架构的 4 条规则- 表现力——需要明确表达类的目的,这是通过正确的命名、小尺寸和遵守单一责任原则来实现的(我们将在下面更详细地讨论它)。
- 最少的类和方法- 如果您希望将类分解为尽可能小且单向的,您可能会走得太远(反模式 - 霰弹枪法)。这一原则要求保持系统紧凑,不要走得太远,为每个喷嚏创建一个类。
- 缺乏重复- 令人困惑的额外代码是糟糕的系统设计的标志,并被移动到一个单独的地方。
- 所有测试的执行- 通过所有测试的系统是受控的,因为任何更改都可能导致测试失败,这可以向我们表明方法内部逻辑的更改也会导致预期行为的更改。
界面
也许创建一个适当的类的最重要的阶段之一是创建一个适当的接口,它将代表一个隐藏类的实现细节的良好抽象,同时将代表一组彼此明显一致的方法。让我们仔细看看 SOLID 原则之一 -接口隔离:客户端(类)不应该实现他们不会使用的不必要的方法。也就是说,如果我们正在谈论使用最少数量的方法构建接口,这些方法旨在执行该接口的唯一任务(对我来说,这与单一职责非常相似),那么最好创建几个较小的方法一个而不是一个臃肿的界面。幸运的是,一个类可以实现多个接口,就像继承的情况一样。您还需要记住接口的正确命名:名称应尽可能准确地反映其任务。当然,它越短,引起的混乱就越少。通常在接口级别编写文档注释,这反过来又帮助我们详细描述该方法应该做什么、它需要什么参数以及它将返回什么。班级
让我们看看类的内部组织。或者更确切地说,是构造类时应该遵循的一些观点和规则。通常,一个类应以变量列表开头,并按特定顺序排列:- 公共静态常量;
- 私有静态常量;
- 私有实例变量。
班级规模
现在我想谈谈班级规模。 让我们记住 SOLID 的原则之一——单一职责。 单一责任——单一责任原则。它规定每个对象只有一个目标(职责),其所有方法的逻辑都是为了确保这一目标。也就是说,基于此,我们应该避免大型、臃肿的类(这本质上是一种反模式——“神圣对象”),如果一个类中有很多不同的、异构逻辑的方法,我们需要思考关于将其分成几个逻辑部分(类)。反过来,这将提高代码的可读性,因为如果我们知道给定类的大致用途,我们就不需要太多时间来理解方法的用途。您还需要留意类名:它应该反映它包含的逻辑。比方说,如果我们有一个类,其名称有 20 多个单词,我们需要考虑重构。每个有自尊的类都不应该有如此多的内部变量。事实上,每个方法都与其中一个或多个方法一起使用,这会导致类内更大的耦合(这正是它应该的样子,因为类应该作为一个整体)。结果,增加班级的连贯性会导致班级的连贯性减少,当然,我们的班级数量也会增加。对于一些人来说,这很烦人;他们需要更多地去上课,看看特定的大型任务是如何运作的。除此之外,每个类都是一个小模块,应该尽可能地与其他模块连接。这种隔离减少了向类添加额外逻辑时需要进行的更改数量。对象
封装
这里我们首先讲一下OOP的原则之一——封装。因此,隐藏实现并不意味着在变量之间创建一个方法层(不加考虑地限制通过单个方法、getter 和 setter 的访问,这不好,因为整个封装点都丢失了)。隐藏访问的目的是形成抽象,也就是说,类提供了我们处理数据的通用具体方法。但用户不需要确切地知道我们如何处理这些数据——它可以工作,这很好。德墨忒耳定律
您还可以考虑德米特法则:它是一小组规则,有助于管理类和方法级别的复杂性。因此,假设我们有一个对象Car
并且它有一个方法 - move(Object arg1, Object arg2)
。根据迪米特定律,该方法仅限于调用:
- 对象本身的方法
Car
(换句话说,就是 this); - 在 中创建的对象的方法
move
; - 作为参数传递的对象的方法 -
arg1
,arg2
; - 内部对象的方法
Car
(同this)。
数据结构
数据结构是相关元素的集合。当将对象视为数据结构时,它是由方法处理的一组数据元素,隐含地暗示其存在。也就是说,它是一个对象,其目的是存储和操作(处理)存储的数据。与常规对象的主要区别在于,对象是一组对隐含存在的数据元素进行操作的方法。你明白吗?在常规对象中,主要方面是方法,内部变量旨在其正确操作,但在数据结构中则相反:方法支持并帮助处理存储元素,这是这里的主要元素。一种数据结构是数据传输对象(DTO)。这是一个具有公共变量的类,没有方法(或只有读/写方法),可以在使用数据库、解析来自套接字的消息等时传递数据。通常,此类对象中的数据不会长期存储,而是会被保存。几乎立即转换为我们的应用程序所使用的实体。反过来,实体也是一种数据结构,但其目的是参与应用程序不同级别的业务逻辑,而 DTO 是将数据传输到应用程序或从应用程序传输数据。示例 DTO:@Setter
@Getter
@NoArgsConstructor
public class UserDto {
private long id;
private String firstName;
private String lastName;
private String email;
private String password;
}
一切似乎都很清楚,但在这里我们了解了混合体的存在。 混合对象是包含处理重要逻辑、存储内部元素和访问方法(获取/设置)的方法的对象。此类对象很混乱并且很难添加新方法。您不应该使用它们,因为尚不清楚它们的用途 - 存储元素或执行某种逻辑。您可以在此处了解可能的对象类型。
创建变量的原则
让我们思考一下变量,或者更确切地说,思考一下创建变量的原则是什么:- 理想情况下,您应该在使用变量之前立即声明并初始化它(而不是创建它并忘记它)。
- 只要有可能,将变量声明为final,以防止其值在初始化后发生更改。
- 不要忘记计数器变量(通常我们在某种循环中使用它们
for
,也就是说,我们不能忘记重置它们,否则它会破坏我们的整个逻辑)。 - 您应该尝试在构造函数中初始化变量。
- 如果要选择使用带引用还是不带引用的对象(
new SomeObject()
),请选择不带引用( ),因为该对象一旦使用,就会在下次垃圾回收时被删除,不会浪费资源。 - 使变量的生命周期尽可能短(变量的创建和最后一次访问之间的距离)。
- 在循环之前立即初始化循环中使用的变量,而不是在包含循环的方法的开头初始化。
- 始终从最有限的范围开始,仅在必要时扩展它(您应该尝试使变量尽可能本地化)。
- 每个变量仅用于一个目的。
- 避免具有隐藏含义的变量(该变量在两个任务之间左右为难,这意味着它的类型不适合解决其中之一)。
方法
让我们直接转向逻辑的实现,即方法。-
第一条规则是紧凑性。理想情况下,一个方法不应超过 20 行,因此,如果公共方法显着“膨胀”,则需要考虑将分离的逻辑移至私有方法中。
-
第二条规则是命令
if
、else
命令while
等中的块不应高度嵌套:这会显着降低代码的可读性。理想情况下,嵌套不应超过两个块{}
。还建议使这些块中的代码紧凑且简单。
-
第三条规则是一个方法必须只执行一个操作。也就是说,如果一个方法执行复杂、多样的逻辑,我们将其划分为子方法。因此,该方法本身将是一个外观,其目的是按正确的顺序调用所有其他操作。
但是,如果操作看起来太简单而无法创建单独的方法怎么办?是的,有时这看起来就像用大炮射麻雀,但小方法可以带来很多好处:
- 更容易阅读代码;
- 在开发过程中,方法往往会变得更加复杂,如果方法最初很简单,那么使其功能复杂化会更容易一些;
- 隐藏实施细节;
- 促进代码重用;
- 更高的代码可靠性。
-
向下的规则是,代码应该从上到下阅读:越低,逻辑深度越深,反之,越高,方法越抽象。例如,开关命令非常不紧凑且不受欢迎,但如果您不能不使用开关,则应尝试将其尽可能移到最低级别的方法中。
-
方法参数- 有多少是理想的?理想情况下,根本没有))但这真的会发生吗?但是,您应该尝试尽可能少地使用它们,因为数量越少,该方法就越容易使用,也越容易测试。如果有疑问,请尝试猜测使用具有大量输入参数的方法的所有场景。
-
另外,我想强调以布尔标志作为输入参数的方法,因为这自然意味着该方法实现多个操作(如果为 true,则执行一个操作,如果为 false,则执行另一个操作)。正如我上面所写,这不好,应该尽可能避免。
-
如果一个方法有大量传入参数(极值是7,但在2-3之后你应该考虑一下),你需要将一些参数分组到一个单独的对象中。
-
如果有多个相似的方法(重载),则必须以相同的顺序传递相似的参数:这会增加可读性和可用性。
-
当你向方法传递参数时,你必须确保它们都会被使用,否则参数有什么用呢?把它从界面上剪下来就可以了。
-
try/catch
它本质上看起来不太好,所以一个好的举措是将其移动到一个中间的单独方法(处理异常的方法)中:public void exceptionHandling(SomeObject obj) { try { someMethod(obj); } catch (IOException e) { e.printStackTrace(); } }
GO TO FULL VERSION