面向對象設計4原則 原

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();  
    } 
}

其對應的類圖爲:

image

若這時添加大樂透彩種的校驗,需要修改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;
	 }
 }

修改後的類圖爲:

image

這樣無論添加任何彩種,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原則!

舉個我工作中的例子 關於紅包回收業務需求 我們的業務需求文檔寫了如下需求:

  1. 紅包過期應該進行自動回收
  2. 紅包領取後30天內有效,過期應該回收。
  3. 紅包活動過期,應該回收未使用的紅包。

這個是明顯的不遵循DRY,當然產品經理可能沒有聽說過DRY,如果你遇到了這種情況,請默默的在心裏將需求凝練下即可。例如:

  1. 應按規則回收紅包,規則如下:
    a. 未使用的在紅包活動過期後回收
    b. 已領取部分使用的自領取之日起30天后進行回收
    c. 已使用完畢的不進行回收

SRP(單一職責)

系統中每一個對象應該具有單一職責,所有對象的服務都應該聚焦在實現該職責上。

應用舉例

假設系統中有如下一個簡單的Car類,其內部結果如下類圖:

image

下面我們針對這個簡單的例子,找出其不符合SRP的地方。

找出一個類中不符合SRP的方法爲:

  1. 做填空,該 【XXX類】 自己 【XXX 方法】,找出語義不通順的地方
  2. 結合自身業務理解進行進一步分析,最終確定不符合SRP的部分。

以Car類爲例子 我們先進行第一步 :

該 Car 自己 start

該 Car 自己 stop

該 Car 自己 getOil

** 該 Car 自己 wash (?車自己洗車) **

** 該 Car 自己 drive (?車自己駕駛,難道是自動駕駛的車)**

我們找出兩個方法可能不遵循SRP,一個是wash,一個是drive。

下面我們執行第二步,根據根據業務理解進行分析。
這裏我們沒有什麼業務背景,僅依據生活經驗進行分析。

  1. 車一般有其他人或機構進行清洗,不屬於車的部分。應該從Car移除
  2. drive,處理自動駕駛車以外,車均由司機駕駛,自動駕駛車的駕駛員可以理解爲電腦,所以drive也不屬於Car類,應該從Car類移除。

從上邊的小例子 我們可以看出:

  1. 方法名稱要與具體實現的功能相符,否則第一步無法部分進行。

  2. 對業務的理解很重要,否則無法最終決定違反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

image

相應的代碼如下:

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);
	}

組合

組合讓你使用來自一組其他的行爲,並且可以在運行時切換該行爲。

組合類圖舉例:

image

在組合中,由其他行爲組成的對象(本例子中是Unit類)擁有那些行爲(本例中指Weapon的attack方法)。當擁有者對象被銷燬時(Unit被銷燬),其所有行爲也被銷燬(Weapon的所有實現也被銷燬)。組合中的行爲不存在組合之外。

聚合

當一個類被用作另一個類的一部分時,但仍然可以存在於該類之外。(組合單式沒有結束)

聚合舉例類圖:

image

總結

類應該對擴展開發,對修改而關閉。(OCP)

通過將共同之物抽取出來並置於單一地方避免重複的程序代碼(DRY)

系統中每一個對象應該具有單一職責,所有對象的服務都應該聚焦在實現該職責上。(SRP)

子類型必須能夠替換其基類型。(LSP)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章