为什么SOLID原则仍然是现代软件架构的基础?- StackOverflow


尽管自构思 SOLID 原则以来的 20 年来计算发生了很大变化,但它们仍然是设计软件的最佳实践。
SOLID 原则是经过时间考验的用于创建高质量软件的准则。但在多范式编程和云计算的世界里,它们还能叠加吗?我将探索 SOLID 代表什么(字面上和比喻上),解释为什么它仍然有意义,并分享一些关于它如何适用于现代计算的例子。
SOLID是一组从 Robert C. Martin(鲍勃大叔) 在 2000 年代初期的著作中提炼出来的原则。它被提议作为一种专门考虑面向对象 (OO) 编程质量的方式。总体而言,SOLID 原则对如何拆分代码、哪些部分应该是内部的或公开的以及代码应该如何使用其他代码提出了争论。我将深入研究下面的每个字母并解释其原始含义,以及可应用于 OO 编程之外的扩展含义。
 
发生了什么变化?
在 2000 年代初期,Java 和 C++ 是王者。当然,在我的大学课程中,Java 是我们选择的语言,我们的大部分练习和课程都使用它。Java 的流行催生了书籍、会议、课程和其他材料的家庭手工业,以帮助人们从编写代码到编写好的代码。
从那时起,软件行业发生了深刻的变化。几个值得注意的:

  • 动态类型语言,如 Python、Ruby,尤其是 JavaScript,已经变得和 Java 一样流行——在某些行业和类型的公司中超过了它。
  • 非面向对象范式,尤其是函数式编程 (FP),在这些新语言中也更为常见。甚至 Java 本身也引入了 lambdas!元编程(添加和更改对象的方法和特征)等技术也很受欢迎。还有“更软”的面向对象风格,例如 Go,它具有静态类型但没有继承。所有这些都意味着类和继承在现代软件中不如过去重要。
  • 开源软件已经激增。早先,最常见的做法是编写供客户使用的闭源编译软件,而现在,您的依赖项是开源的更为常见。因此,在编写库时曾经必不可少的那种逻辑和数据隐藏不再那么重要。
  • 微服务和软件即服务迅速涌现。与其将应用程序部署为将其所有依赖项链接在一起的大型可执行文件,不如部署一个与其他服务(您自己的或由第三方提供支持的服务)对话的小型服务。

总的来说,SOLID 真正关心的许多事情——比如类和接口、数据隐藏和多态——已经不再是程序员每天要处理的事情。
 
什么没变?
这个行业现在在很多方面都不同了,但有些事情没有改变,也可能不会改变。这些包括:
  • 代码是由人编写和修改的。代码编写一次,阅读多次。无论是内部的还是外部的,总是需要记录良好的代码,尤其是记录良好的 API。
  • 代码被组织成模块。在某些语言中,这些是类。在其他情况下,它们可能是单独的源文件。在 JavaScript 中,它们可能是导出对象。无论如何,存在某种方法可以将代码分离和组织成不同的、有界的单元。因此,总是需要决定如何最好地将代码组合在一起。
  • 代码可以是内部的,也可以是外部的。有些代码是为您自己或您的团队编写的。编写其他代码供其他团队甚至外部客户使用(通过 API)。这意味着需要某种方式来决定哪些代码是“可见的”,哪些是“隐藏的”。

 
现代SOLID
  • 单一职责原则:

“一个类改变的理由永远不应该超过一个。”
新定义:“每个模块都应该做一件事并做好。”
这个原则与高内聚的话题密切相关。本质上,您的代码不应将多个角色或用途混合在一起。
这也适用于微服务设计;如果您有一个服务来处理所有这三个功能,那么它就试图做太多事情。
  • 开闭原则

原始定义:“软件实体应该对扩展开放,对修改关闭。”
新定义:“您应该能够在不重写模块的情况下使用和添加模块。”
这在面向对象的领域是免费的。在 FP 世界中,您的代码必须定义明确的“挂钩点”以允许修改。
  • 里氏替换原则

原始定义:“如果 S 是T的子类型,那么类型 T 的对象可以替换为类型 S 的对象,而不会改变程序的任何理想属性。”
新定义: 如果声明这些事物的行为方式相同,您应该能够用一种事物替换另一种事物。
在动态语言中,重要的是,如果你的程序“承诺”做某事(例如实现一个接口或一个函数),你需要遵守你的承诺,不要让你的客户感到惊讶。
许多动态语言使用鸭子duck类型来实现这一点。本质上,您的函数正式或非正式地声明它期望其输入以特定方式运行并根据该假设进行。
下面是一个使用 Ruby 的例子:
# @param input [to_s]
def split_lines(input)
 input.to_s.split("\n")
end

在这种情况下,函数并不关心input是什么类型——只关心它有一个to_s函数,它的行为方式与所有to_s函数的行为方式相同,即将输入转换为字符串。许多动态语言没有办法强制这种行为,所以这更像是一个纪律问题,而不是一种形式化的技术。
这是一个使用 TypeScript 的 FP 示例。在这种情况下,高阶函数接受一个过滤器函数,它需要一个数字输入并返回一个布尔值:
const isEven = (x: number) : boolean => x % 2 == 0;
const isOdd = (x: number) : boolean => x % 2 == 1;
 
const printFiltered = (arr: number[], filterFunc: (int) => boolean) => {
 arr.forEach((item) => {
   if (filterFunc(item)) {
     console.log(item);
   }
 })
}
 
const array = [1, 2, 3, 4, 5, 6];
printFiltered(array, isEven);
printFiltered(array, isOdd);

  • 接口隔离原则

原始定义:“许多特定于客户端的接口比一个通用接口要好。”
新定义: “不要向客户展示超出他们需要看到的内容”。
只记录您的客户需要知道的内容。这可能意味着使用文档生成器只输出“公共”函数或路由,而让“私有”函数或路由不发出。
在微服务世界中,您可以使用文档或真正的分离来增强清晰度。例如,您的外部客户可能只能以用户身份登录,但您的内部服务可能需要获取用户列表或其他属性。您可以创建一个单独的“仅限外部”用户服务来调用您的主服务,或者您可以只为隐藏内部路由的外部用户输出特定文档。 
  • 依赖倒置原则

原始定义:“取决于抽象,而不是具体。”
新定义: “取决于抽象,而不是具体。”
保持定义不变!在可能的情况下保持事物抽象的想法仍然很重要,即使现代代码中的抽象机制不如严格的面向对象世界中那么强大。
 
结论
再次重申“现代 SOLID”:
  • 不要让阅读你代码的人感到惊讶。
  • 不要让使用你的代码的人感到惊讶。
  • 不要压倒阅读您代码的人。
  • 为您的代码使用合理的边界。
  • 使用正确的耦合级别——将属于在一起的事物放在一起,如果它们分开,则将它们分开。

好的代码就是好的代码——这不会改变,而 SOLID 是实践这一点的坚实基础!