8

为什么我们无法写出真正可重用的代码?

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzIzNjUxMzk2NQ%3D%3D&%3Bmid=2247505728&%3Bidx=1&%3Bsn=bde0c5f262ffecf7c8f46ea164c3d5c8
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

yeaAzqu.jpg!mobile

作者 | Daniel B. Markham

译者 | 王者

策划 | 万佳

为什么实现组件可重用性如此之难?

几周前,Uwe Friedrichsen 在他一篇博文中提出一个这样的问题:

……可重用性是软件的制胜法宝:每当一个新的架构范式出现,“可重用性”就成了是否采用该范式的一个核心考虑因素。业务通常会这样认为:“转向新范式在一开始需要多付出一些成本,但因为可重用,所以很快就会从中获得回报”……但简单地说,任何基于可重用的架构范式从来都不会像承诺的那样,而且承诺总是无法兑现……

他例举了 CORBA、基于组件的架构、EJB、SOA 等例子,然后就问微服务是否会带来不一样的结果。

为什么可重用性的承诺总是无法兑现?为什么我们无法写出真正可重用的代码?

这些都是很好的例子,Friedrichsen 很好地解释了为什么实现可重用性是如此困难。然而,我相信,他忽略了关键的一点:经典的面向对象编程(OO)和纯函数式编程(FP)在可重用性方面会有截然不同的结果,因为它们基于不同的假设。

我们来做个实验,分别用 F# 和 C# 以 FP 和 OO 的方式来实现“FizzBuzz”游戏。

首先是 F#:

let (|DivisibleBy|_|) by n = if n%by=0 then Some DivisibleBy else None
let findMatch = function
| DivisibleBy 3 & DivisibleBy 5 -> "FizzBuzz"
| DivisibleBy 3 -> "Fizz"
| DivisibleBy 5 -> "Buzz"
| _ -> ""
[<EntryPoint>]
let main argv =
[1..100] |> Seq.map findMatch |> Seq.iteri (printfn "%i %s")
0 // we're good. Return 0 to indicate success back to OS

看起来就十几行代码,但请注意以下三点:

  • 代码太“碎片化”了,彼此之间好像没有关联性。有一个奇怪的东西叫 DivisibleBy,然后有几行代码看起来像是 FizzBuzz 的主程序,但实际上不是从这里开始调用的。第三部分才是“真正”的代码行,只有一行。如果你不懂的话,就不知道哪块是哪块。

  • 问题来了:“如果需要添加另一个规则该怎么办”?很明显,你只需要在第二部分的 DivisibleBy 里加点东西就可以了,其他地方不需要改。

  • 有了这几个部分,代码流程看起来就流畅了。如果你是一个 FP 程序员,就会知道,最后一部分该怎么写实际上是由程序员自己决定的。在这里,我使用了管道。不过,我也可以用其他几种方法来做。这部分代码除了计算序列并打印出来之外,其他什么都不做,要怎么做完全取决于我自己。我最终选择了可以最小化认知负担的做法。

例如,对于最后那部分代码,我可以这样写:

let fizzBuzz n  = n |> Seq.map findMatch |> Seq.iteri (printfn "%i %s")
fizzBuzz [1..100]

我把所有东西都放进“fizzBuzz”(我把它叫作节点)里,它可以处理除数字范围外的所有东西,这样改起来就容易了。

fizzBuzz [50..200]

我知道这可能不值一提,但事实并非如此。我可以根据项目预期的使用情况来决定如何组织节点,可以自由地把一些东西放在一起或者不放在一起。我不提供解决方案,只是把一些东西组织成片段,然后以不同的方式将它们组合在一起,从而得到解决方案。

现在,让我们来看一下 C# 代码。

// 来自 https://stackoverflow.com/questions/11764539/writing-fizzbuzz
namespace oop
{
class Program
{
static void DoFizzBuzz1()
{
for (int i = 1; i <= 100; i++)
{
bool fizz = i % 3 == 0;
bool buzz = i % 5 == 0;
if (fizz && buzz)
Console.WriteLine (i.ToString() + " FizzBuzz");
else if (fizz)
Console.WriteLine (i.ToString() + " Fizz");
else if (buzz)
Console.WriteLine (i.ToString() + " Buzz");
else
Console.WriteLine (i);
}
}
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
DoFizzBuzz1();
}
}
}

C# 的代码行数大概是 F# 的三倍。需要注意以下几点:

  • 代码的结构是固定的,有一个命名空间、一个类和一个方法。每个东西都有自己的位置,它们的存在都有自己的理由。

  • 从结构上看,添加新规则似乎会让事情变复杂。我很确定的是,想要添加一个新规则,就需要在两个“bool”代码行后面加一行新代码,然后修改嵌套的 if/else-if/else-if/else 结构。这很容易做到,但我感觉这会让事情变复杂。而在使用 FP 时,我们是从复杂到简单。Stack Overflow 网站上有另一个提供通用规则的 C# 示例,但其他评论者说它看起来过于复杂了。坦率地说,它看起来就像是在一个 OO 应用程序里塞满了大量的 FP。它更通用,但绝对不是 C# 程序员最喜欢的代码。

  • 似乎 C# 更擅长组件化和可重用性,但这也是事出蹊跷的地方。命名空间可以防止组件混在一起,类封装并隐藏了数据,外部就不需要操心内部的细节,方法被声明为静态的,但即使是静态的,对象包装器也会知道“DoFizzBuzz1”是一个特定的实例,与“Program2”提供的实例(或者使用不同的构造函数构造出来的 Program)是不一样的。

在 C# 代码里,我没有创建节点,而是通过结构来组织代码。在 OOP 中,每一样东西都有它们特定的位置,什么时候该放在哪里都有可遵循的规则。

因此,从表面上看,C# 代码更适合用来创建可重用的组件。毕竟,它们的结构看起来更有条理。

要验证这个只有一种方法,就是去构造一个组件。

我可以把 C# 代码部署到另一个容器里,比如在服务器端渲染 HTML,然后发送到客户端吗?

不一定。所有东西都卡在 Main 方法上,而 Main 方法又与 DoFizzBuzz1 方法耦合。此外,1 到 100 的范围与实现也是耦合在一起的。这个类之所以是这样,是因为它是一个 C# 控制台应用程序。F# 和 C# 代码的行数之所以差异巨大,是因为 C# 应用程序是一个模板,所有东西都被放在一个紧密耦合且严格的结构中。

不过,说到底,我有点把组件和可重用性混淆在一起了。这里要讨论的是可重用性,而构建组件是另一个领域的问题。

它们没有绝对的对和错,只是我们在试图重用 30 行 C# 代码时遇到一些问题(代码越多,问题就越严重):所有东西都是耦合在一起的,可变性使得它们之间的关联无法分离。事实上,从设计角度讲,对象既是数据又是代码,所以面向对象就是样子的!

或许,我们需要的是一个“HtmlProgram”类而不是“Program”类。或许,我们需要一个“HtmlRenderer”类,因为与 Html 相关的代码总归要被放在某个地方。

那么 F# 代码呢?只有程序入口的那行代码需要放到其他地方,其他所有东西都在全局命名空间里。如果我需要修改数字范围,非常容易,不会与其他东西耦合。我可以用任何我想要的方式来处理这些节点,这有很大的自由度。而在使用 OO 时,我们需要尽早就设计好,否则使用 OO 就没有意义了。

需要注意的是,这不是一篇抨击 C# 的文章。在这两种编程语言当中,其中一种并不一定不比另一种更好或更差,它们只是用截然不同的方式解决问题。OO 代码可以扩展成大型的单片应用程序,所有东西都有自己的位置。FP 代码的节点可以扩展到创建出一种 DSL,调用者能使用新的语言来做他们想做的任何事情。在使用 OO 时,我最终会得到一大堆数据和代码,保证可以做到我想做的事情。在使用 FP 时,我最终使用了一种新语言,用它来创建任何我想要的东西。

但说到可重用性时,比如在微服务中的可重用性,这两种范式会得出截然不同的答案。纯 FP 范式将创建可重用的代码,但在大型的应用程序中,调用方的复杂性会增加。OO 范式将创建不可重用的代码。在很多情况下,OO 是更好的范例,只是它永远不会创建出一般意义上的可重用组件。

在使用纯 FP 时,你创建的都是可重用组件,只是不知道它们最终会以怎样的方式组合在一起。

从理论方面来看,就更清楚究竟是怎么回事了。所有的代码,无论使用的是哪种编程语言,都是针对某个问题而创建的一种结构形式。结构总是基于两个东西:你所期望的行为和附加规则 (或者说是非功能性的东西)。即使你没有把心里期望的东西列出来,但写代码时,你也会思考这些代码是否创建了一个遵循给定规则的系统。

在使用纯 FP 时,我是没有附加规则的。也就是说,没有 SOLID 原则或者其他可以指导我要以这样或那样的方式编写代码的东西。我写代码的目标是如何以最低的认知复杂性来实现我想要的行为,仅此而已。

在使用 OO 时,附加规则比行为更重要。在开始使用一个新框架时,你必须为对象实现一堆接口,即使它们没有被调用。为什么要这样?因为使用框架的规则比使用框架来实现某些功能更为重要。这就是面向对象的核心假设,一切东西都有自己的位置。

在使用 OO 时,我向外看,构建出一组可以用来表示问题的结构,这样就能很容易地理解和修改它们。在使用 FP 时,我向内看,尽可能在不涉及可变性的情况下,以最简单的转换方式使用原语。

为了重用 C# 代码,以便能够把它部署到新容器里,代码需要进行大量的调整。

大多数情况下,OO 就是要在写代码之前先理清楚需求。它会在你想要的东西(要到很后面或完成之后才会知道) 和可交付的东西之间产生一种自然的阻抗不匹配。

好的 FP 项目创建可重用的组件,在一开始只需要几行代码。不管代码库有多大,好的 OO 项目可以创建易理解的代码结构。

如果你想要真正的组件和可重用性,直接使用 FP,不需要任何附加规则,然后在最后时刻加入任何你需要的东西。

原文链接:

https://danielbmarkham.com/why-are-reusable-components-so-difficult/

今日文章推荐:

Apache ECharts 正式毕业,成为 ASF 顶级项目

点个在看少个 bug :point_down:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK