文章目錄
- 1. 六大原則
- 1.1 單一職責(Single Responsibility Principle,簡稱是SRP)
- 1.2 開閉原則(Open Closed Principle,簡稱是OCP)
- 1.3 里氏替換原則(Liskov Substitution Principle,簡稱LSP)
- 1.4 依賴倒置原則(Dependence Inversion Principle,簡稱DIP)
- 1.5 接口隔離原則(Interface Segregation Principle,簡稱ISP)
- 1.6 迪米特法則(Law Of Demeter,簡稱LOD)
- 1.7 總結
- 2. 各種設計模式瞭解(下個章節講解部分常用的)
- 3. 新知識擴展
- 4. 推薦資料
工欲善其事,必先利其器。 學習技術,貴“恆”,重“精”,忌“浮”。 切不可“這山望着那山高”
先“專心學精一門語言,然後對其它的語言便可融會貫通”
1. 六大原則
六大原則分別爲:單一職責,里氏替換原則,依賴倒置原則,接口隔離原則,迪米特法則,開閉原則.
單一職責原則 告訴我們實現類要職責單一;
里氏替換原則 告訴我們不要破壞繼承體系;
依賴倒置原則 告訴我們要面向接口編程;
接口隔離原則 告訴我們在設計接口的時候要精簡單一;
迪米特法則 告訴我們要降低耦合;
而開閉原則是總綱,開閉原則 告訴我們要對擴展開放,對修改關閉。
下面細細品味一下六大原則
1.1 單一職責(Single Responsibility Principle,簡稱是SRP)
就一個類而言,應該僅有一個引起它變化的原因;
把各個功能獨立出來,讓它們滿足單一職責原則;
舉個栗子:
// 可以看出來,這裏的接口設計的有問題,工作Work 和 生活Lift 沒有分開,這是一個嚴重的錯誤;
// 接口裏展示了兩個職責,工作 與 生活;
// 接口設計的一團糟,我們下面修改下;
public interface Human {
public void work相關1();
public void work相關2();
public void work相關3();
public void work相關4();
... ...
public void life相關1();
public void life相關2();
public void life相關3();
public void life相關4();
... ...
}
修改後的栗子,將 Human 的 工作 抽取爲 Work,生活抽取爲 Life;
當然除了在類中,函數的定義也可以儘量的單一職責,顆粒度多細,取決於實際的場景;
比如 初始化數據 initAllDatas,初始化界面 initAllViews 等等
// Activity onCreate demo 處理演示
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
loginTimes = -1;
Bundle bundle = getIntent().getExtras();
String strEmail = bundle.getString(AppConstants.Email);
etEmail = (EditText) f indViewById(R.id.email);
etEmail.setText(strEmail);
etPassword = (EditText) f indViewById(R.id.password); // 登錄事件
Button btnLogin = (Button) findViewById(R.id.sign_in_button);
btnLogin.setOnClickListener(this);
// 獲取2個MobileAPI,獲取天氣數據,獲取城市數據
loadWeatherData();
loadCityData();
}
// 更改後的代碼效果
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initVariables();
initViews(savedInstanceState);
loadData();
}
注意:
單一職責原則提出了一個編寫程序的標準,用“職責”或“變化原因”來衡量接口或類設計得是否有優良,但是“職責”和“變化原因”都是不可度量的,因項目而異,因環境而異。
避免設計過度:
導致於類/方法/接口過多,反而不好管理,當然也要避免違背。
遵循單一職責原的優點:
-
提高類的可維護性,可讀寫性;可以降低類的複雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;代碼少了,不復雜,可讀性也就好了,哈哈哈。
-
提高系統的可維護性;系統由類組成的,每個類的可維護性高,相對來講整個系統的可維護性就高。可讀性提高了,當然就容易維護了,嘿嘿。
-
降低變更的風險,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。一個類的職責越多,變更的可能性就更大,變更帶來的風險也就越大,
1.2 開閉原則(Open Closed Principle,簡稱是OCP)
用抽象構建框架,用實現擴展細節(抽象化是開閉原則的關鍵)
開閉原則的核心是:對擴展開放,對修改關閉
舉個栗子:
// 錯誤的示範,緩存的栗子
public class OpenCloseTest {
// 這裏存在的問題就是,如果有一天又增加其它緩存方式,
// 又要修改 OpenCloseTest 的loadImage代碼
// 整個loadImage 不僅越來越複雜,脆弱,也會越來越臃腫,可擴展性也很差
// 這裏就違背了開閉原則,擴展不是開放的,修改不是封閉的
public void loadImage(String type) {
if ("內存緩存".equals(type)) { // 內存緩存
... ...
} else if ("硬盤緩存".equals(type)) { // 硬盤緩存
... ...
} else { // 雙緩存
... ...
}
}
}
// 修改後的栗子 1. 增加抽象
public interface ICache {
public Bitmap get(String url);
}
// 內存緩存
public class MemoryCache implements ICache {
}
// 硬盤緩存
public class DiskCache implements ICache {
}
// ... ...
public class OpenCloseTest {
// 外部傳入各種緩存,可擴展性,靈活性高;
// 符合開閉原則,擴展是開放的,修改是封閉的
public void loadImage(ICache cache) {
Bitmap b = cache.get("http://test");
}
}
注意:
開閉原則指導我們,當軟件需要變化時,應該儘量通過擴展的方式來實現變化,而不是通過修改己有的代碼來實現。
這裏的“應該儘量”4個字說明 OCP 原則並不是說絕對不可以修改原始類的。
當我們嗅到原來的代碼“腐化氣味”時,應該儘早地重構,以便使代碼恢復到正常的“進化”過程,而不是通過繼承等方式添加新的實現,這會導致類型的膨脹以及歷史遺留代碼的冗餘。
避免設計過度:
切忌到處都抽象,會導致系統過度設計,過度複雜。這反而是不利於系統的維護。完全的開閉原則是不可能實現的,所以請保持簡單設計,在需要的時候做符合開閉原則的設計。
1.3 里氏替換原則(Liskov Substitution Principle,簡稱LSP)
子類可以擴展父類的功能,但不能改變父類原有的功能;
說白了 里氏替換原則其實就是爲“良好的繼承”制定一些規範;
里氏替換原則是實現開閉原則的重要方式之一;
注意幾點:
-
(關鍵點)子類可以實現父類的抽象方法,但儘量不要覆蓋父類的非抽象方法。
-
子類中可以增加自己特有的方法。
-
當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。(比如: 父類參數 T,子類 參數 S,要求 S 大於 T。要麼同一個類型,要麼 T 是 S 的子類;比如 父類參數 HashMap,子類就是 Map)
-
當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。(比如:父類 返回值爲類型T,子類返回值爲S,要求S必須小於等於T。要麼 S 和 T 是同一個類型,要麼 S 是 T 的子類;如父類要求返回List,那麼子類就應該返回List的實現ArrayList,父類是採用泛型,那麼子類則不能採用泛型,而是具體的返回)
舉個栗子:
public interfaceclass I {
public List testMapRes2();
}
// 父類
public class A implements I {
public int add(int num1, int num2) {
return num1 + num2;
}
// 父類 輸入參數是 HashMap 類型
public String testMap(HashMap map) {
return "父類 A testMap 函數 測試>>>";
}
// 實現父類的抽象方法
@Override
public ArrayList testMapRes2() { // 方法的返回值 ArrayList 比父類的更嚴格
return null;
}
}
// 子類 繼承 父類A,覆蓋了父類的非抽象方法
public class B extends A {
// 錯誤的方法!!!
@Override
public int add(int num1, int num2) {
return num1 - num2;
}
// 子類的輸入參數是 Map 類型
// 子類的輸入參數類型的範圍擴大了
// @Override 是失效的,因爲現在是 重載(Overload)
public String testMap(Map map/*子類的形參比父類的更寬鬆*/) {
return "子類 B testMap 函數 測試<<<";
}
// 添加自己的特有的方法
... ...
}
... ...
public static void main(... ...
B b = new B();
// 結果爲5,覆蓋重寫父類的非抽象方法,導致計算結果有誤
// 可以實現父類的抽象方法,但是不要覆蓋父類的非抽象方法.
b.add(10, 5);
// 結果爲 "父類 A testMap 函數 測試>>>" OK,正確的.
// 如果反過來,父類 Map,子類HashMap,子類被執行,可能會導致業務邏輯混亂,因爲父類實現好的方法,一般是繼承使用就好,.
// 所以子類中方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆
HashMap map = new HashMap();
b.testMap(map);
// 實現I的抽象方法
A a = new A();
a.testMapRes2();
注意:
- 父類設計爲抽象或者接口,讓子類繼續父類或實現父接口,並實現在父類中聲明的方法;
- 儘量避免子類重寫父類的非抽象方法,可以有效降低代碼出錯的可能性;
繼承的優缺點:
優點:
1. 提高代碼的重用性,子類擁有父類的方法和屬性;
2. 提高代碼的可擴展性,子類可形似於父類,但異於父類,保留自我的特性;
缺點:
1. 繼承是侵入性的,只要繼承就必須擁有父類的所有方法和屬性,在一定程度上約束了子類,降低了代碼的靈活性;
2. 增加了耦合,當父類的常量、變量或者方法被修改了,需要考慮子類的修改,所以一旦父類有了變動,很可能會造成非常糟糕的結果,要重構大量的代碼。
比較通用的做法是(只是建議):
原來的父類和子類都繼承一個更通俗的基類,原有的繼承關係去掉,採用依賴、聚合,組合等關係代替。
遵守里氏替換原則,在一定程度上增強程序的健壯性
1.4 依賴倒置原則(Dependence Inversion Principle,簡稱DIP)
高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。
依賴倒置原則的核心思想是面向接口編程,主要是實現解耦;
- 高層模塊不應該依賴底層模塊(具體實現細節),二者都應該依賴其抽象(抽象類或接口)
- 抽象不應該依賴細節
- 細節應該依賴於抽象
這麼去理解上面的這幾句話?
抽象:Java代碼中,抽象就是指 接口(interface) 或者 抽象類(abstract)
高層模塊:負責複雜的業務邏輯
底層模塊:具體實現細節,
舉個栗子
問題由來:類A直接依賴類B,假如要將類A改爲依賴類C,則必須通過修改類A的代碼來達成。這種場景下,類A一般是高層模塊,負責複雜的業務邏輯;類B和類C是低層模塊,負責基本的原子操作;假如修改類A,會給程序帶來不必要的風險。
class B { // 類B和類C是低層模塊,負責基本的原子操作
public String getTestContent() {
return "獲取A測試內容";
}
}
class C { // 類B和類C是低層模塊,負責基本的原子操作
public String getTestContent() {
return "獲取C測試內容";
}
}
// 類A直接依賴類B
class A { // 類A一般是高層模塊,負責複雜的業務邏輯
// 假如要將類A改爲依賴類C,則必須通過修改類A的代碼來達成。
// 假如修改類A,會給程序帶來不必要的風險。
public void test(B b) {
System.out.println("===依賴倒置測試===");
System.out.println(b.getTestContent());
}
}
public class Client {
public static void main(String[] args) {
A aa = new A();
aa.test(new B());
}
}
解決方案:將類A修改爲依賴接口I,類B和類C各自實現接口I,類A通過接口I間接與類B或者類C發生聯繫,則會大大降低修改類A的機率
interface I {
public String getTestContent();
}
class B implements I { // 類B和類C各自實現接口I
@Override
public String getTestContent() {
return "獲取A測試內容";
}
}
class C implements I { // 類B和類C各自實現接口I
... ...
// 類A直接依賴類B
class A { // 類A一般是高層模塊,負責複雜的業務邏輯
// 類A通過接口I間接與類B或者類C發生聯繫,
// 則會大大降低修改類A的機率
public void test(I i) {
System.out.println("===依賴倒置測試===");
System.out.println(i.getTestContent());
}
}
public class Client{
public static void main(String[] args){
A a = new A();
a.test(new A());
a.test(new C());
}
}
注意:
低層模塊儘量都要有抽象類或接口,或者兩者都有。
變量的聲明類型儘量是抽象類或接口。
使用繼承時遵循里氏替換原則。
1.5 接口隔離原則(Interface Segregation Principle,簡稱ISP)
類間的依賴關係應該建立在最小的接口上。接口隔離原則將非常龐大、臃腫的接口拆分成更小的和更具體的接口,這樣客戶將會只需要知道他們感興趣的方法。
接口隔離原則的目的是系統解開耦合,從而容易重構、更改和重新部署。
客戶端不應該依賴它不需要的接口。
Bob大叔(Robert C Martin)在21世紀早期將單一職責、開閉原則、里氏替換、接口隔離以 及依賴倒置(也稱爲依賴反轉) 5個原則定義爲SOLID 原則,作爲面向對象編程的5個基本原則。
舉個栗子:
// 錯誤示範,比如 定義了 改變世界 的接口
public interface 改變世界 {
void 寫代碼();
void 代碼審查();
void APK提測();
void APK發佈();
}
// 小公司 XXOO 剛開始的時候,一步到位
// 程序員A 實現了所有的方法,沒辦法,小公司,身兼多職
public 程序員A implements Travel {
}
'隨着公司發展壯大,流程越來越規範'
依賴它不需要的接口,只是極少實現了部分接口的方法;
'程序員-A' 發現它是需要 寫代碼 就可以了;
'某Leader-B' 只是 代碼審查;
'測試同學-C' 只是 APK測試;
'運營同學-D' 只是 APK發佈;
這就導致接口隔離不是很少,其它人還是實現了多餘的3個接口,這麼辦?
修改後:
interface 程序員相關 {
void 寫代碼();
... ...
}
interface 運營相關 {
void APK發佈();
... ...
}
... ...
這樣對應的人,只需要實現對應的接口就OK了
注意:
根據實際情況,具體業務具體分析,合理使用接口隔離原則;
在特定的場景下,如果很多類實現了同一個接口,並且都只實現了接口的極少部分方法,這時候很有可能就是接口隔離性不好,就要去分析能不能把方法拆分到不同的接口。
避免設計過度:
設計接口的粒度越小,系統越靈活是肯定的。但是過度把接口設計粒度鎖到很小,這樣會增加系統閱讀代碼的複雜度。接口設計儘量使其能完成一個特有的功能,而不能把一個功能再進行拆分,拆分出好多接口來,這就過度設計了,度很難把握。
1.6 迪米特法則(Law Of Demeter,簡稱LOD)
也稱爲最少知識原則(Least Knowledge Principle),
通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,類的內部如何實現與調用者或者依賴者沒關係,調用者或者依賴者只需要知道它需要的方法即可,其他的可一概不用管。
類與類之間的關係越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。
少暴露細節.
舉個栗子:
// 女朋友 讓你給 她 數星星
public class GirlFriend {
// GirlFriend 類 除了 和 My 這個朋友類 有交流,還和 Star 有了交流;
// 迪米特法則說是一個類只和朋友類交流,這裏是不對的!!!
public void commond(My my) {
List<Star> starList = new ArrayList();
for (int i=0;i<1000;i++) {
starList.add(new Star())
}
my.數星星(starList)
}
}
// 修改後的例子
// GirlFriend 避開了 對陌生類 star 的訪問,減少耦合;
public class GirlFriend {
public void commond(My my) {
my.數星星()
}
}
注意:
記住,類只和朋友交流,不要和陌生類交流,有危險,媽媽說的;
避免設計過度:
過分的使用迪米特原則,會產生大量這樣的中介和傳遞類,導致系統複雜度變大。
迪米特法則 的優點:
類之間解耦,弱耦合。
1.7 總結
寫的代碼 應該 保持 高可擴展性、高內聚、低耦合;
在經歷了各版本的變更之後依然保持清晰、靈活、穩定的系統架構。
雖然 實際情況下,會因爲時間,項目需求經常變更,場景的各種原因,但是我們還是朝這個方向努力,努力遵守六大原則 就是 讓代碼更穩定,靈活,清晰的 第一步。
六大原則是一把“雙刃劍”,使用得當是一把神兵利器,使用過度那麼就是自殺行爲。
遵守的差,點將會在小圓內部;
過渡遵守,點將會落在大圓外部;
設計1、設計2屬於良好的設計;
設計3、設計4設計雖然有些不足,但也基本可以接受;
設計5則嚴重不足,對各項原則都沒有很好的遵守;
設計6則遵守過渡了,
設計5和設計6都是迫切需要重構的設計。
2. 各種設計模式瞭解(下個章節講解部分常用的)
- 創建型:
- Factory Method(工廠方法)
- Abstract Factory(抽象工廠)
- Builder(建造者)
- Prototype(原型)
- Singleton(單例)
- 結構型:
- Adapter Class/Object(適配器)
- Bridge (橋接)
- Composite(組合)
- Decorator(裝飾) Android源碼中的ContextWrapper
- Facade(外觀)
- Flyweight(享元)
- Proxy(代理)
- 行爲型:
- Interpreter(解釋器)
- Template Method(模板方法)
- Chain of Responsibility(責任鏈) Android的事件分發機制
- Command(命令)
- Iterator(迭代器)
- Mediator(中介者)
- Memento(備忘錄) 類似遊戲的保存與恢復
- Observer(觀察者)
- Iterator(迭代器)
- Strategy(策略)
- Visitor(訪問者)
3. 新知識擴展
對象池模式
規格模式
僱工模式
黑板模式
空對象模式
4. 推薦資料
《設計模式之禪》,《大話設計模式》,《Android 源碼設計模式解析與實戰》,《圖解設計模式》
多閱讀下別人的源碼,比如 Android 的同學,可以看看 Glide,Retrofit。
http://linbinghe.com/2017/1646c9f3.html 談談23種設計模式在Android源碼及項目中的應用
http://blog.csdn.net/zhengzhb/article/details/7296944 設計模式六大原則(6):開閉原則
http://www.cnblogs.com/yanghuahui/p/3308487.html 用枚舉實現工廠方法模式更簡潔?
https://zhuanlan.zhihu.com/p/33607390 快速理解 設計模式六大原則
https://wenku.baidu.com/view/59684b4326d3240c844769eae009581b6bd9bd15.html 里氏替換原則