写点什么

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

  • 2021-01-28
  • 本文字数:3731 字

    阅读完需:约 12 分钟

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

几周前,Uwe Friedrichsen(codecentric.de CTO)在他的一篇博文中提出了一个这样的问题:


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


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


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


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


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


首先是 F#:


let (|DivisibleBy|_|) by n = if n%by=0 then Some DivisibleBy else Nonelet 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-fizzbuzznamespace 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/


2021-01-28 13:452711
用户头像

发布了 114 篇内容, 共 47.4 次阅读, 收获喜欢 313 次。

关注

评论

发布
暂无评论
发现更多内容

开源大数据调度系统Taier技术公开课——Taier数据开发介绍

袋鼠云数栈

开源技术交流丨ChengYing部署Hadoop集群实战

袋鼠云数栈

袋鼠云思枢:数驹DTengine,助力企业构建高效的流批一体数据湖计算平台

袋鼠云数栈

从洞察到决策,一文解读标签画像体系建设方法论丨DTVision分析洞察篇

袋鼠云数栈

基于开源流批一体数据同步引擎ChunJun数据还原—DDL解析模块的实战分享

袋鼠云数栈

“阿里爸爸”最新总结的303页Spring全家桶高级笔记,都是面试必问的知识点

Java工程师

Java spring Spring全家桶

跨平台|融云 React Native IM SDK 全新改版上线

融云 RongCloud

IM sdk

开源交流丨批流一体数据集成工具ChunJun同步Hive事务表原理详解及实战分享

袋鼠云数栈

开源项目丨Taier1.2版本发布,新增工作流、租户绑定简化等多项功能

袋鼠云数栈

数据资产为王,如何解析企业数字化转型与数据资产管理的关系?

袋鼠云数栈

当我们在聊「开源大数据调度系统Taier」的数据开发功能时,到底在讨论什么?

袋鼠云数栈

DataOps不是工具,而是帮助企业实现数据价值的最佳实践

袋鼠云数栈

如何在BI中增加“路线地图”并进行数据分析?

葡萄城技术团队

想要精准营销,从学习搭建一套对的标签体系开始丨DTVision分析洞察篇

袋鼠云数栈

1分钟创建3000台云电脑 阿里云无影发布多款千人协同生产力方案

Lily

大数据开源项目,一站式全自动化全生命周期运维管家ChengYing(承影)走向何方?

袋鼠云数栈

走好数据中台最后一公里,为什么说数据服务API是数据中台的标配?

袋鼠云数栈

开源技术交流丨一站式全自动化运维管家ChengYing入门介绍

袋鼠云数栈

流批一体开源项目ChunJun技术公开课——ChunJun同步Hive事务表

袋鼠云数栈

用 manim 写一个排序算法动画

ooooooh灰灰

算法 前端 后端 动画 排序

全新升级!《云原生架构白皮书 2022 版》重磅发布

阿里巴巴云原生

阿里云 架构 云原生 白皮书

开源技术交流丨批流一体数据同步引擎ChunJun数据还原-DDL功能模块解析

袋鼠云数栈

【接入指南 之 云云接入】快速接入HONOR Connect平台(下)

荣耀开发者服务平台

开发者 IoT 新手指南 荣耀 honor

活动报名| MongoDB 使用规范及最佳实践线上直播来啦!

MongoDB中文社区

mongodb

自动化测试-存储测试用例,哪个更好

和牛

Python 自动化 测试 8月月更

开源项目丨一文详解一站式大数据平台运维管家ChengYing如何部署Hadoop集群

袋鼠云数栈

袋鼠云申杭:数雁EasyDigit,致力成为金融行业数据洞察平台供应商的“领头雁”

袋鼠云数栈

老忘记带伞,自己动手做一个雨天提醒打伞小工具

华为云开发者联盟

云计算 开发 aPaaS

开源一夏 | layui时间控件 laydate 重置失效

六月的雨在InfoQ

开源 layui 8月月更 laydate

培训预告 | 企业应用现代化实用教程——微服务治理与开发篇

York

容器 微服务 云原生 数字化转型 应用现代化

数据资产为王,解析企业数字化转型与数据资产管理的关系

袋鼠云数栈

为什么我们无法写出真正可重用的代码?_文化 & 方法_Daniel B. Markham_InfoQ精选文章