整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          Go語言有個“好爹”反而被程序員討厭?

          Go語言有個“好爹”反而被程序員討厭?

          o (Golang) 是 Google 開發(fā)的一種編譯型、并發(fā)型,并具有垃圾回收功能的編程語言,于 2009 年 11 月正式宣布推出成為開源項(xiàng)目,2012 年發(fā)布 1.0 版本。

          如今,谷歌仍在繼續(xù)投資該語言,最新的穩(wěn)定版本是 1.22.5。

          在最新的 TIOBE 7 月榜單中,Go 排名第七。與其他所有編程語言一樣,有人喜歡 Go 語言也有人討厭,同樣的功能既會帶來詆毀也會帶來贊美。

          InfoWorld 撰稿分析了開發(fā)人員喜歡或討厭 Go 語言的 8 個原因,具體如下。

          1、易于學(xué)習(xí)

          Go 的設(shè)計(jì)者特意打造了一種易于學(xué)習(xí)的語言,沒有太多復(fù)雜的功能或特異之處。

          喜歡的點(diǎn)在于:對于新程序員和團(tuán)隊(duì)成員來說,更簡單的語言更容易學(xué)習(xí)和掌握。由于老程序員可以很快學(xué)會 Go 的新技巧,因此項(xiàng)目人員配備也更容易。不僅如此,相關(guān)代碼通常也更容易閱讀。

          討厭的點(diǎn)在于:太過簡單反而束縛了手腳。“一個女巫會選擇一本簡略的咒語書嗎?四分衛(wèi)會選擇只有幾個戰(zhàn)術(shù)的戰(zhàn)術(shù)書嗎?一些程序員認(rèn)為,用 Go 編程就像一只手被綁在背后。這種語言缺乏其他語言設(shè)計(jì)者向世界展示的所有聰明才智,而這是要付出高昂代價的。”

          2、Go 不會偏袒任何一方

          最初開發(fā)人員希望創(chuàng)建一種小型語言,為此他們犧牲了其他語言中許多受歡迎的功能。Go 是一種精簡的語言,即可以滿足用戶的需求,又省去了一些繁瑣。

          喜歡的點(diǎn)在于:許多開發(fā)人員都稱贊 Go 的簡單性。Go 不需要他們掌握或保持?jǐn)?shù)十種功能的專業(yè)知識才能熟練使用。

          討厭的點(diǎn)在于:每個人都有一些喜歡的功能和技巧,但 Go 很可能不提供這些功能和技巧。開發(fā)人員有時會抱怨,他們只需用 COBOL 或 Java 或其他喜歡的語言寫一行代碼,就可以完成在 Go 中可以完成的相同任務(wù)。

          3、基于 C 的語法

          Go 的設(shè)計(jì)團(tuán)隊(duì)確實(shí)基于傳統(tǒng) C 語言改進(jìn)了一些缺陷,并簡化了一些細(xì)節(jié),使其看起來和感覺更現(xiàn)代。但在大多數(shù)情況下,Go 完全繼承了始于 C 語言的傳統(tǒng)。

          喜歡的點(diǎn)在于:在 C 語言風(fēng)格中成長起來的程序員會直觀地理解 Go 的大部分內(nèi)容。他們將能夠非常快速地學(xué)習(xí)語法,并且可以花時間學(xué)習(xí) Go 相較 C 或 Java 的一些改進(jìn)之處。

          討厭的點(diǎn)在于:很多方面,Python 的設(shè)計(jì)都是與 C 截然相反的。對于喜歡 Python 方法的人而言,會覺得 Go 有很多讓人討厭的地方。

          4、Go 的規(guī)則太多了

          從一開始,Go 的創(chuàng)建者就希望不僅定義語法,還定義語言的大部分風(fēng)格和使用模式。

          喜歡的點(diǎn)在于:Go 的強(qiáng)慣用規(guī)則確保代碼更容易理解,團(tuán)隊(duì)將減少對風(fēng)格的爭論。

          討厭的點(diǎn)在于:所有這些額外的規(guī)則和慣例都像束縛。“程序員在生活中擁有一點(diǎn)自由有那么糟糕嗎?”

          5、Go 有額外的錯誤處理

          喜歡的點(diǎn)在于:Go 方法承認(rèn)錯誤存在,并鼓勵程序員制定處理錯誤的計(jì)劃。這就鼓勵程序員提前計(jì)劃,并建立起一種彈性,從而開發(fā)出更好的軟件。

          討厭的點(diǎn)在于:不必要的錯誤處理會讓 Go 函數(shù)變得更長、更難理解。通常情況下,deep chain 中的每個函數(shù)都必須包含類似的代碼,這些代碼或多或少會執(zhí)行相同的操作,并產(chǎn)生相同的錯誤。其他語言(如 Java 或 Python)鼓勵程序員將錯誤 "throw" 到鏈上的特定代碼塊中,以 "catch" 它們,從而使代碼更簡潔。

          6、標(biāo)準(zhǔn)庫

          喜歡的點(diǎn)在于:當(dāng)許多標(biāo)準(zhǔn)功能由默認(rèn)庫處理時,大多數(shù)代碼更易于閱讀。因?yàn)闆]有人會編寫自己的版本,或爭論哪個軟件包或第三方庫更好。

          討厭的點(diǎn)在于:一些人認(rèn)為,競爭能更好的推動需求和創(chuàng)新。有些語言支持多個軟件包來處理相同的任務(wù),表明大家對此確實(shí)有著濃厚的興趣和豐富的文化。

          7、可執(zhí)行文件的大小

          Go 團(tuán)隊(duì)的目標(biāo)之一是讓部署 Go 程序變得更容易,他們通過將所有程序打包成一個可執(zhí)行文件來實(shí)現(xiàn)這一目標(biāo)。

          喜歡的點(diǎn)在于:磁盤空間很便宜。當(dāng)安裝了不同版本的庫時,在陌生的位置部署代碼可能是一場噩夢。Go 開發(fā)人員只需構(gòu)建一個可執(zhí)行文件就可以節(jié)省大量時間。

          討厭的點(diǎn)在于:我的磁盤上有多少份 Go 庫?如果我有 100 個程序,那就意味著 100 份。在某種程度上,效率是一個考慮因素。沒錯,磁盤空間比以往任何時候都便宜,但內(nèi)存帶寬和緩存仍然是影響執(zhí)行速度的首要問題。

          8、背靠“好爹”谷歌

          Go 由谷歌開發(fā),這家大公司一直是 Go 的主要支持者之一。大多數(shù)情況下,Go 開發(fā)工作都直接來自 Google 內(nèi)部。

          喜歡的點(diǎn)在于:如今,大量的工作涉及為服務(wù)器和客戶端編寫代碼,而這類工作在 Google 的工作量中占了很大一部分。如果 Go 對谷歌有利,那么對我們這些以同樣方式工作的人也有好處。如果谷歌的工程師們能開發(fā)出自己喜歡的東西,那么任何有類似項(xiàng)目的人都會同樣喜歡它。

          討厭的點(diǎn)在于:這并不是說人們不喜歡谷歌本身,而是程序員不信任中心化組織、供應(yīng)商鎖定和缺乏控制等問題,對任何試圖管理技術(shù)堆棧的人來說都是嚴(yán)重的問題。谷歌的慷慨仍然讓程序員們心存疑慮,尤其是當(dāng)其他語言都擁有了圍繞它們構(gòu)建的龐大的開源社區(qū)。


          Reference

          https://www.infoworld.com/article/2514123/8-reasons-developers-love-go-and-8-reasons-they-dont.html

          [1]Mateusz Piorowski[2] - 2023.07.24

          先來了解一下我的背景吧。我是一名軟件開發(fā)人員,有大約十年的工作經(jīng)驗(yàn),最初使用 PHP,后來逐漸轉(zhuǎn)向 JavaScript。

          大約五年前,我開始使用 TypeScript,從那時起,我就再也沒有使用過 JavaScript。從開始使用 TypeScript 的那一刻起,我就認(rèn)為它是有史以來最好的編程語言。每個人都喜歡它,每個人都在使用它……它就是最好的,對嗎?對吧?對不對?

          是的,然后我開始接觸其他的語言,更現(xiàn)代化的語言。首先是 Go,然后我慢慢地把 Rust 也加了進(jìn)來。

          當(dāng)你不知道存在不同的事物時,就很難錯過它們。

          我在說什么?Go 和 Rust 的共同點(diǎn)是什么?Error,這是最讓我印象深刻的一點(diǎn)。更具體地說,這些語言是如何處理錯誤的。

          JavaScript 依靠拋出異常來處理錯誤,而 Go 和 Rust 則將錯誤視為值。你可能會覺得這沒什么大不了的......但是,好家伙,這聽起來似乎微不足道;然而,它卻改變了游戲規(guī)則。

          讓我們來了解一下它們。我們不會深入研究每種語言,只是想了解一般的處理方式。

          讓我們從 JavaScript/TypeScript 和一個小游戲開始。

          給自己五秒鐘的時間來查看下面的代碼,并回答為什么我們需要用 try/catch 來包裹它。

          try {
            const request={ name: “test”, value: 2n };
            const body=JSON.stringify(request);
            const response=await fetch("https://example.com", {
              method: “POST”,
              body,
            });
            if (!response.ok) {
              return;
            }
            // 處理響應(yīng)
          } catch (e) {
            // 處理錯誤
            return;
          }
          

          那么,我想你們大多數(shù)人都猜到了,盡管我們檢查了 response.ok,但 fetch 方法仍然可能拋出一個異常。response.ok 只能“捕獲” 4xx 和 5xx 的網(wǎng)絡(luò)錯誤。但是,當(dāng)網(wǎng)絡(luò)本身失敗時,它會拋出一個異常。

          但我不知道有多少人猜到 JSON.stringify 也會拋出一個異常。原因是請求對象包含 bigint (2n) 變量,而 JSON 不知道如何將其序列化為字符串。

          所以,第一個問題是,我個人認(rèn)為這是 JavaScript 最大的問題:我們不知道什么可能會拋出一個異常。從 JavaScript 錯誤的角度來看,它與下面的情況是一樣的:

          try {
            let data=“Hello”;
          } catch (err) {
            console.error(err);
          }
          

          JavaScript 不知道;JavaScript 也不在乎。你應(yīng)該知道。

          第二個問題,這是完全可行的代碼:

          const request={ name: “test”, value: 2n };
          const body=JSON.stringify(request);
          const response=await fetch("https://example.com", {
            method: “POST”,
            body,
          });
          if (!response.ok) {
            return;
          }
          

          沒有錯誤,沒有語法檢查,盡管這可能會導(dǎo)致你的應(yīng)用程序崩潰。

          現(xiàn)在,在我的腦海中,我聽到的是:“有什么問題,在任何地方使用 try/catch 就可以了”。這就引出了第三個問題:我們不知道哪個異常被拋出。當(dāng)然,我們可以通過錯誤信息來猜測,但對于規(guī)模較大、可能發(fā)生錯誤的地方較多的服務(wù)/函數(shù)來說,又該怎么辦呢?你確定用一個 try/catch 就能正確處理所有錯誤嗎?

          好了,是時候停止對 JS 的挑剔,轉(zhuǎn)而討論其他問題了。讓我們從這段 Go 代碼開始:

          f, err :=os.Open(“filename.ext”)
          if err !=nil {
            log.Fatal(err)
          }
          // 對打開的 *File f 進(jìn)行一些操作
          

          我們正在嘗試打開一個返回文件或錯誤的文件。你會經(jīng)常看到這種情況,主要是因?yàn)槲覀冎滥男┖瘮?shù)總是返回錯誤,你絕不會錯過任何一個。這是第一個將錯誤視為值的例子。你可以指定哪個函數(shù)可以返回錯誤值,然后返回錯誤值,分配錯誤值,檢查錯誤值,處理錯誤值。

          這也是 Go 被詬病的地方之一——“錯誤檢查代碼”,其中 if err !=nil { … 有時候的代碼行數(shù)比其他部分還要多。

          if err !=nil {
            …
            if err !=nil {
              …
              if err !=nil {
                …
              }
            }
          }
          if err !=nil {
            …
          }
          …
          if err !=nil {
            …
          }
          

          盡管如此,相信我,這些努力還是值得的。

          最后,讓我們看看 Rust:

          let greeting_file_result=File::open(“hello.txt”);
          let greeting_file=match greeting_file_result {
            Ok(file)=> file,
            Err(error)=> panic!("Problem opening the file: {:?}", error),
          };
          

          這里顯示的是三種錯誤處理中最冗長的一種,具有諷刺意味的是,它也是最好的一種。首先,Rust 使用其神奇的枚舉(它們與 TypeScript 的枚舉不同!)來處理錯誤。這里無需贅述,重要的是它使用了一個名為 Result 的枚舉,有兩個變量:OkErr。你可能已經(jīng)猜到,Ok 包含一個值,而 Err 包含……沒錯,一個錯誤 :D。

          它也有很多更方便的處理方式來緩解 Go 的問題。最知名的一個是 ? 操作符。

          let greeting_file_result=File::open(“hello.txt")?;
          

          這里的總結(jié)是,Go 和 Rust 總是知道哪里可能會出錯。它們強(qiáng)迫你在錯誤出現(xiàn)的地方(大部分情況下)立即處理它。沒有隱藏的錯誤,不需要猜測,也不會因?yàn)橐馔獾腻e誤而導(dǎo)致應(yīng)用程序崩潰。

          而這種方法就是更好,好得多。

          好了,是時候?qū)嵲拰?shí)說了;我撒了點(diǎn)小謊。我們無法讓 TypeScript 的錯誤像 Go / Rust 那樣工作。限制因素在于語言本身,它沒有合適的工具來做到這一點(diǎn)。

          但我們能做的就是盡量使其相似。并且讓它變得簡單。

          從這里開始:

          exporttype Safe<T>=| {
                    success: true;
                    data: T;
                }
              | {
                    success: false;
                    error: string;
                };
          

          這里沒有什么花哨的東西,只是一個簡單的通用類型。但這個小東西卻能徹底改變代碼。你可能會注意到,這里最大的不同就是我們要么返回?cái)?shù)據(jù),要么返回錯誤。聽起來熟悉嗎?

          另外......第二個謊言是,我們確實(shí)需要一些 try/catch。好在我們只需要兩個,而不是十萬個。

          exportfunction safe<T>(promise: Promise<T>, err?: string): Promise<Safe<T>>;
          exportfunction safe<T>(func: ()=> T, err?: string): Safe<T>;
          exportfunction safe<T>(
              promiseOrFunc: Promise<T> | (()=> T),
              err?: string
          ): Promise<Safe<T>> | Safe<T> {
              if (promiseOrFunc instanceofPromise) {
                  return safeAsync(promiseOrFunc, err);
              }
              return safeSync(promiseOrFunc, err);
          }
          
          asyncfunction safeAsync<T>(
              promise: Promise<T>,
              err?: string
          ): Promise<Safe<T>> {
              try {
                  const data=await promise;
                  return { data, success: true };
              } catch (e) {
                  console.error(e);
                  if (err !==undefined) {
                      return { success: false, error: err };
                  }
                  if (e instanceofError) {
                      return { success: false, error: e.message };
                  }
                  return { success: false, error: "Something went wrong" };
              }
          }
          
          function safeSync<T>(func: ()=> T, err?: string): Safe<T> {
              try {
                  const data=func();
                  return { data, success: true };
              } catch (e) {
                  console.error(e);
                  if (err !==undefined) {
                      return { success: false, error: err };
                  }
                  if (e instanceofError) {
                      return { success: false, error: e.message };
                  }
                  return { success: false, error: "Something went wrong" };
              }
          }
          

          “哇,真是個天才。他為 try/catch 創(chuàng)建了一個包裝器。” 是的,你說得沒錯;這只是一個包裝器,我們的 Safe 類型作為返回類型。但有時候,簡單的東西就是你所需要的。讓我們將它們與上面的例子結(jié)合起來。

          舊的(16 行)示例:

          try {
            const request={ name: “test”, value: 2n };
            const body=JSON.stringify(request);
            const response=await fetch("https://example.com", {
              method: “POST”,
              body,
            });
            if (!response.ok) {
              // 處理網(wǎng)絡(luò)錯誤
              return;
            }
            // 處理響應(yīng)
          } catch (e) {
            // 處理錯誤
            return;
          }
          

          新的(20 行)示例:

          const request={ name: “test”, value: 2n };
          const body=safe(
            ()=>JSON.stringify(request),
            “Failed to serialize request”,
          );
          if (!body.success) {
            // 處理錯誤(body.error)
            return;
          }
          const response=await safe(
            fetch("https://example.com", {
              method: “POST”,
              body: body.data,
            }),
          );
          if (!response.success) {
            // 處理錯誤(response.error)
            return;
          }
          if (!response.data.ok) {
            // 處理網(wǎng)絡(luò)錯誤
            return;
          }
          // 處理響應(yīng)(body.data)
          

          是的,我們的新解決方案更長,但性能更好,原因如下:

          • 沒有 try/catch
          • 我們在錯誤發(fā)生的地方處理每個錯誤
          • 我們可以為特定函數(shù)指定一個錯誤信息
          • 我們有一個很好的自上而下的邏輯,所有錯誤都在頂部,然后底部只有響應(yīng)

          但現(xiàn)在王牌來了,如果我們忘記檢查這個:

          if (!body.success) {
              // 處理錯誤 (body.error)
              return;
          }
          

          事實(shí)是……我們不能忘記。是的,我們必須進(jìn)行這個檢查。如果我們不這樣做,body.data 將不存在。LSP 會通過拋出 “Property ‘data’ does not exist on type ‘Safe’” 錯誤來提醒我們。這都要?dú)w功于我們創(chuàng)建的簡單的 Safe 類型。它同樣適用于錯誤信息,我們在檢查 !body.success 之前無法訪問 body.error

          這是我們應(yīng)該欣賞 TypeScript 以及它如何改變 JavaScript 世界的時刻。

          以下也同樣適用:

          if (!response.success) {
              // 處理錯誤 (response.error)
              return;
          }
          

          我們不能移除 !response.success 檢查,否則,response.data 將不存在。

          當(dāng)然,我們的解決方案也不是沒有問題。最大的問題是你必須記住要用我們的 safe 包裝器包裝可能拋出異常的 Promise/函數(shù)。這個 “我們需要知道” 是我們無法克服的語言限制。

          這聽起來很難,但其實(shí)并不難。你很快就會意識到,你代碼中的幾乎所有 Promises 都會出錯,而那些會出錯的同步函數(shù)你也知道,而且它們的數(shù)量并不多。

          不過,你可能會問,這樣做值得嗎?我們認(rèn)為值得,而且在我們團(tuán)隊(duì)中運(yùn)行得非常好:)。當(dāng)你看到一個更大的服務(wù)文件,沒有任何 try/catch,每個錯誤都在出現(xiàn)的地方得到了處理,邏輯流暢......它看起來就很不錯。

          這是一個使用 SvelteKit FormAction 的真實(shí)例子:

          exportconst actions={
            createEmail: async ({ locals, request })=> {
              const end=perf(“CreateEmail”);
              const form=await safe(request.formData());
              if (!form.success) {
                return fail(400, { error: form.error });
              }
              const schema=z
                .object({
                  emailTo: z.string().email(),
                  emailName: z.string().min(1),
                  emailSubject: z.string().min(1),
                  emailHtml: z.string().min(1),
                })
              .safeParse({
                emailTo: form.data.get("emailTo"),
                emailName: form.data.get("emailName"),
                emailSubject: form.data.get("emailSubject"),
                emailHtml: form.data.get("emailHtml"),
              });
              if (!schema.success) {
                console.error(schema.error.flatten());
                return fail(400, { form: schema.error.flatten().fieldErrors });
              }
              const metadata=createMetadata(URI_GRPC, locals.user.key)
              if (!metadata.success) {
                return fail(400, { error: metadata.error });
              }
              const response=awaitnewPromise<Safe<Email__Output>>((res)=> {
                usersClient.createEmail(schema.data, metadata.data, grpcSafe(res));
              });
              if (!response.success) {
                return fail(400, { error: response.error });
              }
              end();
              return {
                email: response.data,
              };
            },
          } satisfies Actions;
          

          這里有幾點(diǎn)需要指出:

          • 我們的自定義函數(shù) grpcSafe 可以幫助我們處理 gGRPC 回調(diào)。
          • createMetadata 內(nèi)部返回 Safe,因此我們不需要對其進(jìn)行封裝。
          • zod 庫使用相同的模式 :) 如果我們不進(jìn)行 schema.success 檢查,我們就無法訪問 schema.data

          看起來是不是很簡潔?那就試試吧!也許它也非常適合你 :)

          感謝閱讀。

          附注:下面的代碼對比是不是看起來很像?

          f, err :=os.Open(“filename.ext”)
          if err !=nil {
            log.Fatal(err)
          }
          // 使用打開的 *File f 做一些事情
          
          const response=await safe(fetch(“https://example.com"));
          if (!response.success) {
            console.error(response.error);
            return;
          }
          // 使用 response.data 做一些事情
          

          參考資料

          [1] 原文: https://betterprogramming.pub/typescript-with-go-rust-errors-no-try-catch-heresy-da0e43ce5f78

          [2] Mateusz Piorowski: https://medium.com/@mateuszpiorowski

          一篇文章Go設(shè)計(jì)模式(2)-面向?qū)ο蠓治雠c設(shè)計(jì)里講過,做設(shè)計(jì)最重要的是保留合適的擴(kuò)展點(diǎn)。如何才能設(shè)計(jì)出合適的擴(kuò)展點(diǎn)呢?

          這篇文章會講解一下經(jīng)典的設(shè)計(jì)原則。這些設(shè)計(jì)原則大家可能都聽過,但可能沒有想過為什么會提煉出這些原則,它們有什么作用。對內(nèi)一個設(shè)計(jì)原則,我會盡量找到一個實(shí)例,說明它的重要性。通過實(shí)例來感受原則,比起只看枯燥的文字有效的多。

          在這里需要說明一點(diǎn),設(shè)計(jì)原則是一種思想,設(shè)計(jì)模式是這種思想的具象化。所以當(dāng)我們真正領(lǐng)悟到這種思想后,設(shè)計(jì)的時候會事半功倍。

          本文要闡述的原則如下:

          1. 單一職責(zé)原則
          2. 開放-封閉原則
          3. 里氏替換原則
          4. 接口隔離原則
          5. 依賴倒轉(zhuǎn)原則
          6. 迪米特法則

          單一職責(zé)原則

          理解原則

          單一職責(zé)原則(SRP):一個類只負(fù)責(zé)完成一個職責(zé)或者功能。不要設(shè)計(jì)大而全的類,要設(shè)計(jì)粒度小、功能單一的類。單一職責(zé)原則是為了實(shí)現(xiàn)代碼高內(nèi)聚、低耦合,提高代碼的復(fù)用性、可讀性、可維護(hù)性。

          實(shí)施

          不同的應(yīng)用場景、不同階段的需求背景、不同的業(yè)務(wù)層面,對同一個類的職責(zé)是否單一,可能會有不同的判定結(jié)果。實(shí)際上,一些側(cè)面的判斷指標(biāo)更具有指導(dǎo)意義和可執(zhí)行性,比如,出現(xiàn)下面這些情況就有可能說明這類的設(shè)計(jì)不滿足單一職責(zé)原則:

          • 類中的代碼行數(shù)、函數(shù)或者屬性過多;
          • 類依賴的其他類過多,或者依賴類的其他類過多;
          • 私有方法過多;
          • 比較難給類起一個合適的名字;
          • 類中大量的方法都是集中操作類中的某幾個屬性。

          實(shí)例

          假設(shè)我們要做一個在手機(jī)上玩的俄羅斯方塊游戲,Game類可以設(shè)計(jì)如下:

          type Game struct {
             x int64
             y int64
          }
          func (game *Game) Show() {
             fmt.Println(game.x, game.y)
          }
          func (game *Game) Move() {
             game.x--
             game.y++
          }

          游戲的顯示和移動都放在類Game里。后面需求變更了,不但要在手機(jī)上顯示,還需要在電腦上顯示,而且還有兩人對戰(zhàn)模式,這些更改主要和顯示有關(guān)。

          這時最好將Show和Move拆分到兩個函數(shù),這樣不但可以復(fù)用Move的邏輯,而且今后無論如何更改Show,都不會影響Move所在的類。

          但因?yàn)橐婚_始Game職責(zé)不單一,整個系統(tǒng)中很多位置使用同一個Game變量調(diào)用Show和Move,對這些位置的改動和測試是十分浪費(fèi)時間的。

          開放-封閉原則

          理解原則

          對擴(kuò)展開放、修改關(guān)閉(OCP):添加一個新的功能,應(yīng)該是通過在已有代碼基礎(chǔ)上擴(kuò)展代碼(新增模塊、類、方法、屬性等),而非修改已有代碼(修改模塊、類、方法、屬性等)的方式來完成。

          • 第一點(diǎn),開閉原則并不是說完全杜絕修改,而是以最小的修改代碼的代價來完成新功能的開發(fā)。
          • 第二點(diǎn),同樣的代碼改動,在粗代碼粒度下,可能被認(rèn)定為“修改”;在細(xì)代碼粒度下,可能又被認(rèn)定為“擴(kuò)展”。

          實(shí)施

          我們要時刻具備擴(kuò)展意識、抽象意識、封裝意識。在寫代碼的時候,我們要多花點(diǎn)時間思考一下,這段代碼未來可能有哪些需求變更,如何設(shè)計(jì)代碼結(jié)構(gòu),事先留好擴(kuò)展點(diǎn),以便在未來需求變更的時候,在不改動代碼整體結(jié)構(gòu)、做到最小代碼改動的情況下,將新的代碼靈活地插入到擴(kuò)展點(diǎn)上。

          很多設(shè)計(jì)原則、設(shè)計(jì)思想、設(shè)計(jì)模式,都是以提高代碼的擴(kuò)展性為最終目的的。特別是23種經(jīng)典設(shè)計(jì)模式,大部分都是為了解決代碼的擴(kuò)展性問題而總結(jié)出來的,都是以開閉原則為指導(dǎo)原則的。最常用來提高代碼擴(kuò)展性的方法有:多態(tài)、依賴注入、基于接口而非實(shí)現(xiàn)編程,以及大部分的設(shè)計(jì)模式(比如,裝飾、策略、模板、職責(zé)鏈、狀態(tài))。

          實(shí)例

          假設(shè)我們要做一個API接口監(jiān)控告警,如果TPS或Error超過指定值,則根據(jù)不同的緊急情況通過不同方式(郵箱、電話)通知相關(guān)人員。根據(jù)Go設(shè)計(jì)模式(2)-面向?qū)ο蠓治雠c設(shè)計(jì)里講的方案,我們先找出類。

          業(yè)務(wù)實(shí)現(xiàn)流程為:

          1. 獲取異常指標(biāo)
          2. 獲取異常數(shù)據(jù),和異常指標(biāo)進(jìn)行比較
          3. 通知相關(guān)人員

          所以,我們可以設(shè)置三個類,AlertRules存放報警規(guī)則,Notification用來通知,Alert用來比較。

          //存儲報警規(guī)則
          type AlertRules struct {
          }
          
          func (alertRules *AlertRules) GetMaxTPS(api string) int64 {
             if api=="test" {
                return 10
             }
             return 100
          }
          func (alertRules *AlertRules) GetMaxError(api string) int64 {
             if api=="test" {
                return 10
             }
             return 100
          }
          
          const (
             SERVRE="SERVRE"
             URGENT="URGENT"
          )
          
          //通知類
          type Notification struct {
          }
          
          func (notification *Notification) Notify(notifyLevel string) bool {
             if notifyLevel==SERVRE {
                fmt.Println("打電話")
             } else if notifyLevel==URGENT {
                fmt.Println("發(fā)短信")
             } else {
                fmt.Println("發(fā)郵件")
             }
             return true
          }
          
          //檢查類
          type Alert struct {
             alertRules   *AlertRules
             notification *Notification
          }
          
          func CreateAlert(a *AlertRules, n *Notification) *Alert {
             return &Alert{
                alertRules:   a,
                notification: n,
             }
          }
          func (alert *Alert) Check(api string, tps int64, errCount int64) bool {
             if tps > alert.alertRules.GetMaxTPS(api) {
                alert.notification.Notify(URGENT)
             }
             if errCount > alert.alertRules.GetMaxError(api) {
                alert.notification.Notify(SERVRE)
             }
             return true
          }
          func main() {
             alert :=CreateAlert(new(AlertRules), new(Notification))
             alert.Check("test", 20, 20)
          }

          雖然程序比較簡陋,但是是面向?qū)ο蟮模夷芘堋?/span>

          對于這個需求,有很多可能的變動點(diǎn),最可能變的是增加新的報警指標(biāo)。現(xiàn)在新需求來了,如果每秒內(nèi)接口超時量超過指定值,也需要報警,我們需要怎么做?

          如果在原有代碼上修改,我們需要

          1. AlertRules上添加新的規(guī)則
          2. Check函數(shù)增加新的入?yún)imeoutCount
          3. Check函數(shù)中增加新的判斷邏輯if timeoutCount > alert.alertRules.GetMaxTimeoutCount(api) {
            alert.notification.Notify(SERVRE)
            }

          這會導(dǎo)致一些問題,一是Check可能在多個地方被引用,所以這些位置都需要進(jìn)行修改,二是更改了Check邏輯,需要重新做這部分的測試。如果說我們做第一版沒有預(yù)料到這些變化,但現(xiàn)在我們找到了可能的變更點(diǎn),我們是否有好的方案能夠做好擴(kuò)展,讓下次改動量最小?

          我們把Alert中Check做的事情拆散,放到對應(yīng)的類里,這些類都實(shí)現(xiàn)了AlertHandler接口。

          //優(yōu)化
          type ApiStatInfo struct {
             api          string
             tps          int64
             errCount     int64
             timeoutCount int64
          }
          
          type AlertHandler interface {
             Check(apiStatInfo ApiStatInfo) bool
          }
          
          type TPSAlertHandler struct {
             alertRules   *AlertRules
             notification *Notification
          }
          
          func CreateTPSAlertHandler(a *AlertRules, n *Notification) *TPSAlertHandler {
             return &TPSAlertHandler{
                alertRules:   a,
                notification: n,
             }
          }
          
          func (tPSAlertHandler *TPSAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
             if apiStatInfo.tps > tPSAlertHandler.alertRules.GetMaxTPS(apiStatInfo.api) {
                tPSAlertHandler.notification.Notify(URGENT)
             }
             return true
          }
          
          type ErrAlertHandler struct {
             alertRules   *AlertRules
             notification *Notification
          }
          
          func CreateErrAlertHandler(a *AlertRules, n *Notification) *ErrAlertHandler {
             return &ErrAlertHandler{
                alertRules:   a,
                notification: n,
             }
          }
          
          func (errAlertHandler *ErrAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
             if apiStatInfo.errCount > errAlertHandler.alertRules.GetMaxError(apiStatInfo.api) {
                errAlertHandler.notification.Notify(SERVRE)
             }
             return true
          }
          
          type TimeOutAlertHandler struct {
             alertRules   *AlertRules
             notification *Notification
          }
          
          func CreateTimeOutAlertHandler(a *AlertRules, n *Notification) *TimeOutAlertHandler {
             return &TimeOutAlertHandler{
                alertRules:   a,
                notification: n,
             }
          }
          
          func (timeOutAlertHandler *TimeOutAlertHandler) Check(apiStatInfo ApiStatInfo) bool {
             if apiStatInfo.timeoutCount > timeOutAlertHandler.alertRules.GetMaxTimeOut(apiStatInfo.api) {
                timeOutAlertHandler.notification.Notify(SERVRE)
             }
             return true
          }

          Alert類增加成員變量handlers []AlertHandler,并添加如下函數(shù)

          //版本2
          func (alert *Alert) AddHanler(alertHandler AlertHandler) {
             alert.handlers=append(alert.handlers, alertHandler)
          }
          func (alert *Alert) CheckNew(apiStatInfo ApiStatInfo) bool {
             for _, h :=range alert.handlers {
                h.Check(apiStatInfo)
             }
             return true
          }

          調(diào)用方式如下:

          func main() {
             alert :=CreateAlert(new(AlertRules), new(Notification))
             alert.Check("test", 20, 20)
             //版本2,alert其實(shí)已經(jīng)不需要有成員變量AlertRules和Notification了
             a :=new(AlertRules)
             n :=new(Notification)
             alert.AddHanler(CreateTPSAlertHandler(a, n))
             alert.AddHanler(CreateErrAlertHandler(a, n))
             alert.AddHanler(CreateTimeOutAlertHandler(a, n))
             apiStatInfo :=ApiStatInfo{
                api:          "test",
                timeoutCount: 20,
                errCount:     20,
                tps:          20,
             }
             alert.CheckNew(apiStatInfo)
          }

          這樣今后無論增加多少報警指標(biāo),只需要創(chuàng)建新的Handler類,放入到alert中即可。代碼改動量極小,而且不需要重復(fù)測試。

          系統(tǒng)還有許多改動點(diǎn),大家可以自己嘗試去改動一下,所有代碼位置:https://github.com/shidawuhen/asap/blob/master/controller/design/3principle.go

          里式替換原則

          理解原則

          里氏替換原則(LSP):子類對象能夠替換程序(program)中父類對象出現(xiàn)的任何地方,并且保證原來程序的邏輯行為(behavior)不變及正確性不被破壞。

          多態(tài)與里氏替換原則的區(qū)別:多態(tài)是面向?qū)ο缶幊痰囊淮筇匦裕彩敲嫦驅(qū)ο缶幊陶Z言的一種語法。它是一種代碼實(shí)現(xiàn)的思路。而里式替換是一種設(shè)計(jì)原則,是用來指導(dǎo)繼承關(guān)系中子類該如何設(shè)計(jì)的,子類的設(shè)計(jì)要保證在替換父類的時候,不改變原有程序的邏輯以及不破壞原有程序的正確性。

          實(shí)施

          里式替換原則不僅僅是說子類可以替換父類,它有更深層的含義。

          子類在設(shè)計(jì)的時候,要遵守父類的行為約定(或者叫協(xié)議)。父類定義了函數(shù)的行為約定,那子類可以改變函數(shù)的內(nèi)部實(shí)現(xiàn)邏輯,但不能改變函數(shù)原有的行為約定。這里的行為約定包括:函數(shù)聲明要實(shí)現(xiàn)的功能;對輸入、輸出、異常的約定;甚至包括注釋中所羅列的任何特殊說明。所以我們可以通過幾個點(diǎn)判斷是否違反里氏替換原則:

          • 子類違背父類聲明要實(shí)現(xiàn)的功能:如排序函數(shù),父類按照金額排序,子類按照時間排序
          • 子類違背父類對輸入、輸出、異常的約定
          • 子類違背父類注釋中所羅列的任何特殊說明

          實(shí)例

          里氏替換原則可以提高代碼可擴(kuò)展性。假設(shè)我們需要做一個發(fā)送信息的功能,最初只需要發(fā)送站內(nèi)信。

          type Message struct {
          }
          func (message *Message) Send() {
             fmt.Println("message send")
          }
          func LetDo(notify *Message) {
            notify.Send()
          }
          func main() {
            LetDo(new(Message))
          }

          實(shí)現(xiàn)完成后,許多地方都調(diào)用LetDo發(fā)送信息。后面想用SMS替換站內(nèi)信,處理起來就很麻煩了。所以最好的方案是使用里氏替換原則,絲毫不影響新的通知方法接入。

          //里氏替換原則
          type Notify interface {
            Send()
          }
          type Message struct {
          }
          
          func (message *Message) Send() {
            fmt.Println("message send")
          }
          
          type SMS struct {
          }
          
          func (sms *SMS) Send() {
            fmt.Println("sms send")
          }
          
          func LetDo(notify Notify) {
            notify.Send()
          }
          
          func main() {
            //里氏替換原則
            LetDo(new(Message))
          }

          接口隔離原則

          理解原則

          接口隔離原則(ISP):客戶端不應(yīng)該強(qiáng)迫依賴它不需要的接口

          接口隔離原則與單一職責(zé)原則的區(qū)別:單一職責(zé)原則針對的是模塊、類、接口的設(shè)計(jì)。接口隔離原則提供了一種判斷接口的職責(zé)是否單一的標(biāo)準(zhǔn):通過調(diào)用者如何使用接口來間接地判定。如果調(diào)用者只使用部分接口或接口的部分功能,那接口的設(shè)計(jì)就不夠職責(zé)單一。

          實(shí)施

          如果把“接口”理解為一組接口集合,可以是某個微服務(wù)的接口,也可以是某個類庫的接口等。如果部分接口只被部分調(diào)用者使用,我們就需要將這部分接口隔離出來,單獨(dú)給這部分調(diào)用者使用,而不強(qiáng)迫其他調(diào)用者也依賴這部分不會被用到的接口。如果把“接口”理解為單個API接口或函數(shù),部分調(diào)用者只需要函數(shù)中的部分功能,那我們就需要把函數(shù)拆分成粒度更細(xì)的多個函數(shù),讓調(diào)用者只依賴它需要的那個細(xì)粒度函數(shù)。如果把“接口”理解為OOP中的接口,也可以理解為面向?qū)ο缶幊陶Z言中的接口語法。那接口的設(shè)計(jì)要盡量單一,不要讓接口的實(shí)現(xiàn)類和調(diào)用者,依賴不需要的接口函數(shù)。

          實(shí)例

          假設(shè)項(xiàng)目用到三個外部系統(tǒng):Redis、MySQL、Kafka。其中Redis和Kafaka支持配置熱更新。MySQL和Redis有顯示監(jiān)控功能。對于這個需求,我們需要怎么設(shè)計(jì)接口?

          一種方式是將所有功能放到一個接口中,另一種方式是將這兩個功能放到不同的接口中。下面的代碼按照接口隔離原則編寫:

          //接口隔離原則
          type Updater interface {
             Update() bool
          }
          
          type Shower interface {
             Show() string
          }
          
          type RedisConfig struct {
          }
          
          func (redisConfig *RedisConfig) Connect() {
             fmt.Println("I am Redis")
          }
          
          func (redisConfig *RedisConfig) Update() bool {
             fmt.Println("Redis Update")
             return true
          }
          
          func (redisConfig *RedisConfig) Show() string {
             fmt.Println("Redis Show")
             return "Redis Show"
          }
          
          type MySQLConfig struct {
          }
          
          func (mySQLConfig *MySQLConfig) Connect() {
             fmt.Println("I am MySQL")
          }
          
          func (mySQLConfig *MySQLConfig) Show() string {
             fmt.Println("MySQL Show")
             return "MySQL Show"
          }
          
          type KafkaConfig struct {
          }
          
          func (kafkaConfig *KafkaConfig) Connect() {
             fmt.Println("I am Kafka")
          }
          
          func (kafkaConfig *KafkaConfig) Update() bool {
             fmt.Println("Kafka Update")
             return true
          }
          
          func ScheduleUpdater(updater Updater) bool {
             return updater.Update()
          }
          func ServerShow(shower Shower) string {
             return shower.Show()
          }
          
          func main() {
             //接口隔離原則
             fmt.Println("接口隔離原則")
             ScheduleUpdater(new(RedisConfig))
             ScheduleUpdater(new(KafkaConfig))
             ServerShow(new(RedisConfig))
             ServerShow(new(MySQLConfig))
          }

          這種方案比起將Update和Show放在一個interface中有如下好處:

          1. 不需要做無用功。MySQL不需要寫熱更新函數(shù),Kafka不需要寫監(jiān)控顯示函數(shù)
          2. 復(fù)用性、擴(kuò)展性好。如果接入新的系統(tǒng),只需要監(jiān)控顯示函數(shù),只需要實(shí)現(xiàn)Shower接口,就能復(fù)用ServerShow的功能。

          依賴倒轉(zhuǎn)原則

          理解原則

          依賴倒轉(zhuǎn)原則(DIP):高層模塊不要依賴低層模塊。高層模塊和低層模塊應(yīng)該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實(shí)現(xiàn)細(xì)節(jié)(details),具體實(shí)現(xiàn)細(xì)節(jié)(details)依賴抽象(abstractions)。

          實(shí)施

          在程序代碼中傳遞參數(shù)時或在關(guān)聯(lián)關(guān)系中,盡量引用層次高的抽象層類,即使用接口和抽象類進(jìn)行變量類型聲明、參數(shù)類型聲明、方法返回類型聲明,以及數(shù)據(jù)類型的轉(zhuǎn)換等,而不要用具體類來做這些事情。核心思想是:要面向接口編程,不要面向?qū)崿F(xiàn)編程。

          實(shí)踐

          這個可以直接用里式替換中的例子來講解。LetDo就使用了依賴倒轉(zhuǎn)原則,提高了代碼的擴(kuò)展性,可以靈活地替換依賴的類。

          迪米特法則

          理解原則

          迪米特法則(LOD):不該有直接依賴關(guān)系的類之間,不要有依賴;有依賴關(guān)系的類之間,盡量只依賴必要的接口

          實(shí)施

          迪米特法則主要用來實(shí)現(xiàn)高內(nèi)聚低耦合。

          高內(nèi)聚:就是指相近的功能應(yīng)該放到同一個類中,不相近的功能不要放到同一個類中

          松耦合:在代碼中,類與類之間的依賴關(guān)系簡單清晰

          減少類之間的耦合,讓類越獨(dú)立越好。每個類都應(yīng)該少了解系統(tǒng)的其他部分。一旦發(fā)生變化,需要了解這一變化的類就會比較少。

          實(shí)踐

          假設(shè)我們要做一個搜索引擎爬取網(wǎng)頁的功能,功能點(diǎn)為

          1. 發(fā)起請求
          2. 下載網(wǎng)頁
          3. 分析網(wǎng)頁

          所以我們設(shè)置三個類NetworkTransporter負(fù)責(zé)底層網(wǎng)絡(luò)、用于獲取數(shù)據(jù),HtmlDownloader下載網(wǎng)頁,Document用于分析網(wǎng)頁。下面是符合迪米特法則的代碼

          //迪米特法則
          type Transporter interface {
             Send(address string, data string) bool
          }
          type NetworkTransporter struct {
          }
          
          func (networkTransporter *NetworkTransporter) Send(address string, data string) bool {
             fmt.Println("NetworkTransporter Send")
             return true
          }
          
          type HtmlDownloader struct {
             transPorter Transporter
          }
          
          func CreateHtmlDownloader(t Transporter) *HtmlDownloader {
             return &HtmlDownloader{transPorter: t}
          }
          
          func (htmlDownloader *HtmlDownloader) DownloadHtml() string {
             htmlDownloader.transPorter.Send("123", "test")
             return "htmDownloader"
          }
          
          type Document struct {
             html string
          }
          
          func (document *Document) SetHtml(html string) {
             document.html=html
          }
          
          func (document *Document) Analyse() {
             fmt.Println("document analyse " + document.html)
          }
          
          func main() {
             //迪米特法則
             fmt.Println("迪米特法則")
             htmlDownloader :=CreateHtmlDownloader(new(NetworkTransporter))
             html :=htmlDownloader.DownloadHtml()
             doc :=new(Document)
             doc.SetHtml(html)
             doc.Analyse()
          }

          這種寫法可以對應(yīng)迪米特法則的兩部分

          1. 不該有直接依賴關(guān)系的類之間,不要有依賴。Document不需要依賴HtmlDownloader,Document作用是分析網(wǎng)頁,怎么得到網(wǎng)頁是不需要關(guān)心的。這樣做的好處是無論HtmlDownloader怎么變動,Document都不需要關(guān)心。
          2. 有依賴關(guān)系的類之間,盡量只依賴必要的接口。HtmlDownloader下載網(wǎng)頁必須依賴NetworkTransporter,此處使用接口是為將來如果有更好的底層網(wǎng)絡(luò)功能,可以迅速替換。當(dāng)然,此處有點(diǎn)過渡設(shè)計(jì)的感覺,主要為了契合一下迪米特法則。具體是否需要這么設(shè)計(jì),還是根據(jù)具體情況來判斷。

          總結(jié)

          終于寫完了這6個原則,不過對我的好處也很明顯,重新梳理知識結(jié)構(gòu),對原則的理解也更深了一步。宏觀上看,這些原則都是為了實(shí)現(xiàn)可復(fù)用、可擴(kuò)展、高內(nèi)聚、低耦合的目的。現(xiàn)在大家在掌握了Go面向?qū)ο笳Z法、如何做面向?qū)ο蠓治雠c設(shè)計(jì)、面向?qū)ο笤O(shè)計(jì)原則的基礎(chǔ)上,可以做一些面向?qū)ο蟮氖虑榱恕?/span>

          原則是道,設(shè)計(jì)模式是術(shù),后面會寫一些設(shè)計(jì)模式相關(guān)的內(nèi)容。

          本文所有代碼位置為:https://github.com/shidawuhen/asap/blob/master/controller/design/3principle.go

          資料

          1. 設(shè)計(jì)模式-golang實(shí)現(xiàn)之七大設(shè)計(jì)原則https://blog.csdn.net/liuyonglun/article/details/103768269
          2. 設(shè)計(jì)模式之美https://time.geekbang.org/column/intro/100039001

          最后

          大家如果喜歡我的文章,可以關(guān)注我的公眾號(程序員麻辣燙)

          我的個人博客為:https://shidawuhen.github.io/


          技術(shù)

          1. Go設(shè)計(jì)模式(2)-面向?qū)ο蠓治雠c設(shè)計(jì)
          2. 支付接入常規(guī)問題
          3. HTTP2.0基礎(chǔ)教程
          4. Go設(shè)計(jì)模式(1)
          5. MySQL開發(fā)規(guī)范
          6. HTTPS配置實(shí)戰(zhàn)
          7. Go通道實(shí)現(xiàn)原理
          8. Go定時器實(shí)現(xiàn)原理
          9. HTTPS連接過程
          10. 限流實(shí)現(xiàn)2
          11. 秒殺系統(tǒng)
          12. 分布式系統(tǒng)與一致性協(xié)議
          13. 微服務(wù)之服務(wù)框架和注冊中心
          14. Beego框架使用
          15. 淺談微服務(wù)
          16. TCP性能優(yōu)化
          17. 限流實(shí)現(xiàn)1
          18. Redis實(shí)現(xiàn)分布式鎖
          19. Golang源碼BUG追查
          20. 事務(wù)原子性、一致性、持久性的實(shí)現(xiàn)原理
          21. CDN請求過程詳解
          22. 常用緩存技巧
          23. 如何高效對接第三方支付
          24. Gin框架簡潔版
          25. InnoDB鎖與事務(wù)簡析
          26. 算法總結(jié)

          讀書筆記

          1. 原則
          2. 資治通鑒
          3. 敏捷革命
          4. 如何鍛煉自己的記憶力
          5. 簡單的邏輯學(xué)-讀后感
          6. 熱風(fēng)-讀后感
          7. 論語-讀后感
          8. 孫子兵法-讀后感

          思考

          1. 服務(wù)端團(tuán)隊(duì)假期值班方案
          2. 項(xiàng)目流程管理
          3. 對項(xiàng)目管理的一些看法
          4. 對產(chǎn)品經(jīng)理的一些思考
          5. 關(guān)于程序員職業(yè)發(fā)展的思考
          6. 關(guān)于代碼review的思考
          7. Markdown編輯器推薦-typora

          主站蜘蛛池模板: 日本香蕉一区二区三区| 国产精品一区电影| 亚洲国产一区二区a毛片| 天堂一区二区三区在线观看| 国产日韩精品一区二区在线观看 | 免费无码一区二区| 好看的电影网站亚洲一区| 亚洲欧洲专线一区| 日韩欧国产精品一区综合无码| 国产无吗一区二区三区在线欢| 一区二区三区高清| 一区二区在线免费观看| 国产伦精品一区二区三区视频小说| 少妇激情av一区二区| 亚洲色无码一区二区三区| 一区二区三区美女视频| 国产剧情一区二区| 亚洲欧美一区二区三区日产| 中文字幕人妻丝袜乱一区三区| 竹菊影视欧美日韩一区二区三区四区五区 | 国产精品无码一区二区在线观一| 亚洲av无码片区一区二区三区| 久久精品国产亚洲一区二区| 日韩一区二区三区视频| 国产在线精品一区在线观看| 91视频国产一区| 国精产品999一区二区三区有限 | 中文字幕一区二区三区在线观看| 亚洲av鲁丝一区二区三区| 国产一区二区三区免费观看在线 | 濑亚美莉在线视频一区| 国产精品区一区二区三| 国产一区二区免费在线| 精品3d动漫视频一区在线观看| 老湿机一区午夜精品免费福利| 波多野结衣中文一区二区免费| 日韩有码一区二区| 国精品无码一区二区三区在线| 成人影片一区免费观看| 久久一区二区精品| 亚洲爆乳精品无码一区二区 |