C# 多態的實現
封裝、繼承、多態,面向對象的三大特性,前兩項理解相對容易,但要理解多態,特別是深入的了解,對於初學者而言可能就會有一定困難了。我一直認為學習OO的最好方法就是結合實踐,封裝、繼承在實際工作中的應用隨處可見,但多態呢?也許未必,可能不經意間用到也不會把它跟“多態”這個詞對應起來。在此拋磚引玉,大家討論,個人能力有限,不足之處還請指正。
之前看到過類似的問題:如果面試時主考官要求你用一句話來描述多態,盡可能的精煉,你會怎么回答?當然答案有很多,每個人的理解和表達不盡相同,但我比較趨向這樣描述:通過繼承實現的不同對象調用相同的方法,表現出不同的行為,稱之為多態。
例1:
public class Animal { public virtual void Eat() { Console.WriteLine("Animal eat"); } } public class Cat : Animal { public override void Eat() { Console.WriteLine("Cat eat"); } } public class Dog : Animal { public override void Eat() { Console.WriteLine("Dog eat"); } } class Tester { static void Main(string[] args) { Animal[] animals = new Animal[3]; animals[0] = new Animal(); animals[1] = new Cat(); animals[2] = new Dog(); for (int i = 0; i < 3; i++) { animals[i].Eat(); } } }
輸出如下:
Animal eat...
Cat eat...
Dog eat...
在上面的例子中,通過繼承,使得Animal對象數組中的不同的對象,在調用Eat()方法時,表現出了不同的行為。
多態的實現看起來很簡單,要完全理解及靈活的運用c#的多態機制,也不是一件容易的事,有很多需要注意的地方。
1. new的用法
先看下面的例子。
例2:
public class Animal { public virtual void Eat() { Console.WriteLine("Animal eat"); } } public class Cat : Animal { public new void Eat() { Console.WriteLine("Cat eat"); } } class Tester { static void Main(string[] args) { Animal a = new Animal(); a.Eat(); Animal ac = new Cat(); ac.Eat(); Cat c = new Cat(); c.Eat(); } }
運行結果為:
Animal eat...
Animal eat...
Cat eat...
可以看出,當派生類Cat的Eat()方法使用new修飾時,Cat的對象轉換為Animal對象后,調用的是Animal類中的Eat()方法。其實可以理解為,使用new關鍵字后,使得Cat中的Eat()方法和Animal中的Eat()方法成為毫不相關的兩個方法,只是它們的名字碰巧相同而已。所以, Animal類中的Eat()方法不管用還是不用virtual修飾,也不管訪問權限如何,或者是沒有,都不會對Cat的Eat()方法產生什么影響(只是因為使用了new關鍵字,如果Cat類沒用從Animal類繼承Eat()方法,編譯器會輸出警告)。
我想這是設計者有意這么設計的,因為有時候我們就是要達到這種效果。嚴格的說,不能說通過使用new來實現多態,只能說在某些特定的時候碰巧實現了多態的效果。
2.override實現多態
真正的多態使用override來實現的。回過去看前面的例1,在基類Animal中將方法Eat()用virtual標記為虛擬方法,再在派生類Cat和Dog中用override對Eat()修飾,進行重寫,很簡單就實現了多態。需要注意的是,要對一個類中一個方法用override修飾,該類必須從父類中繼承了一個對應的用virtual修飾的虛擬方法,否則編譯器將報錯。
好像講得差不多了,還有一個問題,不知道你想沒有。就是多層繼承中又是怎樣實現多態的。比如類A是基類,有一個虛擬方法method()(virtual修飾),類B繼承自類A,並對method()進行重寫(override修飾),現在類C又繼承自類B,是不是可以繼續對method()進行重寫,並實現多態呢?看下面的例子。
例3:
public class Animal { public virtual void Eat() { Console.WriteLine("Animal eat"); } } public class Dog : Animal { public override void Eat() { Console.WriteLine("Dog eat"); } } public class WolfDog : Dog { public override void Eat() { Console.WriteLine("WolfDog eat"); } } class Tester { static void Main(string[] args) { Animal[] animals = new Animal[3]; animals[0] = new Animal(); animals[1] = new Dog(); animals[2] = new WolfDog(); for (int i = 0; i < 3; i++) { animals[i].Eat(); } } }
運行結果為:
Animal eat...
Dog eat...
WolfDog eat...
在上面的例子中類Dog繼承自類Animal,對方法Eat()進行了重寫,類WolfDog又繼承自Dog,再一次對Eat()方法進行了重寫,並很好地實現了多態。不管繼承了多少層,都可以在子類中對父類中已經重寫的方法繼續進行重寫,即如果父類方法用override修飾,如果子類繼承了該方法,也可以用override修飾,多層繼承中的多態就是這樣實現的。要想終止這種重寫,只需重寫方法時用sealed關鍵字進行修飾即可。
3. abstract-override實現多態
先在我們在來討論一下用abstract修飾的抽象方法。抽象方法只是對方法進行了定義,而沒有實現,如果一個類包含了抽象方法,那么該類也必須用abstract聲明為抽象類,一個抽象類是不能被實例化的。對於類中的抽象方法,可以再其派生類中用override進行重寫,如果不重寫,其派生類也要被聲明為抽象類。看下面的例子。
例4:
public abstract class Animal { public abstract void Eat(); } public class Cat : Animal { public override void Eat() { Console.WriteLine("Cat eat"); } } public class Dog : Animal { public override void Eat() { Console.WriteLine("Dog eat"); } } public class WolfDog : Dog { public override void Eat() { Console.WriteLine("Wolfdog eat"); } } class Tester { static void Main(string[] args) { Animal[] animals = new Animal[3]; animals[0] = new Cat(); animals[1] = new Dog(); animals[2] = new WolfDog(); for (int i = 0; i < animals.Length; i++) { animals[i].Eat(); } } }
運行結果為:
Cat eat...
Dog eat...
Wolfdog eat...
從上面可以看出,通過使用abstract-override可以和virtual-override一樣地實現多態,包括多層繼承也是一樣的。不同之處在於,包含虛擬方法的類可以被實例化,而包含抽象方法的類不能被實例化。
前言:我們都知道面向對象的三大特性:封裝,繼承,多態。封裝和繼承對於初學者而言比較好理解,但要理解多態,尤其是深入理解,初學者往往存在有很多困惑,為什么這樣就可以?有時候感覺很不可思議,由此,面向對象的魅力體現了出來,那就是多態,多態用的好,可以提高程序的擴展性。常用的設計模式,比如簡單工廠設計模式,核心就是多態。
其實多態就是:允許將子類類型的指針賦值給父類類型的指針。也就是同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果。在運行時,可以通過指向基類的指針,來調用實現派生類中的方法。如果這邊不理解可以先放一放,先看下面的事例,看完之后再來理解這句話,就很容易懂了。
理解多態之前首先要對面向對象的里氏替換原則和開放封閉原則有所了解。
里氏替換原則(Liskov Substitution Principle):派生類(子類)對象能夠替換其基類(超類)對象被使用。通俗一點的理解就是“子類是父類”,舉個例子,“男人是人,人不一定是男人”,當需要一個父類類型的對象的時候可以給一個子類類型的對象;當需要一個子類類型對象的時候給一個父類類型對象是不可以的!
開放封閉原則(Open Closed Principle):封裝變化、降低耦合,軟件實體應該是可擴展,而不可修改的。也就是說,對擴展是開放的,而對修改是封閉的。因此,開放封閉原則主要體現在兩個方面:對擴展開放,意味着有新的需求或變化時,可以對現有代碼進行擴展,以適應新的情況。對修改封閉,意味着類一旦設計完成,就可以獨立完成其工作,而不要對類進行任何修改。
對這兩個原則有一定了解之后就能更好的理解多態。
我們都知道,喜鵲(Magpie)、老鷹(Eagle)、企鵝(Penguin)都是屬於鳥類,我們可以根據這三者的共有特性提取出鳥類(Bird)做為父類,喜鵲喜歡吃蟲子,老鷹喜歡吃肉,企鵝喜歡吃魚。
創建基類Bird如下,添加一個虛方法Eat():
/// <summary> /// 鳥類:父類 /// </summary> public class Bird { /// <summary> /// 吃:虛方法 /// </summary> public virtual void Eat() { Console.WriteLine("我是一只小小鳥,我喜歡吃蟲子~"); } }
創建子類Magpie如下,繼承父類Bird,重寫父類Bird中的虛方法Eat():
/// <summary> /// 喜鵲:子類 /// </summary> public class Magpie:Bird { /// <summary> /// 重寫父類中Eat方法 /// </summary> public override void Eat() { Console.WriteLine("我是一只喜鵲,我喜歡吃蟲子~"); } }
創建一個子類Eagle如下,繼承父類Bird,重寫父類Bird中的虛方法Eat():
/// <summary> /// 老鷹:子類 /// </summary> public class Eagle:Bird { /// <summary> /// 重寫父類中Eat方法 /// </summary> public override void Eat() { Console.WriteLine("我是一只老鷹,我喜歡吃肉~"); } }
創建一個子類Penguin如下,繼承父類Bird,重寫父類Bird中的虛方法Eat():
/// <summary> /// 企鵝:子類 /// </summary> public class Penguin:Bird { /// <summary> /// 重寫父類中Eat方法 /// </summary> public override void Eat() { Console.WriteLine("我是一只小企鵝,我喜歡吃魚~"); } }
到此,一個基類,三個子類已經創建完畢,接下來我們在主函數中來看下多態是怎樣體現的。
static void Main(string[] args) { //創建一個Bird基類數組,添加基類Bird對象,Magpie對象,Eagle對象,Penguin對象 Bird[] birds = { new Bird(), new Magpie(), new Eagle(), new Penguin() }; //遍歷一下birds數組 foreach (Bird bird in birds) { bird.Eat(); } Console.ReadKey(); }
運行結果:
由此可見,子類Magpie,Eagle,Penguin對象可以賦值給父類對象,也就是說父類類型指針可以指向子類類型對象,這里體現了里氏替換原則。
父類對象調用自己的Eat()方法,實際上顯示的是父類類型指針指向的子類類型對象重寫父類Eat后的方法。這就是多態。
多態的作用到底是什么呢?
其實多態的作用就是把不同的子類對象都當作父類來看,可以屏蔽不同子類對象之間的差異,寫出通用的代碼,做出通用的編程,以適應需求的不斷變化。
以上程序也體現了開放封閉原則,如果后面的同事需要擴展我這個程序,還想再添加一個貓頭鷹(Owl),很容易,只需要添加一個Owl類文件,繼承Bird,重寫Eat()方法,添加給父類對象就可以了。至此,該程序的擴展性得到了提升,而又不需要查看源代碼是如何實現的就可以擴展新功能。這就是多態帶來的好處。
還是剛才的例子,我們發現Bird這個父類,我們根本不需要使用它創建的對象,它存在的意義就是供子類來繼承。所以我們可以用抽象類來優化它。
我們把Bird父類改成抽象類,Eat()方法改成抽象方法。代碼如下:
/// <summary> /// 鳥類:基類 /// </summary> public abstract class Bird { /// <summary> /// 吃:抽象方法 /// </summary> public abstract void Eat(); }
抽象類Bird內添加一個Eat()抽象方法,沒有方法體。也不能實例化。
其他類Magpie,Eagle,Penguin代碼不變,子類也是用override關鍵字來重寫父類中抽象方法。
Main主函數中Bird就不能創建對象了,代碼稍微修改如下:
static void Main(string[] args) { //創建一個Bird基類數組,添加 Magpie對象,Eagle對象,Penguin對象 Bird[] birds = { new Magpie(), new Eagle(), new Penguin() }; //遍歷一下birds數組 foreach (Bird bird in birds) { bird.Eat(); } Console.ReadKey(); }
執行結果:
由此可見,我們選擇使用虛方法實現多態還是抽象類抽象方法實現多態,取決於我們是否需要使用基類實例化的對象.
比如說 現在有一個Employee類作為基類,ProjectManager類繼承自Employee,這個時候我們就需要使用虛方法來實現多態了,因為我們要使用Employee創建的對象,這些對象就是普通員工對象。
再比如說 現在有一個Person類作為基類,Student,Teacher 類繼承Person,我們需要使用的是Student和Teacher創建的對象,根本不需要使用Person創建的對象,
所以在這里Person完全可以寫成抽象類。
總而言之,是使用虛方法,或者抽象類抽象方法實現多態,視情況而定,什么情況?以上我說的兩點~
接下來~~~~
我要問一個問題,喜鵲和老鷹都可以飛,這個飛的能力,我怎么來實現呢?
XXX答:“在父類Bird中添加一個Fly方法不就好了~~”
我再問:“好的,照你說的,企鵝繼承父類Bird,但是不能企鵝不能飛啊,這樣在父類Bird中添加Fly方法是不是不合適呢?”
XXX答:“那就在能飛的鳥類中分別添加Fly方法不就可以了嗎?”
對,這樣是可以,功能完全可以實現,可是這樣違背了面向對象開放封閉原則,下次我要再擴展一個鳥類比如貓頭鷹(Owl),我還要去源代碼中看下Fly是怎么實現的,然后在Owl中再次添加Fly方法,相同的功能,重復的代碼,這樣是不合理的,程序也不便於擴展;
其次,如果我還要添加一個飛機類(Plane),我繼承Bird父類,合適嗎?
很顯然,不合適!所以我們需要一種規則,那就是接口了,喜鵲,老鷹,飛機,我都實現這個接口,那就可以飛了,而企鵝我不實現這個接口,它就不能飛~~
添加一個接口IFlyable,代碼如下:
/// <summary> /// 飛 接口 /// </summary> public interface IFlyable { void Fly(); }
喜鵲Magpie實現IFlyable接口,代碼如下:
/// <summary> /// 喜鵲:子類,實現IFlyable接口 /// </summary> public class Magpie:Bird,IFlyable { /// <summary> /// 重寫父類Bird中Eat方法 /// </summary> public override void Eat() { Console.WriteLine("我是一只喜鵲,我喜歡吃蟲子~"); } /// <summary> /// 實現 IFlyable接口方法 /// </summary> public void Fly() { Console.WriteLine("我是一只喜鵲,我可以飛哦~~"); } }
老鷹Eagle實現IFlyable接口,代碼如下:
/// <summary> /// 老鷹:子類實現飛接口 /// </summary> public class Eagle:Bird,IFlyable { /// <summary> /// 重寫父類Bird中Eat方法 /// </summary> public override void Eat() { Console.WriteLine("我是一只老鷹,我喜歡吃肉~"); } /// <summary> /// 實現 IFlyable接口方法 /// </summary> public void Fly() { Console.WriteLine("我是一只老鷹,我可以飛哦~~"); } }
在Main主函數中,創建一個IFlyable接口數組,代碼實現如下:
static void Main(string[] args) { //創建一個IFlyable接口數組,添加 Magpie對象,Eagle對象 IFlyable[] flys = { new Magpie(), new Eagle() }; //遍歷一下flys數組 foreach (IFlyable fly in flys) { fly.Fly(); } Console.ReadKey(); }
執行結果:
由於企鵝Penguin沒有實現IFlyable接口,所以企鵝不能對象不能賦值給IFlyable接口對象,所以企鵝,不能飛~
好了,剛才我提到了飛機也能飛,繼承Bird不合適的問題,現在有了接口,這個問題也可以解決了。如下,我添加一個飛機Plane類,實現IFlyable接口,代碼如下:
/// <summary> /// 飛機類,實現IFlyable接口 /// </summary> public class Plane:IFlyable { /// <summary> /// 實現接口方法 /// </summary> public void Fly() { Console.WriteLine("我是一架飛機,我也能飛~~"); } }
在Main主函數中,接口IFlyable數組,添加Plane對象:
class Program { static void Main(string[] args) { //創建一個IFlyable接口數組,添加 Magpie對象,Eagle對象,Plane對象 IFlyable[] flys = { new Magpie(), new Eagle(), new Plane() }; //遍歷一下flys數組 foreach (IFlyable fly in flys) { fly.Fly(); } Console.ReadKey(); } }
執行結果:
由此,可以看出用接口實現多態程序的擴展性得到了大大提升,以后不管是再擴展一個蝴蝶(Butterfly),還是鳥人(Birder)創建一個類,實現這個接口,在主函數中添加該對象就可以了。
也不需要查看源代碼是如何實現的,體現了開放封閉原則!
接口充分體現了多態的魅力~~
以上通過一些小的事例,給大家介紹了面向對象中三種實現多態的方式,或許有人會問,在項目中怎么使用多態呢?多態的魅力在項目中如何體現?
那么接下來我做一個面向對象的簡單計算器,來Show一下多態在項目中使用吧!
加減乘除運算,我們可以根據共性提取出一個計算類,里面包含兩個屬性 Number1和Number2,還有一個抽象方法Compute();代碼如下:
/// <summary> /// 計算父類 /// </summary> public abstract class Calculate { public int Number1 { get; set; } public int Number2 { get; set; } public abstract int Compute(); }
接下來,我們添加一個加法器,繼承計算Calculate父類:
/// <summary> /// 加法器 /// </summary> public class Addition : Calculate { /// <summary> /// 實現父類計算方法 /// </summary> /// <returns>加法計算結果</returns> public override int Compute() { return Number1 + Number2; } }
再添加一個減法器,繼承計算Calculate父類:
/// <summary> /// 減法器 /// </summary> public class Subtraction : Calculate { /// <summary> /// 實現父類計算方法 /// </summary> /// <returns>減法計算結果</returns> public override int Compute() { return Number1 - Number2; } }
在主窗體FormMain中,編寫計算事件btn_Compute_Click,代碼如下:
private void btn_Compute_Click(object sender, EventArgs e) { //獲取兩個參數 int number1 = Convert.ToInt32(this.txt_Number1.Text.Trim()); int number2 = Convert.ToInt32(this.txt_Number2.Text.Trim()); //獲取運算符 string operation = cbb_Operator.Text.Trim(); //通過運算符,返回父類類型 Calculate calculate = GetCalculateResult(operation); calculate.Number1 = number1; calculate.Number2 = number2; //利用多態,返回運算結果 string result = calculate.Compute().ToString(); this.lab_Result.Text = result; } /// <summary> /// 通過運算符,返回父類類型 /// </summary> /// <param name="operation"></param> /// <returns></returns> private Calculate GetCalculateResult(string operation) { Calculate calculate = null; switch (operation) { case "+": calculate = new Addition(); break; case "-": calculate = new Subtraction(); break; } return calculate; }
在該事件中主要調用GetCalculateResult方法,通過運算符,創建一個對應的加減乘除計算器子類,然后賦值給父類,其實這就是設計模式中的簡單工廠設計模式,我給你一個運算符你給我生產一個對應的加減乘除計算器子類,返回給我。。其實大多數的設計模式的核心就是多態,掌握好多態,設計模式看起來也很輕松。
現階段工作已經完成,但是過了一段時間,又添加新的需求了,我還要擴展一個乘法了,那好,很簡單只要創建一個乘法計算器繼承Calculate父類即可,看代碼:
/// <summary> /// 乘法計算器 /// </summary> public class Multiplication:Calculate { public override int Compute() { return Number1*Number2; } }
然后在GetCalculateResult函數中添加一個case 就好了:
switch (operation) { case "+": calculate = new Addition(); break; case "-": calculate = new Subtraction(); break; case "*": calculate = new Multiplication(); break; }
執行結果:
好了,就這么方便,一個新的功能就擴展完畢了,我根本不需要查看源代碼是如何實現的,這就是多態的好處!
多態性意味着有多重形式。在面向對象編程范式中,多態性往往表現為"一個接口,多個功能"。
多態性可以是靜態的或動態的。在靜態多態性中,函數的響應是在編譯時發生的。在動態多態性中,函數的響應是在運行時發生的。
-------------------------------------------------------------------------------------------------
什么時候用接口什么時候用抽象?
抽象:是對相同屬性和方法的提煉而得
接口:是對相同行為不同實現方法的提煉
如: 每種支付方式 支付之前都需要校驗一下支付金額是不是真確的,不能小於等於0 。因為校驗方式,校驗代碼都是一樣的,所以我們可以定義一個 抽象類給抽象出來.
public abstract class AbstractPayWay implements PayWay{ private Double money; private boolean verify(){ return money != null && money > 0; } /** * 這里實現了 PayWay 中的 pay 接口 方法 所以 AbstractPayWay 的子類 無需 實現該 方法, * 只需要 實現 doPay() 方法,並且 如果 doPay()方法被成功調用則說明肯定 校驗成功了。 */ @Override public boolean pay(){ boolean verify = this.verify(); if(!verify){ System.out.println("支付金額驗證錯誤!"); return false; } return this.doPay(); } public abstract boolean doPay(); }
所以 WeixinPayWay ZhifubaoPayWay 支付的具體實現類可以改改為
/** * 繼承 AbstractPayWay 所以無需 寫公共的校驗邏輯,直接寫支付業務邏輯 * @author cyy */ public class WeixinPayWay extends AbstractPayWay{ @Override public boolean doPay() { System.out.println("這里無需校驗支付金額,直接調用支付方法就行"); System.out.println("微信支付成功"); return false; } }
public class ZhifubaoPayWay extends AbstractPayWay{ @Override public boolean doPay() { System.out.println("這里無需校驗支付金額,直接調用支付方法就行"); System.out.println("支付寶支付成功"); return false; } }
參考:
https://blog.csdn.net/wab719591157/article/details/73741919
https://blog.csdn.net/u012135077/article/details/48286837
https://blog.csdn.net/u013538542/article/details/45365019
轉:
java 中什么時候用抽象類,什么時候用 接口(面向對象的繼承與多態)
抽象類:強調的是把共同(共有、相同)的屬性方法, 抽象出來,統一寫在一個地方(他們的實現代碼是一樣的),方便維護。(面向對象三大特性中的繼承特性)
接口: 抽象的是行為 - 同一種行為的不同實現方式。當多個對象都擁有相同的行為,但是行為的具體實現方式不一樣的時候可以用接口抽象(面向對象中的多態特性)
所以一般在實際項目中接口和抽象類是配合使用而不是相互替代
例如:
所有的訂單都有單號,單價,數量。都擁有,而且相同,所以可以用一個抽象類給統一描述出來。
public abstract class AbstractOrder { private String serialNo; // 單號 private Double money; // 單價 private int number; // 數量 }
再有一個商品訂單 還有一個獨有的 商品名稱 屬性。所以 在新新建一個 ProductOrder 繼承 AbstractOrder
public class ProductOrder extends AbstractOrder{ private String productName; }
另外 所有的訂單都需要支付,但是支付方式又不一樣比如,微信支付,支付寶支付,同一種行為,但是具體的行為方式又不一樣。所以用一個接口給抽象出來(規定一個行為標准)
public interface PayWay { public boolean pay(); }
public class WeixinPayWay implements PayWay{
@Override public boolean pay() { System.out.println("微信支付成功"); return false; } }
public class ZhifubaoPayWay implements PayWay{ @Override public boolean pay() { System.out.println("支付寶支付成功"); return false; } }
因為所有訂單都需要支付 所以 只需要 改造 AbstractOrder 類在里面增加一個 支付行為
public abstract class AbstractOrder { private String serialNo; // 單號 private Double money; // 單價 private int number; // 數量 private PayWay payWay; // 支付行為 }
在比如 每種支付方式 支付之前都需要校驗一下支付金額是不是真確的,不能小於等於0 。因為校驗方式,校驗代碼都是一樣的,所以我們可以定義一個 抽象類給抽象出來
public abstract class AbstractPayWay implements PayWay{ private Double money; private boolean verify(){ return money != null && money > 0; } /** * 這里實現了 PayWay 中的 pay 接口 方法 所以 AbstractPayWay 的子類 無需 實現該 方法, * 只需要 實現 doPay() 方法,並且 如果 doPay()方法被成功調用則說明肯定 校驗成功了。 */ @Override public boolean pay(){ boolean verify = this.verify(); if(!verify){ System.out.println("支付金額驗證錯誤!"); return false; } return this.doPay(); } public abstract boolean doPay(); }
所以 WeixinPayWay ZhifubaoPayWay 支付的具體實現類可以改改為
/** * 繼承 AbstractPayWay 所以無需 寫公共的校驗邏輯,直接寫支付業務邏輯 * @author cyy */ public class WeixinPayWay extends AbstractPayWay{ @Override public boolean doPay() { System.out.println("這里無需校驗支付金額,直接調用支付方法就行"); System.out.println("微信支付成功"); return false; } }
public class ZhifubaoPayWay extends AbstractPayWay{ @Override public boolean doPay() { System.out.println("這里無需校驗支付金額,直接調用支付方法就行"); System.out.println("支付寶支付成功"); return false; } }
---------------------------------------------------------------------------------
問題:怎么在開發中嘗試提煉抽象能力?
還得多 練習說設計模式和重構,當然在算法的基礎上
在此之前堅持三個原則
第一次用到某個功能時,你寫一個特定的解決方法;
第二次又用到的時候,你拷貝上一次的代碼;
第三次出現的時候,你才着手"抽象化",寫出通用的解決方法
轉
從一個算命的段子談談抽象
這兩天有幸去參加了張逸老師(《架構之美》(評注版)
以及《重構 : 改善既有代碼的設計》(評注版)
的作者)的重構方面的培訓(公司組織),寫一篇文章分享一下這一兩天聽到的經典段子。
ps:這篇文章,可能沒啥干貨。因為我不可能去講如何抽象,這種題材在公眾號上看,大家看的太累了。而且大家去買任何一本重構方面的書,估計都講的比我好。所以這篇文章,大家當段子看看即可。如果時間寶貴,慎重!可以關閉,偶不會怪你的!
段子
基本上所有的文章講抽象舉例的時候,都是舉一些數學公式為例子。因此數學公式就是從大量數據中抽象出來的一個定律。那今天要分享的就是一個算命的段子。
話說,某日,三個書生去算命,他們問:"老先生,看看我們今年鄉試,誰能夠高中?。"
老先生看了看他們的面相,只亮出了一根手指,就像下面這樣
不久,三個書生鄉試完畢,紛紛稱贊,老先生算的准。
那么這是為什么呢?聰明的各位讀者想到了么?
因為
-
沒有書生考上,這個一根手指代表一個書生都沒考上
-
一個書生考上,這個一根手指代表只有一個書生考上
-
二個書生考上,這個一根手指代表只有一個書生沒考上
-
三個書生考上,這個時候可以這么解釋,正所謂,道生一,一生二,二生三,三生萬物。這個一可以理解為圓滿,三個都考上,不是很圓滿么。
這時候,有人不服,問要是有四個書生考試怎么辦?OK,也可以解釋
-
沒有書生考上,這個一根手指代表一個書生都沒考上
-
一個書生考上,這個一根手指代表只有一個書生考上
-
二個書生考上,這個一根手指代表只有一對書生考上
-
三個書生考上,這個一根手指代表只有一個書生沒考上
-
四個書生考上,這個一根手指代表圓滿,大家都考上。
至於五個書生的情況,大家自行推導吧。
OK,講完了。這個抽象就是從紛繁復雜的事物中提煉本質的過程。你們看,這個段子里的一就是對一切現象的抽象。那么一個程序員的抽象能力,就是他的軟件設計能力。大家回憶一下自己做項目,從業務那邊接需求,然后轉化為代碼,就是一個抽象的過程。你抽象的能力如何,就代表着你的代碼質量如何。
如果你的抽象能力很LOW,那你的代碼也很LOW,根本無法降低問題的復雜度。縱使你做了n個CRUD的項目,你也無法得到能力的提高。
什么時候抽象
首先,不要瞎抽象,很多人,沒事瞎抽象,最后系統就抽筋了。而且,最開始系統需求不明確,需求變更頻繁,瞎抽象,不利於后期維護。推薦還是邊寫邊抽象,一開始不要想太多。
具體原則,出自Martin Fowler在《Refactoring》
中提出的三次原則
第一次用到某個功能時,你寫一個特定的解決方法;
第二次又用到的時候,你拷貝上一次的代碼;
第三次出現的時候,你才着手"抽象化",寫出通用的解決方法
理由:
(1)省事。如果一種功能只有一到兩個地方會用到,就不需要在"抽象化"上面耗費時間了。
(2)容易發現模式. 說句實在話,我拿到一個需求,我也無法一下看出該用什么設計模式。都是寫着寫着,發現該用什么設計模式。所以邊寫邊抽象化,比較符合實際情況。
(3)防止過度冗余。如果一種功能同時有多個實現,管理起來非常麻煩,修改的時候需要修改多處。因此需要進行抽象化。
如何抽象
這塊就不細講了,大家隨便看幾本重構方面的書,都講的比我好。推薦《clean code》這本。
總的來說,遵循羅伯特·馬丁提出的五大原則 —— SOLID原則即可。
-
S Single Responsibility Principle - 單一職責原則
-
O Open Close Principle - 開閉原則
-
L Liskov Principle of Substitution - 里式替換原則
-
I Interface Segregation Principle - 接口隔離原則
-
D Dependency Inversion Principle - 依賴倒置原則
這幾大原則,任何一本講設計模式的書基本都有提到,就不贅述了。大家有興趣可以自行查閱。
總結
頻繁的需求變更會對軟件的生命周期造成嚴重的殺傷力。如果你的抽象能力不足,你的代碼就會越發的臃腫,最后不得以進行二次開發。合理的抽象,可以降低軟件代碼的復雜度,增強軟件的生命力,更是一個程序員編程能力和設計能力的體現。希望大家在這方面下足功夫。