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)用葡盗,而不是一味的去為了遵守而遵守它螟左。