Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 国产91在线|亚洲,国产精品久久久久久久久岛,欧美激情第二页

          整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

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

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

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

          如今,谷歌仍在繼續投資該語言,最新的穩定版本是 1.22.5。

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

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

          1、易于學習

          Go 的設計者特意打造了一種易于學習的語言,沒有太多復雜的功能或特異之處。

          喜歡的點在于:對于新程序員和團隊成員來說,更簡單的語言更容易學習和掌握。由于老程序員可以很快學會 Go 的新技巧,因此項目人員配備也更容易。不僅如此,相關代碼通常也更容易閱讀。

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

          2、Go 不會偏袒任何一方

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

          喜歡的點在于:許多開發人員都稱贊 Go 的簡單性。Go 不需要他們掌握或保持數十種功能的專業知識才能熟練使用。

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

          3、基于 C 的語法

          Go 的設計團隊確實基于傳統 C 語言改進了一些缺陷,并簡化了一些細節,使其看起來和感覺更現代。但在大多數情況下,Go 完全繼承了始于 C 語言的傳統。

          喜歡的點在于:在 C 語言風格中成長起來的程序員會直觀地理解 Go 的大部分內容。他們將能夠非常快速地學習語法,并且可以花時間學習 Go 相較 C 或 Java 的一些改進之處。

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

          4、Go 的規則太多了

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

          喜歡的點在于:Go 的強慣用規則確保代碼更容易理解,團隊將減少對風格的爭論。

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

          5、Go 有額外的錯誤處理

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

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

          6、標準庫

          喜歡的點在于:當許多標準功能由默認庫處理時,大多數代碼更易于閱讀。因為沒有人會編寫自己的版本,或爭論哪個軟件包或第三方庫更好。

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

          7、可執行文件的大小

          Go 團隊的目標之一是讓部署 Go 程序變得更容易,他們通過將所有程序打包成一個可執行文件來實現這一目標。

          喜歡的點在于:磁盤空間很便宜。當安裝了不同版本的庫時,在陌生的位置部署代碼可能是一場噩夢。Go 開發人員只需構建一個可執行文件就可以節省大量時間。

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

          8、背靠“好爹”谷歌

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

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

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


          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

          先來了解一下我的背景吧。我是一名軟件開發人員,有大約十年的工作經驗,最初使用 PHP,后來逐漸轉向 JavaScript。

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

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

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

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

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

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

          讓我們從 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;
            }
            // 處理響應
          } catch (e) {
            // 處理錯誤
            return;
          }
          

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

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

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

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

          JavaScript 不知道;JavaScript 也不在乎。你應該知道。

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

          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;
          }
          

          沒有錯誤,沒有語法檢查,盡管這可能會導致你的應用程序崩潰。

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

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

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

          我們正在嘗試打開一個返回文件或錯誤的文件。你會經常看到這種情況,主要是因為我們知道哪些函數總是返回錯誤,你絕不會錯過任何一個。這是第一個將錯誤視為值的例子。你可以指定哪個函數可以返回錯誤值,然后返回錯誤值,分配錯誤值,檢查錯誤值,處理錯誤值。

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

          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。你可能已經猜到,Ok 包含一個值,而 Err 包含……沒錯,一個錯誤 :D。

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

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

          這里的總結是,Go 和 Rust 總是知道哪里可能會出錯。它們強迫你在錯誤出現的地方(大部分情況下)立即處理它。沒有隱藏的錯誤,不需要猜測,也不會因為意外的錯誤而導致應用程序崩潰。

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

          好了,是時候實話實說了;我撒了點小謊。我們無法讓 TypeScript 的錯誤像 Go / Rust 那樣工作。限制因素在于語言本身,它沒有合適的工具來做到這一點。

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

          從這里開始:

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

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

          另外......第二個謊言是,我們確實需要一些 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 創建了一個包裝器。” 是的,你說得沒錯;這只是一個包裝器,我們的 Safe 類型作為返回類型。但有時候,簡單的東西就是你所需要的。讓我們將它們與上面的例子結合起來。

          舊的(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) {
              // 處理網絡錯誤
              return;
            }
            // 處理響應
          } 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) {
            // 處理網絡錯誤
            return;
          }
          // 處理響應(body.data)
          

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

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

          但現在王牌來了,如果我們忘記檢查這個:

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

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

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

          以下也同樣適用:

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

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

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

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

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

          這是一個使用 SvelteKit FormAction 的真實例子:

          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;
          

          這里有幾點需要指出:

          • 我們的自定義函數 grpcSafe 可以幫助我們處理 gGRPC 回調。
          • createMetadata 內部返回 Safe,因此我們不需要對其進行封裝。
          • zod 庫使用相同的模式 :) 如果我們不進行 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設計模式(2)-面向對象分析與設計里講過,做設計最重要的是保留合適的擴展點。如何才能設計出合適的擴展點呢?

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

          在這里需要說明一點,設計原則是一種思想,設計模式是這種思想的具象化。所以當我們真正領悟到這種思想后,設計的時候會事半功倍。

          本文要闡述的原則如下:

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

          單一職責原則

          理解原則

          單一職責原則(SRP):一個類只負責完成一個職責或者功能。不要設計大而全的類,要設計粒度小、功能單一的類。單一職責原則是為了實現代碼高內聚、低耦合,提高代碼的復用性、可讀性、可維護性。

          實施

          不同的應用場景、不同階段的需求背景、不同的業務層面,對同一個類的職責是否單一,可能會有不同的判定結果。實際上,一些側面的判斷指標更具有指導意義和可執行性,比如,出現下面這些情況就有可能說明這類的設計不滿足單一職責原則:

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

          實例

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

          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里。后面需求變更了,不但要在手機上顯示,還需要在電腦上顯示,而且還有兩人對戰模式,這些更改主要和顯示有關。

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

          但因為一開始Game職責不單一,整個系統中很多位置使用同一個Game變量調用Show和Move,對這些位置的改動和測試是十分浪費時間的。

          開放-封閉原則

          理解原則

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

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

          實施

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

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

          實例

          假設我們要做一個API接口監控告警,如果TPS或Error超過指定值,則根據不同的緊急情況通過不同方式(郵箱、電話)通知相關人員。根據Go設計模式(2)-面向對象分析與設計里講的方案,我們先找出類。

          業務實現流程為:

          1. 獲取異常指標
          2. 獲取異常數據,和異常指標進行比較
          3. 通知相關人員

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

          //存儲報警規則
          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("發短信")
             } else {
                fmt.Println("發郵件")
             }
             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)
          }

          雖然程序比較簡陋,但是是面向對象的,而且能跑。

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

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

          1. AlertRules上添加新的規則
          2. Check函數增加新的入參timeoutCount
          3. Check函數中增加新的判斷邏輯if timeoutCount > alert.alertRules.GetMaxTimeoutCount(api) {
            alert.notification.Notify(SERVRE)
            }

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

          我們把Alert中Check做的事情拆散,放到對應的類里,這些類都實現了AlertHandler接口。

          //優化
          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,并添加如下函數

          //版本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
          }

          調用方式如下:

          func main() {
             alert :=CreateAlert(new(AlertRules), new(Notification))
             alert.Check("test", 20, 20)
             //版本2,alert其實已經不需要有成員變量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)
          }

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

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

          里式替換原則

          理解原則

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

          多態與里氏替換原則的區別:多態是面向對象編程的一大特性,也是面向對象編程語言的一種語法。它是一種代碼實現的思路。而里式替換是一種設計原則,是用來指導繼承關系中子類該如何設計的,子類的設計要保證在替換父類的時候,不改變原有程序的邏輯以及不破壞原有程序的正確性。

          實施

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

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

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

          實例

          里氏替換原則可以提高代碼可擴展性。假設我們需要做一個發送信息的功能,最初只需要發送站內信。

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

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

          //里氏替換原則
          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):客戶端不應該強迫依賴它不需要的接口

          接口隔離原則與單一職責原則的區別:單一職責原則針對的是模塊、類、接口的設計。接口隔離原則提供了一種判斷接口的職責是否單一的標準:通過調用者如何使用接口來間接地判定。如果調用者只使用部分接口或接口的部分功能,那接口的設計就不夠職責單一。

          實施

          如果把“接口”理解為一組接口集合,可以是某個微服務的接口,也可以是某個類庫的接口等。如果部分接口只被部分調用者使用,我們就需要將這部分接口隔離出來,單獨給這部分調用者使用,而不強迫其他調用者也依賴這部分不會被用到的接口。如果把“接口”理解為單個API接口或函數,部分調用者只需要函數中的部分功能,那我們就需要把函數拆分成粒度更細的多個函數,讓調用者只依賴它需要的那個細粒度函數。如果把“接口”理解為OOP中的接口,也可以理解為面向對象編程語言中的接口語法。那接口的設計要盡量單一,不要讓接口的實現類和調用者,依賴不需要的接口函數。

          實例

          假設項目用到三個外部系統:Redis、MySQL、Kafka。其中Redis和Kafaka支持配置熱更新。MySQL和Redis有顯示監控功能。對于這個需求,我們需要怎么設計接口?

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

          //接口隔離原則
          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不需要寫熱更新函數,Kafka不需要寫監控顯示函數
          2. 復用性、擴展性好。如果接入新的系統,只需要監控顯示函數,只需要實現Shower接口,就能復用ServerShow的功能。

          依賴倒轉原則

          理解原則

          依賴倒轉原則(DIP):高層模塊不要依賴低層模塊。高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實現細節(details),具體實現細節(details)依賴抽象(abstractions)。

          實施

          在程序代碼中傳遞參數時或在關聯關系中,盡量引用層次高的抽象層類,即使用接口和抽象類進行變量類型聲明、參數類型聲明、方法返回類型聲明,以及數據類型的轉換等,而不要用具體類來做這些事情。核心思想是:要面向接口編程,不要面向實現編程。

          實踐

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

          迪米特法則

          理解原則

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

          實施

          迪米特法則主要用來實現高內聚低耦合。

          高內聚:就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中

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

          減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的類就會比較少。

          實踐

          假設我們要做一個搜索引擎爬取網頁的功能,功能點為

          1. 發起請求
          2. 下載網頁
          3. 分析網頁

          所以我們設置三個類NetworkTransporter負責底層網絡、用于獲取數據,HtmlDownloader下載網頁,Document用于分析網頁。下面是符合迪米特法則的代碼

          //迪米特法則
          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()
          }

          這種寫法可以對應迪米特法則的兩部分

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

          總結

          終于寫完了這6個原則,不過對我的好處也很明顯,重新梳理知識結構,對原則的理解也更深了一步。宏觀上看,這些原則都是為了實現可復用、可擴展、高內聚、低耦合的目的。現在大家在掌握了Go面向對象語法、如何做面向對象分析與設計、面向對象設計原則的基礎上,可以做一些面向對象的事情了。

          原則是道,設計模式是術,后面會寫一些設計模式相關的內容。

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

          資料

          1. 設計模式-golang實現之七大設計原則https://blog.csdn.net/liuyonglun/article/details/103768269
          2. 設計模式之美https://time.geekbang.org/column/intro/100039001

          最后

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

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


          技術

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

          讀書筆記

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

          思考

          1. 服務端團隊假期值班方案
          2. 項目流程管理
          3. 對項目管理的一些看法
          4. 對產品經理的一些思考
          5. 關于程序員職業發展的思考
          6. 關于代碼review的思考
          7. Markdown編輯器推薦-typora

          主站蜘蛛池模板: 在线免费观看一区二区三区| 国产精品无码一区二区在线观| 无码av不卡一区二区三区| 午夜在线视频一区二区三区 | 日韩在线一区视频| 奇米精品一区二区三区在线观看| 成人丝袜激情一区二区| 久久亚洲国产精品一区二区| 日本一区二区高清不卡| 中文字幕人妻丝袜乱一区三区 | 3d动漫精品成人一区二区三| 精品永久久福利一区二区| av一区二区三区人妻少妇| 亚洲制服中文字幕第一区| а天堂中文最新一区二区三区| 国产一区二区在线视频| 日韩一区二区在线视频| 狠狠色成人一区二区三区| 国内精品一区二区三区东京| 中文字幕在线视频一区| 国产精品视频一区二区三区| 国产精品一区12p| 欧美亚洲精品一区二区| 亚洲国产精品一区二区第一页| 色老头在线一区二区三区| 一区二区三区观看免费中文视频在线播放| 无码人妻精品一区二区三区66 | 久久精品国内一区二区三区| 久久久久人妻精品一区三寸蜜桃| 国产伦精品一区二区| 国内精品无码一区二区三区| 亚洲一区二区三区在线网站| 中文字幕Av一区乱码| 国精产品一区二区三区糖心| 欲色影视天天一区二区三区色香欲| 国产精品亚洲午夜一区二区三区| 一区二区三区影院| 国产伦一区二区三区高清| 亚洲日本中文字幕一区二区三区| 国模一区二区三区| 一区二区三区视频免费观看 |