OOP 五大設(shè)計(jì)原則

1肩袍、概述

OOP(Object-oriented programming),指面向?qū)ο蟪绦蛟O(shè)計(jì)。也是目前主流的程序設(shè)計(jì)思想扒腕。這邊討論下由Robert C. Martin在21世紀(jì)初定義的關(guān)于OOP的五個(gè)原則SOLID年扩。正確的應(yīng)用慌申,可以讓程序更加易于維護(hù)、重用和擴(kuò)展睡汹。其主要目的也是為了讓軟件具有低耦合肴甸,高內(nèi)聚和強(qiáng)封裝。通過將這些原則有機(jī)的結(jié)合在一起帮孔,能夠讓開發(fā)者編寫出更高質(zhì)量的強(qiáng)大代碼雷滋。關(guān)于這些設(shè)計(jì)原則不撑,最好是在實(shí)習(xí)開發(fā)需要中根據(jù)實(shí)際情況靈活使用文兢,不要為了遵守而遵守。但是如果對于一個(gè)實(shí)際需求或者功能焕檬,如果沒有充足的違背這些原則的理由姆坚,默認(rèn)還是遵守,尤其是當(dāng)代碼量很龐大的時(shí)候实愚。對于一個(gè)復(fù)雜的工程有時(shí)候就算可能會犧牲一點(diǎn)點(diǎn)運(yùn)行的時(shí)間或者空間兼呵,也最好不要違背這些原則。接下來詳細(xì)介紹一下腊敲。

2击喂、單一職責(zé)原則

SRP(Single-responsibility principle):There should be never more than one reason for a class to change. 說的是一個(gè)類只應(yīng)該有一個(gè)引起它改變的理由。

這個(gè)原則的思想我覺得是OOP這幾個(gè)原則中最重要的一個(gè)碰辅。聲明一個(gè)對象/類應(yīng)該只有一個(gè)職責(zé)懂昂,并且它應(yīng)該由類完全封裝。這個(gè)原則將導(dǎo)致類中有更強(qiáng)的內(nèi)聚力而依賴類之間更松散的耦合没宾,從而有更好的可讀性和更低復(fù)雜性的代碼凌彬。當(dāng)一個(gè)類有各種各樣的責(zé)任時(shí),要理解和編輯這個(gè)類會變得更加困難循衰。因此铲敛,如果有多個(gè)職責(zé),那最好將其拆分開來会钝。這一點(diǎn)Android 中的?RecyclerView(Android RecyclerView內(nèi)部機(jī)制)做的非常好伐蒋。其將里面的Recycler類和LayoutManager類各自負(fù)責(zé)的功能進(jìn)行隔離,為了做到職責(zé)之間的分離迁酸,甚至犧牲了一些運(yùn)行時(shí)的空間和時(shí)間咽弦。

例子如下:

```

class Rectangle{

? ? ? ? private int mVar1;

? ? ? ? private?int mVar2;

? ? ? ? public double calculateArea(){

????????}

? ? ? ?punlic? void drawView(){

????????}

}

```

上面一個(gè)類 里面有兩個(gè)方法calculateArea()和drawView()。area()負(fù)責(zé)返回矩形大小胁出,drawView()負(fù)責(zé)繪制矩形型型。然后area()函數(shù)會由另外一個(gè)計(jì)算模塊需要調(diào)用來使用,而draw()是由GUI的模塊來使用全蝶,這時(shí)候如果GUI模塊發(fā)生變化闹蒜,就有可能引起draw()的改變寺枉,而如果drawView()中用到了mVar1和mVar2屬性,并且calculateArea()也用到了mVar2和mVar2屬性绷落。那么修改draw()的代碼就有可能會觸發(fā)area()中代碼的修改姥闪。改成這樣就比較合適了:

```

class Rectangle{

? ? ? ? RectangleGeometry mRectangleGeometry1;

? ? ? ? RectangleView mRectangleView2;

? ? ? ? ...

}

class RectangleGeometry{

? ? ? ? private int mVar1;

? ? ? ? private int mVar2;

? ??????public double calculateArea(){

? ? ? ? }

? ? ? ? ...

}

class RectangleView{

? ? ? ? private int mVar1;

? ? ? ? private int mVar2;

? ??????public double drawView(){

? ? ? ? }

? ? ? ? ...

}

```

上面的代碼將負(fù)責(zé)計(jì)算矩形的職責(zé)和繪制矩形的職責(zé)分離開來,這樣當(dāng)需要其中一塊需要修改的時(shí)候砌烁,就不會涉及到另外一塊的修改筐喳。但是這么做的問題就是有可能會產(chǎn)生類個(gè)數(shù)的膨脹和碎片化。這個(gè)就需要開發(fā)者自己去平衡了函喉。

3避归、開閉原則

OCP(The Open-Closed Principle):Software entities (classes, modules, functions, etc…) should be open for extension, but closed for modification. 說的是軟件開發(fā)應(yīng)該對擴(kuò)展開發(fā),對修改關(guān)閉管呵。當(dāng)開發(fā)者想要向?qū)ο筇砑有鹿δ軙r(shí)梳毙,應(yīng)該通過通過將AbstractClass / Interface的繼承來擴(kuò)展它。而不應(yīng)該在去修改現(xiàn)有的類捐下。因?yàn)橛锌赡芰硪粋€(gè)對象也在使用這個(gè)類账锹,這樣如果擅自修改可能會引起其他使用這個(gè)類的對象出錯(cuò)。而通過繼承(這邊指廣義上的繼承坷襟,包括繼承類奸柬、實(shí)現(xiàn)接口、甚至使用組合或者其他設(shè)計(jì)模式來繼承原有類的功能)婴程,來擴(kuò)展原有類則不僅可以復(fù)用原有類的功能廓奕,并且新增功能來實(shí)現(xiàn)特定需求。

例子如下:

```

public class Rectangle {

? ? ? ? private double mLength;?

? ? ? ? private double mHeight;?

? ? ? ? private double mArea;

?????????// getters/setters??

}

class Circle {?

????????private double mRadius;?

????????private double mArea;

?????????// getters/setters ...

}

public class AreaFactory {?

????public double calculateArea(ArrayList... shapes) {

? ? ? ? ????double area? ;

? ? ? ????? for (Object shape : shapes) {

? ? ? ? ? ? ????if (shape instanceof Rectangle) {

? ? ? ? ? ? ? ? Rectangle rect = (Rectangle)shape;

? ? ? ? ? ? ? ? area += (rect.getLength() * rect.getHeight());? ? ? ? ? ? ? ?

? ? ? ? ? ? } else if (shape instanceof Circle) {

? ? ? ? ? ? ? ? Circle circle = (Circle)shape;

? ? ? ? ? ? ? ? area +=

? ? ? ? ? ? ? ? (circle.getRadius() * cirlce.getRadius() * Math.PI);

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? throw new RuntimeException("Shape not supported");

? ? ? ? ? ? }? ? ? ? ? ?

? ? ? ? }

? ? ? ? return area;

? ? }

}

```

上面的代碼就沒有遵守開閉原則排抬,如果每次多一個(gè)圖形對象懂从,AreaFactory 中的calculateArea()就要進(jìn)行修改。同時(shí)每一個(gè)圖形對象都要有一個(gè)area屬性蹲蒲。改成這樣比較合適:

```

public?abstract?class Shape {?

? ?? private double mArea;

? ? ?// getters/setters?

?????double getArea();?

}

public class Rectangle implements Shape{?

????private double length;?

? ? private double height;?

? ? // getters/setters?

????@Override public double getArea() {

?????return (length * height);?

?????}

}

public class Circle implements Shape{?

????private double radius;?

????// getters/setters ...?

? ? @Override public double getArea() {?

????return (radius * radius * Math.PI);?

?????}

}

public class AreaFactory {?

????public double calculateArea(ArrayList... shapes) {

? ? ? ? double area = 0;

? ? ? ? for (Shape shape : shapes) {

? ? ? ? ? ? area += shape.getArea();

? ? ? ? }

? ? ? ? return area;

? ? }

}

```

經(jīng)過如上修改以后番甩,需要增加一個(gè)新的形狀只需要繼承Shape 并實(shí)現(xiàn)其抽象方法就好。并不需要修改AreaFactory 對象的任何代碼届搁。

4缘薛、李氏替換原則

LSP(The Liskov Substitution Principle):subtypes must be substitutable for their base types. 這個(gè)原則講的是子類必須完全可替代父類。正如它的名字卡睦,LSP是由Barbara Liskov定義的宴胧。對象應(yīng)該可以由其子類型的實(shí)例替換,并且不會從客戶端的角度影響系統(tǒng)的功能表锻。開發(fā)者應(yīng)該始終能夠使用基類并能獲得想要的結(jié)果恕齐。通常,當(dāng)想要表示一個(gè)對象時(shí)瞬逊,開發(fā)者會根據(jù)它的屬性對其類進(jìn)行設(shè)計(jì)显歧,但正確的方式應(yīng)該是根據(jù)對象的行為仪或,也就是方法來設(shè)計(jì)類。這樣就能保存抽象出來的行為就算由不同子類實(shí)現(xiàn)士骤,但調(diào)用的時(shí)候不受具體是什么子類的影響范删。從而可以讓代碼易于重用和整個(gè)類的層次結(jié)構(gòu)易于理解。從這可以看出LSP和前面說的OCP有很強(qiáng)的相互關(guān)系拷肌。Robert C. Martin甚至說“違反LSP是對OCP的潛在侵犯”到旦。而遵守這一原則也是在鼓勵開發(fā)性要面向接口編程。

例子如下:

```

public?interface Car {

? public void startEngine();

}

public Ferrari implements Car {

? ...

? @Override

? public void?startEngine() {

? ? ? //logic ...

? }

}

public Tesla implements Car{

? ? private?IsCharged;

? @Override

? public void?startEngine() {

? ? ? ? if (!IsCharged)

? ? ? ? ? ? return;

? ? ? //logic ...

? }

}


public static void letStartEngine(Car car) {

? ????????car.startEngine();

}

```

正如代碼中中巨缘,有兩類汽車添忘。一類是燃料汽車和一類是電動汽車。電動汽車只有在充電時(shí)才能啟動带猴。如果汽車是電動的而不是充電的話昔汉,LetStartEngine方法將直接返回不執(zhí)行下面的邏輯代碼懈万,這就打破了LSP原則拴清。因?yàn)榛愊雽?shí)現(xiàn)啟動汽車這一行為,但其子類電動車確有可能無法執(zhí)行這一行為会通。這就有可能導(dǎo)致其他對象里面調(diào)用汽車啟動這一行為之后的后續(xù)一系列邏輯出錯(cuò)口予。想象一下,如果Car類是很早以前其他人寫的涕侈,整個(gè)軟件有多處使用Car類startEngine()地方沪停,而且執(zhí)行完startEngine()會立刻向下執(zhí)行與之相關(guān)的代碼。作為Tesla類的開發(fā)者不應(yīng)該去修改Car類(違反前面的開閉原則)裳涛,更不應(yīng)該去修改那些調(diào)用Car類startEngine()方法的類木张。所以只能修改自己的Tesla類來使其startEngine()的產(chǎn)生的效果不會影響其它類因?yàn)镃ar類的實(shí)現(xiàn)是Tesla就出現(xiàn)錯(cuò)誤。

可以作如下修改:

```

public interface Car {

? public void startEngine();

}

public Ferrari implements Car {

? ...

? @Override

? public double startEngine() {

? ? ? ? //logic ...

? }

}

public Tesla implements Car{

? ...

? @Override

? public double startEngine() {

? ? ? ? if (!IsCharged)

? ? ? ? ? ? TurnOnCar();

? ? ? //logic ...

? }

}

public void letStartEngine(Car car) {

? car.startEngine();

}

```

在電動車執(zhí)行啟動這一行為時(shí)端三,當(dāng)它沒有充電舷礼,對其進(jìn)行充電操作,確保汽車啟動這一行為的有效執(zhí)行郊闯。即確保了超類所擁有的性質(zhì)和操作在子類中仍然成立妻献。

5、接口隔離原則

ISP(The Interface Segregation Principle):Classes that implement interfaces, should not be forced to implement methods they do not use.這個(gè)原則在說接口實(shí)現(xiàn)的類不應(yīng)該被強(qiáng)制實(shí)現(xiàn)它們不使用的方法团赁。這個(gè)原則說的是關(guān)于如何編寫接口的育拨。一旦接口變得太大/太胖,就需要將其拆分為更具體更小的接口欢摄。如果一個(gè)接口添加太多不應(yīng)該存在的方法熬丧,那么實(shí)現(xiàn)接口的類也必須實(shí)現(xiàn)這些方法。ISP旨在使系統(tǒng)保持解耦怀挠,從而更容易重構(gòu)析蝴,更改和維護(hù)矗钟。

例子如下:

```

public interface OnClickListener {

? ? void onClick(View v);

? ? void onLongClick(View v);

? ? void onTouch(View v, MotionEvent event);

}

```

上面是一個(gè)對于點(diǎn)擊按鈕的監(jiān)聽接口,如果設(shè)計(jì)成上面的形式嫌变,那么實(shí)現(xiàn)這個(gè)接口的開發(fā)者吨艇,比如說想對一個(gè)按鈕進(jìn)行監(jiān)聽,就需要同時(shí)實(shí)現(xiàn)上面三種方法,就算我其實(shí)只想監(jiān)聽它的點(diǎn)擊事件腾啥,不想監(jiān)聽它的長按事件和觸摸事件东涡。Android開發(fā)中常常一個(gè)界面中會有好幾個(gè)按鈕,這時(shí)候如果每個(gè)按鈕都要只要實(shí)現(xiàn)點(diǎn)擊事件的監(jiān)聽倘待,而如果Android官方對其監(jiān)聽事件是這樣設(shè)計(jì)的話估計(jì)要被開發(fā)者吐槽了疮跑。正確的設(shè)計(jì)方法應(yīng)該如下:

```

public interface OnClickListener {

? ? void onClick(View v);

}

public interface OnLongClickListener {

? ? void onLongClick(View v);

}

public interface OnTouchListener {

? ? void onTouch(View v, MotionEvent event);

}

```

這樣開發(fā)者的需求是要實(shí)現(xiàn)什么事件的監(jiān)聽就實(shí)現(xiàn)對應(yīng)的接口就好了,不需要實(shí)現(xiàn)其他不必要的接口凸舵。

6祖娘、依賴倒置原則

DIP(Dependency Inversion Principle):High level modules should not depend on low level modules rather both should depend on abstraction. Abstraction should not depend on details; rather detail should depend on abstraction. 高級模塊不應(yīng)該依賴于低級模塊,兩者都應(yīng)該依賴于抽象啊奄。抽象不應(yīng)該依賴于細(xì)節(jié)渐苏,而細(xì)節(jié)應(yīng)該依賴于抽象。這個(gè)原則是5大原則里面最不好理解的菇夸,這里的高級模塊只內(nèi)部包含了很多低級模塊的模塊琼富。而這些高級模塊里面的低級模塊,其定義應(yīng)該是抽象類或者接口庄新,具體實(shí)現(xiàn)才是實(shí)現(xiàn)這些抽象類和接口的低級模塊鞠眉。抽象類之間的交互應(yīng)該通過抽象類或者接口來進(jìn)行,而不應(yīng)該是具體的實(shí)現(xiàn)類择诈,同時(shí)具體實(shí)現(xiàn)類之間的交互也應(yīng)該通過抽象類或者接口械蹋。這么做的好處自然就是降低模塊間的耦合。

例子如下:

```

class Program {

????public void work() {

????}

}

class Engineer{

????Program program;

????public void setProgram(Program p) {

????program = p;

????}

????public void manage() {

????????program.work();

????}

}

```

上述代碼的問題在于它打破了依賴倒置原則中的高級模塊不應(yīng)該依賴于低級模塊羞芍。兩者都應(yīng)該取決于抽象哗戈。一個(gè)高級類的Engineer類,它持有一個(gè)名為Program的低級類涩金。如果Engineer類非常復(fù)雜谱醇,包含非常復(fù)雜的邏輯。要像Engineer引入的一個(gè)新的NewProgram 類步做,那就要修改Engineer中的代碼副渴,而Engineer又很復(fù)雜,會牽扯到Engineer的其他部分全度。同時(shí)之前的Program 類也有可能受到影響≈缶纾現(xiàn)在對其進(jìn)行重構(gòu):
```

interface IProgram {

????public void work();

}

class Program implements IProgram{

????public void work() {

????}

}

class NewProgram implements IProgram{

????public void work() {

????}

}

class Engineer{

????IProgram program;

????public void setProgram(IProgram p) {

????????program = p;

????}

????public void manage() {

????????program.work();

????}

}

```

在這個(gè)新設(shè)計(jì)中,通過IProgram接口添加了一個(gè)新的抽象層。這樣就解決了之前代碼中的問題勉盅。有一個(gè)新的NewProgram 類時(shí)佑颇,Engineer不需要改變,這樣最小化的影響了Engineer類中的其它功能草娜。

7挑胸、總結(jié)

在開發(fā)任何軟件時(shí),有兩個(gè)非常重要的概念:

① 內(nèi)聚:當(dāng)系統(tǒng)的不同部分一起工作以獲得比每個(gè)部分單獨(dú)工作時(shí)更好的結(jié)果宰闰。

② 耦合:可以看作是一種類之間的依賴程度茬贵。

其實(shí)上面這些原則,核心目的就是為了使開發(fā)者能寫出高內(nèi)聚移袍,低耦合的代碼解藻。還是最前面所說的,這些原則還是要靈活運(yùn)用葡盗,而不是一味的去為了遵守而遵守它螟左。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市觅够,隨后出現(xiàn)的幾起案子胶背,更是在濱河造成了極大的恐慌,老刑警劉巖蔚约,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奄妨,死亡現(xiàn)場離奇詭異涂籽,居然都是意外死亡苹祟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門评雌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來树枫,“玉大人,你說我怎么就攤上這事景东∩扒幔” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵斤吐,是天一觀的道長搔涝。 經(jīng)常有香客問我,道長和措,這世上最難降的妖魔是什么庄呈? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮派阱,結(jié)果婚禮上诬留,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好文兑,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布盒刚。 她就那樣靜靜地躺著,像睡著了一般绿贞。 火紅的嫁衣襯著肌膚如雪因块。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天籍铁,我揣著相機(jī)與錄音贮聂,去河邊找鬼歌懒。 笑死子眶,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的失晴。 我是一名探鬼主播靡狞,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼耻警,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了甸怕?” 一聲冷哼從身側(cè)響起甘穿,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梢杭,沒想到半個(gè)月后温兼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡武契,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年募判,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片咒唆。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡届垫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出全释,到底是詐尸還是另有隱情装处,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布浸船,位于F島的核電站妄迁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏李命。R本人自食惡果不足惜登淘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望项戴。 院中可真熱鬧形帮,春花似錦槽惫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至合冀,卻和暖如春各薇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背君躺。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工峭判, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人棕叫。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓林螃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親俺泣。 傳聞我的和親對象是個(gè)殘疾皇子疗认,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345