摘要:同時,在方法命名上也投入一精力,盡可能地使方法名保持簡單,它將幫助你在重構代碼時,更好的達到單一職責。
這是理解SOLID原則中,關于單一職責原則如何幫助我們編寫低耦合和高內聚的第二篇文章。單一職責原則是什么
之前的第一篇文章闡述了依賴倒置原則(DIP)能夠使我們編寫的代碼變得低耦合,同時具有很好的可測試性,接下來我們來簡單了解下單一職責原則的基本概念:
Every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class.每一個模塊或者類所對應的職責,應對應系統若干功能中的某個單一部分,同時關于該職責的封裝都應當通過這個類來完成。
往簡單來講:
A class or module should have one, and only one, reason to be changed.一個類或者模塊應當用于單一的,并且唯一的緣由被更改。
如果僅僅通過這兩句話去理解, 一個類或者模塊如果如果越簡單(具有單一職責),那么這個類或者模塊就越容易被更改是有一些困難的。為了便于我們理解整個概念,我們將分別從三個不同的角度來分析這句話,這三個角度是:
Single: 單一
Responsibility: 職責
Change: 改變
什么是單一Only one; not one of several.唯一的,而不是多個中的某個。
Synonyms: one, one only, sole, lone, solitary, isolated, by itself.
同義詞:一,僅有的一個,唯一,獨個,獨自存在的,孤立的,僅自己。
單一意味著某些工作是獨立的。比如,在類中,類方法僅完成某家獨立的事情,而不是兩件,如下:
class UserComponent { // 這是第一件事情,獲取用戶詳情數據 getUserInfo(id) { this.api.getUserInfo(id).then(saveToState) } // 這是第二件事情,渲染視圖的邏輯 render() { const { userInfo } = this.state; return} }
- Name: { userInfo.name }
- Surname: { userInfo.surname }
- Email: { userInfo.email }
看了上面的代碼,你可能很快就會聯想到,這些代碼基本存在于所有的React組件中。
確實,對于一些小型的項目或者演示型項目,這樣編寫代碼不會產生太大的問題。但是如果在大型或者復雜度很高的項目中,仍然按照這樣的風格,則是一件比較糟糕的事情,因為一個組件往往做了它本不應當做的事情(承擔了過多的職責)。
這樣會帶來什么壞處呢?比如對于以上的api服務,在將來的某天你做出了一些修改,增加了一些額外的邏輯,那么為了使代碼能夠正常工作,你至少需要修改項目中的兩個地方以適應這個修改,一處修改是在API服務中,而另一處則在你的組件中。如果進一步思考的,我們會發現,修改次數與在項目直接使用API服務的次數成正比,如果項目足夠復雜,足夠大,一處簡單的邏輯修改,就需要做出一次貫穿整個系統的適配工作。
那么我們如果避免這種情況的發生呢?很簡單,我們僅僅需要將關于用戶詳情數據的邏輯提升到調用層,在上面的例子中,我們應當使用React.component.prop來接受用戶詳情數據。這樣,UserComponent組件的工作不再與如何獲取用戶詳情數據的邏輯耦合,從而變得單一。
對于鑒別什么是單一,什么不是單一,有很多不同的方式。一般來說,只需要牢記,讓你的代碼盡可能的少的去了解它已經做的工作。(譯者注:我理解意思應當是,應當盡可能的讓已有的類或者方法變得簡單、輕量,不需要所有事情都親自為之)
總之,不要讓你的對象成為上帝對象。
A God Object aka an Object that knows everything and does everything.什么是職責上帝對象,一個知道一切事情,完成一切事情的對象。
In object-oriented programming, a God object is an object that knows too much or does too much. The God object is an example of an anti-pattern.
在面向對象編程中,上帝對象指一個了解太情或者做太多事情的對象。上帝對象是反模式的一個典型。
職責指軟件系統中,每一個指派給特定方法、類、包和模塊所完成的工作或者動作。
Too much responsibility leads to coupling.太多的職責導致耦合。
耦合性代表一個系統中某個部分對系統中另一個部分的了解程度。舉個例子,如果一段客戶端代碼在調用class A的過程中,必須要先了解有關class B的細節,那么我們說A和B耦合在了一起。通常來說,這是一件糟糕的事情。因為它會使針對系統本身的變更復雜化,同時會在長期越來越糟。
為了使一個系統到達適當的耦合度,我們需要在以下三個方面做出調整
組件的內聚性
如何測量每個組件的預期任務
組件如何專注于任務本身
低內聚性的組件在完成任務時,和它們本身的職責關聯并不緊密。比如,我們現在有一個User類,這個類中我們保存了一些基本信息:
class User { public age; public name; public slug; public email; }
對于屬性本身,如果對于每個屬性聲明一些getter或者setter方法是沒什么問題的。但是如果我們加一些別的方法,比如:
class User { public age; public name; public slug; public email; // 我們為什么要有以下這些方法? checkAge(); validateEmail(); slugifyName(); }
對于checkAge、validateEmail、slugifyName的職責,與Userclass本身關系并不緊密,因此就會這些方法就會使User的內聚性變低。
仔細思考的話,這些方法的職責和校驗和格式化用戶信息的關系更緊密,因此,它們應當從User中被抽離出來,放入到另一個獨立的UserFieldValidation類中,比如:
class User { public age; public name; public slug; public email; } class UserFieldValidation { checkAge(); validateEmail(); slugifyName(); }什么是變更
變更指對于已存在代碼的修改或者改變。
那么問題來了,什么原因迫使我們需要對源碼進行變更?從眾多過期的軟件系統的歷史數據的研究來看,大體有三方面原因促使我們需要作出變更:
增加新功能
修復缺陷或者bug
重構代碼以適配將來作出的變更
做為一個程序員,我們天天不都在做這三件事情嗎?讓我們來用一個例子完整的看一下什么是變更,比方說我們完成了一個組件,現在這個組件性能非常好,而且可讀性也非常好,也許是你整個職業生涯中寫的最好的一個組件了,所以我們給它一個炫酷的名字叫作SuperDuper(譯者注:這個名字的意思是超級大騙子)
class SuperDuper { makeThingsFastAndEasy() { // Super readable and efficient code } }
之后過了一段時間,在某一天,你的經理要求你增加一個新功能,比如說去調用別的class中的每個函數,從而可以使當前這個組件完成更多的工作。你決定將這個類以參數的形式傳入構造方法,并在你的方法調用它。
這個需求很簡單,只需要增加一行調用的代碼即可,然后你做了以下變更(增加新功能):
class SuperDuper { constructor(notDuper: NotSoDuper) { this.notDuper = notDuper } makeThingsFastAndEasy() { // Super readable and efficient code this.notDuper.invokeSomeMethod() } }
好了,之后你針對你做的變更代碼運行了單元測試,然后你突然發現這條簡單的代碼使100多條的測試用例失敗了。具體原因是因為在調用notDuper方法之前,你需要針對一些額外的業務邏輯增加條件判斷來決定是否調用它。
于是你針對這個問題又進行了一次變更(修復缺陷或者bug),或許還會針對一些別的邊界條件進行一些額外的修復和改動:
class SuperDuper { constructor(notDuper: NotSoDuper) { this.notDuper = notDuper } makeThingsFastAndEasy() { // Super readable and efficient code if (someCondition) { this.notDuper.invokeSomeMethod() } else { this.callInternalMethod() } } }
又過了一段時間,因為這個SuperDuper畢竟是你職業生涯完成的最棒的類,但是當前調用noDuper的方法實在是有點不夠逼格,于是你決定引入事件驅動的理念來達到不在SuperDuper內部直接調用noDuper方法的目的。
這次實際是對已經代碼的一次重構工作,你引入了事件驅動模型,并對已有的代碼做出了變更(重構代碼以適配將來作出的變更):
class SuperDuper { makeThingsFastAndEasy() { // Super readable and efficient code ... dispatcher.send(actionForTheNotDuper(payload)) // Send a signal } }
現在再來看我們的SuperDuper類,已經和最原始的樣子完全不一樣了,因為你必須針對新的需求、存在的缺陷和bug或者適配新的軟件架構而做出變更。
因此為了便于我們做出變更,在代碼的組織方式上,我們需要用心,這樣才會使我們在做出變更時更加容易。
如何才能使代碼貼近這些原則很簡單,只需要牢記,使代碼保持足夠簡單。
Gather together the things that change for the same reasons. Separate those things that change for different reasons.孤立變化將由于相同原因而做出改變的東西聚集在一起,將由于不同原因而做出改變的東西彼此分離。
對于所編寫的做出變更的代碼,你需要仔細的檢查它們,無論是從整體檢查,還是有邏輯的分而治之,都可以達到孤立變化的目的。你需要更多的了解你所編寫的代碼,比如,為什么這樣寫,代碼到底做了什么等等,并且,對于一些特別長的方法和類要格外關注。
Big is bad, small is good…追蹤依賴大即是壞,小即是好。
對于一個類,檢查它的構造方法是否包含了太多的參數,因為每一個參數都作為這個類的依賴存在,同時這些參數也擁有自身的依賴。如果可能的話,使用DI機制來動態的注入它們。
Use Dependency Injection追蹤方法參數使用依賴注入
對于一個方法,檢查它是否包含了太多參數,一般來講,一個方法的參數個數往往代表了其內部所實現的職能。
同時,在方法命名上也投入一精力,盡可能地使方法名保持簡單,它將幫助你在重構代碼時,更好的達到單一職責。長的函數名稱往往意味著其內部有糟糕的味道。
Name things descriptively盡早重構描述性命名。
盡可能早的重構代碼,當你看到一些代碼可以以更簡明的方式進行時,重構它。這將幫助你在項目進行的整個周期不斷的整理代碼以便于更好的重構。
Refactor to Design Patterns善于做出改變按設計模式重構代碼
最后,在需要做出改變時,果斷地去做。當然這些改變會使系統的耦合性更低,內聚性更高,而不是往相反的方向,這樣你的代碼會一直建立在這些原則之上。
Introduce change where it matters. Keep things simple but not simpler.譯者注在重要的地方介紹改變。保持事情的簡單性,但不是一味追求簡單。
單一職責原則其實在我們日常工作中經常會接觸到,比方說
我們經常會聽到DIY(dont repeat yourself)原則,其本身就是單一職責的一個縮影,為了達到DIY,對于代碼中的一些通用方法,我們經常會抽離到獨立的utils目錄甚至編寫為獨立的工具函數庫, 比如lodash和ramda等等
OAOO, 指Once And Only Once, 原則本身的含義可以自行搜索,實際工作中我們對于相同只能模塊的代碼應當盡可能去在抽象層合并它們,提供抽象類,之后通過繼承的方式來滿足不同的需求
我們都會很熟悉單例模式這個模式,但在使用時一定要小心,因為本質上單例模式與單一職責原則相悖,在實踐中一定要具體情況具體分析。同時也不要過度優化,就如同文章中最后一部分提及的,我們要保證一件事情的簡單性,但不是一味地為了簡單而簡單。
前端的技術棧中,redux對于數據流層的架構思想,便充分體現了單一職責原則的重要性,action作為對具體行為的抽象, store用來描述應用的狀態,reducer作為針對不同行為如何對store作出修改的抽象。
react中經常提及的木偶組件(dump component)其實和文章中第一部分的例子如出一轍
工廠模式和命令模式也一定程度體現了單一職責原則,前者對于作為生產者存在并不需要關心消費者如何消費對象實例,后者以命令的方式封裝功能本身就是單一職責原則的體現。
我能夠想到的就這么多,寫的比較亂,拋磚引玉,如有錯誤,還望指正。
關注公眾號 全棧101,只談技術,不談人生
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/107174.html
摘要:接口隔離原則是什么客戶端代碼不應當被迫依賴于它們不需要的方法。 這是理解SOLID原則,關于接口隔離原則如何幫助我們創建簡單的抽象接口,并使客戶端代與接口之間存在的更少的依賴關系。 接口隔離原則是什么 Clients should not be forced to depend on methods that they do not use.客戶端代碼不應當被迫依賴于它們不需要的方法。...
摘要:事件驅動模型對于一些復雜的事件驅動模型,比如拖拽,往往使用開閉原則會達到意想不到的效果。 這是理解SOLID原則,介紹什么是開閉原則以及它為什么能夠在對已有的軟件系統或者模塊提供新功能時,避免不必要的更改(重復勞動)。 開閉原則是什么 Software entities (classes, modules, functions, etc.) should be open for ext...
摘要:什么是里氏替換原則某個對象實例的子類實例應當可以在不影響程序正確性的基礎上替換它們。除了在編程語言層面,在前端實際工作中,你可能會聽到一個叫作的概念,這個概念我認為也是里氏替換原則的一直延伸。 這是理解SOLID原則,關于里氏替換原則為什么提倡我們面向抽象層編程而不是具體實現層,以及為什么這樣可以使代碼更具維護性和復用性。 什么是里氏替換原則 Objects should be rep...
摘要:設計原則梳理,參考核心技術與最佳實踐敏捷開發原則模式與實踐,文章面向對象設計的五大原則設計模式原則單一職責原則定義特性僅有一個引起類變化的原因一個類只承擔一項職責職責變化的原因避免相同的職責分散到不同的類,功能重復問題一個類承擔的職責過多, PHP設計原則梳理,參考《PHP核心技術與最佳實踐》、《敏捷開發原則、模式與實踐》,文章PHP面向對象設計的五大原則、設計模式原則SOLID 單一...
摘要:它是良好應用設計的大原則,包含單一責任原則開放封閉原則里氏替換原則接口分離原則依賴倒置原則讓我們通過代碼示例來深究下這五個原則。實探單一責任原則代表一個類有且僅有一個改變的原因,換言之,一個類的職責范疇是嚴謹明確的。 聲明:本文并非博主原創,而是來自對《Laravel 4 From Apprentice to Artisan》閱讀的翻譯和理解,當然也不是原汁原味的翻譯,能保證90%的原...
閱讀 2588·2021-11-24 09:38
閱讀 2615·2019-08-30 15:54
閱讀 934·2019-08-30 15:52
閱讀 1918·2019-08-30 15:44
閱讀 2725·2019-08-30 13:48
閱讀 778·2019-08-29 16:21
閱讀 1008·2019-08-29 14:03
閱讀 2223·2019-08-28 18:15