面向数据的编程 · Laurent


我学习了DDD领域驱动设计、六边形架构和面向数据的编程。我在 Airbnb与Daniel Low在Krispr的工作中使用了前两个。当我们的需求发生变化时,它可以更轻松地扩展项目、重新设计其依赖项和交付机制。然而,今天,我正在写我最近的兴趣:面向数据的编程。
 
面向数据的编程
Yehonathan Sharvit推广该术语Data-Oriented Programming以包含一组原则,使开发代码库变得更容易。虽然这些原则都不是新颖的,但它们在行业中并不是主流,它们确实使使用代码变得容易得多。
多年来阅读 Yehonathan 的博客文章后,我买了他的书,我很喜欢!当谈到技术写作风格时,这是一股清新的空气。这本书讲述了两个开发人员一起学习面向数据的编程原理的故事。它使用了一种引人入胜的对话风格,让你想更多地了解他们的故事以及他们正在解决的下一个问题。
在这篇文章中,我将解释我从书中得到的 3 个关键思想:

  1. OOP 迫使您一次性同时做出太多限制设计并导致重写的决定
  2. 使用通用和开放的数据结构表示所有数据,更容易探索和扩展系统的状态
  3. 不可变的数据结构是必须的,它使推理程序更容易

 
1. OOP迫使你同时做出太多限制设计和导致重写的决定
在做一个副业的时候,直到最近,我默认的选择是使用Python的OOP。我注意到,随着项目的发展,我倾向于重写很大一部分代码,以更好地表达我的想法,选择更好的名字,更好的类层次,以不同的方式表示数据或副作用。OOP吸引了我的完美主义倾向,有时会让我想重写系统中没有必要重写的部分。它刺激了我的智力,看我如何能在数据上提出最好的抽象,最容易使用的模式。

在读完面向数据编程的OOP章节后,我改变了主意。我觉得有必要尝试用Clojure构建我所有的副业,并从一开始就实践代码和数据的分离。这就是我在最近的项目中采用的风格:一个跟踪我的预算并自动对进入的交易进行分类的工具(就像Mint,但在对交易进行分类方面做得更好)。我只写了一次项目,还没有感觉到重做它的核心部分的冲动。最近,我也开始重构代码,把所有的数据从Clojure文件中提取出来,放到一个配置文件中,这非常容易,只需要10分钟。

{
 :start "01/01/2021"
 :end
"12/31/2021"
 :sources [{:type
"folder-recursive" :value "/Users/laurent/Downloads"}]
 :input-folder
"/Users/laurent/Downloads"
 :report-name
"2021"
 :output-folder
"/Users/laurent/Documents/budget"
 :exclusion-rules '[(
"description%" #{"AUTOPAY PAYMENT"})]
 :categories-rules
 '[
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   ;; High priority rules                             ;;
   ;; These entries categorize specific transactions  ;;
   ;; with a specific date, they take precedence over ;;
   ;; the other rules                                 ;;
   ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


   (
"venmo"                   "laurent/haircut" {:date "03/25/2021" :amount 40.0 :note "Tried a new hair style, it turned out great, go back there" :rating "A"})
   ...
   ]'
 }

简而言之,我将继续分离数据和代码,并从一开始就为我的副项目采用函数式风格。它只是让我更有效率,并消除了我重构为更好抽象的倾向。
 
2. 不可变的数据结构是必须的,它使推理程序更容易
阅读代码时,您必须牢记很多上下文和抽象概念。我在与其他人的工作中发现,与阅读散文不同,阅读一段复杂的代码对于最有经验的程序员来说也是一项挑战。在使用不具有不可变数据结构的语言(大多数语言)时,我们要牢记的一个特别考虑因素是操作可能会发生in place或return a new copy.
例如在 Python 中,对列表进行排序:
list.sort() 对列表进行排序并替换原始列表,而 sorted(list) 返回列表的排序副本,而不更改原始列表。
一些语言和社区已经找到了区分这两者的方法,例如 Ruby 社区已经对!字符进行了标准化,以指示发生变化的操作。然而,记住约定并应用它取决于各个程序员。这就是为什么,即使我阅读 ruby​​ 代码,有时我也必须检查我使用的是哪种操作风格。
面向数据的编程中,Yehonathan 提倡不可变和持久的数据结构。首先是一些定义:

  • 不可变数据结构

一旦有了值就不能改变的数据结构。它也被称为“冻结”数据结构。这是一个抽象的概念。
  • 持久数据结构

使用结构共享实现高效的不可改变的数据结构的方法
  • 两个数据结构之间的结构共享

当数据结构引用同一块内存时,即使它们彼此不同。一个例子说明了这一点:对列表 ["a", "b"] 和列表 ["a", "b", "c"] 进行映像。后者可以建立在前者之上,重用内存布局并在其末尾添加“c”。

不可变数据结构是主流,我已经看到它们在业界的应用,特别是在Java的Guava中,但持久性的数据结构不是,这是一种耻辱,因为它们是表示不可变数据结构的有效方法, Clojure是我所知道的唯一一门语言,该语言中的所有数据结构默认都是持久性的。
 
建议:如果你在面试中提到持久化数据结构作为解决问题的一种方式,请确保你知道如何实现它们(阅读一些论文,例如这篇)。

当你使用不可变数据结构(无论是否持久化)时,一整类bug就会消失,你不必一直考虑操作是否会突变底层数据,它们根本不会这样做。

我喜欢Yehonathan在《面向数据的编程》中解释所有这些概念,并链接到使用这种数据结构的常见语言的库。它为我澄清了这些术语,并使我更加确信,不可变数据结构几乎总是要走的路。唯一的例外是对性能高度敏感的代码,但我的工作中很少写这种代码。
 
3. 使用通用和开放的数据结构表示所有的数据,这使得探索一个系统的状态和扩展它变得更加容易。
早些时候,我提到我如何配置我的预算分析器。在配置中,你可能已经注意到这一行。

("venmo" "laurent/haircut" {:date "03/25/2021"
                            :amount 40.0
                            :note
"Tried a new hair style, it turned out great, go back there"
                            :rating
"A"})

项目的代码中没有任何东西知道:note和:rating,我只是在写规则时加入了它们,以对交易进行分类,记录我喜欢那个发型的事实。这之所以可能,只是因为我使用通用和开放的数据结构来表示我的所有数据。在Clojure中,我使用列表、向量、地图和原始类型来表示我程序中的所有数据。如果你习惯于OOP,这听起来可能很可怕,Yehonathan在他的书中再一次很好地驳斥了为什么它一点也不可怕的原因!
由于方法上的这一小小的改变,我能够在数据上使用丰富的通用操作(例如由Clojure标准库提供),我也可以轻松地进行序列化和反序列化,检查系统的运行状态并为数据提供时间旅行机制。
  
结语
如果你喜欢我上面介绍的一些想法,我鼓励你阅读《面向数据的编程》,看看你如何以不同于你所习惯的角度来处理编程任务。它已经成为我在副业和不受制于OOP代码库时的新工作方式。