- 原文連接:https://academy.realm.io/posts/donn-felker-solid-part-2/
- 譯文出自:kailaisi的簡書
- 譯 者:kailaisi
這是安卓開發系列文章中關于SOLID原則的第二部分。如果你沒有沒有閱讀過第一部分,或者不了解什么是SOLID原則,請點擊第一部分,其中我們介紹了SOLID原則并討論了單一職責原則。
SOLID中的"O"是開閉原則的縮略詞。開閉原則描述如下
一個軟件實體如類、模塊和函數應該對擴展開放,對修改關閉。
雖然聽起來簡單,但是如果你在自己的腦海里多思考幾次,你可能就會懷疑自己對這句話的理解。簡單來說就是,你應該努力寫出這樣的代碼:當需求變更的時候,并不一定需要改變原有代碼就可以實現功能。由于在Android中使用Java語言進行開發,因此可以通過繼承和多態來實現這一功能。
一個開閉原則的簡單實例
下面的例子是一個非常典型的開閉原則及其實現。非常簡單,但卻能夠很好的說明開閉原則。
假設有一個應用程序,能夠計算任意形狀面積。這是幾年前我在明尼蘇達州農作物保險公司遇到的一個非常簡單問題。app程序必須能夠計算出指定區域的農作物總的保險報價。正如你所知道的,農作物有各種形狀和大小,有可能是圓的,有可能是三角形的也可能是其他各種多邊形。
OK,讓我們回到我們之前的例子中....
作為一名優秀的程序員,我們將這個面積計算類命名為 AreaManager
。這個 AreaManager
是單一職責的類:計算形狀的總面積 。
假設我們現在有一塊矩形的農作物,我omen用一個Rectangle
類來表示。相關類代碼如下:
public class Rectangle {
private double length;
private double height;
// getters/setters ...
}
public class AreaManager {
public double calculateArea(ArrayList<Rectangle>... shapes) {
double area = 0;
for (Rectangle rect : shapes) {
area += (rect.getLength() * rect.getHeight());
}
return area;
}
}
AreaManager
類現在運行良好,直到幾周之后,我們又有一種新的形狀——圓形:
public class Circle {
private double radius;
// getters/setters ...
}
由于有新的形狀需要考慮,我們必須修改我們的AreaManager
類:
public class AreaManager {
public double calculateArea(ArrayList<Object>... shapes) {
double area = 0;
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;
}
}
從這段代碼開始,我們察覺到了問題。
如果我們遇到一個三角形,或者其他形狀呢,這時候我們就必須一次又一次的修改AreaManager
類。
這個類的設計就違背了開閉原則,沒有做到對修改的封閉性以及對擴展的開放性。我們必須避免這種事情的發生~
基于繼承的開閉原則的實現
AreaManager
類的職責是計算各種形狀的面積,而每一種形狀都有其獨特的計算面積的方法,因此將面積的計算放入到各個形狀類中是特別合理的。
AreaManager
類仍然需要知道所有的形狀,否則它就無法判斷所有的形狀類是否都包含了計算面積的方法。當然了,我們可以通過反射來實現。其實有一種更簡單的方式也可以實現——讓所有的形狀類都繼承一個接口: Shape
(也可以是抽象類)
public interface Shape {
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);
}
}
現在,我們可以通過這個抽象方法將AreaManager
構造成一個符合開閉原則的類。
public class AreaManager {
public double calculateArea(ArrayList<Shape> shapes) {
double area = 0;
for (Shape shape : shapes) {
area += shape.getArea();
}
return area;
}
}
通過這種方式, AreaManager
類符合了對修改關閉,對擴展開放的要求。如果我們需要增加一種新形狀,比如:八邊形。新的類只需要繼承Shape
接口即可,AreaManager
根本不需要做任何的修改。
Android中的開閉原則
在農作物保險工作中,Shape類非常有用,但是這種模式在Android中如何應用呢?其實開閉原則和語言無關,它適用于任何語言。Android中也有一些典型的開閉原則的應用實例。我們慢慢來....
很多Android開發人員可能沒有注意到-Android內置的一些控件,比如Button、Switch、Checkbox都是TextView類。我們來看一下關于各種繼承TextView類的截圖:
這意味著Android控件對修改封閉,對繼承開放。如果你想自定義一個CurrencyTextView
,并修改字體的顯示樣式,你只要簡單的繼承TextView
類,并重寫你自己的邏輯即可。Android控件不關心你是否創建了新類,它只在意你的類是否遵循了TextView的特定約束。Android通過特定的約束將自定義的控件繪制在屏幕上。
ViewGroup也是這樣的:
Android有各種不同的ViewGroup(RelativeLayout,LinearLayout等等),Android系統也能夠很好的協同合作。你可以通過繼承ViewGroup類來實現自己的ViewGroup。
通過繼承抽象類View,TextView,ViewGroup,允許你編寫符合開放原則的控件。
結論
開閉原則不僅限于Android控件,但是Android控件是所有的開發者每天都能夠用到的一種典型的開閉原則的是想方式。你也可以自己編寫更加有好的符合開閉原則的代碼。通過一些簡單的抽象方法,你可以很方便的創建一些能夠進行繼承和擴展的類,而不必在每次增加新特性的時候去修改原有代碼。
對于一些新的項目,你可能找不到需要進行抽象的類。此外,只是為了實現模式而產生一些過度復雜的代碼是不明智的。據我以往的經驗,當多次修改一個類之后,就會發現需要使用開閉原則。這時候,我會對代碼進行充分的測試,然后重構代碼以實現對修改關閉,對擴展開放。有了個覆蓋率高的測試代碼,我才能在剩余的時間編寫更多可維護的代碼。
敬請期待本系列中的下一篇文章——里氏替換原則,這是目前為止我最喜歡的開發原則。