不要存在多於一個致使類變動的緣由。通俗的說,即一個類的代碼只負責一項職責。java
繼承有一些優勢:編程
1. 提升代碼的重用性,子類擁有父類的方法和屬性;
2. 提升代碼的可擴展性,子類可形似於父類,但異於父類,保留自個人特性;
缺點:侵入性、不夠靈活、高耦合
1. 繼承是侵入性的,只要繼承就必須擁有父類的全部方法和屬性,在必定程度上約束了子類,下降了代碼的靈活性;
2. 增長了耦合,當父類的常量、變量或者方法被修改了,須要考慮子類的修改,因此一旦父類有了變更,極可能會形成
很是糟糕的結果,要重構大量的代碼。
由於繼承帶來的侵入性,加強了耦合性,也下降了代碼靈活性,父類修改代碼,子類也會受到影響,此時就須要里氏替換原則。設計模式
a.子類必須實現父類的抽象方法,但不得重寫(覆蓋)父類的非抽象(已實現)方法。ide
public class A { public void fun(int a,int b){ System.out.println(a+"+"+b+"="+(a+b)); } } public class B extends A{ @Override public void fun(int a,int b){ System.out.println(a+"-"+b+"="+(a-b)); } } public class demo { public static void main(String[] args){ System.out.println("父類的運行結果"); A a=new A(); a.fun(1,2); //父類存在的地方,能夠用子類替代 //子類B替代父類A System.out.println("子類替代父類後的運行結果"); B b=new B(); b.fun(1,2); } }
運行結果:
父類的運行結果
1+2=3
子類替代父類後的運行結果
1-2=-1
b.子類中能夠增長本身特有的方法。函數
public class A { public void fun(int a,int b){ System.out.println(a+"+"+b+"="+(a+b)); } } public class B extends A{ public void newFun(){ System.out.println("這是子類的新方法..."); } } public class demo { public static void main(String[] args){ System.out.print("父類的運行結果:"); A a=new A(); a.fun(1,2); //父類存在的地方,能夠用子類替代 //子類B替代父類A System.out.print("子類替代父類後的運行結果:"); B b=new B(); b.fun(1,2); //子類B的新方法 b.newFun(); } }
運行結果:
父類的運行結果:1+2=3
子類替代父類後的運行結果:1+2=3
這是子類的新方法...
c.當子類覆蓋或實現父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬鬆。this
public class LSP { class A { public void fun(HashMap map){ System.out.println("父類被執行..."); } } class B extends A{ public void fun(Map map){ System.out.println("子類被執行..."); } } public static void main(String[] args){ System.out.print("父類的運行結果:"); LSP lsp =new LSP(); LSP.A a= lsp.new A(); HashMap<Object, Object> map=new HashMap<Object, Object>(); a.fun(map); //父類存在的地方,能夠用子類替代 //子類B替代父類A System.out.print("子類替代父類後的運行結果:"); LSP.B b=lsp.new B(); b.fun(map); } }
運行結果:
父類的運行結果:父類被執行...
子類替代父類後的運行結果:父類被執行...
符合條件
咱們應當注意,子類並不是重寫了父類的方法,而是重載了父類的方法。由於子類和父類的方法的輸入參數是不一樣的。
子類方法的參數Map比父類方法的參數HashMap的範圍要大,因此當參數輸入爲HashMap類型時,只會執行父類的方法,不會執行父類的重載方法。這符合里氏替換原則。
//將子類方法的參數範圍縮小會怎樣?
import java.util.Map; public class A { public void fun(Map map){ System.out.println("父類被執行..."); } } import java.util.HashMap; public class B extends A{ public void fun(HashMap map){ System.out.println("子類被執行..."); } } import java.util.HashMap; public class demo { static void main(String[] args){ System.out.print("父類的運行結果:"); A a=new A(); HashMap map=new HashMap(); a.fun(map); //父類存在的地方,均可以用子類替代 //子類B替代父類A System.out.print("子類替代父類後的運行結果:"); B b=new B(); b.fun(map); } }
運行結果:
父類的運行結果:父類被執行...
子類替代父類後的運行結果:子類被執行...
在父類方法沒有被重寫的狀況下,子方法被執行了,這樣就引發了程序邏輯的混亂。
因此子類中方法的前置條件必須與父類中被覆寫的方法的前置條件相同或者更寬鬆。不符合裏式替換
d.當子類的方法實現父類的抽象方法時,方法的後置條件(即方法的返回值)要比父類更嚴格。spa
public class LSP1 { abstract class A { public abstract Map fun(); } class B extends A{ @Override public HashMap fun(){ HashMap b=new HashMap(); b.put("b","子類被執行..."); return b; } } public static void main(String[] args){ LSP1 lsp =new LSP1(); LSP1.A a=lsp.new B(); System.out.println(a.fun()); } }
運行結果:
{b=子類被執行...}設計
若在繼承時,子類的方法返回值類型範圍比父類的方法返回值類型範圍大,在子類重寫該方法時編譯器會報錯。code
看上去很難以想象,由於咱們會發如今本身編程中經常會違反里氏替換原則,程序照樣跑的好好的。因此你們都會產生這樣的疑問,假如我非要不遵循里氏替換原則會有什麼後果?對象
後果就是:你寫的代碼出問題的概率將會大大增長。
高層模塊不該該依賴低層模塊,兩者都應該依賴其抽象;抽象不該該依賴實現類;實現類應該依賴抽象。
/** * 大衆汽車類 * @author 葉漢偉 */ public class DaZhong { public void run(){ System.out.println("開大衆汽車"); } } /** * 司機類,依賴具體的實現類參數,暫時只能開大衆,開不了寶馬 * @author 葉漢偉 */ public class Driver { public void drive(DaZhong daZhong){ daZhong.run(); } } public class Client { public static void main(String[] args){ Driver Tom=new Driver(); DaZhong daZhong=new DaZhong(); Tom.drive(daZhong); } }
/** * 寶馬車類 * @author 葉漢偉 */ public class BaoMa { public void run(){ System.out.println("開寶馬車"); } }
面向接口編程,多態
對象的依賴關係能夠經過三種方法來實現:
1.接口聲明依賴對象
在接口處就聲明瞭依賴的對象。司機接口IDriver,其方法drive()的形參是ICar類型的,那麼咱們能夠說IDrive與ICar發生了依賴關係,Dazhong,baoma依賴ICar注入,依賴倒置了。接口聲明依賴的方法也叫接口注入。
/** * 車子接口 * @author 葉漢偉 */ public interface ICar { public void run(); } /** * 大衆汽車類 * @author 葉漢偉 */ public class DaZhong implements ICar{ public void run(){ System.out.println("開大衆汽車"); } } /** * 寶馬車類 * @author 葉漢偉 */ public class BaoMa implements ICar{ public void run(){ System.out.println("開寶馬車"); } } /** * 司機接口 * @author 葉漢偉 */ public interface IDriver {
//接口聲明依賴對象,接口注入ICar,這裏變成了car依賴ICar
public void drive(ICar car);
} /** * 司機類 * @author 葉漢偉 */ public class Driver implements IDriver{
//依賴接口 public void drive(ICar car){ car.run(); } } public class Client { public static void main(String[] args){ IDriver Tom=new Driver(); //Tom開大衆汽車 ICar daZhong=new DaZhong(); Tom.drive(daZhong); //Tom開寶馬 ICar baoMa=new BaoMa(); Tom.drive(baoMa); } }
2.構造函數傳遞依賴對象
/** * 司機接口 * @author 葉漢偉 */ public interface IDriver { public void drive(); } /** * 司機類 * @author 葉漢偉 */ public class Driver implements IDriver{ private ICar car; //經過構造函數注入依賴對象,這裏變成了car依賴ICar public Driver(ICar car){ this.car=car; } public void drive(){ this.car.run(); } }
/** * 司機接口 * @author 葉漢偉 */ public interface IDriver { public void setCar(ICar car); public void drive(); } /** * 司機類 * @author 葉漢偉 */ public class Driver implements IDriver{ private ICar car; //setter方法傳遞依賴對象,,這裏變成了car依賴ICar
public void setCar(ICar car){ this.car=car; } public void drive(){ this.car.run(); } }
5、迪米特法則(Law of Demeter,簡稱LoD) 低耦合,一個對象應該對其餘對象保持最少的瞭解(中介者模式就是這個的應用)
6、開放封閉原則(Open Close Principle,簡稱OCP)儘可能經過擴展軟件實體的行爲來實現變化,而不是經過修改已有的代碼來實現變化
一句話歸納: 單一職責原則告訴咱們實現類要職責單一;里氏替換原則告訴咱們不要破壞繼承體系;依賴倒置原則告訴咱們要面向接口編程;接口隔離原則告訴咱們在設計接口的時候要精簡單一;迪米特法則告訴咱們要下降耦合。而開閉原則是總綱,他告訴咱們要對擴展開放,對修改關閉。
總結:
最後總結一下如何去遵照這六個原則。對這六個原則的遵照並非是和否的問題,而是多和少的問題,也就是說,咱們通常不會說有沒有遵照,而是說遵照程度的多少。任何事都是過猶不及,設計模式的六個設計原則也是同樣,制定這六個原則的目的並非要咱們刻板的遵照他們,而須要根據實際狀況靈活運用。對他們的遵照程度只要在一個合理的範圍內,就算是良好的設計。咱們用一幅圖來講明一下。
圖中的每一條維度各表明一項原則,咱們依據對這項原則的遵照程度在維度上畫一個點,則若是對這項原則遵照的合理的話,這個點應該落在紅色的同心圓內部;若是遵照的差,點將會在小圓內部;若是過分遵照,點將會落在大圓外部。一個良好的設計體如今圖中,應該是六個頂點都在同心圓中的六邊形
在上圖中,設計一、設計2屬於良好的設計,他們對六項原則的遵照程度都在合理的範圍內;設計三、設計4設計雖然有些不足,但也基本能夠接受;設計5則嚴重不足,對各項原則都沒有很好的遵照;而設計6則遵照過渡了,設計5和設計6都是迫切須要重構的設計。