爲什麼我們無法寫出真正可重用的代碼?

{"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}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章