做者:yangwq
博客:https://yangwq.cnjava
軟件設計是一門關注長期變化的學問,平常開發中需求不斷變化,那咱們該怎麼編寫出能夠支撐長期變化的代碼呢?大多數人都認同的解決方案是利用設計模式,這裏就有一個問題:怎麼融匯貫通的將設計模式應用到實際項目中呢?這就是咱們本篇文章的主題:設計原則。程序員
我的認爲設計原則是軟件設計的基石之一,全部語言均可以利用設計原則開發出可擴展性、可維護性、可讀性高的項目,學好設計原則,就等於咱們擁有了指南針,不會迷失在各個設計模式的場景中。spring
鄭曄老師的《軟件設計之美》指出:設計模式是在特定問題上應用設計原則的解決方案。咱們能夠類比設計原則是心法,設計模式是招式,二者相輔相成,雖然脫離對方都能使用,可是不能融會貫通。設計模式
本章主要涉及的設計原則有:mybatis
接下來對各個原則進行詳細說明,有錯誤或語義不明確的地方歡迎你們指正。app
本原則的定義經歷過一些變化。之前的定義是:一個模塊(模塊、類、接口)僅有一個引發變化的緣由,後面升級爲: 一個模塊(模塊、類、接口)對一類且僅對一類行爲者負責。框架
咱們重點關注的是「變化」一詞。下面咱們用代碼來進行示例:ide
背景:設計一個訂單接口,能作到建立、編輯訂單和會員的贈送及過時。函數
public interface OrderService { int createOrder(); int updateOrder(); // 下單完成後分配vip給用戶 int distributionVIP(); // vip過時 int expireVIP(); }
OrderService包含對訂單、VIP的操做,無論是訂單業務或VIP業務的改變,咱們都須要改變這個類。這樣有什麼問題?有多個引發OrderService變化的緣由致使這個類不能穩定下來,對VIP代碼的改動有可能致使本來運行正常的訂單功能發生故障,沒有作到高內聚、低耦合。工具
一個模塊最理想的狀態是不改變,其次是少改變。咱們能夠將對VIP的處理單獨放到一個類:
public interface OrderService { int createOrder(); int updateOrder(); } public interface VIPService{ // 下單完成後分配vip給用戶 int distributionVIP(); // vip過時 int expireVIP(); }
這樣咱們對訂單或VIP的改動都不會影響到對方正常的功能,極大程度上減小了問題發生的機率。
這個定義比上面的定義多加了一個內容:變化的來源。
上面的例子可能區分不出來變化的來源,像vip這類功能通常都是訂單系統體系內的。從下面這個例子說明:
背景:在上面例子的背景下,增長對地址信息的維護。
public interface OrderService { int createOrder(); int updateOrder(); // 訂單地址的修改 int updateOrderAddress(); }
OrderService中對訂單地址的修改,多是訂單負責人提出的需求,也多是物流部門提出來:須要共用訂單地址。
這裏就須要區分兩種業務場景。
若是是訂單負責人提出的,那上面這個設計就是合理的,由於咱們維護的是訂單附屬內容,並且變化的來源只有訂單系統。
但若是是物流部門提出共用訂單地址,那就須要將更改地址的接口抽離出來,由於這個需求變化的來源有兩撥人:多是訂單,也多是物流部門。改動以下:
public interface OrderService { int createOrder(); int editOrder(); } public interface AddressService { // 訂單修改地址 int updateAddressByOrder(); // 物流修改地址 int updateAddressByLogistics(); }
爲了職責明確咱們有對接口的命名進行重構,這樣更容易被使用者接受,經過將地址的變化隔離在AddressService,後續維護地址只用修改這個類,提高了代碼的可讀性和可維護性。
定義:對擴展開放,對修改關閉。簡而言之: 不修改已有代碼(儘量不更改已有代碼的狀況下),新需求用新代碼實現。
如何作到?分離關注點,找出共性構建模型/抽象,設計擴展點。
代碼示例:
背景:設計一套通用的文件上傳下載功能,須要支持本地盤和阿里雲OSS。一開始的設計多是這樣的:
public void FileUtil { void upload(UploadParam uploadParam) { if(type == 1){ // 上傳文件到本地盤 }else if (type == 2){ // 上傳文件到阿里雲OSS } } void download(DownloadParam downloadParam){ if(type == 1){ // 從本地盤下載文件 }else if (type == 2){ // 從阿里雲OSS下載文件 } } }
上面的設計有什麼問題?首先第一點UploadParam 和 DownloadParam 參數職責太重,不一樣方式的上傳、下載參數混合在一個類,可讀性不高,並且加入其餘存儲方式的時候可能只加了上傳,漏掉了下載的改動,容易產生問題。
那咱們先經過分離關注點:不一樣存儲方式都須要提供對應的上傳、下載操做。因而咱們能夠將動做拆分紅上傳、下載,參數須要按不一樣場景選用不一樣的對象。改動後以下:
// 全部參數的父類接口 public interface BaseFileParam{ } // 統一的上傳下載接口類 public interface FileService<U,D>{ /** * 上傳 */ void upload(); /** * 下載 */ void download(); } // 抽象實現,將參數做爲屬性放到類中,子類可使用 public abstract class AbstractFileService<U,D> implements FileService<U,D>{ protected U uploadParam; protected D downloadParam; public AbstractFileService() { } protected FileService<U, D> buildUploadParam(U uploadParam){ this.uploadParam = uploadParam; return this; } protected FileService<U, D> buildDownloadParam(D downloadParam){ this.downloadParam = downloadParam; return this; } protected U getUploadParam() { return uploadParam; } protected D getDownloadParam() { return downloadParam; } } // OSS實現 public class OssFileServe extends AbstractFileService<OssFileServe.OssUpload, OssFileServe.OssDownload> { /** * 上傳到阿里雲 */ @Override public void upload() { } /** * 從阿里雲下載文件 */ @Override public void download() { } public class OssUpload implements BaseFileParam{ } public class OssDownload implements BaseFileParam{ } } // 本地盤實現 public class LocalFileService extends AbstractFileService<LocalFileService.LocalFileUploadParams, LocalFileService.LocalFileDownloadParams> { /** * 上傳到本地磁盤 */ @Override public void upload() { } @Override public void download() { } public static class LocalFileUploadParams implements BaseFileParam { } public static class LocalFileDownloadParams implements BaseFileParam { } } // 使用入口 public class FileServiceDelegate { public FileService<? extends BaseFileParam,? extends BaseFileParam> getFileService(String type, BaseFileParam upload, BaseFileParam download){ if("local".equals(type)){ return new LocalFileService().buildUploadParam(upload != null ? (LocalFileService.LocalFileUploadParams) upload : null) .buildDownloadParam(download != null ? (LocalFileService.LocalFileDownloadParams) download : null); }else if ("oss".equals(type)) { return new OssFileServe().buildUploadParam(upload != null ? (OssFileServe.OssUpload) upload : null) .buildDownloadParam(download != null ? (OssFileServe.OssDownload) download : null); }else { throw new RuntimeException("未知的上傳類型"); } } public void upload(String type, BaseFileParam baseFileParam){ getFileService(type,baseFileParam, null).upload(); } public void download(String type, BaseFileParam baseFileParam){ getFileService(type,null, baseFileParam).download(); } }
以上是比較粗糙的方案,只作案例演示。後續若是須要加入亞馬遜S3存儲,咱們須要改動的點:
// 加入S3實現 public class S3FileService extends AbstractFileService<S3FileService.S3UploadParams, S3FileService.S3DownloadParams> { /** * 上傳到S3 */ @Override public void upload() { } /** * 從S3下載文件 */ @Override public void download() { } public class S3UploadParams implements BaseFileParam { } public class S3DownloadParams implements BaseFileParam { } } // 修改入口類 public class FileServiceDelegate { public FileService<? extends BaseFileParam,? extends BaseFileParam> getFileService(String type, BaseFileParam upload, BaseFileParam download){ if("local".equals(type)){ return new LocalFileService().buildUploadParam(upload != null ? (LocalFileService.LocalFileUploadParams) upload : null) .buildDownloadParam(download != null ? (LocalFileService.LocalFileDownloadParams) download : null); }else if ("oss".equals(type)) { return new OssFileServe().buildUploadParam(upload != null ? (OssFileServe.OssUpload) upload : null) .buildDownloadParam(download != null ? (OssFileServe.OssDownload) download : null); } // 加入S3處理 else if("s3".equals(type)){ return new S3FileService().buildDownloadParam(upload != null ? (S3FileService.S3DownloadParams) upload : null) .buildDownloadParam(download != null ? (S3FileService.S3DownloadParams) download : null); }else { throw new RuntimeException("未知的上傳類型"); } } public void upload(String type, BaseFileParam baseFileParam){ getFileService(type,baseFileParam, null).upload(); } public void download(String type, BaseFileParam baseFileParam){ getFileService(type,null, baseFileParam).download(); } }
上面咱們修改了兩個地方,一個是加入了S3的實現類,另外一個是更改入口類加入了S3的處理,這就符合新功能用新代碼實現,但可能有人說改動了入口類,其實只要改動的代碼沒有影響到原有的功能,小幅度的修改是能夠接受的。
定義:子類必須可以替換其父類,並保證原來程序的邏輯行爲不變及正確性不被破壞。
如何實現?站在父類的角度設計接口,子類須要知足基於行爲的IS-A關係,更具體的來說:子類遵照父類的行爲約定,約定包含:功能主旨,異常,輸入,輸出,註釋等。
違背功能主旨:
public interface OrderService { Order updateById(Order order); } public class OrderServiceImpl { public Order findById(Order order) { // 其實是經過訂單編號進行更新的 return orderMapper.updateBySn(order); } }
父類的定義本來是按訂單ID更新,在子類實現中卻變成了按訂單編號更新,這個方法就違背了功能主旨。會出現什麼問題?使用者會發現執行結果與本身指望的不一致,並且有隱藏BUG:一開始傳了訂單編號,後面訂單編號沒了,這個方法就報錯了,更嚴重一點,若是是使用mybatis的xml判斷了編號不爲空進行條件拼接,此時因爲編號爲空就沒有了條件過濾而後更改了整個表的數據。
異常:父類規定接口不能拋出異常,而子類拋出了異常。
輸入:父類輸入整數類型就行,子類要求正整數才能執行。
輸出:父類執行方法要求有異常時返回null,子類重寫後直接將異常拋出來了。
關於里氏替換原則,咱們就只要記住一點:從父類角度設計行爲一致的子類。
定義:不該強迫使用者依賴於它們不用的方法。 通俗的理解:對接口設計應用單一職責,根據調用者設計不一樣的接口。
示例:
public class UserController{ int addUser(User user); int updateUser(User user); int deleteUser(int id); // 鎖定用戶 int lockUser(User user); }
上面是一個對訂單crud的接口,如今有其餘項目組的同事須要鎖定用戶的功能,而後你可能一拍腦殼直接把上面整個接口UserController扔給他(或者直接扔一個swagger文檔),這樣同事會很懵逼:我只要鎖定用戶就行,爲何還要這麼多接口?
這樣作暴露的問題:
因此咱們儘可能要最小化暴露接口,根據不一樣的調用者僅提供他們當前須要的接口,提供的公共接口越多越難以維護。
接口隔離原則與單一職責的區別:
一、單一職責要求的的是模塊、類、接口的職責單一,
二、接口隔離原則要求的是暴露給使用者的接口儘量少。
能夠這麼理解:一個類某個職責有10個接口都暴露給其餘模塊使用,按單一原則來說是合理的,可是按接口隔離來說是不容許的。
定義:高層模塊不直接依賴底層模塊,依賴於抽象,底層模塊不依賴於細節,細節依賴於抽象。
這一點若是咱們是使用spring開發的項目就已經用到了。spring的依賴注入就是依賴倒置原則的體現。
// 之前沒有使用spring的時候,咱們是這樣初始化service的 // 存在的問題:一、若是須要替換成一個新的實現類,改動點太多,簡單點說就是高耦合; // 二、使用者不須要關注具體的實現類,只關注有哪些接口能用就行; // 三、對象實例不能共享,每一個使用的地方都是新建的實例,實際上用同一個實例就好了。 UserService userService = new UserServiceImpl();
經過spring的IOC容器,咱們只要定義好依賴關係,IOC容器就能夠幫咱們管理對應的實例,起到了鬆耦合的做用。
還有其餘的使用場景嗎?
有,舉例:
public class UserServiceImpl { private KafkaProducer producer; public int addUser(User user){ // 建立用戶 // 發送消息到消息隊列,由感興趣的系統訂閱並消費。 producer.send(msg); } }
這裏初看沒有什麼問題,但若是後續咱們更換了kafka爲rabbitmq,那上面使用到kafka的類都須要從新調整。
咱們利用"高層模塊不直接依賴底層模塊,依賴於抽象"對上面代碼進行調整,讓咱們的實現類UserServiceImpl不直接依賴KafkaProducer,而是依賴接口類MessageSender。
public class UserServiceImpl { private MessageSender sender; public int addUser(User user){ // 建立用戶 // 發送消息到消息隊列,由感興趣的系統訂閱並消費。 sender.send(msg); } } public interface MessageSender { void send(Map<String,String> params); } // kafka 實現 public class KafkaProducer implements MessageSender{ public void send(Map<String,String> params) { } }
這樣一來,就算咱們切換成RabbitMq,改動的點無非是對MessageSender實現的更改,而有了spring的IOC容器,咱們很容易就能夠更改實例實現。
// rabbitmq 實現 public class RabbitmqProducer implements MessageSender{ public void send(Map<String,String> params) { } }
控制反轉:控制反轉是一個比較籠統的設計思想,並非一種具體的實現方法,通常用來指導框架層面的設計。這裏的控制指的是程序執行流程的控制,反轉是從程序員變爲框架控制。
依賴注入:一種具體的編碼技巧,不直接使用new建立對象,而是在外部將對象建立好後經過構造函數、方法、方法參數傳遞給類使用。
這三個原則是偏理論性的概念,主要目的是指導咱們學習設計原則後不要過分設計。
定義: 儘可能保持簡單。保持簡單可讓咱們的代碼可讀性更高,維護起來也更容易。但這是一個比較抽象的概念:對於「簡單」的定義沒有統一規範,每一個人的理解都不一致,這個時候就須要code review,同事有不少疑問的代碼就要考慮是否是代碼不夠「簡單」。
實踐過程當中怎麼編寫知足KISS原則的代碼?如下幾點供你們參考:
定義: 你不會須要它。咱們能夠這樣理解:如非必要,勿增功能。
這一個原則咱們能夠用在兩個方面:需求和代碼實現。
對於產品人員提出的需求,按照二八原則,80%的功能是用不上的,因此咱們能夠不作對用戶沒有價值的需求。
對於開發人員的代碼實現,除非編寫的模塊之後會頻繁變化,這種狀況咱們能夠提早構建擴展點,但若是模塊變化不多,咱們就不須要作過多的擴展點,保持功能正常運行就行。
KISS原則和YAGNI原則區別:
KISS原則關注的怎麼作,YAGNI原則關注的是需不須要作。
定義:不要重複本身。普遍的認知是不寫重複代碼,更深刻一點的理解是不要對你的知識和意圖進行復制。
在我看來:解決重複代碼是每一個程序員都會作的事情,可是重複的代碼必定要解決嗎?首先要明白解決重複代碼的重點是創建抽象,那這個抽象有沒有存在的意義?咱們應該根據實際的業務場景,若是發現引發該抽象改變的緣由超過一個,這說明該抽象沒有存在的意義。
例如,咱們開發crud接口中常見的VO和Entity:
public class UserEntity { private String username; private String name; private Integer age; private String password; } public class UserVO { private String username; private String name; private Integer age; // 用戶擁有的菜單 private List menuList; }
咱們若是按DRY原則將重複的代碼合併到一個類:
public class BaseUser{ private String username; private String name; private Integer age; private String phone; } public class UserEntity extends BaseUser{ private String password; } public class UserVO { // 用戶擁有的菜單 private List menuList; }
改爲這樣會有什麼問題?若是後續UserVO不容許暴露age屬性或者須要對手機號加密,這個時候就須要改動BaseUser和UserEntity,對UserVO的維護就會改動到BaseUser和UserEntity,一方面違反了單一職責,另外一方面須要對發現全部使用BaseUser、UserEntity、UserVO的地方進行測試,增長了維護成本。
基於以上考慮,咱們須要將對UserVO的改動隔離起來:還原成剛開始重複代碼的場景。
實行DRY原則的方式:
三次法則(Rule of Three):
第一次先寫了一段代碼,不考慮複用性;
第二次在另外一個地方寫了一段相同的代碼,能夠標記爲需清除重複代碼,可是暫不處理;
再次在另外一個地方寫了一樣的代碼,如今能夠考慮解決重複代碼了。
本篇的宗旨是給你們樹立一個觀點:設計原則是設計模式的基礎,而不是設計模式的附屬物。設計模式是在特定問題應用設計原則的解決方案。可是隻用設計原則開發軟件離目標是有誤差的,因此咱們也要借鑑設計模式:熟悉不一樣場景下設計原則的使用方式,這樣才能開發出可擴展性、可維護性、可讀性高的軟件。
本篇文章若有錯誤或語義不明確的地方歡迎你們指正。