設計模式的六大原則:1、單一職責原則,其核心就是控制類別的粒度大小、將物件解耦、提高其內聚性;2、開閉原則,可以透過「抽象約束、封裝變化」來實現;3、里氏替換原則,主要闡述了有關繼承的一些原則;4、依賴倒置原則,降低了客戶與實現模組之間的耦合;5、接口隔離原則,是為了約束接口、降低類別對介面的依賴性;6、迪米特法則,要求限制軟體實體之間通訊的寬度和深度。
本教學操作環境:windows7系統、java8版、DELL G3電腦。
對於設計模式,自己很早之前就看了好多本設計模式書籍,其中一些還看了好幾遍,也一直希望自己能在編碼的時候把這些設計模式用上去。可是,在日常的打碼中,用的做多的就是單例,其次是觀察者和建造者模式 ( builder ) 用得比較多,其他的基本很少用到。
用不到的原因是還是不能夠理解設計模式的思想,無法將這些設計模式和編碼遇到的問題連結起來,從而用不到設計模式。
其實設計模式的提出都是為了解決一個常見的問題而總結出來的辦法。所以當你思考採用何種設計模式的時候,你應該先問自己當前問題的是什麼?根據問題去選取合適的設計模式。
等你熟悉了設計模式的以後,你會發現部分設計模式之間存在包含關係,甚至很相像,但是不同的設計模式解決的問題是不一樣的。
當我們在設計一個模組的時候可以從以下幾個角度去考慮:
這個模組與其他模組的關係是什麼樣的?
模組中哪些部分是不變的,哪些部分是在不斷變化的,又是如何變化的?
類別與類別之間的關係是怎麼樣的,為什麼需要依賴,怎麼可以不依賴?
要不要加上一個介面?介面的存在是為了解決什麼問題?
當然,本文並不是教你如何使用設計模式。而是講解設計模式的設計原則。設計模式在被設計出來的時候,也是遵循一些規則的。
設計模式六大原則,具體如下:
單一職責原則(類別與方法,介面)
開閉原則(擴展開放,修改關閉)
#里氏替換原則(基底類別和子類別之間的關係)
依賴倒置原則(依賴抽象接口,而不是具體物件)
介面隔離原則(介面依功能細分)
迪米特法則(類別與類別之間的親疏關係)
##每個設計原則旁邊都有個括號,是用來解釋,或是描述應用範圍的。以下將詳細介紹每一個原則。
#單一職責原則(Single Responsibility Principle,SRP)又稱為單一功能原則。這裡的職責是指類別變化的原因,單一職責原則規定一個類別應該有且僅有一個引起它變化的原因,否則類別應該被拆分(There should never be more than one reason for a class to change)。
該原則提出物件不應該承擔太多職責,如果一個物件承擔了太多的職責,至少存在以下兩個缺點:
一個職責的變化可能會削弱或抑制這個類別實現其他職責的能力;
當客戶端需要該物件的某一個職責時,不得不將其他不需要的職責全都包含進來,造成冗餘程式碼或程式碼的浪費。
單一職責原則的優點
#單一職責原則的核心就是控制類別的粒度大小、將物件解耦、提高其內聚性。如果遵循單一職責原則將有以下優點。
降低類別的複雜度。一個類別只負責一項職責,其邏輯肯定比負責多項職責簡單得多。
提高類別的可讀性。複雜性降低,自然其可讀性會提高。
提高系统的可维护性。可读性提高,那自然更容易维护了。
变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
单一职责原则的实现方法
单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
示例
public interface UserService { public void login(String username, String password); public void register(String email, String username, String password); public void logError(String msg); public void sendEmail(String email); }
这段代码很显然存在很大的问题,UserService 既要负责用户的注册和登录,还要负责日志的记录和邮件的发送,并且后者的行为明显区别于前者。<br/>
假设我要修改发送邮件的逻辑就得修改这个类,这时候 qa 还得回归登录注册逻辑,这样明显不合理。
因此我们需要进行拆分,根据具体的职能可将其具体拆分如下:
UserService:只负责登录注册
public interface UserService { public void login(String username, String password); public void register(String email, String username, String password); }
LogService :只负责日志<br/>
public interface LogService { public void logError(String msg); }
EmailService: 只负责发送邮件
public interface EmailService { public void sendEmail(String email); }
这时候,咱们再去回顾前面提到的优点,就能深深体会了。
这里只是讲了接口,其实对类也一样,甚至方法也是一样的。
对于类来说,根据类名,确保里面提供的方法都是属于这个类的。
对于方法,不要把不相关的对象实例作为参数传进来。如果你发现某个方法依赖某个不相关的对象,那么这个方法的实现可能就存在问题。
比如 android 中图片下载后显示到 imageView 中,我提供如下的方法:
loadImage(String url, ImageView view) { // 下载图片,展示图片 }
对于 loadImage 这个方法,参数 url 是ok 的,但是参数 ImageView 却是不合理的。因为这里做了两个操作,下载图片,展示图片。应该将这个方法在进行拆分:
// 下载图片 loadImage(String url) { } // 显示图片 displayImage(String url, ImageView view) { // 调用 getBitmap (url) 获取图片 // 获取图片后将其设置到 view 中。 } // 根据 url 获取图片, getBitmap(String url) { }
这样整个逻辑就很清晰。后续需要修改下载逻辑,也不会影响到展示逻辑。当然其实还有个问题是,这两个方法要不要放在一个类里面?
开闭原则的实现方法:可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。
因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
示例
// 矩形 public class Rectangle { public double getWidth() { return width; } public double getHeight() { return height; } }
需要计算矩形的面积
// 面积计算器 public class AreaCalculator { public double area(Rectangle shape){ return shape.getWidth() * shape.getHeight(); } }
假设这时候,又多了一个圆形类
// 圆形 public class Circular { public double getRadius(){ return radius; } }
同样也需要计算他的面积,这时候就会变成下面这样子:
public class AreaCalculator { public double area(Object shape){ if(shape instanceof Rectangle) { Rectangle rectangle = (Rectangle) shape; return rectangle.getWidth() * rectangle.getHeight(); } else if (shape instanceof Circular) { Circular circular = (Circular) shape; return circular.getRadius() * circular.getRadius() * Math.PI; } else { throw new RuntimeException("There is no such type."); } } }
这么更改完成,完全没有问题。但是在真实的生产环境中,情况更为复杂,更改涉及的部分较多,那样就可能导致牵一发动全身。并且,以前编写的经过测试的一些功能需要重新测试,甚至导致某些功能不可用。
改进版,把计算面积这个公有逻辑变成一个接口:
public interface Shape { public double getArea(); } public class Rectangle implements Shape{ public double getWidth() { return width; } public double getHeight() { return height; } public double getArea() { return getWidth() * getHeight(); } }
这样,当需求变更,需要计算圆形面积的时候,我们只需创建一个圆形的类,并实现 Shape 接口即可:<br/>
public class Circular implements Shape { public double getRadius(){ return radius; } public double getArea() { return getRadius() * getRadius() * Math.PI; } }
計算三角形面積、四邊形面積... 的時候,我們只需讓它們去實作 Shape 介面即可,無需修改原始程式碼。
里氏替換原則主要闡述了有關繼承的一些原則,也就是什麼時候應該使用繼承,什麼時候不應該使用繼承,以及其中蘊含的原理。里氏替換原是繼承復用的基礎,它反映了基底類別與子類別之間的關係,是對開閉原則的補充,是實現抽象化的具體步驟的規範。
里氏替換原則的作用
#里氏替換原則的主要作用如下。
里氏替換原則是實現開閉原則的重要方式之一。
它克服了繼承中重寫父類別造成的可重複使用性變差的缺點。
它是動作正確性的保證。即類別的擴充不會為現有的系統引入新的錯誤,降低了程式碼出錯的可能性。
加強程式的健壯性,同時變更時可以做到非常好的相容性,提高程式的維護性、可擴充性,降低需求變更時引入的風險。
里氏替換原則的實作方法(繼承)
里氏替換原則通俗來講就是:子類別可以擴充父類別的功能,但不能改變父類別原有的功能。也就是說:子類別繼承父類別時,除新增新的方法完成新增功能外,盡量不要重寫父類別的方法。
根據上述理解,對里氏替換原則的定義可以總結如下:
#子類別可以實作父類別的抽象方法,但不能覆寫父類別的非抽象方法
子類別中可以增加自己特有的方法
當子類別的方法重載父類別的方法時,方法的前置條件(即方法的輸入參數)要比父類別的方法更寬鬆
#當子類別的方法實作父類別的方法(重寫/重載或實作抽象方法),方法的後置條件(即方法的的輸出/傳回值)要比父類別的方法更嚴格或相等
透過重寫父類別的方法來完成新的功能寫起來雖然簡單,但是整個繼承體系的可重複使用性會比較差,特別是運用多型別比較頻繁時,程式運行出錯的機率會非常大。
如果程式違反了里氏替換原則,則繼承類別的物件在基底類別出現的地方會出現運行錯誤。
這時其修正方法是:取消原來的繼承關係,重新設計它們之間的關係。
關於里氏替換原則的例子,最有名的是「正方形不是長方形」。當然,生活中也有很多類似的例子,例如,企鵝、鴕鳥和幾維鳥從生物學的角度來劃分,它們屬於鳥類;但從類的繼承關係來看,由於它們不能繼承“鳥”會飛的功能,所以它們不能定義成「鳥」的子類。同樣,由於「氣球魚」不會游泳,所以不能定義成「魚」的子類;「玩具炮」炸不了敵人,所以不能定義成「砲」的子類等。
對於正方形和長方形最好的做法是再增加一個父類,他們同時繼承自這個父類。
依賴倒置原則是實現開閉原則的重要途徑之一,它降低了客戶與實作模組之間的耦合。
由於在軟體設計中,細節具有多變性,而抽象層則相對穩定,因此以抽象為基礎搭建起來的架構要比以細節為基礎搭建起來的架構要穩定得多。這裡的抽象指的是介面或抽象類別,而細節是指具體的實作類別。
使用介面或抽象類別的目的是製定好規範和契約,而不去涉及任何具體的操作,把展現細節的任務交給它們的實作類別去完成。
依賴、倒置原則的作用
依賴倒置原則的主要作用如下。
依賴倒置原則可以降低類別間的耦合性。
依賴倒置原則可以提高系統的穩定性。
依賴倒置原則可以減少並行開發所引起的風險。
依賴倒置原則可以提高程式碼的可讀性和可維護性。
依赖倒置原则的实现方法
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
每个类尽量提供接口或抽象类,或者两者都具备。
变量的声明类型尽量是接口或者是抽象类。
任何类都不应该从具体类派生。
使用继承时尽量遵循里氏替换原则。
依赖倒置原则在“顾客购物程序”中的应用。
分析:本程序反映了 “顾客类”与“商店类”的关系。商店类中有 sell() 方法,顾客类通过该方法购物以下代码定义了顾客类通过韶关网店 ShaoguanShop 购物
class Customer { public void shopping(ShaoguanShop shop) { //购物 System.out.println(shop.sell()); } }
但是,这种设计存在缺点,如果该顾客想从另外一家商店(如婺源网店 WuyuanShop)购物,就要将该顾客的代码修改如下:
class Customer { public void shopping(WuyuanShop shop) { //购物 System.out.println(shop.sell()); } }
顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。
存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。
解决方法是:定义“婺源网店”和“韶关网店”的共同接口 Shop,顾客类面向该接口编程,其代码修改如下:
class Customer { public void shopping(Shop shop) { //购物 System.out.println(shop.sell()); } } class Customer { public void shopping(Shop shop) { //购物 System.out.println(shop.sell()); } }
这样,不管顾客类 Customer 访问什么商店,或者增加新的商店,都不需要修改原有代码了,其类如下图所示:
package principle; public class DIPtest { public static void main(String[] args) { Customer wang=new Customer(); System.out.println("顾客购买以下商品:"); wang.shopping(new ShaoguanShop()); wang.shopping(new WuyuanShop()); } } //商店 interface Shop { public String sell(); //卖 } //韶关网店 class ShaoguanShop implements Shop { public String sell() { return "韶关土特产:香菇、木耳……"; } } //婺源网店 class WuyuanShop implements Shop { public String sell() { return "婺源土特产:绿茶、酒糟鱼……"; } } //顾客 class Customer { public void shopping(Shop shop) { //购物 System.out.println(shop.sell()); } }
程序的运行结果如下:
顾客购买以下商品: 韶关土特产:香菇、木耳…… 婺源土特产:绿茶、酒糟鱼……
介面隔離原則(Interface Segregation Principle,ISP)要求程式設計師盡量將臃腫龐大的介面拆分成更小的和更具體的接口,讓接口中只包含客戶感興趣的方法。
2002 年羅伯特·C.馬丁給「介面隔離原則」的定義是:客戶端不應該被迫依賴它不使用的方法(Clients should not be forced to depend on methods they do not use)。這個原則還有另一個定義:一個類別對另一個類別的依賴應該是建立在最小的介面上(The dependency of one class to another one should depend on the smallest possible interface)。
以上兩個定義的意思是:要為各個類別建立它們需要的專用接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類別去調用。
介面隔離原則和單一職責都是為了提高類別的內聚性、降低它們之間的耦合性,體現了封裝的思想,但兩者是不同的:
#單一職責原則注重的是職責,而介面隔離原則注重的是對介面依賴的隔離。
單一職責原則主要是約束類,它針對的是程式中的實作和細節;接口隔離原則主要約束接口,主要針對抽象和程式整體框架的建構。
介面隔離原則的優點
#介面隔離原則是為了約束介面、降低類別對介面的依賴性,遵循介面隔離原則有以下5 個優點。
將臃腫龐大的介面分解為多個粒度小的接口,可以預防外來變更的擴散,提高系統的靈活性和可維護性。
介面隔離提高了系統的內聚性,減少了對外交互,並降低了系統的耦合性。
如果介面的粒度大小定義合理,能夠保證系統的穩定性;但是,如果定義太小,則會造成介面數量過多,使設計複雜化;如果定義太大,靈活性降低,無法提供客製化服務,為整體專案帶來無法預料的風險。
使用多個專門的介面也能夠體現物件的層次,因為可以透過介面的繼承,實現對總介面的定義。
能減少專案工程中的程式碼冗餘。過大的大介面裡面通常放置許多不用的方法,實現這個介面的時候,就被迫設計冗餘的程式碼。
介面隔離原則的實作方法
在特定應用介面隔離原則時,應該依照下列幾個規則來衡量。
介面盡量小,但是要有極限。一個介面只服務於一個子模組或業務邏輯。
為依賴介面的類別客製化服務。只提供呼叫者需要的方法,屏蔽不需要的方法。
了解環境,拒絕盲從。每個項目或產品都有選定的環境因素,環境不同,介面拆分的標準就不同深入了解業務邏輯。
提高內聚,減少對外互動。使介面用最少的方法去完成最多的事情。
對於介面隔離,大家還是可以參考單一職責提到的範例:
public interface UserService { public void login(String username, String password); public void register(String email, String username, String password); public void logError(String msg); public void sendEmail(String email); }
这时候,应该就能理解拆分的好处了。
迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle,LKP),产生于 1987 年美国东北大学(Northeastern University)的一个名为迪米特(Demeter)的研究项目,由伊恩·荷兰(Ian Holland)提出,被 UML 创始者之一的布奇(Booch)普及,后来又因为在经典著作《程序员修炼之道》(The Pragmatic Programmer)提及而广为人知。
迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
迪米特法则的优点
迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。
降低了类之间的耦合度,提高了模块的相对独立性。
由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。
迪米特法则的实现方法
从迪米特法则的定义和特点可知,它强调以下两点:
从依赖者的角度来说,只依赖应该依赖的对象。
从被依赖者的角度说,只暴露应该暴露的方法。
所以,在运用迪米特法则时要注意以下 6 点。
在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
在类的结构设计上,尽量降低类成员的访问权限。
在类的设计上,优先考虑将一个类设置成不变类。
在对其他类的引用上,将引用其他对象的次数降到最低。
不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
谨慎使用序列化(Serializable)功能
明星与经纪人的关系实例。
分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则,其类图如下图所示。
package principle; public class LoDtest { public static void main(String[] args) { Agent agent=new Agent(); agent.setStar(new Star("林心如")); agent.setFans(new Fans("粉丝韩丞")); agent.setCompany(new Company("中国传媒有限公司")); agent.meeting(); agent.business(); } } //经纪人 class Agent { private Star myStar; private Fans myFans; private Company myCompany; public void setStar(Star myStar) { this.myStar=myStar; } public void setFans(Fans myFans) { this.myFans=myFans; } public void setCompany(Company myCompany) { this.myCompany=myCompany; } public void meeting() { System.out.println(myFans.getName()+"与明星"+myStar.getName()+"见面了。"); } public void business() { System.out.println(myCompany.getName()+"与明星"+myStar.getName()+"洽淡业务。"); } } //明星 class Star { private String name; Star(String name) { this.name=name; } public String getName() { return name; } } //粉丝 class Fans { private String name; Fans(String name) { this.name=name; } public String getName() { return name; } } //媒体公司 class Company { private String name; Company(String name) { this.name=name; } public String getName() { return name; } }
程序的运行结果如下:
粉丝韩丞与明星林心如见面了。 中国传媒有限公司与明星林心如洽淡业务。
到此,设计模式的六大原则就讲完了。
更多编程相关知识,请访问:编程教学!!
以上是設計模式的六大原則是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!