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 個原因,具體如下。
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ì)者向世界展示的所有聰明才智,而這是要付出高昂代價的。”
最初開發(fā)人員希望創(chuàng)建一種小型語言,為此他們犧牲了其他語言中許多受歡迎的功能。Go 是一種精簡的語言,即可以滿足用戶的需求,又省去了一些繁瑣。
喜歡的點(diǎn)在于:許多開發(fā)人員都稱贊 Go 的簡單性。Go 不需要他們掌握或保持?jǐn)?shù)十種功能的專業(yè)知識才能熟練使用。
討厭的點(diǎn)在于:每個人都有一些喜歡的功能和技巧,但 Go 很可能不提供這些功能和技巧。開發(fā)人員有時會抱怨,他們只需用 COBOL 或 Java 或其他喜歡的語言寫一行代碼,就可以完成在 Go 中可以完成的相同任務(wù)。
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 有很多讓人討厭的地方。
從一開始,Go 的創(chuàng)建者就希望不僅定義語法,還定義語言的大部分風(fēng)格和使用模式。
喜歡的點(diǎn)在于:Go 的強(qiáng)慣用規(guī)則確保代碼更容易理解,團(tuán)隊(duì)將減少對風(fēng)格的爭論。
討厭的點(diǎn)在于:所有這些額外的規(guī)則和慣例都像束縛。“程序員在生活中擁有一點(diǎn)自由有那么糟糕嗎?”
喜歡的點(diǎn)在于:Go 方法承認(rèn)錯誤存在,并鼓勵程序員制定處理錯誤的計(jì)劃。這就鼓勵程序員提前計(jì)劃,并建立起一種彈性,從而開發(fā)出更好的軟件。
討厭的點(diǎn)在于:不必要的錯誤處理會讓 Go 函數(shù)變得更長、更難理解。通常情況下,deep chain 中的每個函數(shù)都必須包含類似的代碼,這些代碼或多或少會執(zhí)行相同的操作,并產(chǎn)生相同的錯誤。其他語言(如 Java 或 Python)鼓勵程序員將錯誤 "throw" 到鏈上的特定代碼塊中,以 "catch" 它們,從而使代碼更簡潔。
喜歡的點(diǎn)在于:當(dāng)許多標(biāo)準(zhǔn)功能由默認(rèn)庫處理時,大多數(shù)代碼更易于閱讀。因?yàn)闆]有人會編寫自己的版本,或爭論哪個軟件包或第三方庫更好。
討厭的點(diǎn)在于:一些人認(rèn)為,競爭能更好的推動需求和創(chuàng)新。有些語言支持多個軟件包來處理相同的任務(wù),表明大家對此確實(shí)有著濃厚的興趣和豐富的文化。
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í)行速度的首要問題。
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 的枚舉,有兩個變量:Ok 和 Err。你可能已經(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)
是的,我們的新解決方案更長,但性能更好,原因如下:
但現(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)需要指出:
看起來是不是很簡潔?那就試試吧!也許它也非常適合你 :)
感謝閱讀。
附注:下面的代碼對比是不是看起來很像?
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ì)的時候會事半功倍。
本文要闡述的原則如下:
單一職責(zé)原則(SRP):一個類只負(fù)責(zé)完成一個職責(zé)或者功能。不要設(shè)計(jì)大而全的類,要設(shè)計(jì)粒度小、功能單一的類。單一職責(zé)原則是為了實(shí)現(xiàn)代碼高內(nèi)聚、低耦合,提高代碼的復(fù)用性、可讀性、可維護(hù)性。
不同的應(yīng)用場景、不同階段的需求背景、不同的業(yè)務(wù)層面,對同一個類的職責(zé)是否單一,可能會有不同的判定結(jié)果。實(shí)際上,一些側(cè)面的判斷指標(biāo)更具有指導(dǎo)意義和可執(zhí)行性,比如,出現(xiàn)下面這些情況就有可能說明這類的設(shè)計(jì)不滿足單一職責(zé)原則:
假設(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ò)展代碼(新增模塊、類、方法、屬性等),而非修改已有代碼(修改模塊、類、方法、屬性等)的方式來完成。
我們要時刻具備擴(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è)我們要做一個API接口監(jiān)控告警,如果TPS或Error超過指定值,則根據(jù)不同的緊急情況通過不同方式(郵箱、電話)通知相關(guān)人員。根據(jù)Go設(shè)計(jì)模式(2)-面向?qū)ο蠓治雠c設(shè)計(jì)里講的方案,我們先找出類。
業(yè)務(wù)實(shí)現(xià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)接口超時量超過指定值,也需要報警,我們需要怎么做?
如果在原有代碼上修改,我們需要
這會導(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è)計(jì)的時候,要遵守父類的行為約定(或者叫協(xié)議)。父類定義了函數(shù)的行為約定,那子類可以改變函數(shù)的內(nèi)部實(shí)現(xiàn)邏輯,但不能改變函數(shù)原有的行為約定。這里的行為約定包括:函數(shù)聲明要實(shí)現(xiàn)的功能;對輸入、輸出、異常的約定;甚至包括注釋中所羅列的任何特殊說明。所以我們可以通過幾個點(diǎn)判斷是否違反里氏替換原則:
里氏替換原則可以提高代碼可擴(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é)單一。
如果把“接口”理解為一組接口集合,可以是某個微服務(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è)項(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中有如下好處:
依賴倒轉(zhuǎn)原則(DIP):高層模塊不要依賴低層模塊。高層模塊和低層模塊應(yīng)該通過抽象(abstractions)來互相依賴。除此之外,抽象(abstractions)不要依賴具體實(shí)現(xiàn)細(xì)節(jié)(details),具體實(shí)現(xiàn)細(xì)節(jié)(details)依賴抽象(abstractions)。
在程序代碼中傳遞參數(shù)時或在關(guān)聯(lián)關(guān)系中,盡量引用層次高的抽象層類,即使用接口和抽象類進(jìn)行變量類型聲明、參數(shù)類型聲明、方法返回類型聲明,以及數(shù)據(jù)類型的轉(zhuǎn)換等,而不要用具體類來做這些事情。核心思想是:要面向接口編程,不要面向?qū)崿F(xiàn)編程。
這個可以直接用里式替換中的例子來講解。LetDo就使用了依賴倒轉(zhuǎn)原則,提高了代碼的擴(kuò)展性,可以靈活地替換依賴的類。
迪米特法則(LOD):不該有直接依賴關(guān)系的類之間,不要有依賴;有依賴關(guān)系的類之間,盡量只依賴必要的接口
迪米特法則主要用來實(shí)現(xiàn)高內(nèi)聚低耦合。
高內(nèi)聚:就是指相近的功能應(yīng)該放到同一個類中,不相近的功能不要放到同一個類中
松耦合:在代碼中,類與類之間的依賴關(guān)系簡單清晰
減少類之間的耦合,讓類越獨(dú)立越好。每個類都應(yīng)該少了解系統(tǒng)的其他部分。一旦發(fā)生變化,需要了解這一變化的類就會比較少。
假設(shè)我們要做一個搜索引擎爬取網(wǎng)頁的功能,功能點(diǎn)為
所以我們設(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)迪米特法則的兩部分
終于寫完了這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
大家如果喜歡我的文章,可以關(guān)注我的公眾號(程序員麻辣燙)
我的個人博客為:https://shidawuhen.github.io/
技術(shù)
讀書筆記
思考
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。