JAVA設計模式之六大設計原則
阿新 • • 發佈:2018-12-15
在程式設計中,我們通常要遵循以下六大原則:
單一職責原則
- 官方定義:
- 就一個類(介面、結構體、方法等等)而言,有且僅有一個引起它變化的原因。
- 個人理解:
- 通俗的來講做一件事就是專注做一件事,不可以三心二意。任務物件只是專注於一項職責,不去承擔太多的責任。當任務物件的職責發生變化時,不會對其他的物件產生影響。
- 遵循單一職責原的優點:
- 可以大大降低耦合度。
- 降低類的複雜度。
- 提高類的可讀性。
- 降低因變更而引起的風險。
- 提高類的複用性和可維護性。
- 單一職責應用:
- 背景:有一個類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());
}
}
}
開閉原則
- 官方定義:
- 一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。
- 個人理解:
- 如果修改或者新增一個功能,應該是通過擴充套件原來的程式碼,而不是通過修改原來的程式碼。
- 應用場景:略。
總的來說,程式設計儘量的貼近軟體程式設計的總的原則:高內聚,低耦合。