記在前面:這個(gè)《設(shè)計(jì)模式》系列的文章,想了很久才決定寫的岩馍,一是還是本人的原則,只有通過自己表達(dá)出來的東西抖韩,才是真正屬于你的東西蛀恩,所以即使寫的不好,有什么理解不到位的茂浮,被人指出來也挺好的双谆,證明屬于我的東西還是有缺漏嘛。二是設(shè)計(jì)模式這個(gè)東西有點(diǎn)虛席揽,特別這篇原則顽馋,總覺得還欠缺很多理解。三是看了好幾篇設(shè)計(jì)模式幌羞,下面的討論基本分兩種寸谜,要么就是一堆Mark,要么就是一堆FXXk属桦,也是有點(diǎn)擔(dān)心會(huì)被罵吧熊痴,不過錯(cuò)了被指正也是很正常。不說廢話了聂宾,上正文果善。
本文屬于系列文章《設(shè)計(jì)模式》,附上文集鏈接
一. 單一職責(zé)原則
- 定義:不要存在多于一個(gè)導(dǎo)致類變更的原因系谐。簡(jiǎn)單說巾陕,就是一個(gè)類只負(fù)責(zé)一項(xiàng)職責(zé)。
- 為什么要這個(gè)原則:試想蔚鸥,一個(gè)類T惜论,用來實(shí)現(xiàn)兩個(gè)職責(zé)t1和t2,在需求變更的情況下需要修改t1止喷,可能導(dǎo)致原本正常工作的t2職責(zé)出錯(cuò)馆类。
但是這個(gè)原則,其實(shí)看到的話弹谁,很多人應(yīng)該都會(huì)覺得沒啥好看的吧乾巧,因?yàn)樘?jiǎn)單了句喜,而且在寫代碼的時(shí)候,應(yīng)該都不希望因?yàn)樾薷牧艘粋€(gè)功能導(dǎo)致其他的功能發(fā)生故障沟于。
但是翻了好幾篇博客咳胃,還有看到的書的作者也是說到,其實(shí)并沒有很多類設(shè)計(jì)能完全遵守到單一職責(zé)原則旷太,因?yàn)檫@個(gè)原則受太多因素制約了展懈,比如下面這個(gè)例子:
我們要造車,然后要車跑起來
public class Car {
public void run(String carType){
System.out.println("啟動(dòng)"+carType+"引擎跑起來了");
}
}
public class Client {
public static void main(String[] args) {
Car c = new Car();
c.run("BMW");
c.run("Benz");
}
}
結(jié)果:
啟動(dòng)BMW引擎跑起來了
啟動(dòng)Benz引擎跑起來了
后面發(fā)現(xiàn)還有類似上世紀(jì)的小包車這種需要拉才能跑起來的車供璧,我們?cè)趺崔k呢存崖?
首先按照單一職責(zé)原則,我們來另起一個(gè)類睡毒,來實(shí)現(xiàn)這個(gè)功能
public class Trolley {
public void run(String carType){
System.out.println("拉著"+carType+"跑起來了");
}
}
public class Client {
public static void main(String[] args) {
Car c = new Car();
c.run("BMW");
c.run("Benz");
Trolley trolley = new Trolley();
trolley.run("上世紀(jì)的小包車");
}
}
結(jié)果:
啟動(dòng)BMW引擎跑起來了
啟動(dòng)Benz引擎跑起來了
拉著上世紀(jì)的小包車跑起來了
但是這里就有一個(gè)問題了来惧,如果后期還有斗車這種要推的車,還有自行車這種要騎的車演顾,還有幾十種其他的車供搀,那我們難道要一個(gè)類一個(gè)類的寫嗎?這樣會(huì)造成類膨脹的钠至。而且修改了類葛虐,還得大幅度修改客戶端
所以在這里,再引入這個(gè)原則的同時(shí)棕洋,就得違背下這個(gè)原則了挡闰,如下:
public class Car {
public void run(String carType){
System.out.println("啟動(dòng)"+carType+"引擎跑起來了");
}
public void run2(String carType){
System.out.println("拉著"+carType+"跑起來了");
}
}
public class Client {
public static void main(String[] args) {
Car c = new Car();
c.run("BMW");
c.run("Benz");
c.run2("上世紀(jì)的小包車");
}
}
結(jié)果:
啟動(dòng)BMW引擎跑起來了
啟動(dòng)Benz引擎跑起來了
拉著上世紀(jì)的小包車跑起來了
這樣做的一個(gè)好處就是方便,直接在類里面新添加一個(gè)方法掰盘,在客戶端只需要做小小的改動(dòng)就可以用摄悯。這只是其中一個(gè)辦法,當(dāng)然愧捕,還有很多種辦法奢驯,比如在run方法中根據(jù)carType判斷來選擇方式等等,但是這都違背了單一職責(zé)原則次绘。所以這個(gè)原則怎么用呢瘪阁?個(gè)人理解,很多時(shí)候我們?cè)趯?shí)際的編碼過程中并不能完全遵守這個(gè)原則邮偎,我們只能盡量做到這個(gè)原則管跺。不然的話就是死守教規(guī)了。
二. 里氏替換原則
定義:所有引用父類的地方必須能透明地使用其子類的對(duì)象禾进。
為什么要用這個(gè)原則:試想一下豁跑,在一個(gè)類P1中,實(shí)現(xiàn)了方法m1泻云,然后現(xiàn)在需要擴(kuò)展m1方法艇拍,新的方法m2由P1的子類來完成狐蜕,在完成m2的同時(shí),有可能會(huì)將m1的方法破壞掉卸夕,使得m1不能正常工作层释。
上代碼,還是用上文的例子:
public class Car {
public void run(String carType){
System.out.println("啟動(dòng)"+carType+"引擎跑起來了");
}
}
public class Client {
public static void main(String[] args) {
Car c = new Car();
c.run("BMW");
}
}
結(jié)果:
啟動(dòng)BMW引擎跑起來了
現(xiàn)在需要加一個(gè)功能,就是對(duì)行車的速度進(jìn)行匯報(bào)快集,怎么做贡羔?首先想到的是不是直接修改Car的run方法,但是要注意碍讨,這個(gè)不是一個(gè)好的方法治力,修改了這個(gè)run方法,可能會(huì)導(dǎo)致本來在使用這個(gè)方法的其他對(duì)象產(chǎn)生錯(cuò)誤的結(jié)果勃黍,所以不能這樣,我們使用繼承晕讲,然后重寫run方法覆获,如下:
public class AdvancedCar extends Car{
@Override
public void run(String carType) {
super.run(carType);
System.out.println("車的速度是80KM/h");
}
}
public class Client {
public static void main(String[] args) {
AdvancedCar car = new AdvancedCar();
car.run("BMW");
}
}
結(jié)果:
啟動(dòng)BMW引擎跑起來了
車的速度是80KM/h
然后這里的確實(shí)現(xiàn)了功能,而且代碼還很整潔瓢省,只是一個(gè)繼承然后加多一句就好了弄息,但是問題就來了,假設(shè)再來需求勤婚,上班高峰期的時(shí)候塞車摹量,不需要匯報(bào)速度,下班加班趕回家才需求匯報(bào)速度(保平安嘛)馒胆,那咋辦缨称?在這個(gè)AdvancedCar中,run方法一經(jīng)調(diào)用祝迂,就會(huì)匯報(bào)速度的喔睦尽。用里氏替換原則來看,Car出現(xiàn)的地方型雳,AdvancedCar就不能用了当凡,我們來用里氏替換原則修改下代碼:
public class AdvancedCar extends Car{
public void showSpeed(){
System.out.println("車的速度是80KM/h");
}
}
public class Client {
public static void main(String[] args) {
AdvancedCar car = new AdvancedCar();
car.run("BMW");
car.showSpeed();
}
}
結(jié)果:
啟動(dòng)BMW引擎跑起來了
車的速度是80KM/h
這樣修改之后,Car出現(xiàn)的地方纠俭,AdvancedCar也能直接使用沿量,因?yàn)閞un方法的原有功能并沒有被破壞,而且要滿足上一段需求的話冤荆,只需要直接在Client這里加判斷就好朴则。
所以我自己的粗略理解就是:子類可以擴(kuò)展父類的方法,但不應(yīng)該復(fù)寫父類的方法匙赞。
三. 依賴倒置原則
定義:高層模塊不應(yīng)該依賴低層模塊佛掖,二者都應(yīng)該依賴其抽象妖碉;抽象不應(yīng)該依賴細(xì)節(jié);細(xì)節(jié)應(yīng)該依賴抽象芥被。
為什么要用這個(gè)原則:假設(shè)一個(gè)類P組合了一個(gè)類A欧宜,然后用A實(shí)現(xiàn)了相關(guān)的功能,然后現(xiàn)在要將A實(shí)現(xiàn)的功能改成B類來實(shí)現(xiàn)拴魄,這里的修改方法是只能去修改P的代碼冗茸,而這種直接修改代碼是會(huì)帶來不必要的風(fēng)險(xiǎn)的。
上例子,還是用車的例子:
public class Car {
// 給汽車加油
public void refuel(Gasoline90 gasoline){
System.out.println("加了型號(hào)為"+gasoline.getClass().getSimpleName()+"的汽油");
}
}
public class Gasoline90 { }
public class Client {
public static void main(String[] args) {
Car car = new Car();
car.refuel(new Gasoline90());
}
}
結(jié)果:
加了型號(hào)為Gasoline90的汽油
現(xiàn)在問題來了匹中,加油站沒有90汽油了夏漱,只有93和97,而汽車沒油了顶捷,難道但是加油refuel(Gasoline90 gasoline)只能加90汽油挂绰,咋辦,難不成直接修改car的代碼,給它多加兩個(gè)方法服赎,分別可以加93和97汽油葵蒂?很明顯不科學(xué)嘛,一點(diǎn)代碼復(fù)用都沒有重虑,更別談設(shè)計(jì)模式了践付,所以在這里,就要改了缺厉。
// 定義一個(gè)接口Gasoline
public interface Gasoline { }
//在Car類的refuel方法中傳入Gasoline參數(shù)
public class Car {
public void refuel(Gasoline gasoline){
System.out.println("加了型號(hào)為"+gasoline.getClass().getSimpleName()+"的汽油");
}
}
// 寫90和97兩個(gè)汽油的類
public class Gasoline90 implements Gasoline{ }
public class Gasoline97 implements Gasoline{ }
// 場(chǎng)景
public class Client {
public static void main(String[] args) {
Car car = new Car();
//car.refuel(new Gasoline90());
System.out.println("----汽車站沒有90汽油了-----");
car.refuel(new Gasoline97());
}
}
結(jié)果:
----汽車站沒有90汽油了-----
加了型號(hào)為Gasoline97的汽油
在上面的例子永高,傳參的時(shí)候傳入了一個(gè)Gasoline類型,只要汽車要加的汽油提针,全都實(shí)現(xiàn)這個(gè)借口命爬,就可以讓汽車自由加油,管它什么93,97,1997都好关贵,而且還不用修改Car的代碼遇骑,只需要實(shí)現(xiàn)Gasoline接口就好。
四. 接口隔離原則
咋看一下揖曾,我以為像現(xiàn)實(shí)生活中那樣落萎,隔離病原體是把病原體隔離開來,那接口隔離原則難道是隔離接口炭剪?編程界的名詞確實(shí)不能一般對(duì)待练链。
- 定義:客戶端不應(yīng)該依賴它不需要的接口;一個(gè)類對(duì)另一個(gè)類的依賴應(yīng)該建立在最小的接口上
- 為什么需要這個(gè)原則:假想我們?cè)O(shè)計(jì)了一個(gè)接口I奴拦,里面有五個(gè)方法媒鼓,分別是m1,m2,m3,m4,m5,而有兩個(gè)類A和B,分別需要用m1m2m5,m3,m4,m5方法绿鸣。那么無論是那一個(gè)類疚沐,在實(shí)現(xiàn)接口I的時(shí)候,都要將其本身不需要的類進(jìn)行實(shí)現(xiàn)潮模,很明顯亮蛔,這不是一個(gè)好設(shè)計(jì)。
上代碼擎厢,還是用車的例子:
// 接口
public interface ICar {
public void run(String carType);
public void showSpeed();
public void playMusic(String songName);
}
// 實(shí)現(xiàn)類
public class Car implements ICar{
public void run(String carType){
System.out.println("啟動(dòng)"+carType+"引擎跑起來了");
}
public void showSpeed() {
System.out.println("汽車的速度為80KM/h");
}
public void playMusic(String songName) {
System.out.println("放起了動(dòng)聽的"+songName);
}
}
// 場(chǎng)景
public class Client {
public static void main(String[] args) {
Car car = new Car();
car.run("BMW");
car.showSpeed();
car.playMusic("成都");
}
}
這里問題就來了究流,并不是所有的車都有放音樂的功能,也并不是所有的車都有展示速度的功能动遭,但是只有上面代碼這個(gè)車的話芬探,我們?cè)谛陆ㄆ渌噷?duì)象的時(shí)候,卻帶上了全部的功能厘惦,顯然偷仿,這是不科學(xué)的。改進(jìn)下宵蕉,將上面的接口拆分成兩個(gè)接口專業(yè)的車IProfessionalCar
和娛樂功能的車IEntertainingCar
// 接口
public interface IProfessionalCar {
public void run(String carType);
public void showSpeed();
}
public interface IEntertainingCar {
public void run(String carType);
public void playMusic(String songName);
}
// 實(shí)現(xiàn)類
public class ProfessionalCar implements IProfessionalCar {
public void run(String carType){
System.out.println("啟動(dòng)"+carType+"引擎跑起來了");
}
public void showSpeed() {
System.out.println("汽車的速度為80KM/h");
}
}
public class EntainingCar implements IEntertainingCar {
public void run(String carType){
System.out.println("啟動(dòng)"+carType+"引擎跑起來了");
}
public void playMusic(String songName) {
System.out.println("放起了動(dòng)聽的"+songName);
}
}
// 場(chǎng)景
public class Client {
public static void main(String[] args) {
IProfessionalCar professionalCar = new ProfessionalCar();
professionalCar.run("F1方程式");
professionalCar.showSpeed();
EntainingCar entainingCar = new EntainingCar();
entainingCar.run("壞了速度儀表盤的SUV");
entainingCar.playMusic("成都");
}
}
結(jié)果:
啟動(dòng)F1方程式引擎跑起來了
汽車的速度為80KM/h
啟動(dòng)壞了速度儀表盤的SUV引擎跑起來了
放起了動(dòng)聽的成都
接口隔離原則的要求我們炎疆,建立單一接口,不要建立龐大臃腫的接口国裳,盡量細(xì)化接口,接口中的方法盡量少全跨。這通過分散定義多個(gè)接口缝左,可以預(yù)防外來變更的擴(kuò)散,提高系統(tǒng)的靈活性和可維護(hù)性浓若。
五. 迪米特法則
- 定義:一個(gè)對(duì)象應(yīng)該對(duì)其他對(duì)象保持最少的了解渺杉。
- 為什么需要這個(gè)原則:原因就是一個(gè)對(duì)象對(duì)另一個(gè)對(duì)象了解得越多,那么挪钓,它們之間的耦合性也就越強(qiáng)是越,當(dāng)修改其中一個(gè)對(duì)象時(shí),對(duì)另一個(gè)對(duì)象造成的影響也就越大碌上。
上例子倚评,還是用車:
// 車
public class Car {
private String carType;
public Car(String carType){
this.carType = carType;
}
public void run(){
System.out.println("啟動(dòng)"+carType+"引擎跑起來了");
}
public void refuel(Gasoline gasoline){
System.out.println("加了型號(hào)為"+gasoline.getName()+"汽油");
}
}
// 汽油
public class Gasoline {
private String name;
public Gasoline(String name) {
this.name = name;
}
public String getName() {
return name;
}
private boolean quality = true;
public boolean getQuality(){
return this.quality;
}
}
// 人
public class Person {
private Car car;
public void setCar(Car car) {
this.car = car;
}
public void drive(){
car.run();
}
public void refuel(Gasoline gasoline){
if(gasoline.getQuality()){
System.out.println("油的質(zhì)量過關(guān),可以放心加");
car.refuel(gasoline);
}
}
// 場(chǎng)景類
public class Client {
public static void main(String[] args) {
Person jack = new Person();
jack.setCar(new Car("Suv"));
jack.drive();
System.out.println("*********開了三百公里馏予,沒油了********");
jack.refuel(new Gasoline("90"));
}
}
結(jié)果:
啟動(dòng)Suv引擎跑起來了
*********開了三百公里天梧,沒油了********
油的質(zhì)量過關(guān),可以放心加
加了型號(hào)為90汽油
我們可以看到霞丧,一個(gè)很符合生活的場(chǎng)景呢岗,jack開車,然后開太久了,沒油了后豫,于是加油悉尾,但是問題就來了,在生活中挫酿,加油這個(gè)動(dòng)作應(yīng)該是jack和加油站的工作人員進(jìn)行交涉构眯,然后由加油站的工作人員來完成,而在上面這里饭豹,則是由jack自己完成鸵赖。而且對(duì)油的質(zhì)量的檢驗(yàn),我們普通人怎么會(huì)拄衰,肯定不行啊它褪,萬一90,93,97的檢驗(yàn)方法各不相同,萬一以后的油質(zhì)量越來越不好翘悉,檢驗(yàn)步驟要變茫打,難道要修改Person類的方法,不對(duì)啊妖混。
我們來改下
// 增加類加油站工人
public class WorkerInPetrolStation {
public void refuel(Car car, String gasolineName) {
Gasoline gasoline = new Gasoline(gasolineName);
if (gasoline.getQuality()) {
System.out.println("油的質(zhì)量過關(guān)老赤,可以放心加");
car.refuel(gasoline);
}
}
}
// 將Person的refuel方法修改成依賴工人
public class Person {
private Car car;
public void setCar(Car car) {
this.car = car;
}
public void drive(){
car.run();
}
public void refuel(WorkerInPetrolStation worker, String gasolineName){
worker.refuel(this.car, gasolineName);
}
}
// Car,Gasoline不變
// 場(chǎng)景類
public class Client {
public static void main(String[] args) {
Person jack = new Person();
jack.setCar(new Car("Suv"));
jack.drive();
System.out.println("*********開了三百公里制市,沒油了********");
jack.refuel(new WorkerInPetrolStation(),"90");
}
}
結(jié)果:
啟動(dòng)Suv引擎跑起來了
*********開了三百公里抬旺,沒油了********
油的質(zhì)量過關(guān),可以放心加
加了型號(hào)為90汽油
現(xiàn)在無論以后油那邊怎么變祥楣,都和我們Persion無關(guān)开财,交給加油站工人嘛,這是他們的飯碗误褪。
迪米特法則的初衷是降低類之間的耦合责鳍,由于每個(gè)類都減少了不必要的依賴,因此的確可以降低耦合關(guān)系兽间。但是凡事都有度历葛,雖然可以避免與非直接的類通信,但是要通信嘀略,必然會(huì)通過一個(gè)“中介”來發(fā)生聯(lián)系恤溶。過分的使用迪米特原則,會(huì)產(chǎn)生大量這樣的中介和傳遞類屎鳍,導(dǎo)致系統(tǒng)復(fù)雜度變大宏娄。
六. 開閉原則
- 定義:對(duì)修改關(guān)閉,對(duì)擴(kuò)展開放
- 為什么使用這個(gè)原則:這不就是代碼重用的一個(gè)終極目標(biāo)嗎逮壁,不實(shí)現(xiàn)這個(gè)原則孵坚,改代碼改到懷疑人生傲竿稹!而上面的五個(gè)原則卖宠,其實(shí)就是這個(gè)原則的體現(xiàn)巍杈。
但是這個(gè)原則,正是因?yàn)樘呒?jí)了扛伍,所以太虛了筷畦,虛的沒什么套路可尋,只能靠經(jīng)驗(yàn)刺洒,領(lǐng)悟來慢慢體會(huì)鳖宾。
總結(jié):
以上就是我對(duì)這六個(gè)原則的簡(jiǎn)單理解,例子也不知道舉得恰不恰當(dāng)逆航,文字的描述也不知道是不是到位鼎文,但是這也就是我的理解了,等深入這行有一定時(shí)間因俐,或許會(huì)對(duì)這篇東西覺得很傻拇惋,噗嗤一聲,完全不屑抹剩。但也是以后的事了撑帖。
歡迎前來責(zé)罵。澳眷。胡嘿。