Effective Kotlin中文版
  • ReadMe
  • 前言
  • 第一部分:良好的代码
    • 第一章:安全性
      • 第1条:限制可变性
      • 第2条:最小化变量的作用域
      • 第3条:尽可能消除平台类型
      • 第4条: 不要暴露需要推断的类型
      • 第5条:指明你期望的参数和状态
      • 第 6 条: 优先使用标准错误,而不是自定错误
      • 第7条:当返回结果可能缺失时,优先使 null 或 Failure
      • 第8条:妥善处理空值
      • 第9条: 使用 use 来关闭资源
      • 第10条:编写单元测试
    • 第二章:可读性
      • 第11条:为了可读性设计代码
      • 第12条:操作符的行为应该与其名称一致
      • 第13条:避免返回或操作 Unit?
      • 第14条: 在变量不清晰时指定其类型
      • 第15条:考虑显式引用接收者
      • 第16:属性应该代表状态,而非行为
      • 第17条:考虑使用具名参数
      • 第18条:遵守编程惯例
  • 第二部分:良好的设计
    • 第三章:可重用性
      • 第19条:不要重复知识
      • 第20条:不要重复实现常用算法
      • 第21条 使用属性代理来提取公共的属性模式
      • 第22条:当实现公共算法时使用泛型
      • 第23条:避免隐藏类型参数
      • 第24条:在使用泛型时考虑型变
      • 第25条:在不同的平台上提取公共模块进行重用
    • 第四章:抽象设计
      • 第26条:每个方法都应该基于单一的抽象级别而编写
      • 第27条:使用抽象来保护代码不受更改
      • 第28条:指定 Api 的稳定性
      • 第29条:考虑包装扩展 API
      • 第30条:最小化元素的可见性
      • 第31条:用文档定义合约
      • 第32条:遵守抽象合约
    • 第五章:对象的创建
      • 第33条:考虑使用工厂方法代替构造函数
      • 第34条:考虑带命名默认参数的主构造函数
      • 第35条:考虑为复杂的对象创建定义 DSL
    • 第六章:类的设计
      • 第36条:组合优于继承
      • 第37条:使用数据修饰符来表示一组数据
      • 第38条:使用函数类型而不是接口来传递操作和行为
      • 第39条:类层次结构优于标签类
      • 第40条:遵守 equals 的合约
      • 第41条:遵守 hashCode 的合约
      • 第42条:遵守 compareTo 的合约
      • 第43条: 考虑将 API 的非必要部分提取到扩展函数中
      • 第44条:避免在成员中定义扩展
  • 第三部分:性能
    • 第七章:让开发成本更低
      • 第45条:避免不必要的对象创建
      • 第46条:给高阶函数使用 inline 修饰符
      • 第47条:考虑使用内联类
      • 第48条:消除过时的对象引用
    • 第八章:高效的集合处理
      • 第49条:在具有多个处理步骤的大型集合上,优先使用 Sequence
      • 第50条:限制操作步骤的数量
      • 第51条:性能关键处考虑使用原语的数组
      • 第52条:考虑使用可变集合
Powered by GitBook
On this page
  • 层次的抽象
  • 单一抽象层次原则
  • 程序架构中的抽象级别
  • 总结
  1. 第二部分:良好的设计
  2. 第四章:抽象设计

第26条:每个方法都应该基于单一的抽象级别而编写

Not Kotlin-specific、Basics

Previous第四章:抽象设计Next第27条:使用抽象来保护代码不受更改

Last updated 2 years ago

计算机是一种极其复杂的设备,但我们能够轻松使用它,是因为它的复杂性被划分为不同层次上的不同元素。

从程序员的角度来看,计算机最底层的抽象是在硬件上。再往上,因为我们通常为处理器(CPU)编写代码,所以紧跟着的层次就是处理器的控制命令,为了可读性,它们用非常简单的语言表示,并被一对一翻译成机器码,这种语言叫做汇编语言。用汇编语言编程是困难的,以这种方式构建当今的应用程序是绝对不可想象的。为了简化编程,工程师们引入了编译器:将一种语言解释成另一种语言(通常是低级语言)的程序,首先,编译器是用汇编语言编写的,它们把文本形式编写的代码翻译成汇编指令。这就是第一个高级语言是如何被创建的。它们又被用来为了更好的语言编写编译器。 因此,引入了 C、C++等高级语言。这些语言是用来编写程序和应用程序的,后来,抽象机器和解释性语言的概念被发明了出来,很难再把 Java 或 JavaScript 这样的语言放在这个金字塔上,但抽象层的通用概念仍然是一个想法。

拥有分离良好的层次最大的优势是:当你在特定层次上操作时,它们可以依赖于较低的层次按预期工作,而无需完全理解细节,我们可以在不了解汇编程序或 JVM 字节码的情况下进行编程,这很方便。类似地,当汇编程序或 JVM 字节码需要更改时,只要创建者调整上层 —— 原生语言或被编译到 JVM 的地方 —— 他们就不需要担心更改引用程序。程序员在单一层次上操作时,它们通常为上层工作,这是所有开发人员需要知道的,它是非常方便的。

层次的抽象

如你所见,在计算机科学中,层次是逐步往上的。 这就是为什么计算机科学家开始区分高级别的东西。越高级,就离物理层面越远,在编程中,我们说层次越高,就是离处理器越远。级别越高,我们所关心的细节就越少。但本质是用缺乏对程序的控制去换取简化的编程。在 C 语言中,内存管理是工作的重要组成部分。 而在 Java 中,垃圾收集器会自动帮你处理它,但是优化内存使用要困难的多。

单一抽象层次原则

就像计算机科学问题被提取到各个层次一样,我们也可以在代码中去创造这样的抽象。 我们使用的最主要的基础工具就是函数。同样,就算在计算机中一样,我们喜欢一次只在一个抽象层次上操作,这就是为什么编程社区提出了 “单一抽象层” 原则,该原则规定:每一个函数都应该按照单一抽象层来编写。

假设你需要创建一个类表示一台咖啡机,其中只有一个按钮用来煮咖啡, 煮咖啡是一项复杂的操作,需要咖啡机的许多不同部件,我们将用一个类来表示它,这个类只有一个名为 makeCoffee 函数,我们可以在这个独特的函数中实现所有必要的逻辑:

class CoffeeMachine { 
    fun makeCoffee() {
        // Declarations of hundreds of variables
        // Complex logic to coordinate everything
        // with many low-level optimizations
    }
}

这个函数可以有数百行,相信我,我见过这样的事情。 特别是在那些比较老的程序中,这样的函数是完全不可读的。 要理解函数的一般行为是非常困难的,因为阅读它时,我们会不断地把注意力放在细节上。也很难找到任何东西,想象一下,你被要求做一个小修改,比如修改水的温度,要做到这一点,你可能需要理解整个函数,这是荒谬的。我们的记忆是有限的,我们不希望程序员在不必要的细节上浪费时间,这就是为什么最好将高级步骤提取为单独的函数。

class CoffeeMachine { 
    fun makeCoffee() {
        boilWater()
        brewCoffee()
        pourCoffee()
        pourMilk()
    } 
 

    private fun boilWater() {
        // ...
    }
    
    private fun brewCoffee() {
        // ...
    }

    private fun pourCoffee() {
        // ...
    }

    private fun pourMilk() {
       // ...
    }
}

现在你可以清楚地看到这一函数的流程,这些私有函数就像书中的章节,正因如此,如果你需要改变某些内容,你便能够直接跳到具体执行内容的地方。我们刚刚提取了更高层次的处理,这大大简化了我们对第一个过程的理解。我们让它更具有可读性,如果有人想在较低层次上理解它们,他们可以直接跳到那个部分去阅读它。 通过提取非常简单的抽象,我们提高了可读性。

遵循这个规则,所有这些新函数都应该一样简单,这是一条通用规则 —— 函数应该是最小的, 并且有最小化的职责。如果它们中间的一个比较复杂,则应该提取这部分。因此,我们应该事先写出许多小而易读的函数,所有这些函数都处于同一个抽象级别。在每个抽象层次上,我们都是用抽象协议(方法和类)进行操作的,如果你想了解它们,可以随时跳到它们定义的地方(在 IntelliJ 或 Android Stduio 按住 Ctrl(Mac 则是 Command) + 点击函数名,会跳到函数实现的地方),通过这种方式,我们提取这些函数时不会有任何损失,并使我们代码更具可读性。

额外的好处是,以这种方式提取的函数更容易复用和测试,假设我们现在需要做一个单独的功能来生产浓缩咖啡,区别是它不含牛奶。 当提取流程的某些部分后,我们就可以轻松的重用它们:

fun makeEspressoCoffee() {
    boilWater()
    brewCoffee()
    pourCoffee()
}

这也使得我们现在可以单独对较小的函数进行单元测试,比如对 boilWater 或 brewCoffee ,而不是像 makeCoffee 或 makeEspressCoffee 这样更复杂的函数测试。

程序架构中的抽象级别

抽象层次的概念也适用于比函数更高的级别,我们分离抽象以隐藏子系统的细节,允许分离关注点以促进代码间的可操作性和平台独立性。这意味着用问题领域的术语来定义更高的层次:

当我们设计模块化系统时,这个概念也很重要。 分离模块是一个强大的操作,可以隐藏层次特定的元素。当我们编写应用程序时,一般情况下,那些表示输入或输出(前端的 view、后端的 Http 请求)的模块,通常处在较低的层次。另一方面,那些代表用例和业务逻辑的层次,通常都很高。

我们认为良好的项目都是层次分离的,在一个分层良好的项目中,任何人都可以在任意一个层次上查看系统,并得到一致的观点,程序通常需要分层。

总结

在编程中,创建单独的抽象层一个流行的概念。它帮助我们组织知识,并隐藏子系统的细节,允许分离关注点以促进平台之间操作性和独立性。 我们用很多方法分离抽象,比如函数、类、模块。我们应该尽量不要让一层太大,在单一层次上操作的较小的抽象更容易理解。抽象级别的一般概念是:越接近具体的操作、处理器或 I/O ,它的级别就越低,在较低的抽象层中,我们为了更高的层次定义了术语语言(API)。