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
o (Golang) 是 Google 開發的一種編譯型、并發型,并具有垃圾回收功能的編程語言,于 2009 年 11 月正式宣布推出成為開源項目,2012 年發布 1.0 版本。
如今,谷歌仍在繼續投資該語言,最新的穩定版本是 1.22.5。
在最新的 TIOBE 7 月榜單中,Go 排名第七。與其他所有編程語言一樣,有人喜歡 Go 語言也有人討厭,同樣的功能既會帶來詆毀也會帶來贊美。
InfoWorld 撰稿分析了開發人員喜歡或討厭 Go 語言的 8 個原因,具體如下。
Go 的設計者特意打造了一種易于學習的語言,沒有太多復雜的功能或特異之處。
喜歡的點在于:對于新程序員和團隊成員來說,更簡單的語言更容易學習和掌握。由于老程序員可以很快學會 Go 的新技巧,因此項目人員配備也更容易。不僅如此,相關代碼通常也更容易閱讀。
討厭的點在于:太過簡單反而束縛了手腳。“一個女巫會選擇一本簡略的咒語書嗎?四分衛會選擇只有幾個戰術的戰術書嗎?一些程序員認為,用 Go 編程就像一只手被綁在背后。這種語言缺乏其他語言設計者向世界展示的所有聰明才智,而這是要付出高昂代價的。”
最初開發人員希望創建一種小型語言,為此他們犧牲了其他語言中許多受歡迎的功能。Go 是一種精簡的語言,即可以滿足用戶的需求,又省去了一些繁瑣。
喜歡的點在于:許多開發人員都稱贊 Go 的簡單性。Go 不需要他們掌握或保持數十種功能的專業知識才能熟練使用。
討厭的點在于:每個人都有一些喜歡的功能和技巧,但 Go 很可能不提供這些功能和技巧。開發人員有時會抱怨,他們只需用 COBOL 或 Java 或其他喜歡的語言寫一行代碼,就可以完成在 Go 中可以完成的相同任務。
Go 的設計團隊確實基于傳統 C 語言改進了一些缺陷,并簡化了一些細節,使其看起來和感覺更現代。但在大多數情況下,Go 完全繼承了始于 C 語言的傳統。
喜歡的點在于:在 C 語言風格中成長起來的程序員會直觀地理解 Go 的大部分內容。他們將能夠非常快速地學習語法,并且可以花時間學習 Go 相較 C 或 Java 的一些改進之處。
討厭的點在于:很多方面,Python 的設計都是與 C 截然相反的。對于喜歡 Python 方法的人而言,會覺得 Go 有很多讓人討厭的地方。
從一開始,Go 的創建者就希望不僅定義語法,還定義語言的大部分風格和使用模式。
喜歡的點在于:Go 的強慣用規則確保代碼更容易理解,團隊將減少對風格的爭論。
討厭的點在于:所有這些額外的規則和慣例都像束縛。“程序員在生活中擁有一點自由有那么糟糕嗎?”
喜歡的點在于:Go 方法承認錯誤存在,并鼓勵程序員制定處理錯誤的計劃。這就鼓勵程序員提前計劃,并建立起一種彈性,從而開發出更好的軟件。
討厭的點在于:不必要的錯誤處理會讓 Go 函數變得更長、更難理解。通常情況下,deep chain 中的每個函數都必須包含類似的代碼,這些代碼或多或少會執行相同的操作,并產生相同的錯誤。其他語言(如 Java 或 Python)鼓勵程序員將錯誤 "throw" 到鏈上的特定代碼塊中,以 "catch" 它們,從而使代碼更簡潔。
喜歡的點在于:當許多標準功能由默認庫處理時,大多數代碼更易于閱讀。因為沒有人會編寫自己的版本,或爭論哪個軟件包或第三方庫更好。
討厭的點在于:一些人認為,競爭能更好的推動需求和創新。有些語言支持多個軟件包來處理相同的任務,表明大家對此確實有著濃厚的興趣和豐富的文化。
Go 團隊的目標之一是讓部署 Go 程序變得更容易,他們通過將所有程序打包成一個可執行文件來實現這一目標。
喜歡的點在于:磁盤空間很便宜。當安裝了不同版本的庫時,在陌生的位置部署代碼可能是一場噩夢。Go 開發人員只需構建一個可執行文件就可以節省大量時間。
討厭的點在于:我的磁盤上有多少份 Go 庫?如果我有 100 個程序,那就意味著 100 份。在某種程度上,效率是一個考慮因素。沒錯,磁盤空間比以往任何時候都便宜,但內存帶寬和緩存仍然是影響執行速度的首要問題。
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 的枚舉,有兩個變量:Ok 和 Err。你可能已經猜到,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)
是的,我們的新解決方案更長,但性能更好,原因如下:
但現在王牌來了,如果我們忘記檢查這個:
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;
這里有幾點需要指出:
看起來是不是很簡潔?那就試試吧!也許它也非常適合你 :)
感謝閱讀。
附注:下面的代碼對比是不是看起來很像?
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)-面向對象分析與設計里講過,做設計最重要的是保留合適的擴展點。如何才能設計出合適的擴展點呢?
這篇文章會講解一下經典的設計原則。這些設計原則大家可能都聽過,但可能沒有想過為什么會提煉出這些原則,它們有什么作用。對內一個設計原則,我會盡量找到一個實例,說明它的重要性。通過實例來感受原則,比起只看枯燥的文字有效的多。
在這里需要說明一點,設計原則是一種思想,設計模式是這種思想的具象化。所以當我們真正領悟到這種思想后,設計的時候會事半功倍。
本文要闡述的原則如下:
單一職責原則(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)-面向對象分析與設計里講的方案,我們先找出類。
業務實現流程為:
所以,我們可以設置三個類,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)
}
雖然程序比較簡陋,但是是面向對象的,而且能跑。
對于這個需求,有很多可能的變動點,最可能變的是增加新的報警指標。現在新需求來了,如果每秒內接口超時量超過指定值,也需要報警,我們需要怎么做?
如果在原有代碼上修改,我們需要
這會導致一些問題,一是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中有如下好處:
依賴倒轉原則(DIP):高層模塊不要依賴低層模塊。高層模塊和低層模塊應該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實現細節(details),具體實現細節(details)依賴抽象(abstractions)。
在程序代碼中傳遞參數時或在關聯關系中,盡量引用層次高的抽象層類,即使用接口和抽象類進行變量類型聲明、參數類型聲明、方法返回類型聲明,以及數據類型的轉換等,而不要用具體類來做這些事情。核心思想是:要面向接口編程,不要面向實現編程。
這個可以直接用里式替換中的例子來講解。LetDo就使用了依賴倒轉原則,提高了代碼的擴展性,可以靈活地替換依賴的類。
迪米特法則(LOD):不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量只依賴必要的接口
迪米特法則主要用來實現高內聚低耦合。
高內聚:就是指相近的功能應該放到同一個類中,不相近的功能不要放到同一個類中
松耦合:在代碼中,類與類之間的依賴關系簡單清晰
減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的類就會比較少。
假設我們要做一個搜索引擎爬取網頁的功能,功能點為
所以我們設置三個類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()
}
這種寫法可以對應迪米特法則的兩部分
終于寫完了這6個原則,不過對我的好處也很明顯,重新梳理知識結構,對原則的理解也更深了一步。宏觀上看,這些原則都是為了實現可復用、可擴展、高內聚、低耦合的目的。現在大家在掌握了Go面向對象語法、如何做面向對象分析與設計、面向對象設計原則的基礎上,可以做一些面向對象的事情了。
原則是道,設計模式是術,后面會寫一些設計模式相關的內容。
本文所有代碼位置為:https://github.com/shidawuhen/asap/blob/master/controller/design/3principle.go
大家如果喜歡我的文章,可以關注我的公眾號(程序員麻辣燙)
我的個人博客為:https://shidawuhen.github.io/
技術
讀書筆記
思考
*請認真填寫需求信息,我們會在24小時內與您取得聯系。