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

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"几周前,Uwe Friedrichsen(codecentric.de CTO)在他的"},{"type":"link","attrs":{"href":"https:\/\/blog.codecentric.de\/en\/2015\/10\/the-broken-promise-of-re-use\/","title":"","type":null},"content":[{"type":"text","text":"一篇博文"}]},{"type":"text","text":"中提出了一个这样的问题:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"……可重用性是软件的制胜法宝:每当一个新的架构范式出现,“可重用性”就成了是否采用该范式的一个核心考虑因素。业务通常会这样认为:“转向新范式在一开始需要多付出一些成本,但因为可重用,所以很快就会从中获得回报”……但简单地说,任何基于可重用的架构范式从来都不会像承诺的那样,而且承诺总是无法兑现……"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"他例举了CORBA、基于组件的架构、EJB、SOA等例子,然后就问微服务是否会带来不一样的结果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为什么可重用性的承诺总是无法兑现?为什么我们无法写出真正可重用的代码?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这些都是很好的例子,Friedrichsen很好地解释了为什么实现可重用性是如此困难。然而,我相信,他忽略了关键的一点:经典的面向对象编程(OO)和纯函数式编程(FP)在可重用性方面会有截然不同的结果,因为它们基于不同的假设。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们来做个实验,分别用F#和C#以FP和OO的方式来实现“FizzBuzz”游戏。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先是F#:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"let (|DivisibleBy|_|) by n = if n%by=0 then Some DivisibleBy else None\nlet findMatch = function\n | DivisibleBy 3 & DivisibleBy 5 -> \"FizzBuzz\"\n | DivisibleBy 3 -> \"Fizz\"\n | DivisibleBy 5 -> \"Buzz\"\n | _ -> \"\"\n[]\nlet main argv =\n [1..100] |> Seq.map findMatch |> Seq.iteri (printfn \"%i %s\")\n 0 \/\/ we're good. Return 0 to indicate success back to OS"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看起来就十几行代码,但请注意以下三点:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代码太“碎片化”了,彼此之间好像没有关联性。有一个奇怪的东西叫DivisibleBy,然后有几行代码看起来像是FizzBuzz的主程序,但实际上不是从这里开始调用的。第三部分才是“真正”的代码行,只有一行。如果你不懂的话,就不知道哪块是哪块。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"问题来了:“如果需要添加另一个规则该怎么办”?很明显,你只需要在第二部分的DivisibleBy里加点东西就可以了,其他地方不需要改。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有了这几个部分,代码流程看起来就流畅了。如果你是一个FP程序员,就会知道,最后一部分该怎么写实际上是由程序员自己决定的。在这里,我使用了管道。不过,我也可以用其他几种方法来做。这部分代码除了计算序列并打印出来之外,其他什么都不做,要怎么做完全取决于我自己。我最终选择了可以最小化认知负担的做法。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,对于最后那部分代码,我可以这样写:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"let fizzBuzz n = n |> Seq.map findMatch |> Seq.iteri (printfn \"%i %s\")\n fizzBuzz [1..100]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我把所有东西都放进“fizzBuzz”(我把它叫作节点)里,它可以处理除数字范围外的所有东西,这样改起来就容易了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"fizzBuzz [50..200]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我知道这可能不值一提,但事实并非如此。我可以根据项目预期的使用情况来决定如何组织节点,可以自由地把一些东西放在一起或者不放在一起。我不提供解决方案,只是把一些东西组织成片段,然后以不同的方式将它们组合在一起,从而得到解决方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"现在,让我们来看一下C#代码。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\/\/来自https:\/\/stackoverflow.com\/questions\/11764539\/writing-fizzbuzz\nnamespace oop\n{\n class Program\n {\n static void DoFizzBuzz1()\n {\n for (int i = 1; i <= 100; i++)\n {\n bool fizz = i % 3 == 0;\n bool buzz = i % 5 == 0;\n if (fizz && buzz)\n Console.WriteLine (i.ToString() + \" FizzBuzz\");\n else if (fizz)\n Console.WriteLine (i.ToString() + \" Fizz\");\n else if (buzz)\n Console.WriteLine (i.ToString() + \" Buzz\");\n else\n Console.WriteLine (i);\n }\n }\n static void Main(string[] args)\n {\n Console.WriteLine(\"Hello World!\");\n DoFizzBuzz1();\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"C#的代码行数大概是F#的三倍。需要注意以下几点:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代码的结构是固定的,有一个命名空间、一个类和一个方法。每个东西都有自己的位置,它们的存在都有自己的理由。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从结构上看,添加新规则似乎会让事情变复杂。我很确定的是,想要添加一个新规则,就需要在两个“bool”代码行后面加一行新代码,然后修改嵌套的if\/else-if\/else-if\/else结构。这很容易做到,但我感觉这会让事情变复杂。而在使用FP时,我们是从复杂到简单。Stack Overflow网站上有另一个提供通用规则的C#示例,但其他评论者说它看起来过于复杂了。坦率地说,它看起来就像是在一个OO应用程序里塞满了大量的FP。它更通用,但绝对不是C#程序员最喜欢的代码。"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"似乎C#更擅长组件化和可重用性,但这也是事出蹊跷的地方。命名空间可以防止组件混在一起,类封装并隐藏了数据,外部就不需要操心内部的细节,方法被声明为静态的,但即使是静态的,对象包装器也会知道“DoFizzBuzz1”是一个特定的实例,与“Program2”提供的实例(或者使用不同的构造函数构造出来的Program)是不一样的。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在C#代码里,我没有创建节点,而是通过结构来组织代码。在OOP中,每一样东西都有它们特定的位置,什么时候该放在哪里都有可遵循的规则。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,从表面上看,C#代码更适合用来创建可重用的组件。毕竟,它们的结构看起来更有条理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要验证这个只有一种方法,就是去构造一个组件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我可以把C#代码部署到另一个容器里,比如在服务器端渲染HTML,然后发送到客户端吗?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不一定。所有东西都卡在Main方法上,而Main方法又与DoFizzBuzz1方法耦合。此外,1到100的范围与实现也是耦合在一起的。这个类之所以是这样,是因为它是一个C#控制台应用程序。F#和C#代码的行数之所以差异巨大,是因为C#应用程序是一个模板,所有东西都被放在一个紧密耦合且严格的结构中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不过,说到底,我有点把组件和可重用性混淆在一起了。这里要讨论的是可重用性,而构建组件是另一个领域的问题。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它们没有绝对的对和错,只是我们在试图重用30行C#代码时遇到一些问题(代码越多,问题就越严重):所有东西都是耦合在一起的,可变性使得它们之间的关联无法分离。事实上,从设计角度讲,对象既是数据又是代码,所以面向对象就是样子的!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或许,我们需要的是一个“HtmlProgram”类而不是“Program”类。或许,我们需要一个“HtmlRenderer”类,因为与Html相关的代码总归要被放在某个地方。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么F#代码呢?只有程序入口的那行代码需要放到其他地方,其他所有东西都在全局命名空间里。如果我需要修改数字范围,非常容易,不会与其他东西耦合。我可以用任何我想要的方式来处理这些节点,这有很大的自由度。而在使用OO时,我们需要尽早就设计好,否则使用OO就没有意义了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"需要注意的是,这不是一篇抨击C#的文章。在这两种编程语言当中,其中一种并不一定不比另一种更好或更差,它们只是用截然不同的方式解决问题。OO代码可以扩展成大型的单片应用程序,所有东西都有自己的位置。FP代码的节点可以扩展到创建出一种DSL,调用者能使用新的语言来做他们想做的任何事情。在使用OO时,我最终会得到一大堆数据和代码,保证可以做到我想做的事情。在使用FP时,我最终使用了一种新语言,用它来创建任何我想要的东西。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但说到可重用性时,比如在微服务中的可重用性,这两种范式会得出截然不同的答案。纯FP范式将创建可重用的代码,但在大型的应用程序中,调用方的复杂性会增加。OO范式将创建不可重用的代码。在很多情况下,OO是更好的范例,只是它永远不会创建出一般意义上的可重用组件。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用纯FP时,你创建的都是可重用组件,只是不知道它们最终会以怎样的方式组合在一起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"从理论方面来看,就更清楚究竟是怎么回事了。所有的代码,无论使用的是哪种编程语言,都是针对某个问题而创建的一种结构形式。结构总是基于两个东西:你所期望的行为和附加规则(或者说是非功能性的东西)。即使你没有把心里期望的东西列出来,但写代码时,你也会思考这些代码是否创建了一个遵循给定规则的系统。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用纯FP时,我是没有附加规则的。也就是说,没有SOLID原则或者其他可以指导我要以这样或那样的方式编写代码的东西。我写代码的目标是如何以最低的认知复杂性来实现我想要的行为,仅此而已。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用OO时,附加规则比行为更重要。在开始使用一个新框架时,你必须为对象实现一堆接口,即使它们没有被调用。为什么要这样?因为使用框架的规则比使用框架来实现某些功能更为重要。这就是面向对象的核心假设,一切东西都有自己的位置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用OO时,我向外看,构建出一组可以用来表示问题的结构,这样就能很容易地理解和修改它们。在使用FP时,我向内看,尽可能在不涉及可变性的情况下,以最简单的转换方式使用原语。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了重用C#代码,以便能够把它部署到新容器里,代码需要进行大量的调整。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大多数情况下,OO就是要在写代码之前先理清楚需求。它会在你想要的东西(要到很后面或完成之后才会知道)和可交付的东西之间产生一种自然的阻抗不匹配。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好的FP项目创建可重用的组件,在一开始只需要几行代码。不管代码库有多大,好的OO项目可以创建易理解的代码结构。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你想要真正的组件和可重用性,直接使用FP,不需要任何附加规则,然后在最后时刻加入任何你需要的东西。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文链接:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/danielbmarkham.com\/why-are-reusable-components-so-difficult\/","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/danielbmarkham.com\/why-are-reusable-components-so-difficult\/"}],"marks":[{"type":"underline"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章