設計模式學習筆記(一)
一般說設計模式都是指面向對象的設計模式,因為面向對象語言可以借助封裝、繼承、多態等特性更好的達到復用性、可拓展性、可維護性。
面向對象一般指以類、對象為組織代碼的基本單元,并將封裝、繼承、多態、抽象四個特性(抽象有的定義里并不認為是四大特性)作為代碼設計與實現的基石。
-
封裝:通過訪問權限控制,只對外暴露必要的操作,保護數據。
-
繼承:代碼復用,結構美感。不過 Java 語言不支持多重繼承,原因是如果 BC 都繼承了 A 并重寫了某個方法,D 同時繼承 BC 會產生歧義。
-
多態:提高代碼的復用性,主要通過兩種方式實現
- 繼承:父類引用指向子類對象
- 實現:接口引用指向具體實現類
-
抽象:有時并不計入四大特性,用來保護實現,例如接口就是對實現的一種抽象,無需關注實現
有些設計看似是面向對象
- 濫用 getter、setter 方法。lombok 的注解確實很方便,但這樣其實違背了面向對象的封裝特性,例如 createTime 等字段其實是不需要 setter 方法的,需要在創建對象的時候就確定。
- 濫用全局變量、全局方法(Constants、Utils)。這樣會導致修改后所有引用的地方都重新編譯,而且有的時候只需要其中的某幾個變量(或方法)卻導入了整個類。
- 功能拆分,不要定義一個大而全的類。例如 Constants 拆分為 DateConstants、RedisConstants、MysqlConstants
定義數據與方法分離的類
。傳統的 MVC 開發中,數據在相應的 BO、VO、PO 中,而操作卻封裝在對應的 Controller、Service 中,這就是典型的面向過程,也就是“貧血模型”
的開發方法。不過這樣的開發方式依然很流行,因為大部分的需求并不復雜,甚至只是從數據庫中找到查哪些字段,組織對應的 VO,先寫 service 反推 controller。
如何理解接口與抽象類
隨著 jdk 版本的更新,接口也可以有默認實現,也可以定義變量作為常量使用。抽象類依然不允許被實例化,繼承抽象類必須重寫抽象類的所有方法。
先說結論:抽象類的作用更多是為了代碼復用,而接口的作用則更偏向與“協議”,具備什么樣的功能。
public class BaseEntity implements Serializable {private static final long serialVersionUID = 8417380540303280008L;@ApiModelProperty(value = "所屬用戶標識")@Column(name = "USER_ID")private String userId;@ApiModelProperty(value = "記錄是否有效,默認為1表示有效")@Column(name = "ACTIVE")private String active;@ApiModelProperty(value = "創建時間,默認為當前時間")@Column(name = "CREATED_AT", updatable = false)@DateTimeFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND)@JsonFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND, timezone = Constants.TIMEZONE)private Date createdAt;@ApiModelProperty(value = "更新時間")@Column(name = "UPDATED_AT")@DateTimeFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND)@JsonFormat(pattern = Constants.PATTERN_DATE_HOUR_MINUTE_SECOND, timezone = Constants.TIMEZONE)private Date updatedAt;}
例如我們有一個上面的類,對于一個正常的刪除來講,一方面我們要查詢這個數據是否存在(例如有些系統刪除不存在的空數據會返回錯誤),另一方面判斷當前登錄用戶是否具有刪除權限(即資源的 USER_ID 是否為當前登錄人或是否是當前登錄人的下屬),最后還需要記錄日志。
public void delete(String uuid) {Entity entity = getEntityFromDB(uuid);if (entity == null) {throw ......}if (entity instanceof BaseEntity) {BaseEntity e = (BaseEntity)entity;if (!e.getActive.equals("1")) {} else {e.setActive("0");saveEntityToDB(entity);}}
}protected abstract T saveToDataBase(T entity);protected abstract Entity getEntityFromDB(String uuid);
借助抽象類與多態,可以提高代碼的復用性,減少重復代碼。
如何理解基于接口而非實現編程
假如目前有一個上傳圖片到公有云的需求
public class uploadPictureAliyunImpl {// 獲取合法 tokenpublic String getToken() {};// 如果目錄不存在就創建目錄public boolean createDictoryIfNotExists() {};// 上傳圖片public boolean uploadPictureToAliyun() {};}public class Main() {public static void Main () {uploadPictureAliyunImpl impl = new uploadPictureAliyunImpl();String token = impl.getToken();........}
}
如果這樣實現,后期替換為其他云廠商,例如自有的私有云,就需要替換很多代碼,實際上這種情況只需要定義一個上傳圖片的接口,由不同的存儲來實現就行。
基于接口編程,即進行更好的抽象設計,不暴露過多實現。
為什么說多用組合少用繼承
以鳥(bird)為例,可以分為是否會飛、是否會下蛋…
當繼承層次越來越深,關系會越來越復雜,會嚴重影響代碼的穩定性與可維護性。但是當繼承層次很淺且業務穩定時,依然可以利用繼承和多態特性來實現特定功能。
繼承實際上可以替換為組合來實現,例如定義兩個接口:
public interface Flyable {void fly();
}public interface Eggable {void egg();
}
每一種鳥類根據自己的情況來實現對應接口即可,但是這會引入新的問題,例如有 n 個鳥類實現的 fly 接口都是一樣的,那代碼重復會十分嚴重,解決方式就是“委托”:
public class DefaultFlyableImpl implements Flyable {void fly() {......}
}public class AAABird implements Flyable {// 其實一般是使用注解注入private DefaultFlyableImpl defaultFlyImpl = new DefaultFlyableImpl();void fly() {defaultFlyImpl.fly();}}