Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

值类型导向编程 #4909

Merged
merged 4 commits into from
Jan 2, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 54 additions & 56 deletions TODO1/value-oriented-programming.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
> * 原文作者:[MattDiephouse](https://matt.diephouse.com)
> * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner)
> * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO1/value-oriented-programming.md](https://github.com/xitu/gold-miner/blob/master/TODO1/value-oriented-programming.md)
> * 译者:
> * 校对者:
> * 译者:[nanjingboy](https://github.com/nanjingboy)
> * 校对者:[Bruce-pac](https://github.com/Bruce-pac)

# Value-Oriented Programming
# 值类型导向编程

At WWDC 2015, in a very influential session titled [_Protocol-Oriented Programming in Swift_](https://developer.apple.com/videos/play/wwdc2015/408/), Dave Abrahams explained how Swift’s protocols can be used to overcome some shortcomings of classes. He suggested this rule: “Don’t start with a class. Start with a protocol”.
2015 WWDC 大会上,在一个具有影响力的会议([面向协议的 Swift 编程](https://developer.apple.com/videos/play/wwdc2015/408/))中,Dave Abrahams 解释了如何用 Swift 的协议来解决类的一些缺点。他提出了这条规则:“不要从类开始,从协议开始”。

To illustrate the point, Dave described a protocol-oriented approach to a primitive drawing app. The example worked from a few of primitive shapes:
为了说明这一点,Dave 通过面向协议的方法描述了一个基本绘图应用。该示例使用了一些基本形状:

```
protocol Drawable {}
Expand All @@ -28,21 +28,21 @@ struct Diagram: Drawable {
}
```

These are value types. That eliminates many of the problems of an object-oriented approach:
这些是值类型。它解决了面向对象方法中的许多问题:

1. Instances aren’t shared implicitly

The reference semantics of objects add complexity when passing objects around. Changing a property of an object in one place can affect other code that has access to that object. Concurrency requires locking, which adds tons of complexity.

2. No problems from inheritance

Reusing code via inheritance is fragile. Inheritance also couples interfaces to implementations, which makes reuse more difficult. This is its own topic, but even OO programmers will tell you to prefer “composition over inheritance”.

3. No imprecise type relationships

With subclasses, it’s difficult to precisely identify types. e.g. with `NSObject.isEqual()`, you must be careful to only compare against compatible types. Protocols work with generics to precisely identify types.
1. 实例不能隐式共享

To handle the actual drawing, a `Renderer` protocol was added that describes the primitive drawing operations:
对象的引用在对象传递时增加了复杂性。在一个地方改变对象的属性可能会影响有权访问该对象的其他代码。并发需要锁定,这增加了大量的复杂性。

2. 无继承问题

通过继承来重用代码的方式是脆弱的。继承还将接口与实现耦合在一起,这使得代码重用变得更加困难。这是它的特性,但即使是使用面向对象的程序员也会告诉你他更喜欢“组合而不是继承”。

3. 明确的类型关系

对于子类,很难精确识别其类型。比如 `NSObject.isEqual()`,你必须小心且只能与兼容类型比较。协议和泛型协同工作可以精确识别类型。

为了处理实际的绘图操作,我们可以添加一个描述基本绘图操作的 `Renderer` 协议:

```
protocol Renderer {
Expand All @@ -52,7 +52,7 @@ protocol Renderer {
}
```

Each type could then `draw` with a `Renderer`.
然后每种类型都可以使用 `Renderer` 的 `draw` 方法进行绘制。

```
protocol Drawable {
Expand Down Expand Up @@ -83,7 +83,7 @@ extension Diagram : Drawable {
}
```

This made it possible to define different renderers that worked easily with the given types. A main selling point was the ability to define a test renderer, which let you verify drawing by comparing strings:
这使得定义根据给定类型并能为此轻松工作的各种渲染器变的可能。一个最主要的卖点是定义测试渲染器的能力,它允许你通过比较字符串来验证绘制:

```
struct TestRenderer : Renderer {
Expand All @@ -96,7 +96,7 @@ struct TestRenderer : Renderer {
}
```

But you could also easily extend platform-specific types to make them work as renderers:
你也可以轻松扩展平台特定的类型,使其成为渲染器:

```
extension CGContext : Renderer {
Expand All @@ -118,7 +118,7 @@ extension CGContext : Renderer {
}
```

Lastly, Dave showed that you can extended the protocol to provide conveniences:
最后,Dave 表明你可以通过扩展协议来提供方便:

```
extension Renderer {
Expand All @@ -128,11 +128,9 @@ extension Renderer {
}
```

I think that approach is pretty compelling. It’s much more testable. It also allows us to interpret the data differently by providing separate renderers. And value types neatly sidestep a number of problems that an object-oriented version would have.
我认为这种方法非常棒,它具有更好的可测试性。它还允许我们通过提供不同的渲染器,从而使用不同的方式解释数据。并且值类型巧妙地回避了面对对象版本中可能遇到的许多问题。

But I think there’s a better way to write this code.

Despite the improvements, logic and side effects are still tightly coupled in the protocol-oriented version. `Polygon.draw` does 2 things: it converts the polygon into a number of lines and then renders those lines. So when it comes time to test the logic, we need to use `TestRenderer`—which, despite what the WWDC talk implies, is a mock.
虽然有所改进,但逻辑和副作用仍然在面向协议的版本中强度耦合。`Polygon.draw` 做了两件事:它将多边形转换为多条线,然后渲染这些线。因此,当需要测试这些逻辑时,我们需要使用 `TestRenderer` — 尽管 WWDC 暗示它只是一个模拟。

```
extension Polygon : Drawable {
Expand All @@ -145,7 +143,7 @@ extension Polygon : Drawable {
}
```

We can separate logic and effects here by turning them into separate steps. Instead of the `Renderer` protocol, with `move`, `line`, and `arc`, let’s declare value types that represent the underlying operations.
我们可以将逻辑和效果拆分成不同的步骤来区分它们。使用 `move``line` `arc` 来替代 `Renderer` 协议,让我们声明代表这些底层操作的值类型。

```
enum Path: Hashable {
Expand All @@ -168,7 +166,7 @@ enum Path: Hashable {
}
```

Now, instead of calling those methods, `Drawable`s can return a set of `Path`s that are used to draw them:
现在,`Drawable` 可以通过返回一组用于绘制的 `path` 来替代方法调用:

```
protocol Drawable {
Expand Down Expand Up @@ -198,7 +196,7 @@ extension Diagram : Drawable {
}
```

And now `CGContext` to be extended to draw those paths:
现在 `CGContext` 通过扩展来绘制这些路径:

```
extension CGContext {
Expand Down Expand Up @@ -230,7 +228,7 @@ extension CGContext {
}
```

And we can add our convenience method for creating circles:
我们可以添加用来创建 circle 的便捷方法:

```
extension Path {
Expand All @@ -240,11 +238,11 @@ extension Path {
}
```

This works just the same as before and requires roughly the same amount of code. But we’ve introduced a boundary that lets us separate two parts of the system. That boundary lets us:
这与之前的运行效果一样,并需要大致相同数量的代码。但我们引入了一个边界,让我们将系统的两个部分分开。这个边界让我们:

1. 没有模拟测试

1. Test without a mock

We don’t need `TestRenderer` anymore. We can verify that a `Drawable` will be drawn correctly testing the values return from its `paths` property. `Path` is `Equatable`, so this is a simple test.
我们不再需要 `TestRenderer` 了,我们可以通过测试从 `paths` 属性返回的值来验证 `Drawable` 是否可以正确绘制。`Path` 是 `可进行相等比较` 的,所以这是一个简单的测试。

```
let polygon = Polygon(corners: [(x: 0, y: 0), (x: 6, y: 0), (x: 3, y: 6)])
Expand All @@ -256,28 +254,28 @@ let paths: Set<Path> = [
XCTAssertEqual(polygon.paths, paths)
```

2. Insert more steps
With the value-oriented approach, we can take our `Set<Path>` and transform it directly. Say you wanted to flip the result horizontally. You calculate the size and then return a new `Set<Path>` with flipped coordinates.
In the protocol-oriented approach, it would be somewhat difficult to transform our drawing steps. To flip horizontally, you need to know the final width. Since that width isn’t known ahead of time, you’d need to write a `Renderer` that (1) saved all the calls to `move`, `line`, and `arc` and then (2) pass it another `Render` to render the flipped result.
(This theoretical renderer is creating the same boundary we created with the value-oriented approach. Step 1 corresponds to our `.paths` method; step 2 corresponds to `draw(Set<Paths>)`.)
3. Easily inspect data while debugging
Say you have a complex `Diagram` that isn’t drawing correctly. You drop into the debugger and find where the `Diagram` is drawn. How do you find the problem?
If you’re using the protocol-oriented approach, you’ll need to create a `TestRenderer` (if it’s available outside your tests) or you’ll need to use a real renderer and actually render somewhere. Inspecting that data will be difficult.
But if you’re using the value-oriented approach, you only need to call `paths` to inspect this information. Debuggers can display values much more easily than effects.

The boundary adds another semantic layer, which opens up additional possibilities for testing, transformation, and inspection.

I’ve used this approach on a number of projects and found it immensely helpful. Even with a simple example like the one given here, values have a number of benefits. But those benefits become much more obvious and helpful when working in larger, more complex systems.

If you’d like to see a real world example, check out [PersistDB](https://github.com/PersistX/PersistDB), the Swift persistence library I’ve been working on. The public API presents `Query`s, `Predicate`s, and `Expression`s. These are reduced to `SQL.Query`s, `SQL.Predicate`s, and `SQL.Expression`s. And each of those is reduced to a `SQL`, a value representing some actual SQL.
2. 插入更多步骤

使用值类型导向方法,我们可以使用 `Set<Path>` 并直接对其进行转换。假设你想要水平翻转结果。你只要计算尺寸,然后返回一个新的 `Set<Path>` 翻转坐标即可。

在面向协议的方法中,绘制转换步骤会有些困难。如果想要水平翻转,你需要知道最终宽度。由于预先不知道这个宽度,你需要实现一个 `Renderer`,(1)它保存了所有的方法调用(`move``line` `arc`)。(2)然后将其传递给另一个 `Render` 来渲染翻转结果。

(这个假设的渲染器创建了我们通过值类型导向方法创建的渲染器相同的边界。步骤 1 对应于 `.paths` 方法;步骤 2 对应于 `draw(Set<Paths>)`。)

3. 在调试时轻松检查数据

假设你有一个没有正确绘制的复杂 `Diagram`。你进入调试器并找到绘制 `Diagram` 的位置。你如何定位这个问题?

如果你正在使用面向协议的方法,你需要创建一个 `TestRenderer`(如果它在测试之外可用),或者你需要使用真实的渲染器并实际渲染某一部分。数据检查将变得很困难。

但如果你使用值类型导向方法,你只需要调用 `paths` 来检查这些信息。相对于渲染效果,调试器更容易显示数据值。


边界增加了另一个语义,为测试、转换和检查带来了更多的可能性。

我已经在很多项目中使用了这种方法,并发现它非常有用。即使是像本文给出的简单例子,值类型也具有很多好处。但在更大、更复杂的系统中,这些好处将变得更加明显和有用。

如果你想看一个真实的例子,请查看 [PersistDB](https://github.com/PersistX/PersistDB)。我一直在研究的 Swift 持久存储库。公共 API 提供 `Query``Predicate` `Expression`。它们是 `SQL.Query``SQL.Predicate` `SQL.Expression` 的简化版。它们中的每一个都会被转换成一个 `SQL`(一个代表一些实际 SQL 的值)。

> 如果发现译文存在错误或其他需要改进的地方,欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。

Expand Down