OCP(開閉原則)
類應該對擴展開放,對修改而關閉。
應用舉例
本人是做彩票業務的,就以彩票舉例吧。下面是一段設計不良的校驗投注號碼的代碼
public boolean validate(String drawNum){
if (type.equals("PL3")) {
PL3Validate validatePL3 = new PL3Validate();
validatePL3.validate();
}
else if (type.equals("PL5")) {
PL5Validate validatePL5 = new PL5Validate();
validatePL5.validate();
}
}
其對應的類圖爲:
若這時添加大樂透彩種的校驗,需要修改OCPDemo中的validate的代碼,加入另外一個else if 分支,這違反了OCP原則,並沒有對修改而關閉。
可以進行如下修改:
我們添加抽象類AbstractNumberValidate,讓PL3Validate和PL5Validate繼承該類,OCPDemo僅依賴AbstractNumberValidate類。上面的代碼修改爲:
AbstractNumberValidate validate;
public static class PL3ValidateImpl extends AbstractNumberValidate{
public boolean validate(String drawNum){
return false;
}
}
修改後的類圖爲:
這樣無論添加任何彩種,OCPDemo的validate都不需要更改。若這時添加大樂透彩種的校驗,只需要添加一個DLTValidate類繼承AbstractNumberValidate實現自己的校驗規則,並注入到OCPDemo中即可。
這裏僅僅以繼承的方式來解決上邊的問題,解法不唯一。
OCP不僅僅是繼承
OCP關係到靈活性,而不只是繼承。 例如:你在類中有一些private的方法,(這就是禁止爲修改而關閉),但是你有一些public方法以不同的方式調用private方法(允許爲擴展而開放)
OCP的核心是 讓你有效的擴展程序,而不是改變之前的程序代碼。
DRY(不自我重複)
通過將共同之物抽取出來並置於單一地方避免重複的程序代碼。
舉例說明
Java初學者,使用JDBC,查詢數據庫中數據時,會有如下代碼,每調用一個查詢均會有 3部分,執行查詢,提取結果,關閉結果集合。
//調用查詢
stmt = conn.createStatement();
result = stmt.executeQuery("select * from person");//執行sql語句,結果集放在result中
//提取結果
while(result.next()){//判斷是否還有下一行
String name = result.getString("name");//獲取數據庫person表中name字段的值
Person p=new Person();
p.setName(name);
}
//關閉結果集合
result.close();
stmt.close();
如果每調用查詢一次數據庫均要寫上述代碼,絕對會非常的累,也違反DRY原則,系統中會出現大量的重複代碼。 下面讓我們看看Spring的JdbcTemplate如何遵循DRY原則。上邊的模式,有一定的套路,Spring總結了套路,封裝成了模板,經過Spring的封裝,只需傳入Sql,和結果集合轉換的類。代碼如下:
//實際只需調用queryForObject即可
@Override
public <T> T queryForObject(String sql, Class<T> requiredType) throws DataAccessException {
return queryForObject(sql, getSingleColumnRowMapper(requiredType));
}
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
//執行SQL
rs = stmt.executeQuery(sql);
//----提取結果-start
ResultSet rsToUse = rs;
if (nativeJdbcExtractor != null) {
rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
}
return rse.extractData(rsToUse);
//--------提取結果-end
}
finally {
//關閉結果集合
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
DRY不僅應用於編碼
抽取出重複程序代碼是運用DRY的好開始,但DRY的內涵可不只是如此!當試圖避免重複程序代碼時,實際也在試着確保你對應用程序中每一個功能和需求只實現一次。
其實無論編寫需求,開發用例或者編寫代碼都應該遵守DRY原則!
舉個我工作中的例子 關於紅包回收業務需求 我們的業務需求文檔寫了如下需求:
- 紅包過期應該進行自動回收
- 紅包領取後30天內有效,過期應該回收。
- 紅包活動過期,應該回收未使用的紅包。
這個是明顯的不遵循DRY,當然產品經理可能沒有聽說過DRY,如果你遇到了這種情況,請默默的在心裏將需求凝練下即可。例如:
- 應按規則回收紅包,規則如下:
a. 未使用的在紅包活動過期後回收
b. 已領取部分使用的自領取之日起30天后進行回收
c. 已使用完畢的不進行回收
SRP(單一職責)
系統中每一個對象應該具有單一職責,所有對象的服務都應該聚焦在實現該職責上。
應用舉例
假設系統中有如下一個簡單的Car類,其內部結果如下類圖:
下面我們針對這個簡單的例子,找出其不符合SRP的地方。
找出一個類中不符合SRP的方法爲:
- 做填空,該 【XXX類】 自己 【XXX 方法】,找出語義不通順的地方
- 結合自身業務理解進行進一步分析,最終確定不符合SRP的部分。
以Car類爲例子 我們先進行第一步 :
該 Car 自己 start
該 Car 自己 stop
該 Car 自己 getOil
** 該 Car 自己 wash (?車自己洗車) **
** 該 Car 自己 drive (?車自己駕駛,難道是自動駕駛的車)**
我們找出兩個方法可能不遵循SRP,一個是wash,一個是drive。
下面我們執行第二步,根據根據業務理解進行分析。
這裏我們沒有什麼業務背景,僅依據生活經驗進行分析。
- 車一般有其他人或機構進行清洗,不屬於車的部分。應該從Car移除
- drive,處理自動駕駛車以外,車均由司機駕駛,自動駕駛車的駕駛員可以理解爲電腦,所以drive也不屬於Car類,應該從Car類移除。
從上邊的小例子 我們可以看出:
-
方法名稱要與具體實現的功能相符,否則第一步無法部分進行。
-
對業務的理解很重要,否則無法最終決定違反SRP的部分。
2點說明
- DRY和SRP往往一同出現,DRY關注把一個功能片段放到一個單獨的地方。 SRP是關於一個類只做一件事。
- 內聚力的另外一個名稱就是SRP。
LSP(里氏替換原則)
子類型必須能夠替換其基類型。
違反LSP的情形舉例
假設我們有一個Graph2D 用於製作2D平面,現在要新創建一個Graph3D類,用於構建立體圖,下面我們使用違反LSP原則的方式實現。
public static class Graph2D{
int x;
int y;
public void setGraph(int x,int y){
this.x=x;
this.y=y;
}
}
public static class Graph3D extends Graph2D{
int z;
public void setGraph(int x,int y,int z){
this.x=x;
this.y=y;
this.z=z;
}
}
public static void main(String[] args) {
Graph3D Graph3D=new Graph3D();
// 由於繼承,使用者會非常迷茫,如何設置x,y,z
Graph3D.setGraph(x, y);//來自父類Graph2D
Graph3D.setGraph(x, y, z);//自己的
}
上邊的代碼我們讓Graph3D繼承了Graph2D,造成Graph3D的使用者對setGraph產生了疑惑。 因爲有2個setGraph方法。若不瞭解內部實現的人,將難以使用。
如何解決不滿足LSP的情況
一共有3種處理方式:委託,聚合,組合。
委託
將特定工作的責任委派給另外一個類或方法。
如果你想要使用另一個類的功能性,但不想改變該功能,考慮以委託代替繼承。
下面我們以委託的方式,解決上的問題,修改後代碼,僅有一個setGraph方法,不會產生不必要的麻煩。
原本的類圖爲:
以委託的方式修改後的類圖,這時Graph3D依賴時Graph2D
相應的代碼如下:
public static class Graph2D{
int x;
int y;
public void setGraph(int x,int y){
this.x=x;
this.y=y;
}
}
public static class Graph3D {
int z;
private Graph2D graph2D;//將平面部分委託給Graph2D處理
public void setGraph(int x,int y,int z){
graph2D.setGraph(x, y);
this.z=z;
}
}
public static void main(String[] args) {
Graph3D graph3D=new Graph3D();
graph3D.setGraph(x, y, z);
}
組合
組合讓你使用來自一組其他的行爲,並且可以在運行時切換該行爲。
組合類圖舉例:
在組合中,由其他行爲組成的對象(本例子中是Unit類)擁有那些行爲(本例中指Weapon的attack方法)。當擁有者對象被銷燬時(Unit被銷燬),其所有行爲也被銷燬(Weapon的所有實現也被銷燬)。組合中的行爲不存在組合之外。
聚合
當一個類被用作另一個類的一部分時,但仍然可以存在於該類之外。(組合單式沒有結束)
聚合舉例類圖:
總結
類應該對擴展開發,對修改而關閉。(OCP)
通過將共同之物抽取出來並置於單一地方避免重複的程序代碼(DRY)
系統中每一個對象應該具有單一職責,所有對象的服務都應該聚焦在實現該職責上。(SRP)
子類型必須能夠替換其基類型。(LSP)