有幾天沒發文章了,一直有人在公眾號問我關於觀察者模式的問題,所以我決定抽時間寫一寫關於設計模式的內容。今天先介紹一些基礎的東西。
六大原則
我以前在面試別的人的時候,總是喜歡聊聊設計模式,因為總感覺功能部分都能寫出來,但是程式碼質量和程式碼設計的東西熟練,才能更好地跟團隊配合,方便產品的迭代。
六大原則是:
- 單一職責原則
- 里氏替換原則
- 依賴倒置原則
- 介面隔離原則
- 迪米特原則
- 開閉原則
也有人說是五大原則的,少了迪米特原則。乍一看,其中開發者們最熟悉的或者說聽得最多的也就是開閉原則了,其它聽起來都會有一些陌生。下面就一個個介紹一下。
單一職責原則
這是一個最簡單,卻最難做到的原則。為什麼這麼說呢?
它的定義只有一句話:不要存在多於一個導致類變更的原因。通俗的說,即一個類只負責一項職責。但為什麼說很難做到呢,我剛才想去我的程式碼中找到一個比較合適的例子來說明這個問題,卻沒有一個具有代表性的,因為職責這個概念有些主觀,介面根據職責劃分。下面想個簡單的例子吧。
例如一個使用者系統,有姓名和年齡兩個屬性:
public class Person {
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public void changeAge(String age) {
}
public void changeName(String age) {
}
}複製程式碼
這個類建立有什麼問題呢?name和age的set,get方法是資料型別,也就是業務物件,但是changeAge和changeName需要跟伺服器進行互動,屬於業務邏輯,甚至於在這兩個方法中還需要呼叫set,get方法。
根據單一職責原則,我們需要將業務和資料分開:
public class PersonObj {
private String name;
private String age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
}複製程式碼
public class PersonLogic {
public void changeAge(String age) {
}
public void changeName(String age) {
}
}複製程式碼
單一原則的好處:
- 可以降低類的複雜度,一個類只負責一項職責,其邏輯肯定要比負責多項職責簡單的多;
- 提高類的可讀性,提高系統的可維護性;
- 變更引起的風險降低,變更是必然的,如果單一職責原則遵守的好,當修改一個功能時,可以顯著降低對其他功能的影響。
里氏替換原則
定義1:如果對每一個型別為 T1的物件 o1,都有型別為 T2 的物件o2,使得以 T1定義的所有程式 P 在所有的物件 o1 都代換成 o2 時,程式 P 的行為沒有發生變化,那麼型別 T2 是型別 T1 的子型別。
定義2:所有引用基類的地方必須能透明地使用其子類的物件。
根據定義我們可以這樣理解:
- 子類必須完全實現父類的方法
- 子類可以有自己的個性
- 覆寫或實現父類的方法時輸入引數可以寬於或等於父類引數
- 覆寫或實現父類的方法時輸出結果可以窄於或等於父類引數
第一點好理解,後面是什麼意思呢?看個例子:
public class Father {
public void printf(HashMap map){
System.out.printf("父類方法");
}
}複製程式碼
public class Son {
public void printf(Map map){
System.out.printf("父類方法");
}
}複製程式碼
這樣只要傳入的引數是HashMap都是執行父類的方法,子類由於比父類引數要寬,相當於過載了printf方法。這樣做的目的不容易引起邏輯的混亂
依賴導致原則
定義:高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。
依賴倒置原則的核心思想是面向介面程式設計,我們依舊用一個例子來說明面向介面程式設計比相對於面向實現程式設計好在什麼地方。場景是這樣的,父親給孩子講故事,需要給他一本書,或一份報紙:
class Book{
public String getContent(){
return "讀書";
}
}
class NewsPaper{
public String getContent(){
return "報紙";
}
}
class Father{
public void read(Book book){
System.out.println("爸爸"+book.getContent());
}
public void read(NewsPaper news){
System.out.println("爸爸"+news.getContent());
}
}
public class Client{
public static void main(String[] args){
Father f = new Father();
f.read(new Book());
f.read(new NewsPaper());
}
}複製程式碼
上述程式碼沒有什麼問題,但是有一天我們再新增一個可讀的東西,如pad,那我們要重新寫一個pad類,同時Father類還要新增一個read方法。如果哪天再增加一個類,還有做如上處理,太麻煩了。
我們可以這樣優化一下:
interface IReader{
public String getContent();
}複製程式碼
然後讓Book Newspaper Pad類都實現這個介面,這樣父類只需要寫一個方法即可:
class Father{
public void read(IReader reader){
System.out.println("爸爸"+reader.getContent());
}
}複製程式碼
介面隔離原則
定義:客戶端不應該依賴它不需要的介面;一個類對另一個類的依賴應該建立在最小的介面上。
簡單翻譯一下這句話,就是一個類去實現介面的時候,不應該去實現他不需要的方法。
interface I {
public void method1();
public void method2();
public void method3();
public void method4();
public void method5();
}
class A{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method2();
}
public void depend3(I i){
i.method3();
}
}
class B implements I{
public void method1() {
System.out.println("類B實現介面I的方法1");
}
public void method2() {
System.out.println("類B實現介面I的方法2");
}
public void method3() {
System.out.println("類B實現介面I的方法3");
}
public void method4() {}
public void method5() {}
}
class C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
class D implements I{
public void method1() {
System.out.println("類D實現介面I的方法1");
}
//對於類D來說,method2和method3不是必需的,但是由於介面A中有這兩個方法,
//所以在實現過程中即使這兩個方法的方法體為空,也要將這兩個沒有作用的方法進行實現。
public void method2() {}
public void method3() {}
public void method4() {
System.out.println("類D實現介面I的方法4");
}
public void method5() {
System.out.println("類D實現介面I的方法5");
}
}
public class Client{
public static void main(String[] args){
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
}複製程式碼
可以看到,如果介面過於臃腫,只要介面中出現的方法,不管對依賴於它的類有沒有用處,實現類中都必須去實現這些方法,這顯然不是好的設計。如果將這個設計修改為符合介面隔離原則,就必須對介面I進行拆分。在這裡我們將原有的介面I拆分為三個介面。
nterface I1 {
public void method1();
}
interface I2 {
public void method2();
public void method3();
}
interface I3 {
public void method4();
public void method5();
}
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class B implements I1, I2{
public void method1() {
System.out.println("類B實現介面I1的方法1");
}
public void method2() {
System.out.println("類B實現介面I2的方法2");
}
public void method3() {
System.out.println("類B實現介面I2的方法3");
}
}
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class D implements I1, I3{
public void method1() {
System.out.println("類D實現介面I1的方法1");
}
public void method4() {
System.out.println("類D實現介面I3的方法4");
}
public void method5() {
System.out.println("類D實現介面I3的方法5");
}
}複製程式碼
介面隔離原則的含義是:建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。也就是說,我們要為各個類建立專用的介面,而不要試圖去建立一個很龐大的介面供所有依賴它的類去呼叫。本
迪米特法則
定義:一個物件應該對其他物件保持最少的瞭解。
說白點就是儘量降低耦合。
我在網上找到了這樣一個簡單例子,可以看一下,做個對比:
有一個集團公司,下屬單位有分公司和直屬部門,現在要求列印出所有下屬單位的員工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());
}
}
}複製程式碼
修改後,為分公司增加了列印人員ID的方法,總公司直接呼叫來列印,從而避免了與分公司的員工發生耦合。
開閉原則
這應該是我們聽得最多的一個原則了,在平時的專案開發中,用的也最多,因為誰也不想,一次產品的迭代,還需要修改核心程式碼,然後全部重新測試一次。
定義:一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。
這個定義沒有那麼複雜難理解,我不再做過多的解釋,我這裡還是通過一個小例子來說明:
public interface ICar {
public String getName();
public float getPrice();
}
public class Car implements ICar{
private String name;
private float price;
public Car(String name,float price){
this.name = name;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public float getPrice() {
return price;
}
}複製程式碼
當有一天我們獲取車的價格需要打折時,可以重新寫一個類SaleCar:
public class SaleCar extends Car{
public SaleCar(String name, float price) {
super(name, price);
}
@Override
public float getPrice() {
return super.getPrice()*8/10;
}
}複製程式碼
我們這樣做的目的是當有新功能出現的時候,儘量不要去修改原有的邏輯,可以實現一個新的類,然後覆寫父類的方法,這樣,原有的邏輯沒有變,新的需求也實現了。當有一天出現bug了,可以直接修改這一個類就可以。
更多的開發知識,可以關注我的公眾號: