Introduction 介绍
Software design principles are the foundation of software development. As a software engineer, you can find them in your work tools, languages, frameworks, paradigms, and patterns. They are the core pillars of “good” and “readable” code. Once you understand them, you can see them everywhere.
软件设计原则是软件开发的基础。作为一名软件工程师,您可以在您的工作工具、语言、框架、范例和模式中找到它们。它们是“好”和“可读”代码的核心支柱。一旦你了解了它们,你就可以在任何地方看到它们。
The skill of seeing and applying them is what distinguishes a good engineer from a bad one. No one framework or tool can improve your quality of writing good code without understanding the fundamentals; moreover, without them, you become a hostage of that tool.
观察和应用它们的技能是区分好工程师和坏工程师的关键。如果不了解基础知识,任何一种框架或工具都无法提高您编写优秀代码的质量;此外,如果没有它们,您就会成为该工具的人质。
This article isn’t a reference guide but rather my try to systemize a list of core principles that need to be refreshed from time to time.
本文不是参考指南,而是我尝试将需要不时刷新的核心原则列表系统化的尝试。
Abstraction 抽象
Abstraction is one of the most significant principles in general. To abstract something means to focus on the important part while neglecting other details. Abstraction can be interpreted in two main ways: as a process of generalization and as the result of this generalization itself.
一般来说,抽象是最重要的原则之一。抽象意味着关注重要部分而忽略其他细节。抽象可以用两种主要方式解释:作为概括的过程和作为概括本身的结果。
In software development, it often comes paired with encapsulation, a method used to hide the implementation of the abstracted parts. You can observe abstraction in various forms. For example, when you define a type, you abstract yourself from the memory representation of a variable. Similarly, when you abstract an interface or the signature of a function, you focus on what’s important: the contract to work with. When designing a class, you select only the relevant attributes for your domain and the specific business use cases. There are tons of other examples, but
在软件开发中,它通常与封装相结合,封装是一种用于隐藏抽象部分的实现的方法。您可以观察各种形式的抽象。例如,当您定义类型时,您将自己从变量的内存表示中抽象出来。同样,当您抽象接口或函数签名时,您会关注重要的事情:要使用的契约。设计类时,您只需选择与您的领域和特定业务用例相关的属性。还有大量其他示例,但抽象的主要目的是您不需要了解实现的细节即可使用某些东西;因此,您可以更好地专注于对您来说至关重要的事情。the main purpose of abstraction is that you don’t need to know the details of implementation to work with something; therefore, you can better focus on what is essential for you.
在软件开发中,它通常与封装相结合,封装是一种用于隐藏抽象部分的实现的方法。您可以观察各种形式的抽象。例如,当您定义类型时,您将自己从变量的内存表示中抽象出来。同样,当您抽象接口或函数签名时,您会关注重要的事情:要使用的契约。设计类时,您只需选择与您的领域和特定业务用例相关的属性。还有大量其他示例,但抽象的主要目的是您不需要了解实现的细节即可使用某些东西;因此,您可以更好地专注于对您来说至关重要的事情。
This principle is not exclusive to application development. You, as a programmer, abstracted through language syntax from underlying actions with an operation system. The OS, in turn, abstract your language translater from underlying operations with a CPU, memory, NIC, and so on. The more you go deeper, the more you understand that this is just a matter of abstraction.
这一原则并不只适用于应用程序开发。作为一名程序员,您通过语言语法从操作系统的底层操作中进行抽象。反过来,操作系统将语言翻译器从 CPU、内存、NIC 等底层操作中抽象出来。你越深入,你就越明白这只是一个抽象的问题。
Source: Reddit 来源:Reddit
Encapsulate what varies 封装不同的内容
As you can see, abstraction can manifest itself in different forms — from data (implementation) abstraction to hierarchical. A general rule of thumb for using abstraction is the principle: “Encapsulate what varies.” Identify the potentially changeable part and declare a concrete interface for it. This way, even if the internal logic changes, the client will still have the same interaction.
正如您所看到的,抽象可以以不同的形式表现出来——从数据(实现)抽象到层次化。使用抽象的一般经验法则是以下原则:“封装变化的内容”。识别潜在可更改的部分并为其声明一个具体的接口。这样,即使内部逻辑发生变化,客户端仍然会有相同的交互。
Suppose you need to calculate a currency conversion. At the moment, you only have two currencies. You can come up with something like this:
假设您需要计算货币换算。目前,您只有两种货币。你可以想出这样的东西:
if (baseCurrency == "USD" and targetCurrency == "EUR") return amount * 0.90;
if (baseCurrency == "EUR" and targetCurrency == "USD") return amount * 1.90;
But another type of currency may be added in the future, which would require changes to the client code. Instead, it is better to abstract and encapsulate all the logic in a separate method and call that method from the client side when needed.
但将来可能会添加另一种货币,这需要更改客户端代码。相反,最好将所有逻辑抽象并封装在一个单独的方法中,并在需要时从客户端调用该方法。
function convertCurrency(amount, baseCurrency, targetCurrency) {if (baseCurrency == "USD" and targetCurrency == "EUR") return amount * 0.90;if (baseCurrency == "EUR" and targetCurrency == "USD") return amount * 1.90;if (baseCurrency == "USD" and targetCurrency == "UAH") return amount * 38.24;…
}
DRY 干燥
DRY (don’t repeat yourself), also known as DIE (duplication is evil), states that you shouldn’t duplicate information or knowledge across your code base.
DRY(不要重复自己),也称为 DIE(重复是邪恶的),指出您不应在代码库中重复信息或知识。
“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system” — Andy Hunt and Dave Thomas, The Pragmatic Programmer.
“每条知识都必须在系统内有一个单一的、明确的、权威的表示”——安迪·亨特和戴夫·托马斯,务实的程序员。
The benefit of reducing code repetition is the simplicity of changing and maintaining. If you duplicate your logic in several places and then find a bug, you’re likely to forget to change it in one of the places, which will lead to different behavior for seemingly identical functionality. Instead, find a repetitive functionality, abstract it in the form of a procedure, class, etc., give it a meaningful name and use it where needed. This advocates a single point of change and minimizes the breaking of unrelated functionality.
减少代码重复的好处是易于更改和维护。如果您在多个位置重复逻辑,然后发现错误,您可能会忘记在其中一个位置更改它,这将导致看似相同的功能出现不同的行为。相反,找到一个重复的功能,以过程、类等的形式将其抽象,给它一个有意义的名称,并在需要的地方使用它。这提倡单点更改并最大限度地减少对不相关功能的破坏。
KISS 吻
The KISS (keep it simple, stupid) phrase was coined by aircraft engineer Kelly Johnson, who challenged his engineering team that the jet aircraft they were designing must be repairable by an average mechanic in the field under combat conditions with only specific tools.
KISS(保持简单,愚蠢)短语是由飞机工程师凯利·约翰逊创造的,他向他的工程团队提出挑战,要求他们设计的喷气式飞机必须由现场的普通机械师在战斗条件下仅使用特定工具即可修复。
The main idea behind it is to focus on the simplicity of a system, which increases understanding and reduces overengineering while using only the tools you really need.
其背后的主要思想是关注系统的简单性,这可以增加理解并减少过度设计,同时仅使用您真正需要的工具。
YAGNI 亚格尼
When you design a solution to the problem, you are thinking about two things: how to better adapt it to the current system and how to make it extensible for possible future requirements. In the second case, the desire to build a premature feature for the sake of better extensibility is usually wrong: even if you now think that this will reduce the cost of integration, the maintenance and debugging of such code may not be obvious and unnecessarily complicated. Thus, you violate the previous principle by increasing the redundant complexity of the solution to the current problem. Also, don’t forget here’s a good chance that your presumed functionality may not be needed in the future, and then you’re just wasting resources.
当您设计问题的解决方案时,您会考虑两件事:如何更好地使其适应当前系统以及如何使其可扩展以适应未来可能的需求。在第二种情况下,为了更好的可扩展性而构建过早的功能的愿望通常是错误的:即使您现在认为这会降低集成成本,但此类代码的维护和调试可能并不明显并且不必要地复杂。因此,您通过增加当前问题的解决方案的冗余复杂性来违反前面的原则。另外,不要忘记,很有可能将来可能不需要您假定的功能,那么您只是在浪费资源。
That’s is what YAGNI or “You aren’t gonna need it” all about. Don’t get it wrong; you should think about what will be with your solution in the future, but only add code when you actually need it.
这就是 YAGNI 或“你不需要它”的全部内容。别误会;您应该考虑您的解决方案将来的用途,但仅在实际需要时添加代码。
LoD 详细程度
The Law of Demeter (LoD), sometimes referred to as the principle of least knowledge, advises against talking to “strangers”. Because LoD is usually considered with OOP, a “stranger” in that context means any object not directly associated with the current one.
德米特法则 (LoD),有时被称为最少知识原则,建议不要与“陌生人”交谈。由于 LoD 通常与 OOP 一起考虑,因此上下文中的“陌生人”意味着与当前对象不直接关联的任何对象。
The benefit of using Demeter’s Law is maintainability, expressed by avoiding immediate contact between unrelated objects.
使用德米特定律的好处是可维护性,通过避免不相关对象之间的直接接触来表达。
As a result, when you interact with an object and one of the following scenarios is not met, you violate this principle:
因此,当您与对象交互并且不满足以下场景之一时,您就违反了这一原则:
- When the object is the current instance of a class (accessed via
this
)
当对象是类的当前实例时(通过this
访问) - When the object is a part of a class
当对象是类的一部分时 - When the object passed to a method through the parameters
当对象通过参数传递给方法时 - When the object instantiated inside a method
当对象在方法内实例化时 - When the object is globally available.
当对象全局可用时。
To give an example, let’s consider a situation when a customer wants to make a deposit into a bank account. We might end up here having three classes — Wallet
, Customer
and Bank
.
举个例子,让我们考虑一下客户想要存入银行账户的情况。我们最终可能会拥有三个类 - Wallet
、 Customer
和 Bank
。
class Wallet {private decimal balance;public decimal getBalance() {return balance;}public void addMoney(decimal amount) {balance += amount}public void withdrawMoney(decimal amount) {balance -= amount}
}class Customer {public Wallet wallet;Customer() {wallet = new Wallet();}
}class Bank {public void makeDeposit(Customer customer, decimal amount) {Wallet customerWallet = customer.wallet;if (customerWallet.getBalance() >= amount) {customerWallet.withdrawMoney(amount);//...} else {//...} }
}
You can see the violation of the Demeter law in the makeDeposit
method. Accessing a customer wallet in terms of LoD is right (although it’s strange behavior from the logic perspective). But here, a bank object invokes the getBalance
and withdrawMoney
from the customerWallet
object, thus talking to a stranger (wallet), instead of a friend (customer).
您可以在 makeDeposit
方法中看到违反德米特定律的情况。从 LoD 角度访问客户钱包是正确的(尽管从逻辑角度来看这是奇怪的行为)。但在这里,银行对象从 customerWallet
对象调用 getBalance
和 withdrawMoney
,从而与陌生人(钱包)而不是朋友(客户)对话。 。
Before applying the LoD principle
应用 LoD 原则之前
Here’s how to fix it:
修复方法如下:
class Wallet {private decimal balance;public decimal getBalance() {return balance;}public boolean canWithdraw(decimal amount) {return balance >= amount;}public boolean addMoney(decimal amount) {balance += amount}public boolean withdrawMoney(decimal amount) {if (canWithdraw(amount)) {balance -= amount;}}
}class Customer {private Wallet wallet;Customer() {wallet = new Wallet();}public boolean makePayment(decimal amount) {return wallet.withdrawMoney(amount);}
}class Bank {public void makeDeposit(Customer customer, decimal amount) {boolean paymentSuccessful = customer.makePayment(amount);if (paymentSuccessful) {//...} else {//...} }
}
Now all interaction with a customer wallet is going through the customer object. This abstraction favors loose coupling, easy changing of the logic inside the Wallet
and Customer
classes (a bank object shouldn’t worry about the customer’s internal representation), and testing.
现在,与客户钱包的所有交互都通过客户对象进行。这种抽象有利于松散耦合、轻松更改 Wallet
和 Customer
类内部的逻辑(银行对象不应担心客户的内部表示)以及测试。
After adopting the LoD principle
采用LoD原理后
Generally, you can say that LoD fails when there are more than two dots applied to one object, like object.friend.stranger
instead of object.friend
.
一般来说,当一个对象应用两个以上的点时,您可以说 LoD 失败,例如 object.friend.stranger
而不是 object.friend
。
SoC 片上系统
The Separation of Concerns (SoC) principle suggests breaking a system into smaller parts depending on its concerns. A “concern” in that meaning implies a distinctive feature of a system.
关注点分离 (SoC) 原则建议根据系统的关注点将系统分成更小的部分。这个意义上的“关注”意味着一个系统的显着特征。
For example, if you are modeling a domain then each object can be treated as a special concern. In a layered system, each layer has its own care. In a microservice architecture, each service has its own purpose. This list can continue indefinitely.
例如,如果您正在对一个域进行建模,那么每个对象都可以被视为一个特殊的关注点。在分层系统中,每一层都有自己的职责。在微服务架构中,每个服务都有自己的用途。该列表可以无限期地继续下去。
The main thing to take out about the SoC is:
关于 SoC 的主要内容是:
- Identify the system’s concerns;
确定系统的关注点; - Divide the system into separate parts that solve these concerns independently of each other;
将系统划分为单独的部分,彼此独立地解决这些问题; - Connect these parts through a well-defined interface.
通过明确定义的接口连接这些部件。
In this fashion, the separation of concerts is very similar to the abstraction principle. The result of adhering to SoC is easy-to-understand, modular, reusable, built on stable interfaces and testable code.
在这种方式下,音乐会的分离与抽象原则非常相似。坚持 SoC 的结果是易于理解、模块化、可重用、基于稳定的接口和可测试的代码构建。
SOLID 坚硬的
The SOLID principles are a set of five design principles introduced by Robert Martin aimed to clarify the initial constraint of object-oriented programming and make programs more flexible and adaptable.
SOLID 原则是 Robert Martin 提出的一组五个设计原则,旨在澄清面向对象编程的初始约束,使程序更加灵活和适应性强。
Single responsibility principle
单一责任原则
“A class should have one, and only one, reason to change.”
“一个阶级应该有一个,而且只有一个,改变的理由。”
In other words: 换句话说:
“Gather together the things that change for the same reasons. Separate those things that change for different reasons.”
“将由于相同原因而发生变化的事物聚集在一起。将那些因不同原因而变化的事物分开。”
This is very similar to SoC, isn’t it? The difference between these two principles is that SRP aims at class-level separation, while SoC is a general approach that works both on high (e.g., layers, systems, services) and low-level (classes, functions, etc.) abstraction.
这和SoC非常相似,不是吗?这两个原则之间的区别在于,SRP 旨在类级分离,而 SoC 是一种通用方法,既适用于高层(例如层、系统、服务)又适用于低层(类、函数等)抽象。
The single responsibility principle has all the advantages of SoC, in particular, it promotes high cohesion and low coupling and avoids the god object anti-pattern.
单一责任原则具有SoC的所有优点,特别是它促进了高内聚和低耦合,并避免了神对象反模式。
Open-closed principle 开闭原则
“Software entities should be open for extension but closed for modification.”
“软件实体应该对扩展开放,但对修改关闭。”
When you implement a new feature, you should keep existing code from breaking changes.
当您实现新功能时,应该防止现有代码发生重大更改。
A class is considered open when you can extend it and add required modifications. A class is considered closed when it has clearly defined interfaces and won’t be changing in the future, i.e., it is available to use for another piece of code.
当您可以扩展某个类并添加所需的修改时,该类被视为开放的。当一个类具有明确定义的接口并且将来不会更改时,即它可用于另一段代码时,该类被认为是封闭的。
Imagine a classic OOP inheritance: you created a parent class and then later extended it with a child class with addition functionality. Then, for some reason, you decided to change an internal structure for the parent class (e.g. add a new field or remove some method), that is also accessible or directly impacts the derived class. By doing it, you violate this principle, because now you not only have to change the parent class, you also should adapt the child class for new changes. It’s happening because information hiding is not being properly applied. Instead, if you give a child class a stable contract through a public property or method, you are free to change your internal structure as long as it does not affect that contract.
想象一下经典的 OOP 继承:您创建了一个父类,然后使用具有附加功能的子类对其进行了扩展。然后,由于某种原因,您决定更改父类的内部结构(例如添加新字段或删除某些方法),这也可以访问或直接影响派生类。这样做就违反了这个原则,因为现在您不仅需要更改父类,还应该使子类适应新的更改。发生这种情况是因为信息隐藏没有得到正确应用。相反,如果您通过公共属性或方法为子类提供稳定的契约,则您可以自由地更改内部结构,只要它不影响该契约即可。
This encourages client dependency on abstraction (e.g., interface or abstract class) rather than implementation (a concrete class). Acting in this way, a client that depended on abstraction is considered closed, but at the same time, it’s open for extension because all new modifications that comply with that abstraction can be seamlessly integrated for the client.
这鼓励客户端依赖抽象(例如接口或抽象类)而不是实现(具体类)。以这种方式行事,依赖于抽象的客户端被认为是封闭的,但同时,它是开放的扩展,因为符合该抽象的所有新修改都可以为客户端无缝集成。
Let me give you another example. Let’s imagine we are developing discount calculation logic. So far, we only have two types of discount. Before applying the open-closed principle:
让我再举一个例子。假设我们正在开发折扣计算逻辑。到目前为止,我们只有两种折扣。在应用开闭原则之前:
class DiscountCalculator {public double calculateDiscountedPrice(double amount, DiscountType discount) {double discountAmount = 15.6;double percentage = 4.0;double appliedDiscount;if (discount == 'fixed') {appliedDiscount = amount - discountAmount;}if (discount == 'percentage') {appliedDiscount = amount * (1 - (percentage / 100)) ;}// logic}
}
Now, the client (DiscountCalculator
) is dependent on the external DiscountType
. If we add a new type of discount, we will need to go to this client logic and extend it. That’s undesirable behavior.
现在,客户端 ( DiscountCalculator
) 依赖于外部 DiscountType
。如果我们添加新类型的折扣,我们将需要转到此客户端逻辑并扩展它。这是不受欢迎的行为。
After applying the open-closed principle:
应用开闭原则后:
interface Discount {double applyDiscount(double amount);
}class FixedDiscount implements Discount {private double discountAmount;public FixedDiscount(double discountAmount) {this.discountAmount = discountAmount;}public double applyDiscount(double amount) {return amount - discountAmount;}
}class PercentageDiscount implements Discount {private double percentage;public PercentageDiscount(double percentage) {this.percentage = percentage;}public double applyDiscount(double amount) {return amount * (1 - (percentage / 100));}
}class DiscountCalculator {public double calculateDiscountedPrice(double amount, Discount discount) {double appliedDiscount = discount.applyDiscount(amount);// logic}
}
Here, you take advantage of the open-closed principle and polymorphism instead of adding multiple if statements to determine the type and future behavior of some entity. All classes that implement the Discount
interface are closed with respect to the public applyDiscount
method, but at the same time, they are open for modification of their internal data.
在这里,您利用开闭原则和多态性,而不是添加多个 if 语句来确定某个实体的类型和未来行为。所有实现 Discount
接口的类对于公共 applyDiscount
方法都是封闭的,但同时,它们对内部数据的修改是开放的。
Liskov substitution 利斯科夫换人
“Derived classes must be substitutable for their base classes.”
“派生类必须可以替代它们的基类。”
Or, more formally: 或者,更正式地说:
“Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T” (Barbara Liskov & Jeannette Wing, 1994)
“设 φ(x) 是关于 T 类型的对象 x 的可证明属性。那么 φ(y) 对于 S 类型的对象 y 应该为真,其中 S 是 T 的子类型”(Barbara Liskov & Jeannette Wing,1994)
In simple words, when you extend a class, you shouldn’t break the contract established in it. By “breaking a contract”, it means a failure to fulfill one of the next requirements:
简而言之,当你扩展一个类时,你不应该破坏其中建立的契约。 “违约”是指未能满足以下要求之一:
- Don’t change the parameters in derived classes: child classes should conform parent’s method signatures, i.e., accept the same parameters as the parent does or accept more abstract parameters.
不要更改派生类中的参数:子类应符合父类的方法签名,即接受与父类相同的参数或接受更抽象的参数。 - Don’t change the return type in derived classes: child classes should return the same type as the parent does or return more concrete (subtype) parameters.
不要更改派生类中的返回类型:子类应返回与父类相同的类型或返回更具体(子类型)的参数。 - Don’t throw an exception in derived classes: child classes shouldn’t throw an exception in their methods unless the parent class does. In that case, a type of exception should be the same or be a subtype of the parent’s exception.
不要在派生类中抛出异常:除非父类抛出异常,否则子类不应在其方法中抛出异常。在这种情况下,异常类型应该与父异常的类型相同或者是子类型。 - Don’t strengthen preconditions in derived classes: child classes shouldn’t change the expecting client’s behavior by restricting their work to some condition, e.g., in a parent class, you accept a string, but in a child class, you accept a string not more than 100 characters.
不要在派生类中强化先决条件:子类不应该通过将其工作限制在某些条件来改变预期客户端的行为,例如,在父类中,您接受一个字符串,但在子类中,您接受一个不超过100个字符。 - Don’t weaken postconditions in derived classes: child classes shouldn’t change the expecting client’s behavior by allowing turn down some work, e.g., don’t clean up a state after an operation, don’t close a socket, etc.
不要削弱派生类中的后置条件:子类不应通过允许拒绝某些工作来改变预期客户端的行为,例如,在操作后不清理状态,不关闭套接字等。 - Don’t weaken invariants in derived classes: child classes shouldn’t change conditions defined in the parent class, e.g., don’t reassign the field of the parent class because you may not realize the whole logic around it.
不要削弱派生类中的不变量:子类不应更改父类中定义的条件,例如,不要重新分配父类的字段,因为您可能没有意识到它周围的整个逻辑。
Interface segregation 接口隔离
“Make fine-grained interfaces that are client-specific.”
“制作针对特定客户的细粒度界面。”
Any code shouldn’t depend on methods that it doesn’t need. If a client doesn’t use some behavior of an object, why should it be forced to depend on it? Similarly, if a client doesn’t use some methods, why implementer should be forced to provide this functionality?
任何代码都不应该依赖于它不需要的方法。如果客户端不使用对象的某些行为,为什么要强迫它依赖它?同样,如果客户端不使用某些方法,为什么要强迫实现者提供此功能?
Break “fat” interfaces into more specific ones. If you change a concrete interface, these changes won’t affect unrelated clients.
将“胖”接口分解为更具体的接口。如果更改具体接口,这些更改不会影响不相关的客户端。
Dependency inversion 依赖倒置
“Depend on abstractions, not on concretions.”
“依赖抽象,而不是具体。”
Uncle Bob described this principle as a strict following of OCP and LSP:
Bob 大叔将这一原则描述为 OCP 和 LSP 的严格遵循:
“In this column, we discuss the structural implications of the OCP and the LSP. The structure that results from rigorous use of these principles can be generalized into a principle all by itself. I call it “The Dependency Inversion Principle” (DIP).” — Robert Martin.
“在本专栏中,我们讨论 OCP 和 LSP 的结构影响。严格使用这些原则所产生的结构本身可以概括为一个原则。我称之为“依赖倒置原则”(DIP)。 ——罗伯特·马丁。
Dependency inversion consists of two main statements:
依赖倒置由两个主要语句组成:
- High-level modules should not depend upon low-level modules. Both should depend on abstractions
高层模块不应该依赖于低层模块。两者都应该依赖于抽象 - Abstractions should not depend on details. Details should depend on abstractions.
抽象不应该依赖于细节。细节应该取决于抽象。
For illustration, let’s say we are developing a user service responsible for user management. To persist changes, we decided to use PostgreSQL.
为了说明这一点,假设我们正在开发一个负责用户管理的用户服务。为了持久保存更改,我们决定使用 PostgreSQL。
class UserService {private PostgresDriver postgresDriver;public UserService(PostgresDriver postgresDriver) {this.postgresDriver = postgresDriver;}public void saveUser(User user) {postgresDriver.query("INSERT INTO USER (id, username, email) VALUES (" + user.getId() + ", '" + user.getUsername() + "', '" + user.getEmail() + "')");}public User getUserById(int id) {ResultSet resultSet = postgresDriver.query("SELECT * FROM USER WHERE id = " + id);User user = null;try {if (resultSet.next()) {user = new User(resultSet.getInt("id"), resultSet.getString("username"), resultSet.getString("email"));}} catch (SQLException e) {e.printStackTrace();}return user;}// ...
}
Right now, UserService
is tightly coupled to its dependency (PostgresDriver
). But later on, we decided to migrate to the MongoDB database. Because MongoDB differs from PostgreSQL, we need to rewrite every method in our UserService
class.
现在, UserService
与其依赖项 ( PostgresDriver
) 紧密耦合。但后来,我们决定迁移到 MongoDB 数据库。因为 MongoDB 与 PostgreSQL 不同,所以我们需要重写 UserService
类中的每个方法。
The solution for it is to introduce an interface:
解决办法是引入一个接口:
interface UserRepository {void saveUser(User user);User getUserById(int id);// ...
}class UserPGRepository implements UserRepository {private PostgresDriver driver;public UserPGRepository(PostgresDriver driver) {this.driver = driver;}public void saveUser(User user) {// ...}public User getUserById(int id) {// ...}// ...
}class UserMongoRepository implements UserRepository {private MongoDriver driver;public UserPGRepository(MongoDriver driver) {this.driver = driver;}public void saveUser(User user) {// ...}public User getUserById(int id) {// ...}// ...
}class UserService {private UserRepository repository;public UserService(UserRepository database) {this.repository = database;}public void saveUser(User user) {repository.saveUser(user);}public User getUserById(int id) {return repository.getUserById(id);}// ...
}
Now high-level module (UserService
) is dependent on abstraction (UserRepository
), and abstraction does not depend on details (SQL API for PostgreSQL and Query API for MongoDB); it depends on the interface constructed for the client.
现在高级模块( UserService
)依赖于抽象( UserRepository
),而抽象不依赖于细节(PostgreSQL 的 SQL API 和 MongoDB 的查询 API);这取决于为客户端构建的接口。
In particular, to achieve dependency inversion, the dependency injection technique can be used, which you can read about at the link below:
特别是,为了实现依赖倒置,可以使用依赖注入技术,您可以在下面的链接中阅读该技术:
揭秘依赖注入:软件开发人员的基本指南-CSDN博客依赖类依赖于注入器或容器来提供所需的依赖项,并且依赖项本身被定义为契约或接口,从而允许可互换的实现。您可以创建自己的算法来注册和注入依赖项,也可以使用 DI 容器或 DI 框架来为您执行此操作。客户端:依赖类,也称为客户端类,是依赖依赖来实现其功能的类。总之,依赖注入是设计灵活、模块化和可测试的软件系统的一项有价值的技术。在 DI 中,类或其他依赖组件的依赖关系是从外部构造和提供(注入)的,而不是由依赖组件开始创建的。类应该专注于其自身与汽车行为相关的职责,而引擎的创建和配置可以在代码的不同部分中处理。https://blog.csdn.net/yu101994/article/details/138563452
GRASP 抓牢
The General Responsibility Assignment Principles (GRASP) is a set of nine principles used in object-oriented design present by Craig Larman in his book Applying UML and Patterns.
一般责任分配原则 (GRASP) 是 Craig Larman 在其《应用 UML 和模式》一书中提出的面向对象设计中使用的九个原则。
Similar to SOLID, these principles aren’t built from scratch but rather composed of time-tested programming guidelines in the context of OOP.
与 SOLID 类似,这些原则不是从头开始构建的,而是由 OOP 环境中经过时间考验的编程指南组成。
High cohesion 高凝聚力
“Keep related functionalities and responsibilities together.”
“将相关的功能和责任放在一起。”
The high cohesion principle focused on keeping complexity manageable. In this context, the cohesion is a degree of how close the responsibilities of an object are. If a class has low cohesion, it means it’s doing work unrelated to its primary purpose or doing work that can be delegated to another subsystem.
高内聚原则侧重于保持复杂性易于管理。在这种情况下,内聚力是对象职责的紧密程度。如果一个类的内聚性较低,则意味着它正在执行与其主要目的无关的工作,或者正在执行可以委托给另一个子系统的工作。
Generally, a class designed with high cohesion has a small number of methods, and all these methods are highly related by their functionality. Doing that increases the maintainability, understanding, and reuse of code.
一般来说,一个具有高内聚性的类的方法数量较少,并且所有这些方法的功能都高度相关。这样做可以提高代码的可维护性、理解性和重用性。
Low coupling 低耦合
“Reduce relations between unstable elements.”
“减少不稳定因素之间的关系。”
This principle aims to provide low dependency between elements, therefore preventing side effects arising from a code change. Here, coupling means a measure of how strongly one entity is dependent (has knowledge of or relies on) on another entity.
该原则旨在提供元素之间的低依赖性,从而防止代码更改产生的副作用。这里,耦合是指衡量一个实体对另一个实体的依赖程度(了解或依赖)的程度。
Program elements with high coupling very strongly rely on each other. If you have a class with high coupling, changes in it will lead to local changes in other parts of a system or vice versa. Such a design limits code reuse and takes more time to understand. On the other hand, low coupling supports the design of classes that are more independent, which reduces the impact of change.
具有高耦合度的程序元素彼此之间的依赖性非常强。如果您有一个高耦合的类,那么它的更改将导致系统其他部分的局部更改,反之亦然。这样的设计限制了代码的重用并且需要更多的时间来理解。另一方面,低耦合支持设计更加独立的类,从而减少变更的影响。
The principles of coupling and cohesion go hand in hand. If you have two classes with high cohesion, the connection between them is usually weak. Similarly, if those classes have a low coupling between each other, by definition, they have a high cohesion.
耦合和内聚的原则是相辅相成的。如果您有两个具有高内聚力的类,那么它们之间的联系通常很弱。类似地,如果这些类之间的耦合度较低,那么根据定义,它们就具有较高的内聚性。
The relation between coupling and cohesion
耦合与内聚的关系
Information expert 信息专家
“Place responsibilities with data.”
“用数据来承担责任。”
Information expert pattern answers the question of how we should assign responsibility for knowing a piece of information or doing some work. By following this pattern, an object who has direct access to needed information is considered to be an information expert on it.
信息专家模式回答了我们应该如何分配了解一条信息或做某项工作的责任的问题。通过遵循这种模式,能够直接访问所需信息的对象被认为是该信息的信息专家。
Remember the example of applying Demeter’s Law between a customer and a bank? It’s essentially the same thing:
还记得在客户和银行之间应用德米特定律的例子吗?本质上是同一件事:
- The
Wallet
class is an information expert on knowing balance and managing it.Wallet
类是了解平衡和管理平衡的信息专家。 - The
Customer
class is an information expert regarding its internal structure and behavior.Customer
类是关于其内部结构和行为的信息专家。 - The
Bank
class is an information expert in a bank area.Bank
类是银行领域的信息专家。
The fulfillment of a responsibility often requires gathering information across different parts of a system. Because of that, there should be intermediate information experts. With them, objects keep their internal information, which increases encapsulation and lowers coupling.
履行职责通常需要收集系统不同部分的信息。因此,应该有中级信息专家。通过它们,对象可以保留其内部信息,从而增加封装性并降低耦合。
Creator 创作者
“Assign the responsibility for object creation to a closely related class.”
“将对象创建的责任分配给密切相关的类。”
Who should be responsible for creating a new object instance? According to the Creator pattern, to create a new instance of x
class, a creator class should have one of the following properties:
谁应该负责创建新的对象实例?根据 Creator 模式,要创建 x
类的新实例,创建者类应具有以下属性之一:
- Aggregate
x
; 聚合x
; - Contain
x
; 包含x
; - Records
x
; 记录x
; - Closely use
x
; 密切使用x
; - Have required initialization data for
x
.
具有x
所需的初始化数据。
This principle promotes low coupling because if you find the right creator for an object, i.e., a class that is already somehow related to that object, you won’t increase their connectivity.
这个原则促进了低耦合,因为如果你找到一个对象的正确创建者,即一个已经以某种方式与该对象相关的类,你将不会增加它们的连接性。
Controller 控制器
“Assing the responsibility for handling system messages to a specific class.”
“将处理系统消息的责任分配给特定的类。”
A controller is a system object responsible for receiving and delegating user events to the domain layer. It’s the first element that receives a service request from UI. Usually, one controller is used for handling similar use cases, such as UserController
for managing user entity interaction.
控制器是负责接收用户事件并将其委托给域层的系统对象。它是从 UI 接收服务请求的第一个元素。通常,一个控制器用于处理类似的用例,例如 UserController
用于管理用户实体交互。
Keep in mind that the controller should not do any business work. It should be as thin as possible. It should delegate work to the appropriate classes, not be responsible for it.
请记住,控制器不应执行任何业务工作。它应该尽可能薄。它应该将工作委托给适当的类,而不是对其负责。
As an example, we can find the controller in MVC-like design patterns. Instead of direct communication between the model and the view, MVC introduces the controller — an intermediate part responsible for handling interaction between the view and model. With this component, the model becomes independent of external interaction with it.
作为一个例子,我们可以在类似 MVC 的设计模式中找到控制器。 MVC 引入了控制器——负责处理视图和模型之间交互的中间部分,而不是模型和视图之间的直接通信。有了这个组件,模型就可以独立于与它的外部交互。
Source: MDN Web Docs 来源:MDN 网络文档
Indirection 间接
“To support a low coupling, assign the responsibility to an intermediate class.”
“为了支持低耦合,请将责任分配给中间类。”
There is a famous aphorism of Butler Lampson: “All problems in computer science can be solved by another level of indirection.”
巴特勒·兰普森有一句著名的格言:“计算机科学中的所有问题都可以通过另一个间接层次来解决。”
The indirection principle has the same idea as the dependency inversion principle: to introduce an intermediate between two elements so they become indirect. This is done to support weak coupling and has all the advantages that come with it.
间接原则与依赖倒置原则具有相同的思想:在两个元素之间引入中间体,使它们变得间接。这样做是为了支持弱耦合,并具有随之而来的所有优点。
Polymorphism 多态性
“When related alternative behaviors vary by type, use polymorphism to assign responsibility to the types for which the behavior varies.”
“当相关的替代行为因类型而异时,使用多态性将责任分配给行为不同的类型。”
If you saw the code that checks object types using if
/switch
statements, it probably has a lack of polymorphism application. When required to extend this code with new functionality, you have to go to the place where the condition is checked and add a new if statement. That’s a poor design.
如果您看到使用 if
/ switch
语句检查对象类型的代码,它可能缺乏多态性应用。当需要使用新功能扩展此代码时,您必须转到检查条件的位置并添加新的 if 语句。这是一个糟糕的设计。
Using the polymorphism principle for different classes with related behavior, you unify different types, which leads to interchangeable software components, each responsible for specific functionality. Depending on the language, it can be done in numerous ways, but the common ones are to implement the same interface or use inheritance, in particular, giving the same name to methods for different objects. In the end, you get pluggable, easy-to-extend elements that don’t require changing unrelated code.
对具有相关行为的不同类使用多态性原理,可以统一不同的类型,从而产生可互换的软件组件,每个组件负责特定的功能。根据语言的不同,可以通过多种方式来完成,但常见的是实现相同的接口或使用继承,特别是为不同对象的方法赋予相同的名称。最后,您将获得可插入、易于扩展的元素,不需要更改不相关的代码。
Like with the similar open-closed principle, it’s important to use polymorphism properly. It’s needed to apply only when you are sure that some components will or may vary. You don’t need to create an abstraction for implementing polymorphism, for example, on top of language-internal classes or a framework. They are already stable and you are just doing unnecessary work.
与类似的开闭原则一样,正确使用多态性也很重要。仅当您确定某些组件将会或可能发生变化时才需要应用它。例如,您不需要在语言内部类或框架之上创建抽象来实现多态性。它们已经稳定了,你只是在做不必要的工作。
Pure fabrication 纯属捏造
“To support a high cohesion, assign the responsibility to the convenient class.”
“为了支持高凝聚力,将责任分配给方便的班级。”
Sometimes, to follow high cohesion/low coupling principles, there is no corresponding real-world entity. From this point of view, you are creating a fabrication, something that does not exist in a domain. It’s pure because the responsibilities of this entity are clearly designed. Craig Larman suggests using this principle when an information expert applies logically wrong.
有时,为了遵循高内聚/低耦合原则,没有相应的现实世界实体。从这个角度来看,您正在创建一个虚构的东西,这是一个域中不存在的东西。它是纯粹的,因为这个实体的职责是明确设计的。克雷格·拉曼(Craig Larman)建议,当信息专家的应用逻辑错误时,可以使用这一原则。
For example, a controller pattern is a pure fabrication. A DAO or a repository is also a fabrication. These classes are not exclusive to some domains but are convenient for developers. Although, we could put data access logic directly in the domain classes (since there are expert in their area), this would lead to a violation of high cohesion because data management logic is not directly related to how the domain object behaves. Coupling is also increased because we need to depend on the database interface. And code duplication is likely to be high because managing the data for different domain entities has similar logic. In other words, this leads to mixing different abstractions in one place.
例如,控制器模式是纯粹的制造。 DAO 或存储库也是虚构的。这些类并非某些领域所独有,但对开发人员来说很方便。虽然,我们可以将数据访问逻辑直接放在域类中(因为有他们领域的专家),但这会导致违反高内聚性,因为数据管理逻辑与域对象的行为方式不直接相关。因为我们需要依赖数据库接口,所以耦合性也增加了。而且代码重复可能会很高,因为管理不同域实体的数据具有相似的逻辑。换句话说,这导致将不同的抽象混合在一个地方。
The benefit of using pure fabrication classes is to group related behavior in objects, for which there is no alternative in the real world. This results in a good design with code reuse and low dependency on different responsibilities.
使用纯制造类的好处是将对象中的相关行为分组,这在现实世界中没有其他选择。这会产生具有代码重用性和对不同职责的低依赖性的良好设计。
Protected variations 受保护的变体
“Protect predicted variations by introducing a stable contract.”
“通过引入稳定的合约来保护预测的变化。”
To provide future changes without breaking other parts, you need to introduce a stable contract that stops undetermined impacts. This principle emphasizes the importance of earlier discussed principles for separating responsibilities between different objects: you need to apply the indirection to easily switch between different implementations, you need to use the information expert to decide who should be responsible for the fulfillment of a requirement, you need to design your system with the polymorphism in mind to introduce varying pluggable solutions, etc.
为了在不破坏其他部分的情况下提供未来的更改,您需要引入一个稳定的合约来阻止未确定的影响。该原则强调了前面讨论的分离不同对象之间职责的原则的重要性:您需要应用间接来轻松地在不同实现之间切换,您需要使用信息专家来决定谁应该负责满足需求,您需要在设计系统时考虑到多态性,以引入不同的可插拔解决方案等。
The protected variations principle is a core concept that drives other design patterns and principles.
受保护的变化原则是驱动其他设计模式和原则的核心概念。
“At one level, the maturation of a developer or architect can be seen in their growing knowledge of ever-wider mechanisms to achieve PV, to pick the appropriate PV battles worth fighting, and their ability to choose a suitable PV solution. In the early stages, one learns about data encapsulation, interfaces, and polymorphism — all core mechanisms to achieve PV. Later, one learns techniques such as rule-based languages, rule interpreters, reflective and metadata designs, virtual machines, and so forth — all of which can be applied to protect against some variation.” — Craig Larman, Applying UML and Patterns.
“在某种程度上,开发商或架构师的成熟可以体现在他们对实现光伏发电的更广泛机制的了解不断增长,选择值得打的适当的光伏战争,以及他们选择合适的光伏解决方案的能力。在早期阶段,人们学习数据封装、接口和多态性——所有实现 PV 的核心机制。后来,人们学习基于规则的语言、规则解释器、反射和元数据设计、虚拟机等技术,所有这些都可以用来防止某些变化。” — Craig Larman,应用 UML 和模式。
Conclusion 结论
I am sure that before reading this article you have already used some of the principles discussed unconsciously. Now you have a name for them and it makes it easier to communicate.
我确信在阅读本文之前您已经在不知不觉中使用了所讨论的一些原则。现在您已经为它们起了一个名字,这样沟通起来就更容易了。
You may have noticed that some of the principles have the same underlying idea. Essentially, it’s how it is. For example, information expert, SoC, high cohesion & low coupling, SRP, interface segregation, etc., all have the same point of view to separate concerns between different software elements. And they do it to fulfill protected variations, dependency inversion, and indirection to get maintainable, extensible, understandable, and testable code.
您可能已经注意到,某些原则具有相同的基本思想。本质上,就是这样。例如,信息专家、SoC、高内聚低耦合、SRP、接口隔离等,都有相同的观点来分离不同软件元素之间的关注点。他们这样做是为了实现受保护的变化、依赖倒置和间接获取可维护、可扩展、可理解和可测试的代码。
As with any tool, this is a guideline, not a strict rule. The key is to understand the trade-offs and make informed decisions. I intentionally did not touch upon the topic of the misapplication of software principles so that you could think for yourself and find answers. Not knowing the other side of the coin is as bad as not knowing these principles at all.
与任何工具一样,这是一个指南,而不是严格的规则。关键是要了解权衡并做出明智的决定。我故意不涉及软件原理误用的话题,以便大家自己思考并找到答案。不了解硬币的另一面与根本不了解这些原则一样糟糕。
Thank you for reading this article!
感谢您阅读这篇文章!
Any questions or suggestions? Feel free to write a comment.
有任何问题或建议吗?请随意写评论。