这个缩略图(Thumbnail)由Author 制作。
大家好,让我们开始吧软件设计原则是软件开发的基础。作为一名软件开发者,你可以在工作中接触到这些工具、语言、框架、范式和模式。它们是编写优质且易读代码的核心支柱。一旦你理解了这些原则,你会发现它们无处不在。
区分优秀的工程师和一般的工程师在于是否能看到和应用这些原则。没有一个框架或工具能在不理解基础的情况下帮助你提升编写优质代码的能力;更重要的是,没有这些基础,你会成为该工具的囚徒。
这篇文章不是一份参考指南,而是我尝试将一份需要不时回顾的核心原则列表整理成系统。
抽象:抽象(计算机科学中的抽象概念参见:https://en.wikipedia.org/wiki/Abstraction\(computer_science\)_)是相当重要的原则之一。抽象指的是专注于重要部分,忽略其他细节。抽象可以主要从两个角度来理解:一方面,它是概括的过程;另一方面,它是这个过程的结果。
在软件开发中,抽象通常与封装一起出现,这种方法用于隐藏抽象部分的实现细节。你可以在各种形式下看到抽象。例如,当你定义一种类型时,你是在抽象变量的内存表示。同样,当你抽象一个接口或函数的签名时,你关注的是重要的方面:与之交互的契约。在设计类时,你只选择与你的领域和特定业务用例相关的属性。还有很多其他例子,但抽象的主要目的是你无需了解实现细节即可使用;因此,你可以更好地专注于对你来说最重要的部分。
这个原则不仅适用于应用程序开发。作为一名程序员,你通过语言语法抽象了与操作系统底层操作的交互过程。操作系统反过来将你的编程语言从与CPU、内存、网卡等硬件的底层操作的交互中抽象出来。越深入地理解,就越明白这只是一个抽象的层次问题。
来自: Reddit.
把变动的部分打包起来如你所见,抽象可以以不同的形式表现出来——从数据抽象到层次化抽象。使用抽象的一个通用原则是:“封装变化。”识别可能变化的部分,并定义一个具体的接口给它。这样,即使内部逻辑发生变化,客户端仍然可以保持一致的交互。
假设你需要计算一种货币兑换率的计算。目前,你只拥有两种货币。你可以这样计算:
if (baseCurrency == "USD" && targetCurrency == "EUR") return amount * 0.90;
if (baseCurrency == "EUR" && targetCurrency == "USD") return amount * 1.90;
但将来可能会增加一种新的货币类型,这将需要对客户端代码进行修改。相反,最好将所有相关逻辑抽象并封装到一个单独的方法中,并在需要时从客户端调用此方法。
function convertCurrency(amount, baseCurrency, targetCurrency) {
if (baseCurrency == "USD" && targetCurrency == "EUR") return amount * 0.90;
if (baseCurrency == "EUR" && targetCurrency == "USD") return amount * 1.90;
if (baseCurrency == "USD" && targetCurrency == "UAH") return amount * 38.24;
…
}
保持不重复(DRY)
DRY (简称DIE,重复是邪恶的)意味着你不应该在代码库中重复信息或知识,不要重复代码。
“每一项知识在系统内都必须有一个唯一、明确、权威的表述形式” — 安迪·亨特和戴夫·托马斯,《程序员修炼之道》
减少代码重复的好处在于代码的更改和维护变得更加简单。如果你在多个地方重复了逻辑,然后发现了一个错误,你很可能会忘记在其中一个地方做修改,这会导致看似相同的功能行为出现差异。相反,找到重复的功能,将其提炼成一个过程、类等,给它取一个有意义的名字,并在需要的地方使用。这提倡单一的修改点,并尽量减少对其他功能的影响。
亲吻KISS(保持简单)这一短语是由著名的飞机工程师凯利·约翰逊提出的,他曾对他的工程团队提出挑战,设计的喷气式飞机必须在战时条件下,只能使用特定工具,由普通的维修人员进行维修。
它的主要理念是专注于系统的简洁,这便于理解并减少过度复杂化,只用真正需要的工具。
执行不必要的功能你用不着 (Shí zhí bù xū yào de gōng néng nǐ yòng bù zháo)当你设计解决方案来解决这个问题时,你考虑的是两件事:如何更好地将其适应当前系统,以及如何使其具有扩展性以满足未来的可能需求。在第二种情况下,为了更好的可扩展性而急于添加某个功能通常是错误的:即便你认为这会减少集成的成本,这样的代码在维护和调试时可能变得更加复杂且不必要。这样就违背了之前的原理,增加了为解决当前问题而产生的冗余复杂性。而且,别忘了你假设的功能将来可能根本用不上,这样你就只是在浪费资源。
这就是YAGNI 或者“你不会需要它”所要表达的意思。不过别误会;你应该考虑你的解决方案未来的情况,但等到真正需要的时候再添加代码。
LoD洛德法则(LoD)(Law of Demeter),有时也称为最少知道原则,最好别跟“陌生人”交流。因为在OOP中,“陌生人”指的是任何与当前对象没有直接关联的对象。
使用 Demeter 法则的好处是提高了维护的便利性,通过避免不相关对象之间的直接接触来实现。
因此,当你与某个对象互动,如果以下任一情况未满足,你就违背了这条原则:
- 当对象是当前通过
this
访问的类的实例时 - 当对象是类的成员时
- 当对象是通过参数传递给方法的
- 当对象是在方法内实例化时
- 当对象是全局可访问时
比如说,如果一个客户想要存款到他的银行账户,我们可能涉及三个类——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 deposit(Customer customer, decimal amount) {
Wallet customerWallet = customer.wallet;
if (customerWallet.getBalance() >= amount) {
customerWallet.withdrawMoney(amount);
//...
} else {
//...
}
}
}
makeDeposit
方法中可以看到违反了迪米特定律这一现象。从依赖层次的角度来看,访问客户的钱包是合理的,尽管从逻辑角度来看,这样做显得有些奇怪。但是在这里,银行对象直接与 customerWallet
对象的 getBalance
和 withdrawMoney
方法交互,与陌生人(钱包)进行交互,而不是与朋友(客户)直接交互。
在应用层级决定(Layer of Decision)原则之前
这里告诉你怎么解决这个问题
class Wallet {
private decimal balance;
public decimal getBalance() {
return balance;
}
public boolean canWithdraw(decimal amount) {
return balance >= amount;
}
public void 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 deposit(Customer customer, decimal amount) {
boolean paymentSuccessful = customer.makePayment(amount);
if (paymentSuccessful) {
//...
} else {
//...
}
}
}
现在所有与客户钱包的交互都是通过客户对象进行的。这种设计方式有利于减少银行对象和客户对象之间的依赖,方便调整Wallet
和Customer
类的实现(银行对象不需要关心客户的内部实现细节),也便于进行测试。
用了LoD原则之后
通常来说,当一个对象上应用了超过两个点时,可以说LoD就失效了,比如使用了object.friend.stranger
而是object.friend
。
分离关注点(SoC)原则建议将系统分解为更小的部分,每个部分对应不同的关注点。“关注点”在此意指系统的不同职责或功能。
例如,比如说你在建模一个领域时,每个对象都可以被视为一个特殊关注。在分层系统中,每一层都有自己的关注点。在微服务架构中,每个服务都有其特定的目的。这样的例子可以举出很多。
关于这个系统芯片,最需要注意的是:
- 识别系统的关注点;
- 将系统划分为可以独立解决这些关注点的不同部分;
- 通过一个定义明确的接口将这些部分连接起来。
以这种方式,将音乐会分离非常类似于抽象原则。遵循单一职责原则(SoC)可以带来易于理解、模块化、可重用、基于稳定接口、代码可测试的结果。
SOLIDSOLID 原则是罗伯特·C·马丁提出的一组五个设计原则,旨在帮助澄清面向对象编程的限制,使程序更加灵活和易于适应。
单一职责原则“一个类应该只有一个修改原因。”
换句话说,
“将因相同原因改变的事物聚集在一起,将因不同原因改变的事物分开。”
这和SoC差不多吧?它们之间的主要区别是,SRP侧重于类级别的分离,而SoC则是一种通用的方法,它既适用于高层次(比如层、系统、服务),也适用于低层次(如类、函数等)。
单一职责原则具有与分层架构相同的优点,尤其有利于提高内聚性并减少耦合,并能避免这种反模式(即神对象)。
开闭性原则“软件组件应当易于扩展,但不应进行修改。”
当你实现一个新功能时,确保现有代码不受影响。
一个类被认为是开放的,当你能够扩展它并添加所需的修改时。一个类被认为是封闭的,当它具有明确定义的接口且在未来不会发生变化,也就是说,它可以被其他代码使用。
想象一个经典的面向对象编程的继承模式:你创建了一个父类,后来增加了一个子类,添加了新的功能。然后,由于某种原因,你决定改变父类的内部结构(比如添加一个新字段或移除某个方法),这个变化也会影响到子类的实现。这样做的时候,你违反了这一原则,因为仅仅修改父类是不够的,你还需要适应子类对这些新变化。这是因为信息隐藏没有被正确应用。相反,如果你通过稳定的公共属性或方法为子类提供契约,你就可以自由地更改父类的内部结构,只要不影响这些契约。
这促使客户端依赖于抽象(例如,接口或抽象类)而不是实现(具体类)。这样做,依赖抽象的客户端被视为封闭的,但同时,它又是开放扩展的,因为所有符合该抽象的新修改都可以无缝地集成到客户端里。
让我再给你举一个例子。假设我们正在开发折扣计算逻辑。到目前为止,我们只有两种类型的折扣,而在未应用开闭原则之前。
class DiscountCalculator {
public double calculateDiscountedPrice(double amount, DiscountType discount) {
double 优惠金额 = 15.6;
double 百分比 = 4.0;
double 应用优惠;
if (discount.equals("固定")) {
应用优惠 = amount - 优惠金额;
}
if (discount.equals("百分比")) {
应用优惠 = amount * (1 - (百分比 / 100));
}
// 逻辑处理
}
}
现在,客户端 (DiscountCalculator
) 依赖于外部的 DiscountType
接口或类。如果我们添加一个新的折扣类型,就需要去修改客户端逻辑进行扩展。这样的做法不太理想。
在用了开闭原则之后:
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);
// 计算折扣后的价格
// 逻辑
}
}
在这里,你利用了开闭原则和多态性来避免通过添加多个 if 语句来判断某个实体的类型和行为。所有实现 Discount
接口的类都对公共的 applyDiscount
方法是封闭的,但允许它们对内部数据进行修改。
“派生类必须可以替换其基类。”
或者说,更正式地:
如果φ(x)是关于类型T的对象x的一个可证明属性,那么对于类型S的对象y,φ(y)也应该为真,其中S是T的子类型(subtype)(Barbara Liskov & Jeannette Wing, 1994)。
简单来说,当你继承一个类时,你不应该打破它所承诺的契约。所谓“打破契约”,就是指未能满足以下要求之一:
- 不要在派生类中更改参数:子类应该接受与父类相同的参数,或者比父类更通用的参数。
- 不要在派生类中更改返回类型:子类应该返回与父类相同的类型,或返回更具体的(子类型)返回值。
- 不要在派生类中抛出异常:子类不应在其方法中抛出异常,除非父类也抛出异常的情况。在这种情况下,异常的类型应该相同或为父类异常的子类型。
- 不要在派生类中增加某些限制条件:子类不应该通过增加某些限制条件来改变预期的客户端行为,例如,在父类中接受字符串,但在子类中只接受不超过100个字符的字符串。
- 不要在派生类中允许某些操作降级:子类不应该通过允许某些操作降级来改变预期的客户端行为,例如,不要在操作完成后清理状态,不要关闭套接字等。
- 不要在派生类中减弱不变量:子类不应该改变父类中定义的条件,例如,不要重设父类的字段,因为你可能不了解围绕它的整个逻辑。
“创建面向特定客户端的细粒度的接口。”
任何代码不应该依赖于它不需要的方法。如果一个客户端没有使用某个对象的某些功能,为什么它必须依赖这些行为?同样,如果一个客户端没有使用某些方法的话,为什么实现者必须提供这些功能?
将“胖”接口拆分成更具体的接口。如果修改了某个具体的接口,这些改动不会影响到无关的客户端。
依赖倒置“要依赖抽象的东西,而不是具体的东西。”
鲍勃叔叔在一篇文章中描述了这个原则为严格遵守 OCP 和 LSP:
“在这篇文章里,我们探讨了OCP和LSP的结构意义。这些原则运用得当,其结果可以被提炼为一个原则。我将其命名为“依赖倒置原则”(DIP)。”— 罗伯特·马丁
依赖倒置包含两个主要原则:
- 高层次模块不应该依赖于低层次模块。两者都应依赖于抽象概念。
- 抽象不应依赖于具体的细节。具体的细节应该依赖于抽象。
举例说明,假设我们正在开发一个用户服务,它负责用户管理。我们决定使用PostgreSQL来持久化更改。
class UserService {
private PostgresDriver postgresDriver;
public UserService(PostgresDriver postgresDriver) {
this.postgresDriver = postgresDriver;
}
public void saveUser(User user) {
// 保存用户信息到数据库
postgresDriver.query("INSERT INTO 用户 (id, 用户名, 邮箱) VALUES (" + user.getId() + ", '" + user.getUsername() + "', '" + user.getEmail() + "')");
}
public User getUserById(int id) {
ResultSet resultSet = postgresDriver.query("SELECT * FROM 用户 WHERE id = " + id);
User user = null;
try {
if (resultSet.next()) {
// 根据结果集创建用户对象
user = new User(resultSet.getInt("id"), resultSet.getString("用户名"), resultSet.getString("邮箱"));
}
} catch (SQLException e) {
// 打印异常堆栈
e.printStackTrace();
}
return user;
}
// ...
}
目前,UserService
与其依赖的 (PostgresDriver
) 紧密耦合。但之后我们决定迁移 MongoDB 数据库。因为 MongoDB 和 PostgreSQL 有差异,我们需要重写 UserService
类中的每一个方法。
解决办法就是引入一个接口。
interface 用户存储库 {
void 保存用户(User user);
User 通过ID获取用户(int id);
// ... 该注释保留原样,表示此处有实现细节待补充
}
class PostgreSQL用户存储库 implements 用户存储库 {
private PostgresDriver 驱动; // 这里指PostgreSQL的数据库驱动
public PostgreSQL用户存储库(PostgresDriver driver) {
this.驱动 = driver;
}
public void 保存用户(User user) {
// ... 该注释保留原样,表示此处有实现细节待补充
}
public User 通过ID获取用户(int id) {
// ... 该注释保留原样,表示此处有实现细节待补充
}
// ... 该注释保留原样,表示此处有实现细节待补充
}
class MongoDB用户存储库 implements 用户存储库 {
private MongoDriver 驱动; // 这里指MongoDB的数据库驱动
public MongoDB用户存储库(MongoDriver driver) {
this.驱动 = driver;
}
public void 保存用户(User user) {
// ... 该注释保留原样,表示此处有实现细节待补充
}
public User 通过ID获取用户(int id) {
// ... 该注释保留原样,表示此处有实现细节待补充
}
// ... 该注释保留原样,表示此处有实现细节待补充
}
class 用户服务 {
private 用户存储库 存储库; // 用户服务类负责与用户相关的业务操作
public 用户服务(用户存储库 repository) {
this.存储库 = repository;
}
public void 保存用户(User user) {
存储库.保存用户(user);
}
public User 通过ID获取用户(int id) {
return 存储库.通过ID获取用户(id);
}
// ... 该注释保留原样,表示此处有实现细节待补充
}
现在高级模块 (UserService
) 依赖于抽象类(UserRepository
),而抽象类不依赖于细节(PostgreSQL 的 SQL API 和 MongoDB 的查询 API),它依赖于为客户端构建的接口。
特别地,为了实现依赖倒置,你可以使用依赖注入技术,下面的链接中可以了解更多相关信息。
揭秘依赖注入:软件开发者的必读指南提升代码质量和可维护性,让开发更轻松javascript.plainenglish.io GRASP(面向对象的设计原则).GRASP(通用责任分配原则),包括九项原则,由Craig Larman在其著作《应用UML和模式》中提出,用于面向对象编程的设计原则。
类似于 SOLID,这些原则并非从头开始构建,而是由经过时间考验的面向对象编程原则构成,在面向对象编程的上下文中。
高度内聚性代码“要让相关的功能和职责保持在一起。”
高内聚原则旨在使复杂性易于管理。在此上下文中,内聚度衡量一个对象职责之间的紧密程度。如果一个类的内聚度低,意味着它正在执行与其主要功能无关的工作,或者可以将这些任务委托给其他子系统来完成。
一般来说,高内聚的类设计时包含较少的方法,并且这些方法的功能高度相关。功能紧密相关的类通常包含较少的方法。这样能提高代码的可维护性、易理解性和可重用性。
耦合度低"减少与那些不稳定的人或因素的联系。”
这个原则旨在提供低耦合,从而防止因代码变更而引发的副作用。这里,耦合指的是实体之间的依赖程度(对另一个实体的了解或依赖程度)。
高度耦合的程序元素相互依赖性很强。该类一旦发生变化,其他部分也可能需要相应调整,反之亦然。这样的设计让代码难以重用,也让理解和维护变得更加费时。另一方面,低耦合有助于设计更独立的类,从而减少变更带来的连锁反应。
耦合和内聚这两个概念是一起出现的。如果两个类自身的内聚性很强,那么它们之间的关系通常较松散。同样,如果这些类之间的耦合较弱,那么根据定义,它们的内聚性就很高。
耦合与内聚性之间的关系是怎样的
信息达人“把责任与数据联系起来。”
信息专家模式解决了一个问题,即我们应该如何分配对某个信息或工作的了解责任。按照这种模式,直接访问所需信息的对象被认为是该信息的信息专家。
还记得顾客和银行之间应用迪米特定律的例子吗?其实是一样的。
Wallet
类是关于余额信息和管理的专家。Customer
类是内部结构和行为的专家。Bank
类是银行相关领域的专家。
履行一个责任通常需要在系统不同部分收集信息。因此,系统中应该有一些中间的信息专家。有了他们,对象可以保留其内部信息,这有助于增加封装性并减少耦合。
创建人“将对象创建的责任交给一个紧密相关的类或对象。”
谁应该负责创建一个新的对象实例?根据工厂模式,要创建一个新的 x
类实例,创建者类应该具有以下一个或多个属性:
- 聚合
x
数据; - 包含
x
项; - 记录
x
信息; - 频繁使用
x
; x
必须具有初始化数据。
这一原则促进了低耦合性,因为如果你找到了某个对象的合适的创建者,也就是说,这个类已经与该对象有一定关联,就不会增加它们的耦合程度。
控制台将处理系统消息的职责交给一个特定的类。
一个控制器是一个系统对象,负责接收并委派用户事件到领域逻辑层。UI发出的服务请求首先被控制器接收。通常,一个特定的控制器比如UserController
(用户控制器)处理一组相关的用户操作,例如管理用户交互。
记住,控制器不应该做任何业务工作。它应该尽可能地简单。它应该把任务交给相应的类处理,而不是自己去做。
作为一个例子,我们可以在类似MVC的设计模式中找到控制器。MVC通过引入控制器,避免了模型和视图之间直接通信,控制器负责处理视图和模型之间的交互。通过这个组件,模型就可以独立于外部对其的交互。
迂回为了实现低耦合,可以将责任赋予中间类。
著名计算机科学家巴特·兰普森有句名言:“计算机科学里的所有问题都可以通过增加一个间接层次来解决。”
间接原则与依赖反转原则的理念相同:在两个元素之间引入一个中间体,使它们变得“间接”,这样可以支持弱耦合,并带来所有相关的好处。
多态“当相关的替代行为类型随着类型的不同而变化时,使用多态性将这些行为变化的责任分配给相应的类型。”
如果你看到使用 if
/switch
语句检查对象类型的情况,这可能意味着没有充分应用多态性。当你需要给这段代码添加新功能时,你得找到检查条件的地方,再加一个新的 if 语句。这样的设计就不太好了。
使用多态性原则,为行为相关但类型不同的类可以进行统一,从而实现可互换的软件组件,每个组件负责特定的功能。根据使用的不同语言,实现方式多种多样,但常见的是实现相同的接口或使用继承,尤其是为不同对象的方法命名相同。在实现多态性后,最终你将得到可插拔且易于扩展的元素,而无需修改与之无关的代码。
就像处理开闭原则一样,正确使用多态性同样重要。只有当你确定某些组件可能会变化时,才需要使用多态性。例如,不要在语言自带的类或框架之上创建抽象。这些类和框架已经很稳定了,你只是在做无用功。
纯粹的捏造“为了保持高内聚,应将责任分配给合适的类。”
有时,为了遵循高内聚低耦合的原则,可能没有对应的现实实体。从这个角度看,你正在创造一个 虚构 的东西,它在实际领域中并不存在。它是 纯粹的 ,因为它的职责被清晰地定义了。Craig Larman 建议当信息专家逻辑上出错时使用这一原则。
例如,控制器模式纯粹是人为设计的。DAO 或仓库也纯粹是人为设计的。这些类并不局限于特定领域,而是为了开发者方便设计的。尽管我们可以将数据访问逻辑直接放在领域类中(因为这些领域专家了解自己的领域),但这会导致高内聚性被打破,因为数据管理逻辑与领域对象的行为直接无关。耦合也会增加,因为我们需要依赖数据库接口。此外,代码重复也可能会很高,因为管理不同领域实体的数据具有相似的逻辑。换句话说,这会导致在一个地方混合不同的抽象概念。
使用纯制造类(Pure Fabrication 类)的好处是将相关行为组织在对象中,而在现实中这些行为无法在现实世界中找到对应物。这有助于设计良好的代码,这样可以实现代码复用并减少对不同职责的依赖。
受保护的变种“通过引入一个稳定的合同来保护预测的变量。”
为了在不影响其他部分的情况下提供未来的改动,你需要引入一个稳定的合同来避免不确定的影响。这一原则强调了之前讨论过的在不同对象间分离职责的重要性:你需要运用间接性来轻松地在不同的实现之间切换,你需要运用信息专家原则来决定谁应负责实现该需求,在设计系统时,你需要考虑多态性,以便引入可变的可插拔解决方案等等。
受保护的变异原则是这个核心概念,推动其他设计模式和原则。
结论部分。“从某种程度上来说,一个开发者或架构师的成长体现在他们对实现PV的机制的知识越来越广,能够挑选出值得投入的PV努力,以及选择合适的PV解决方案的能力。在早期阶段,初学者会学到数据封装、接口和多态性——这些都是实现PV的核心机制。后来,人们会学到诸如基于规则的语言、规则解释器、反射和元数据设计、虚拟机(VM)等技术——所有这些技术都可以用于防范特定的变化。” —— Craig Larman,《应用UML和模式》。
在读这篇文章之前,你可能已经无意识中运用了一些这里提到的原则。现在你知道了这些原则的名字,这让跨语言交流变得更加容易。
你也许已经注意到,有些原则具有相同的基本理念。本质上就是这样。例如,信息专家原则、单一职责、高凝聚低耦联、单一职责原则、接口隔离等,所有这些原则都是为了分离不同软件元素的关注点。它们这样做是为了实现保护变化、依赖倒置原则和间接性,目的是获得易于维护、扩展、理解和测试的代码。
就像任何工具一样,这只是一个指南,而不是死板的规定。关键在于理解其中的取舍并作出明智的选择。我故意没有讨论到软件原则被误用的情况,让你自己思考并找到答案。不了解事情的另一面和完全不懂这些原则一样糟糕。
谢谢读到这篇文章!
有问题或建议吗?欢迎在下面留言。
我也会很开心,如果你能关注我一下并给这篇文章点几个赞支持一下!😊
可以在这里查看我最近写的几篇文章:
Pavlo Kolodka (@pavlokolodka 在 Medium 上)
软件开发八个故事
Node.js (一个基于 Chrome V8 引擎的 JavaScript 运行环境)三个故事