通往神秘的道路_通往更好抽象的道路

通往神秘的道路

I recently started writing about refactoring JavaScript to use Collection Pipelines instead of loops. I make a pretty bold claim that this will lead to cleaner code, what I haven’t done yet is make a clear argument for why I think this leads to cleaner code.

我最近开始写有关重构JavaScript以使用Collection Pipelines而不是循环的文章。 我大胆地宣称这将导致更干净的代码,但我尚未做的事情是明确说明为什么认为这会导致更干净的代码。

To me, there is something truly satisfying in taking a collection of arbitrary objects, piping it through a series of individual transformations and filters and getting a new collection out the other side. Seeing a workflow broken down into discrete, often single-line steps, with no side effects is great way to write code.

对我来说,将任意对象的集合,通过一系列单独的转换过滤器传递给其他对象并从中获取新的集合,确实使人感到满意。 看到工作流被分解为离散的,通常是单行的步骤,并且没有副作用,这是编写代码的好方法。

功能构建块 (Functional Building Blocks)

map(), filter() and reduce() are powerful higher order functions for working with collections (arrays) in JavaScript. I refer to them as functional building blocks because:

map()filter()reduce()是强大的高阶函数,用于处理JavaScript中的集合(数组)。 我将它们称为功能构建块,因为:

  • They all return new collections and don’t mutate the original one,

    他们都会返回新的收藏,而不会对原始收藏进行变异,
  • They only do one thing, and

    他们只做一件事,
  • Generally shouldn’t have side effects.

    通常不应该有副作用。

You’d be surprised about how much these three traits can simplify your code.

您会对这三个特征可以简化您的代码多少感到惊讶。

Let us examine each of these three collection methods and try to see why they might help us clean up our code.

让我们检查这三种收集方法中的每一种,并尝试看看它们为什么可以帮助我们清理代码。

地图() (Map())

Map is one of the most important collection methods in my opinion. Map is used to transform each item in a collection into something else. Given some collection of items and a function, map will apply that function to every item and return a new collection of the same size.

我认为地图是最重要的收集方法之一。 Map用于将集合中的每个项目转换为其他项目。 给定一些项和一个函数的集合,map将把该函数应用于每个项并返回相同大小的集合。

Map is such a powerful abstraction because it has a very clear intent. It takes a collection of items and maps it to another collection of items. The items in each array have a 1:1 mapping between each other. When you see a map() being used, you can internalise that as transform, and in fact, transforming data is the #1 place I use map in my code. Consider the following code example where I query DynamoDB (my persistence layer of choice these days) and return an item:

Map是一种功能强大的抽象,因为它具有非常明确的意图。 它需要一个项目集合并将其映射到另一个项目集合。 每个数组中的项目彼此之间具有1:1映射。 当您看到正在使用map() ,可以将其内部化为transform ,实际上,转换数据是我在代码中使用map的第一位。 考虑以下代码示例,其中我查询DynamoDB(这些天我选择的持久层)并返回一个项目:

// src/user/queries/findAllUsers.tsexport const findAllUsers = async (tennantId: string): Promise<User[]> => {
const params = {
TableName: process.env.DYNAMODB_TABLE,
KeyConditionExpression: 'pk = :pk AND begins_with(sk, :sk)',
ExpressionAttributeValues: {
':pk': tennantId,
':sk': 'user:',
},
};

const { Items } = await dynamoDBClient.query(params).promise();

return Items ? Items.map((item: DynamoDBUser) => userFromDynamoDBItem(item)) : [];
};

You’ve probably written a Query like this 100x before as a developer, if not in DynamoDB then in some form of SQL or NoSQL. Unless you use some form of ActiveRecord implementation, your probably don’t like coupling your database schema to the domain representation of your data. Therefore, we want to take a collection of data — in this case the database representation of a User, and transform it into a different representation. Here the userFromDynamoDBItem does exactly what it says — it takes a DynamoDBItem and transforms it into a User.

您可能曾经以开发人员的身份编写过类似100x的查询,如果不在DynamoDB中,则可能以某种形式SQL或NoSQL编写。 除非您使用某种形式的ActiveRecord实现,否则您可能不喜欢将数据库模式耦合到数据的域表示形式。 因此,我们要收集数据-在这种情况下为User的数据库表示形式,并将其转换为其他表示形式。 在这里, userFromDynamoDBItem完全按照它的userFromDynamoDBItem操作-它需要一个DynamoDBItem并将其转换为User。

For completeness here is the interface definition for our User — this is what we want our data to look like when we deal with it in our Application Logic.

为了完整起见,这里是用户的接口定义-这就是我们希望在应用程序逻辑中处理数据时看起来像的样子。

export interface User {
id: string;
givenName: string;
familyName: string;
email: string;
}

And this is what out database representation looks like — you can see why we would want to transform this data before passing it around our application.

这就是数据库表示的样子-您可以了解为什么我们要在数据传递给应用程序之前先转换这些数据。

export interface DynamoDBUser extends BaseDynamoDBRecord {
email: string;
givenName: string;
familyName: string;
}export interface BaseDynamoDBRecord {
pk: string;
sk: string;
__context__: Record<string, string>;
gsi1pk?: string;
gsi1sk?: string;
gsi2pk?: string;
gsi2sk?: string;
type: string;
ttl?: number;
}

Couldn’t we also transform this data with a standard loop?

我们是否也可以使用标准循环来转换此数据?

Yes we could, however, having a method for transforming data that is consistent and clear in intent, reduces cognitive overhead and improves the readability of our code.

是的,我们可以,但是,有一种转换数据的方法,其意图是一致且清晰的,可以减少认知开销并提高代码的可读性。

This is a fairly conceited example but imagine you have this exact functionality in your code and one day your product owner comes to you and says “We want to log the userId for every user we retrieve from the database in any query”.

这是一个相当自负的示例,但请想象您的代码中具有此确切功能,并且有一天您的产品负责人会来找您,并说:“我们希望为在任何查询中从数据库中检索到的每个用户记录userId”。

This might be a strange request but it’s not out of the realm of possibilities. If we’d used a loop to do this transformation, we could easily just throw a statement inside the loop that logs user.id with each iteration of the transform. This may sound appealing but it’s actually a pretty terrible thing to do. By doing this we’ve increased the responsibility of our transform function.

这可能是一个奇怪的请求,但这并非超出可能性范围。 如果我们使用循环来执行此转换,则可以轻松地在循环内抛出一条语句,该语句在转换的每次迭代中记录user.id 这听起来很吸引人,但这实际上是一件非常糟糕的事情。 通过这样做,我们增加了转换功能的责任。

It’s much better to add this requirement to a new collection method, this time we’ll use a forEach

最好将此要求添加到新的收集方法中,这一次,我们将使用forEach

const users = Items
.map((item: DynamoDBUser): User => userFromDynamoDBItem(item))
.forEach((user: User) => console.log(user.id))

In this example, we’ve made our intent very clear. First we transform our users into their domain representation, and then we iterate over the collection and log the ID. This makes it clear that logging is an explicit intent, and not some debugging step left by a developer.

在此示例中,我们已经明确了意图。 首先,我们将用户转换成他们的域表示形式,然后遍历集合并记录ID。 这清楚地表明,日志记录是一个明确的意图,而不是开发人员留下的一些调试步骤。

Even though I’ve not mentioned forEach anywhere in my series so far, I wanted to use this example to show that it still has its place in the world of map(). In general we should only map if we are transforming and item. If we aren’t then forEach is the right choice.

即使我没有提到 forEach到目前为止我系列中的每个地方,我都想使用此示例来表明它仍然在map()的世界中占有一席之地。 通常,只有在进行变换和项目时,才应map 。 如果不是,那么forEach是正确的选择。

过滤() (Filter())

The filter() method is used to filter out any elements that you don’t want in your collection. You give filter() a predicate (expression that returns a boolean) and if the predicate returns true you keep the item, if it returns false you remove it.

filter()方法用于过滤掉集合中不需要的任何元素。 您给filter()一个谓词(返回boolean表达式),如果谓词返回true ,则保留该项目,如果它返回false ,则将其删除。

The major difference between filter() and map() is that filter isn’t transforming or altering any individual item in the collection — it is altering the collection as a whole by removing items from the collection. The items that are returned are not only the same type as the items from the original array, they’re the same item.

filter()map()之间的主要区别是filter不会转换或更改集合中的任何单个项目,而是通过从集合中删除项目来整体更改集合。 返回的项目不仅与原始数组中的项目类型相同,而且是同一项目。

Filter is pretty simple to understand so our example might seem a bit trivial, nevertheless:

过滤器很容易理解,因此我们的示例似乎有些琐碎:

interface Article {
id: string;
title: string;
body: string;
status: 'PUBLISHED' | 'DRAFT'
}

const publicshedArticles: Article[] =
articles.filter((article: Article) => article.status === 'PUBLISHED');

As we can see we take a collection of articles, and only keep the ones that are published. One nice thing about filter is, that because it’s so simple, it works really well in a point free programming style. We can re-write the above filter to be point free as follows:

如我们所见,我们收集了一系列文章,仅保留了已发表的文章。 关于filter一件好事是,因为它是如此简单,所以在无点编程风格下它确实可以很好地工作。 我们可以将上述过滤器重新编写为无点,如下所示:

const isPublished = article => article.status === 'PUBLISHED'

const publishedArticles: Article[] = articles.filter(isPublished);

Again, all of this could have been achieved with the humble loop, however, I will again argue that readability has been improved due to a more explicit declaration of intent. We can see the word filter and know that anything that follows is going to give us a collection, less than or equal in size of the original one, while leaving our individual items unchanged.

同样,所有这些都可以通过卑微的循环来实现,但是,我将再次辩称,由于更加明确的意图声明,可读性得到了提高。 我们可以看到filter一词,并且知道接下来发生的一切都会为我们提供一个集合,该集合的大小小于或等于原始集合,而我们的单个项目保持不变。

减少() (Reduce())

The reduce operation is used to take some collection of items and reduce it down into a single value. It has no opinion about what that value can be; it could be an object, an array, a number, or a string — it doesn’t matter.

reduce操作是用来取物品的一些收集和减少它分解成单个值。 它不知道该价值是多少。 它可以是对象,数组,数字或字符串-没关系。

Ive written about the power of reduce() already here — and even showed how you can implement map() and filter() via reduce().

香港专业教育学院写了关于的功耗reduce()已经在这里-甚至展示了如何实现map()filter()通过reduce().

In this example I showed how you could solve Canva’s HackerRank algorithm challenge using a single reduce function.

此示例中,我展示了如何使用单个reduce函数解决Canva的HackerRank算法难题。

Apart from summing values, one of the most common use cases ofreduce() is to concatenate strings. Given I’ve already mentioned DynamoDB here I might as well show an example of dynamically generating a DynamoDB update expression from an object using reduce()

除了对值求和外, reduce()的最常见用例之一是连接字符串。 鉴于我已经在这里提到过DynamoDB,我不妨展示一个示例,该示例使用reduce()从对象动态生成DynamoDB更新表达式。

const ExpressionAttributeNames = {prop1: 'prop1', prop2: 'prop2', prop3: 'prop3'}

const UpdateExpression = Object.values(ExpressionAttributeNames)
.reduce((acc, curr) => `${acc} #${curr} = :${curr},`, 'SET').slice(0, -1);

console.log(UpdateExpression);
// SET #prop1 = :prop1, #prop2 = :prop2, #prop3 = :prop3

Here we take an object and dynamically create a string that represents the DynamoDB update expression syntax. Don’t worry too much if DynamoDB is unfamiliar to you. Just look at how we went from ExpressionAttributeNames to SET #prop1 = :prop1, #prop2 = :prop2, #prop3 = :prop3 in a simple reduce function. You can use similar logic to generate any expression, a csv or any other string you might need.

在这里,我们获取一个对象,并动态创建一个表示DynamoDB更新表达式语法的字符串。 如果您不熟悉DynamoDB,请不要担心。 只需看一下我们如何通过一个简单的reduce函数从ExpressionAttributeNamesSET #prop1 = :prop1, #prop2 = :prop2, #prop3 = :prop3 。 您可以使用类似的逻辑来生成您可能需要的任何表达式,csv或任何其他字符串。

If you use DynamoDB and want a fully fledged example of how to use my dynamic update expression utility you can view the full gist

如果您使用DynamoDB,并想获得有关如何使用我的动态更新表达式实用程序的完整示例,则可以查看完整的要点

interface DynamoUpdateParams {
  UpdateExpression: string;
  ExpressionAttributeNames: Record<string, string>;
  ExpressionAttributeValues: Record<string, unknown>;
}


export const DynamicUpdateExpressionFromObject = (map: Record<string, unknown>): DynamoUpdateParams => {
  const ExpressionAttributeNames: Record<string, string> = Object.keys(map)
    .reduce((acc, curr) => ({ ...acc, ...{ [`#${curr}`]: curr } }), {});
  const UpdateExpression: string = Object.values(ExpressionAttributeNames)
    .reduce((acc, curr) => `${acc} #${curr} = :${curr},`, 'SET').slice(0, -1);
  const ExpressionAttributeValues = Object.entries(map)
    .reduce((acc, [key, value]) => ({ ...acc, ...{ [`:${key}`]: value } }), {});


  return {
    UpdateExpression,
    ExpressionAttributeNames,
    ExpressionAttributeValues,
  };
};

While reduce is very powerful, it is often tricky to wrap your head around. In most cases, what you really want is a higher level abstraction built on top of reduce. In the gist above, we wrapped our reduce functions in a higher level abstraction called DynamicUpdateExpressionFromObject. In the example that solves Canva’s interview question, we would wrap the reduce inside a function that abstracts away the implementation.

虽然reduce非常强大,但是缠绕头部通常很棘手。 在大多数情况下,您真正​​想要的是在reduce之上构建的更高级别的抽象。 在上面的要点中,我们将reduce函数包装在称为DynamicUpdateExpressionFromObject的更高级别的抽象中。 在解决Canva面试问题的示例中,我们将reduce封装在一个抽象实现的函数内。

结语 (Wrapping Up)

Once you get comfortable with map(), filter(), and reduce(), I promise you’ll be hooked on the beauty and simplicity of your code. Viewing your business logic as pipelines that your data flows through, is an incredibly seductive way of thinking about code. It helps you push all your side effects to the edges of your code, reduces the test paths, provides no crevices for developers to sneak additional responsibilities into your functions, and greatly improves the Cyclomatic Complexity of your code.

一旦您对map()filter()reduce()感到满意,我保证您会迷上代码的优美和简单。 将您的业务逻辑视为数据流经的管道是一种令人难以置信的诱人的代码思考方式。 它可以帮助您将所有副作用都推到代码的边缘,减少测试路径,为开发人员提供更多的责任,而又不给开发人员提供任何缝隙,并极大地提高了循环复杂性 您的代码。

If you want to learn how to practically refactor existing code, keep an eye out for my upcoming e-book that will step through exactly how to do this, and will include many more real world examples and applications. You can pre-order the book for a discounted price of $10 here.

如果您想学习如何实际重构现有代码,请留意我即将出版的电子书,该书将逐步介绍如何执行此操作,并将包括更多实际示例和应用程序。 您可以在此处以10美元的折扣价预订这本书。

普通英语JavaScript (JavaScript In Plain English)

Did you know that we have three publications and a YouTube channel? Find links to everything at plainenglish.io!

您知道我们有三个出版物和一个YouTube频道吗? 在plainenglish.io上找到所有内容的链接!

翻译自: https://medium.com/javascript-in-plain-english/the-road-to-better-abstractions-540620224b7d

通往神秘的道路

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值