一. 6大設計模式
Single Responsibility Principle : 單一職責原則
Liskov Substitution Principle : 裡氏替換原則
Dependence Inversion Principle :依賴倒置原則
Interface Segregation Principle : 接口隔離原則
Law of Demeter : 迪米特法則
Open Closed Principle : 開閉原則
軟件開發之所以會有這些原則,就是因為復雜多變且不可預料的需求。並不是說在實際項目開發中對這六大原則中的每一條都遵循到極致,而是說在項目開發的過程中,根據項目的實際需求盡量的去遵守這些原則。當然要做到這些肯定是不容易的,能真正做到並且做好的恐怕也只能是有經驗之人。
二. Single Responsibility Principle(簡稱SRP):單一職責原則
1.單一職責的定義:應該有且只有一個原因引起類的變更。換句話說就是一個接口只做一件事,即一個職責一個接口。但是困難的是劃分職責時並沒有一個標准,最終都是需要從實際的項目去考慮。我們在設計的時候,盡量單一,然後對於其實現類就要多方面的考慮。不能死套單一職責原則,否則會增加很多類,給維護帶來不便。
2. 接口單一職責實例:
下面是一個User類的類圖:
仔細去考慮這個類的設計,我們就可以看到問題所在。用戶的屬性屬性操作和其他的行為沒有分開。正確的做法是我們應該把屬性抽取成為一個業務對象(Business Object,簡稱BO),而把行為抽取成為一個業務邏輯(Business Logic,簡稱BL)。
依賴單一職責原則設計出的類,類圖如下:
3.單一職責同樣適用於方法
有這樣一個方法,傳遞可變長參數去修改一個用戶的信息。
public boolean changeUserinfo(User user,String ...changeOptions){ //修改密碼,可變參數傳遞要修改的密碼,編寫相應的實現代碼 //修改地址,可變參數傳遞要修改的地址,編寫相應的實現代碼 }
像這樣的寫是絕對不允許的,相信大家也不會這樣寫。這樣的方法就是典型的違背了單一職責原則。正確的寫法如下:
public boolean changeUserpassword(User user,String psw){ //代碼實現 } public boolean changeUseraddress(User user,String psw){ //代碼實現 }
4.總結:接口一定做到單一職責,類的設計盡量做到只有一個原因可以引起它的改變。
二. Liskov Substitution Principle:裡氏替換原則
1. 定義:裡氏替換原則簡單易懂一點的定義就是只要父類出現的地方子類就可以出現,且替換成子類也不會出現任何錯誤或者異常。(但是反過來,有子類出現的地方,父類
不一定可以適用)。
2. 裡氏替換原則是為繼承定義了四個規范
① 子類必須完全實現父類的方法。
abstract class AbstractGun{ public abstract void shoot(); } class Handgun extends AbstractGun { @Override public void shoot() { System.out.println("Handgun shoot ......"); } } class Machinegun extends AbstractGun { @Override public void shoot() { System.out.println("Machinegun shoot ......"); } } class Solider{ private AbstractGun gun;
//此處的AbstractGun gun 是父類出現的地方,子類可以出現 體現在傳給它的實際參數類型可以是Machinegun或者Handgun任一種類型,並且都不會出錯 public void setGun(AbstractGun gun){ this.gun = gun; } public void kill(){ this.gun.shoot(); } } public class Client{ public static void main(String[] args){ Solider solider = new Solider(); //傳入的時候並不知道是哪種類型 ,運行時才知道,而且修改槍支的類型只需要new 不同的對象即可。而不用修改其他的任何地方 solider.setGun(new Machinegun()); solider.kill(); } }
客戶端運行結果:Machinegun shoot ......
此時假設有一個玩具槍,放到實際問題上,玩具槍是不能殺人的。所以給shoot方法一個空實現。
class Toygun extends AbstractGun { @Override public void shoot() { //空實現 } }
客戶端代碼修改如下:
public class Client{ public static void main(String[] args){ Solider solider = new Solider(); //修改部分為紅色 solider.setGun(new Toygun()); solider.kill(); } }
客戶端運行是沒有結果的,這是肯定的,因為Toygun的shoot方法是一個空實現。解決的方法有兩個:第一種解決方案就是在Solider類中增加一個類型判斷。如果是Toygun類
型,就不調用shoot方法。這樣出現的問題是,每多出一個類就要增加一種類型判斷,這樣顯然是不合理的。第二種解決方案就是讓Toygun脫離繼承,建立一個獨立的父類。
總結:如果子類不能完全實現父類的方法,建議斷開父子關系,采用Java類之間關聯關系的另外三種依賴、關聯、組合去實現。
② 子類可以有自己的個性。
裡氏替換原則可以正著用,即父類出現的地方子類一定可以出現,但是反過來子類出現的地方父類就不一定適用。
在上一個代碼實例中修改部分代碼:
class HandgunOne extends Handgun{ public void shoot() { System.out.println("HandgunOne射擊......"); } } class Solider{ //HandgunOne gun是子類出現的地方,父類不一定適用體現在,當在客戶端傳遞(HandgunOne) new Handgun()這樣的參數時,運行會拋出java.lang.ClassCastException 異常。 public void kill(HandgunOne gun){ gun.shoot(); } } public class Client{ public static void main(String[] args){ Solider solider = new Solider(); //下面的代碼會拋出異常,因為像下轉型是不安全的 solider.kill((HandgunOne) new Handgun()); } }
③覆蓋或者實現父類的方法時輸入參數可以被放大。
示例代碼:
import java.util.HashMap; import java.util.Map; class Father{ public void doSomething(HashMap map){ System.out.println("father doSomething"); } } class Son extends Father{ //此方法不是重寫,而是重載 public void doSomething(Map map){ System.out.println("son doSomething"); } } public class Client{ public static void main(String[] args){ //此處為父類出現的地方,一會根據裡氏替換原則會換成子類 Father father = new Father(); father.doSomething(new HashMap()); } }
客戶端輸出:father doSomething
根據裡氏替換原則做對客戶端代碼做如下修改:
public class Client{ public static void main(String[] args){ Son son = new Son(); son.doSomething(new HashMap()); } }
客戶端依然輸出:father doSomething。
父類方法的參數類型為:HashMap,子類方法的參數類型為:Map。很明顯子類將父類的參數類型擴大了,子類代替父類後執行相同的方法,是不會執行子類的方法的。我們要知道,要想讓子類的方法運行,就必須重寫父類的同名方法。因為上面不是重寫而是重載,所以子類的方法並沒有被運行。因此父類的一個方法在子類中可以被重寫,也可以被重載,但是重載時的參數類型必須大於父類同名方法中的參數類型。否則就會出現:子類沒有覆蓋父類的方法,調用時卻運行了子類的方法。以下就是一個代碼示例:
import java.util.HashMap; import java.util.Map; class Father{ public void doSomething(Map map){ System.out.println("father doSomething"); } } class Son extends Father{ //此方法不是重寫,而是重載,但是和前一個不同,它沒有擴大參數類型,而是縮小了參數的類型 public void doSomething(HashMap map){ System.out.println("son doSomething"); } } public class Client{ public static void main(String[] args){ //此處為父類出現的地方,下一個示例會用子類替換掉父類 Father father = new Father(); father.doSomething(new HashMap()); } }
客戶端輸出:father doSomething
用子類替換掉父類代碼示例(只修改客戶端部分代碼):
public class Client{ public static void main(String[] args){ Son son = new Son();
son.doSomething(new HashMap()); } }
客戶端輸出:son doSomething
可以看到出現了問題:子類沒有覆蓋父類的同名方法(只是重載了),但是卻運行了子類的方法。這樣做就出現了邏輯混亂(要想讓子類的方法運行,就必須覆蓋重載父類的同名方,然而實際上子類並沒有覆蓋父類的同名方法,但是還是用了子類的方法)。
總結:父類的一個方法在子類中可以被重寫,也可以被重載,但是重載時的參數類型必須大於父類同名方法中的參數類型
④復寫或實現父類的方法時返回值可以縮小。
三.Dependence Inversion Principle:依賴倒置原則
1.定義:精簡的定義就是面向接口編程。在Java語言中的表現就是為以下的三點
① 模塊間的依賴關系通過接口和抽象類產生,實體類之間不直接發生依賴關系。
代碼示例一:實例類之間產生依賴所出現的問題
class Driver{ //在這裡產生了實體類之間的依賴 public void drive(Benz benz){ benz.run(); } } class Benz{ public void run(){ System.out.println("benz run......"); } } public class Client{ public static void main(String[] args){ Driver driver = new Driver(); driver.drive(new Benz()); } }
假如有一天Driver不開benz了,則此時代碼要修改兩處(修改為紅色部分)
class Driver{ public void drive(BMW bmw){ bmw.run(); } } class Benz{ public void run(){ System.out.println("benz run......"); } } //此處的業務邏輯類的實現是必不可少的 class BMW{ public void run(){ System.out.println("bmw run......"); } } public class Client{ public static void main(String[] args){ Driver driver = new Driver(); driver.drive(new BMW()); } }
因為Driver類和Benz類之間的緊耦合導致只是增加了一輛車就要修改Driver類。因此正確的做法是讓Driver類去依賴一個接口。
interface Car{ public void run(); } class Benz implements Car{ public void run(){ System.out.println("benz run......"); } } class BMW implements Car{ public void run(){ System.out.println("bmw run......"); } } class Driver{ //讓Driver類依賴一個Car這個接口 public void drive(Car car){ car.run(); } }
public class Client{ public static void main(String[] args){ Driver driver = new Driver(); driver.drive(new BMW()); } }
這樣之後再增加的車的種類,只需要修改客戶端傳遞給Driver類的drive方法的類型就可以了。
②接口和抽象類不依賴於實現類。
③實現類依賴接口或者抽象類。
2. 對象的依賴關系有三種實現方式。
①構造函數傳遞依賴對象。
class Driver{ private Car car; Driver(Car car){ this.car = car; } public void drive(){ this.car.run(); } }
②Setter傳遞依賴對象。
class Driver{ private Car car; public void setCar(Car car){ this.car = car; } public void drive(){ this.car.run(); } }
③接口聲明依賴對象,也叫接口注入。
class Driver{ public void drive(Car car){ car.run(); } }
四.Interface Segregation Principle:接口隔離原則
1.定義: 建立單一接口,不要建立臃腫龐大的接口。即接口盡量細化,同時接口中的方法盡量少。在這裡提一下單一職責和接口隔離原則的區別。首先兩個側重點是不一樣的,
單一職責要求類和接口,或者方法的職責單一,側重點在職責,這是根據業務邏輯進行劃分的。而接口隔離原則要接口中的方法盡量少。比如,一個接口或者一個中有十個方法,不
同的方法做不同的事情,但是這個接口總體就是處理一件事情,然後具體細分成了10個方法。不同的模塊根據不同的權限進行訪問,這是符單一職責原則的。但是按照接口隔離的原
則是要求接口接口中的方法盡量少,落實到這個實例就是要求盡量多幾個專門的接口供不同的模塊使用,而不是只有一個臃腫的接口,依據權限去限制不同模塊可以訪問的方法。
2.接口隔離原則是對接口定義的規范。含義主要包含以下4點。
①接口盡量小。根據具體業務把一個接口按照接口隔離原則細化成更多的接口。但是在此基礎之上必須不能違背單一職責原則。
②接口要高內聚。高內聚的意思就是提高接口和類處理能力,減少對外的交互。接口是對外的承諾,因此設計時應該盡量少公布接口中的public方法,承諾越少系統開發越有利且變更風險就越少。
③定制服務。定制服務就是單獨為一個個體提供服務,即只提供訪問者需要的方法。舉一個圖書管理系統的例子,有一個查詢接口BookSearch,包括如下方法:searchById,searchByBookName,searchByCategory,complexSearch,其中前三個方法是提供給學生使用的,後一個方法是提供給管理員使用的,學生對這個方法的訪問是有限制的,調用不會返回任何值。當這四個方法全部公布出去之後,學生對此方法的訪問即使不返回任何值也會使服務器性能下降。因此合理的設計應該是拆分這個接口為兩個接口:SimpleSearch和AdminSearch。SimpleSearch接口提供searchById,searchByBookName,searchByCategory方法,AdminSearch接口提供complexSearch方法,此時學生實現SimpleSearch接口即可,管理員同時實現SimpleSearch和AdminSearch兩個接口。
④接口設計是有限度的。接口設計越小越好,但是結構同時會變得復雜,維護也變得難了。因此就要把握住這個度。
五. Law of Demeter: 迪米特法則
1. 定義: 迪米特法則也叫做最少知識原則(Least Knowledge Principle,LKP),即一個對象應該對其他對象有最少的了解,也就是說一個類要對自己需要耦合或者調用的類知道的最少。我只知道你有多少public方法可以供我調用,而其他的一切都與我無關。
2. 迪米特法則是對類的低耦合做處理明確的要求,在此舉一個例子:學校領導老師點名,老師讓體育委員清點人數。其中第二段代碼的耦合性較第一段代碼有所改善。
①
import java.util.List; import java.util.ArrayList; class Teacher{ public void command(){ System.out.println("老師接到命令,委托體育委員清點人數......"); //耦合了Students類 List<Student> students = new ArrayList<Student>(); for(int i = 0; i < 20; i++ ){ students.add(new Student()); } //耦合了StudentLeader類 StudentLeader StudentLeader = new StudentLeader(); int counts = StudentLeader.counts(students); System.out.println("老師委托體育委員清點人數完畢......"); System.out.println("老師報告學校領導,人數為"+counts); } } class Student{ } class StudentLeader{ //耦合了Students類 public int counts(List<Student> students){ System.out.println("體育委員開始清點人數......"); int counts = students.size(); System.out.println("體育委員清點結束,人數為"+counts+",並且返回人數給老師"); return counts; } } public class Client{ public static void main(String[] args){ System.out.println("周末收假,學校領導命令老師去點名....."); Teacher teacher = new Teacher(); teacher.command(); } }
控制台:
周末收假,學校領導命令老師去點名.....
老師接到命令,委托體育委員清點人數......
體育委員開始清點人數......
體育委員清點結束,人數為20,並且返回人數給老師
老師委托體育委員清點人數完畢......
老師報告學校領導,人數為20
②
import java.util.List; import java.util.ArrayList; class Teacher{ //只需要耦合體育委員 而無需知道students類,降低了Teacher類和Student類的耦合 public void command(StudentLeader StudentLeader ){ System.out.println("老師接到命令,委托體育委員清點人數......"); StudentLeader.counts(); } } class Student{ } class StudentLeader{ private List<Student> students; public StudentLeader(List<Student> students){ this.students = students; } public void counts(){ System.out.println("體育委員開始清點人數......"); int counts = students.size(); System.out.println("體育委員清點結束,人數為"+counts); } } public class Client{ public static void main(String[] args){ System.out.println("周末收假,學校領導命令老師去點名....."); List<Student> students = new ArrayList<Student>(); for(int i = 0; i < 20; i++ ){ students.add(new Student()); } Teacher teacher = new Teacher(); teacher.command(new StudentLeader(students)); } }
控制台:
周末收假,學校領導命令老師去點名.....
老師接到命令,委托體育委員清點人數......
體育委員開始清點人數......
體育委員清點結束,人數為20
3. 迪米特法則的核心觀念就是類間解耦,最終可能產生的結果就是會產生了大量的中轉類。為了把解耦做到極致導致實現一個業務邏輯的實現跳轉了很多類,這也是不可取的做法。因此根據實際權衡利弊才是重要的。
六. Open Closed Principle : 開閉原則
1. 定義:開閉原則是Java裡最基礎的設計原則。具體的定義是:一個軟件實體,比如類,模塊,函數應該對擴展開放,對修改關閉。說的通熟易懂一些就是一個軟件實體應該通過擴展來實現變化,而不是通過修改已有的代碼來實現改變。
2. 舉一個實例的例子來說明一下開閉原則的具體做法。
① 一個簡單的圖書銷售系統。
import java.util.ArrayList; import java.util.List; interface Book{ int getPrice(); String getAuthor(); String getName(); int getCount(); } class NovelBook implements Book{ private int price; private String author; private String name; private int count; public NovelBook(int price, String author, String name, int count) { super(); this.price = price; this.author = author; this.name = name; this.count = count; } @Override public int getPrice() { return this.price; } @Override public String getAuthor() { return this.author; } @Override public String getName() { return this.name; } @Override public int getCount() { return this.count; } } public class BookStore { private static List<Book> books = new ArrayList<Book>(); static{ books.add(new NovelBook(30,"Author1","Java",100)); books.add(new NovelBook(40,"Author2","PHP",400)); books.add(new NovelBook(10,"Author3","JS",20)); books.add(new NovelBook(20,"Author4","Ajax",4)); } public static void main(String[] args){ System.out.println(" 圖書信息"); for(Book book : books){ System.out.println(" 書籍名稱:"+book.getName()+" 書籍作者:"+book.getAuthor()+" 書籍價格:"+book.getPrice()+" 書籍庫存:"+book.getCount()); } } }
控制台:
圖書信息
書籍名稱:Java 書籍作者:Author1 書籍價格:30 書籍庫存:100 書籍名稱:PHP 書籍作者:Author2 書籍價格:40 書籍庫存:400 書籍名稱:JS 書籍作者:Author3 書籍價格:10 書籍庫存:20 書籍名稱:Ajax 書籍作者:Author4 書籍價格:20 書籍庫存:4
②現在業務需求有改變,所有書均打七折。有三個方法可以解決這個問題:
第一種方法:修改接口。增加一個方法getOffPrice專門進行打折處理。
第二種方法:修改實現類,在實現類裡修改getPrice方法。
第三種方法:重新擴展一個類繼承NovelBook,重新復寫getPrice方法。
根據開放擴展關閉修改我原則我們應該選擇第三種解決方法。具體代碼試下如下:
import java.util.ArrayList; import java.util.List; interface Book{ int getPrice(); String getAuthor(); String getName(); int getCount(); } class NovelBook implements Book{ private int price; private String author; private String name; private int count; public NovelBook(int price, String author, String name, int count) { super(); this.price = price; this.author = author; this.name = name; this.count = count; } @Override public int getPrice() { return this.price; } @Override public String getAuthor() { return this.author; } @Override public String getName() { return this.name; } @Override public int getCount() { return this.count; } } class OffNovelBook extends NovelBook{ public OffNovelBook(int price, String author, String name, int count) { super(price, author, name, count); } public int getPrice() { int price = super.getPrice(); price = (int) (price * 0.6); return price; } } public class BookStore { private static List<Book> books = new ArrayList<Book>(); static{ books.add(new OffNovelBook(30,"Author1","Java",100)); books.add(new OffNovelBook(40,"Author2","PHP",400)); books.add(new OffNovelBook(10,"Author3","JS",20)); books.add(new OffNovelBook(20,"Author4","Ajax",4)); } public static void main(String[] args){ System.out.println(" 圖書信息"); for(Book book : books){ System.out.println(" 書籍名稱:"+book.getName()+" 書籍作者:"+book.getAuthor()+" 書籍價格:"+book.getPrice()+" 書籍庫存:"+book.getCount()); } } }
控制台:
圖書信息
書籍名稱:Java 書籍作者:Author1 書籍價格:18 書籍庫存:100 書籍名稱:PHP 書籍作者:Author2 書籍價格:24 書籍庫存:400 書籍名稱:JS 書籍作者:Author3 書籍價格:6 書籍庫存:20 書籍名稱:Ajax 書籍作者:Author4 書籍價格:12 書籍庫存:4