面向数据编程 Data-Oriented Programming [16]

  • Post author:
  • Post category:其他




3.4 使用泛型函数操作数据

乔:现在,让我向你展示我们是如何利用泛型函数来操作数据的。

你:好,我很好奇你将如何实现图书馆管理系统的搜索功能。

乔:好的。首先,根据图3.3中的数据模型,让我们实例化一个图书馆目录数据的Catalog record,我们只有一本书“守望者”:

清单 3.5 Catalog record

var catalogData = 
	{ "booksByIsbn": {
		"978-1779501127": {
			"isbn": "978-1779501127",
			"title": "Watchmen", 
			"publicationYear": 1987,
			"authorIds": ["alan-moore", "dave-gibbons"], 
			"bookItems": [
				{
					"id": "book-item-1",
					"rackId": "rack-17", 
					"isLent": true
				},
				{
					"id": "book-item-2",
					"rackId": "rack-17", 
					"isLent": false
				}
			]
		}
	},
	"authorsById": 
		{ "alan-moore": {
			"name": "Alan Moore", 
			"bookIsbns": ["978-1779501127"]
		},
		"dave-gibbons": {
			"name": "Dave Gibbons", 
			"bookIsbns": ["978-1779501127"]
		}
	}
}

你:我看到了我们谈到的两个索引,booksByIsbn和AuthsById。如何在DO中区分记录和索引?

乔:在实体图中,记录和索引之间有明显的区别。但在我们的代码中,两者都是纯数据。

你:我想这就是为什么这种方法被称为面向数据编程。

乔:注意,在程序中可视化系统数据的任何部分是多么简单。原因是数据被表示为数据!

你:听起来像个lapalissade


TIP

在DO中,数据表示为数据。

乔:的确,这是显而易见的,但通常在面向对象中,数据是由对象表示的,这使得在程序中可视化数据更具挑战性。


TIP

在DO中,我们可以可视化系统数据的任何部分。

你:你将如何从目录数据中检索特定图书的标题?

乔:这是个很好的问题。事实上,在DO系统中,每条信息都有一条路径,我们可以从中检索信息。

你:我不明白。

乔:例如,目录中“守望者”书名的路径是:[“booksByIsbn”,“978-1779501127”,“Title”]。

你:所以呢?

乔:一旦我们有了一条信息的路径,我们就可以用Lodash的_.get()函数检索信息:

清单3.6 从路径检索书名

_.get(catalogData, ["booksByIsbn", "978-1779501127", "title"])

你:它在Java这样的静态类型语言中能顺利工作吗?

乔:这取决于您是只需要传递该值,还是具体访问该值。

你:我不明白。

乔:想象一下,一旦你得到一本书的书名,你就想把这个字符串转换成一个大写的字符串。然后需要对字符串进行静态强制转换。

清单3.7 将字段值转换为字符串,以便将其作为字符串进行操作

((String)watchmen.get("title")).toUpperCase()

你:这很有道理。Map的值具有不同的类型。因此,编译器将其声明为Map<String,Object>。该字段的类型信息将丢失。

乔:这有点烦人,但通常情况下,代码只是传递数据。因此,我们不必过多地处理静态强制转换。


TIP

在静态类型语言中,我们有时需要静态转换字段值。

你:性能怎么样?

乔:在大多数编程语言中,Map是非常有效的。访问Map中的字段比访问类成员稍微慢一些。通常,这并不重要。


TIP

通过访问Map中的字段而不是类成员,不会对性能造成重大影响。

你:让我们回到信息路径这个概念上来。在OO中,我还可以用catalogData.booksByIsbn[“978-1779501127”].title.访问“守望者”这本书的标题。记录字段的类成员和索引键的字符串。

乔:这是有根本区别的。当记录表示为Map时,可以使用泛型函数(如_.get())通过其路径检索信息。但是当记录被表示为对象时,您需要为每种类型的信息路径编写特定的代码。

你:你说的具体代码是什么意思?CatalogData.booksByIsbn[“978-1779501127”].title?中的具体内容是什么?

乔:在Java这样的静态类型语言中,要编写这段代码,您需要导入Catalog和Book的类定义。

你:是用JavaScript这样的动态类型语言吗?

乔:即使在JavaScript中,当您使用从类实例化的对象表示记录时,也不容易编写一个将路径作为参数接收并显示与此路径对应的信息的函数。您必须为每种路径编写特定的代码。您将使用点符号访问类成员,并使用方括号符号访问映射字段。

你:你是不是说在DO中,信息路径是一等公民?

乔:当然可以!信息路径是一等公民。它可以存储在变量中,并作为参数传递给函数。


TIP

在DO中,您可以通过路径和泛型函数检索每条信息。


图3.5 以树的形式显示目录数据。每条信息都可以通过由字符串和整数组成的路径访问。例如,Alan Moore的第一本书的路径是[“catalog”, “authorsById”, “alan-moore”, “bookIsbns”, 0]



3.5计算搜索结果

你:我开始感受到DO的力量了。

乔:等等。这只是个开始。让我向您展示编写代码来检索图书信息并将其显示在搜索结果中是多么简单。你能确切地告诉我哪些信息必须出现在搜索结果中吗?

你:在搜索结果的上下文中,图书信息应该包含ISBN、Title和AuthNames。

乔:你能试着写下“守望者”的BookInfo record是什么样子吗?

你:当然可以,给你…

清单3.8 搜索结果上下文中守望者的BookInfo record

{
	"title": "Watchmen", 
	"isbn": "978-1779501127",
	"authorNames": [ "Alan Moore", "Dave Gibbons",
	]
}

乔:现在,我将逐步向您展示如何使用Lodash中的通用数据操作函数编写一个函数,该函数返回与JSON格式的标题匹配的搜索结果。

你:酷!

Joe:让我们从AuthNames()函数开始,该函数通过查看AuthsById索引来计算图书记录的作者姓名。作者姓名的信息路径为[“AuthsById”,AuthId,“Name”]。

清单3.9 计算一本书的作者姓名

function authorNames(catalogData, book) {
	var authorIds = _.get(book, "authorIds");
	var names = _.map(authorIds, function(authorId) {//1
		return _.get(catalogData, ["authorsById", authorId, "name"]);
	});
	return names;
}
  1. 可以使用.forEach()而不是.map()来完成

你:这个_.map()函数是什么?它闻起来像是函数式编程的东西!你答应过我不用学FP就能实现的!

乔:如果你愿意,你可以使用Lodash的_.forEach()。

你:是的,我喜欢。下一个是什么?

乔:现在,我们需要一个将BookInfo记录转换成BookInfo record的bookInfo函数。

清单3.10将BookInfo 记录转换为BookInfo record

function bookInfo(catalogData, book) { 
	var bookInfo = {//1
		"title": _.get(book, "title"),
		"isbn": _.get(book, "isbn"),
		"authorNames": authorNames(catalogData, book)
	};
	return bookInfo;
}
  1. 不需要为bookInfo创建类

你:查看代码,我发现BookInfo记录有三个字段:Title、ISBN和AuthNames。有没有办法在不查看代码的情况下获得这些信息?

乔:您可以将其添加到数据实体图中,也可以将其写入bookInfo函数的文档中,或者两者兼而有之。

你:我必须习惯这样的想法,在DO中,记录字段信息不是程序的一部分。

乔:的确,这不是计划的一部分,但它给了我们很大的灵活性。

你:有没有办法让我两者兼得?!

乔:是的。在第3部分中,我将向您展示如何将record字段信息作为DO程序的一部分。

你:听起来很有趣!

乔:现在,我们已经准备好编写searchBooksByTitle函数,该函数返回与查询匹配的图书信息。首先,我们找到与查询匹配的Book record(使用_.filter()),然后将每个Book record转换为BookInfo record(使用_.map()和bookInfo())。代码如下:

清单3.11搜索与查询匹配的图书

function searchBooksByTitle(catalogData, query) {
	var allBooks = _.get(catalogData, "booksByIsbn");
	var matchingBooks = _.filter(allBooks, function(book) {//1 //2
			return _.get(book, "title").includes(query);
		}
	);

	var bookInfos = _.map(matchingBooks, function(book) { //2
			return bookInfo(catalogData, book);
		}
	);
	return bookInfos;
}

searchBooksByTitle(catalogData, "Watchmen");
  1. 将Map传递给_.filter()时,它会遍历Map的值
  2. 可以使用_.forEach()完成

你:要访问图书记录的标题,你要写_.get(book,“title”),这对我来说有点奇怪。我希望它是点符号的book.title,或者括号符号的book[“title”]!

乔:记住,那本书是一种record,不是用物体来表示的。这是一张Map。实际上,在JavaScript中,您可以编写_.get(book,“title”)、book.title或book[“title”]。但我更喜欢使用Lodash的_.get()。在某些语言中,点和方括号符号在Map上可能不起作用。

你:搜索实现完成了吗?

乔:差不多了。我们编写的searchBooksByTitle函数是Catalog模块的一部分,它返回一个record集合。我们必须编写一个函数,该函数是Library模块的一部分,它返回一个JSON字符串。

你:你早些时候告诉我,在DO中JSON序列化非常简单。

乔:对。以下是searchBooksByTitleJSON()的代码。它检索Catalog记录,将其传递给searchBooksByTitle(),并使用JSON.stringify()将结果转换为JSON()(这是JavaScript的一部分)。

清单3.12 以JSON格式检索图书馆图书

function searchBooksByTitleJSON(libraryData, query) {
	var results = searchBooksByTitle(_.get(libraryData, "catalog"), query); 
	var resultsJSON = JSON.stringify(results);
	return resultsJSON;
}

你:到目前为止,我们将如何组合我们已经编写的四个函数呢?

乔:函数AuthNames、bookInfo和searchBooksByTitle进入Catalog模块,而searchBooksByTitleJSON进入Library模块。

您会看到两个模块的结果代码,对代码的简洁性感到非常惊讶。

清单3.13计算搜索结果。代码分为两个模块:Library 和Catalog。

class Catalog {
	static authorNames(catalogData, book) {
		var authorIds = _.get(book, "authorIds");
		var names = _.map(authorIds, function(authorId) { //1
			return _.get(catalogData, ["authorsById", authorId, "name"]);
		});
		return names;
	}

	static bookInfo(catalogData, book) {
		var bookInfo = { //2
			"title": _.get(book, "title"),
			"isbn": _.get(book, "isbn"),
			"authorNames": Catalog.authorNames(catalogData, book) 
		};
		return bookInfo;
	}

	static searchBooksByTitle(catalogData, query) {
		var allBooks = _.get(catalogData, "booksByIsbn");
		var matchingBooks = _.filter(allBooks, function(book) { //1 //3
			return _.get(book, "title").includes(query);
		});
		var bookInfos = _.map(matchingBooks, function(book) { //1
			return Catalog.bookInfo(catalogData, book);
		});
		return bookInfos;
	}
}

class Library {
	static searchBooksByTitleJSON(libraryData, query) {
		var catalogData = _.get(libraryData, "catalog");
		var results = Catalog.searchBooksByTitle(catalogData, query);
		var resultsJSON = JSON.stringify(results); //4
		return resultsJSON;
	}
}
  1. 可以使用_.forEach()完成
  2. 不需要为bookInfo创建类
  3. 向_.filter()传递映射时,它会遍历映射的值
  4. 将数据转换为JSON(JavaScript的一部分)

你:让我们检查一下代码是否按预期工作。

乔:当然可以。为此,我们需要创建一个包含Catalog record的Library record。

清单3.14 Library 数据(没有用户管理数据)

var libraryData = {
	"name": "The smallest library on earth",
	"address": "Here and now",
	"catalog": {
		"booksByIsbn": {
			"978-1779501127": {
				"isbn": "978-1779501127",
				"title": "Watchmen",
				"publicationYear": 1987,
				"authorIds": ["alan-moore",
					"dave-gibbons"
				],
				"bookItems": [{
						"id": "book-item-1",
						"rackId": "rack-17",
						"isLent": true
					},
					{
						"id": "book-item-2",
						"rackId": "rack-17",
						"isLent": false
					}
				]
			}
		},
		"authorsById": {
			"alan-moore": {
				"name": "Alan Moore",
				"bookIsbns": ["978-1779501127"]
			},
			"dave-gibbons": {
				"name": "Dave Gibbons",
				"bookIsbns": ["978-1779501127"]
			}
		}
	},
	"userManagement": {
		// omitted for now
	}
};

你:让我们搜索书名与“守望者”相匹配的书籍。

清单3.15 JSON中的搜索结果

Library.searchBooksByTitleJSON(libraryData, "Watchmen");
// returns "[{\"title\":\"Watchmen\",\"isbn\":\"978-1779501127\",\"authorNames\":[\"Alan Moore\",
//	\"Dave Gibbons\"]}]"

您将再次查看清单3.12…中的源代码。几秒钟后,你会觉得自己像在一个Aha!时刻。

你:重要的不是代码是否简洁,而是代码不包含抽象。这只是数据操纵!

乔微笑着回答说:“你成功了,我的朋友!”

乔:这让我想起了10年前我的第一位冥想老师告诉我的话:冥想引导头脑去把握现实,而不是我们的思想造成的抽象。


TIP

在DO中,我们的代码库的许多部分往往只是关于数据操作,而不是抽象。