1. 程式人生 > >JAVA設計模式之六大設計原則

JAVA設計模式之六大設計原則

在程式設計中,我們通常要遵循以下六大原則: 在這裡插入圖片描述

單一職責原則

  • 官方定義:
    • 就一個類(介面、結構體、方法等等)而言,有且僅有一個引起它變化的原因。
  • 個人理解:
    • 通俗的來講做一件事就是專注做一件事,不可以三心二意。任務物件只是專注於一項職責,不去承擔太多的責任。當任務物件的職責發生變化時,不會對其他的物件產生影響。
  • 遵循單一職責原的優點:
    • 可以大大降低耦合度。
    • 降低類的複雜度。
    • 提高類的可讀性。
    • 降低因變更而引起的風險。
    • 提高類的複用性和可維護性。
  • 單一職責應用:
    • 背景:有一個類A,他需要負責T1和T2。但是當職責T1因為需求而改變類A的時候,就會對職責T2造成影響,導致T2不能正常工作。
    • 解決辦法:針對職責T1建立類A,針對職責T2建立類B。這樣就可以達到當修改類A時不會對職責T2造成影響,當修改類B時不會對職責T1造成影響。

里氏替換原則

  • 官方定義:
    • 里氏代換原則(Liskov Substitution Principle LSP)面向物件設計的基本原則之一。 里氏代換原則中說,任何基類可以出現的地方,子類一定可以出現。 LSP是繼承複用的基石,只有當衍生類可以替換掉基類,軟體單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。
  • 個人理解:
    • 繼承必須確保父類所擁有的性質在子類中仍然成立。
    • 或者說子類可以擴充套件父類的功能,但不能改變父類原有的功能。
    • 子類中可以增加自己特有的方法。
    • 子類的方法實現父類的抽象方法時,方法的返回值要比父類的返回值更加嚴謹。
  • 繼承是面向物件的三大特性之一,在程式設計方面帶來了很大的便利性,但是同時也存在一些不好的地方:增加了物件間的耦合性,如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改後,所有涉及到子類的功能都有可能會產生故障。
  • 經典案例之正方形不是長方形:
    • 在大眾的認知範圍內長方形的長和寬是不相等的,正方形的長和寬是相等的,正方形屬於特殊的長方形。
    • 先定義一個長方形類
public class Rectangle {

	private int width;
	private int height;

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getHeight() {
		return height;
	}

	public void setHeight(int height) {
		this.height = height;
	}

	public Rectangle(int width, int height) {
		this.width = width;
		this.height = height;
	}

	public int area() {
		return width * height;
	}

}

  • 再定義一個正方形類,繼承自長方形類
public class Square extends Rectangle {

	private int width;

	public int getWidth() {
		return width;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public Square(int width, int height) {
		super(width, height);
	}
	
	/*
	 * 重寫 area()
	 * 
	 * @see design.Rectangle#area()
	 */
	public int area() {
		return width * width;
	}
}
  • 輸出結果
public class Tester {

	public static void main(String[] args) {
		Rectangle rectangle = new Rectangle(10, 20);
		System.out.println("面積:" + rectangle.area());
	}
	// 輸出結果為面積:200
}
public class Tester {

	public static void main(String[] args) {
		Square rectangle = new Square(10, 20);
		System.out.println("面積:" + rectangle.area());
	}
	// 輸出結果為面積:0
}
  • 分析:為什麼當Rectangle替換為Square之後,面積的結果出錯了呢?因為在Square類裡重寫了area()方法,很明顯違背了里氏替換原則,改變了父類的原有功能,所以導致輸出結果不對。

依賴倒置原則

所謂依賴倒置原則(Dependence Inversion Principle)就是要依賴於抽象,不要依賴於具體。實現開閉原則的關鍵是抽象化,並且從抽象化匯出具體化實現,如果說開閉原則是面向物件設計的目標的話,那麼依賴倒置原則就是面向物件設計的主要手段。

  • 官方定義:
    • 高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。
  • 個人理解:
    • 依賴倒置原則的核心思想就是讓我們面向介面程式設計
    • 抽象是面向物件的三大特性之一,相對於細節的多變性,抽象的東西要穩定的多。在Java中抽象指的就是介面和抽象類,介面和抽象類只是定義好規範和規則,而不去關注任何實現,具體的實現都交給實現類去關注。
  • 應用場景是這樣的,母親給孩子講故事,只要給她一本書,她就可以照著書給孩子講故事了。

class Book{
	public String getContent(){
		return "很久很久以前有一個阿拉伯的故事……";
	}
}
 
class Mother{
	public void narrate(Book book){
		System.out.println("媽媽開始講故事");
		System.out.println(book.getContent());
	}
}
 
public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
	}
}
執行結果:

媽媽開始講故事
很久很久以前有一個阿拉伯的故事……

但是現在問題來了,當我修改了Book類,由原先的讀書變成讀報紙,這個時候Mother類就不會了,那麼這個問題怎麼解決呢?

兩種方法解決: ①第一種:直接去修改Mother類,將Book類直接替換成Newspaper類。這樣是可以解決一時的問題,但是將來我不僅僅是讀書,而且還要讀郵件等等呢,Mother和Book之間的耦合性太高了,所以不是一個好的辦法。 ②第二種:我們引入一個抽象的介面IReader。

interface IReader{
	public String getContent();
}

Mother類與介面IReader發生依賴關係,而Book和Newspaper都屬於讀物的範疇,他們各自都去實現IReader介面,這樣就符合依賴倒置原則了,程式碼修改為:

class Newspaper implements IReader {
	public String getContent(){
		return "林書豪17+9助尼克斯擊敗老鷹……";
	}
}
class Book implements IReader{
	public String getContent(){
		return "很久很久以前有一個阿拉伯的故事……";
	}
}
 
class Mother{
	public void narrate(IReader reader){
		System.out.println("媽媽開始講故事");
		System.out.println(reader.getContent());
	}
}
 
public class Client{
	public static void main(String[] args){
		Mother mother = new Mother();
		mother.narrate(new Book());
		mother.narrate(new Newspaper());
	}
}
執行結果:

媽媽開始講故事
很久很久以前有一個阿拉伯的故事……
媽媽開始講故事
林書豪17+9助尼克斯擊敗老鷹……

這樣修改後,無論以後怎樣擴充套件Client類,都不需要再修改Mother類了。

  • 最佳實踐
    • 每個類儘量都有介面或抽象類,或者抽象類和介面兩者都具備。
    • 變數的顯示型別儘量是介面或者是抽象類。
    • 任何類都不應該從具體類派生。
    • 儘量不要覆寫基類的方法。
    • 結合里氏替換原則使用。

介面隔離原則

  • 官方定義:
    • 客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。
  • 個人理解:
    • 單從字面上理解:使用多個隔離的介面,而不是使用單一的介面。
    • 介面隔離原則的本意是降低類之間的耦合性。在大型的軟體設計中,為了方便維護和擴充套件,降低類之間的依賴和耦合是很必要的。
    • 建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。
  • 舉一反三
    • 一開始我自己覺的介面隔離原則跟之前的單一職責原則很相似,其實不然。
    • 其一,單一職責原則原注重的是職責;而介面隔離原則注重對介面依賴的隔離。
    • 其二,單一職責原則主要是約束類,其次才是介面和方法,它針對的是程式中的實現和細節;而介面隔離原則主要約束介面介面,主要針對抽象,針對程式整體框架的構建。
  • 最佳實踐:
    • 介面儘量小,但是要有限度。對介面進行細化可以提高程式設計靈活性是不掙的事實,但是如果過小,則會造成介面數量過多,使設計複雜化。所以一定要適度。
    • 為依賴介面的類定製服務,只暴露給呼叫的類它需要的方法,它不需要的方法則隱藏起來。只有專注地為一個模組提供定製服務,才能建立最小的依賴關係。
    • 提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情。

迪米特法則

  • 官方定義:
    • 一個物件應該對其他物件保持最少的瞭解。
  • 個人理解:
    • 一個實體應當儘量少的與其他實體之間發生相互作用,使得系統功能模組相對獨立。
    • 也就是說一個軟體實體應當儘可能少的與其他實體發生相互作用。
    • 當一個模組修改時,就會盡量少的影響其他的模組,擴充套件會相對容易,就是說可擴充套件性很好。
  • 舉一個例子:有一個集團公司,下屬單位有分公司和直屬部門,現在要求打印出所有下屬單位的員工ID。先來看一下違反迪米特法則的設計。
//總公司員工
class Employee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}
 
//分公司員工
class SubEmployee{
	private String id;
	public void setId(String id){
		this.id = id;
	}
	public String getId(){
		return id;
	}
}
 
class SubCompanyManager{
	public List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//為分公司人員按順序分配一個ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
}
 
class CompanyManager{
 
	public List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//為總公司人員按順序分配一個ID
			emp.setId("總公司"+i);
			list.add(emp);
		}
		return list;
	}
	
	public void printAllEmployee(SubCompanyManager sub){
		List<SubEmployee> list1 = sub.getAllEmployee();
		for(SubEmployee e:list1){
			System.out.println(e.getId());
		}
 
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}
}
 
public class Client{
	public static void main(String[] args){
		CompanyManager e = new CompanyManager();
		e.printAllEmployee(new SubCompanyManager());
	}
}

現在這個設計的主要問題出在CompanyManager中,根據迪米特法則,只與直接的朋友發生通訊,而SubEmployee類並不是CompanyManager類的直接朋友(以區域性變量出現的耦合不屬於直接朋友),從邏輯上講總公司只與他的分公司耦合就行了,與分公司的員工並沒有任何聯絡,這樣設計顯然是增加了不必要的耦合。按照迪米特法則,應該避免類中出現這樣非直接朋友關係的耦合。修改後的程式碼如下:

class SubCompanyManager{
	public List<SubEmployee> getAllEmployee(){
		List<SubEmployee> list = new ArrayList<SubEmployee>();
		for(int i=0; i<100; i++){
			SubEmployee emp = new SubEmployee();
			//為分公司人員按順序分配一個ID
			emp.setId("分公司"+i);
			list.add(emp);
		}
		return list;
	}
	public void printEmployee(){
		List<SubEmployee> list = this.getAllEmployee();
		for(SubEmployee e:list){
			System.out.println(e.getId());
		}
	}
}
 
class CompanyManager{
	public List<Employee> getAllEmployee(){
		List<Employee> list = new ArrayList<Employee>();
		for(int i=0; i<30; i++){
			Employee emp = new Employee();
			//為總公司人員按順序分配一個ID
			emp.setId("總公司"+i);
			list.add(emp);
		}
		return list;
	}
	
	public void printAllEmployee(SubCompanyManager sub){
		sub.printEmployee();
		List<Employee> list2 = this.getAllEmployee();
		for(Employee e:list2){
			System.out.println(e.getId());
		}
	}
}

開閉原則

  • 官方定義:
    • 一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。
  • 個人理解:
    • 如果修改或者新增一個功能,應該是通過擴充套件原來的程式碼,而不是通過修改原來的程式碼。
  • 應用場景:略。

總的來說,程式設計儘量的貼近軟體程式設計的總的原則:高內聚,低耦合。