一、背景
最近我們團隊有幸接了兩個0到1的項目,一期項目很緊急,團隊成員也是加班加點,從開始編碼到完成僅用了一星期多一點點,期間還不斷反複斟酌代碼如何抽象代碼,如何寫得更優雅,一遍又一遍的調整,我也是一次又次的閱讀每個團隊成員的代碼,雖然還有些不如意,但整體來說還算是滿意,參與項目的成員經過不斷琢磨,對一些功能不斷抽像,團隊進步也是非常明顯,以下舉了幾個樣例。
那麼這次我為什麼對工程代碼抓得更嚴,主要是之前交接了不少其它團隊的工程,由于當時設計不夠好,維護起來非常痛苦,也正是因為這些工程,我閱讀了非常多的代碼,對自己也有很大的啟發和感想,是以希望我自己的團隊能盡可能寫好代碼,減少維護上的一些痛苦。另外就是我們寫的代碼除了給機器執行外,更多的時候是給人讀的,這個讀代碼的可能是後來的維護人員,是以呢也順便總結一下。
二、衡量代碼好環的原則
2.1 評判代碼名額
實際上,咱們平時嘴中常說的“好”和“爛”,是對代碼品質的一種描述。“好”籠統地表示代碼品質高,“爛”籠統地表示代碼品質低。對于代碼品質的描述,除了“好”“爛”這樣比較簡單粗暴的描述方式之外,我們也經常會聽到很多其他的描述方式。這些描述方法語義更豐富、更專業、更細化。我搜集整理了一下,羅列在了下面,一般有幾下幾标準,分别是可讀性、可維護性、可擴充性、可複用性 、靈活性、可測試性等等
-
可讀性 readability
軟體設計大師 Martin Fowler 曾經說過:“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”翻譯成中文就是:“任何傻瓜都會編寫計算機能了解的代碼。好的程式員能夠編寫人能夠了解的代碼。”Google 内部甚至專門有個認證就叫作 Readability。隻有拿到這個認證的工程師,才有資格在 code review 的時候,準許别人送出代碼。可見代碼的可讀性有多重要,畢竟,代碼被閱讀的次數遠遠超過被編寫和執行的次數。
我個人認為,代碼的可讀性應該是評價代碼品質最重要的名額之一。我們在編寫代碼的時候,時刻要考慮到代碼是否易讀、易了解。除此之外,代碼的可讀性在非常大程度上會影響代碼的可維護性。畢竟,不管是修改 bug,還是修改添加功能代碼,我們首先要做的事情就是讀懂代碼。代碼讀不大懂,就很有可能因為考慮不周全,而引入新的 bug。
既然可讀性如此重要,那我們又該如何評價一段代碼的可讀性呢?我們需要看代碼是否符合編碼規範、命名是否達意、注釋是否詳盡、函數是否長短合适、子產品劃分是否清晰、是否符合高内聚低耦合等等。你應該也能感覺到,從正面上,我們很難給出一個覆寫所有評價名額的清單。這也是我們無法量化可讀性的原因。
實際上,code review 是一個很好的測驗代碼可讀性的手段。如果你的同僚可以輕松地讀懂你寫的代碼,那說明你的代碼可讀性很好;如果同僚在讀你的代碼時,有很多疑問,那就說明你的代碼可讀性有待提高了
-
可維護性 maintainability
一般指的是在不破壞原代碼設計的前提下,快速修改bug或增加代碼,不會帶來新bug,表明該代碼的維護性比較好。落實到編碼開發,所謂的“維護”無外乎就是修改 bug、修改老的代碼、添加新的代碼之類的工作。所謂“代碼易維護”就是指,在不破壞原有代碼設計、不引入新的 bug 的情況下,能夠快速地修改或者添加代碼。所謂“代碼不易維護”就是指,修改或者添加代碼需要冒着極大的引入新 bug 的風險,并且需要花費很長的時間才能完成。
-
可擴充性 extensibility
代碼面對未來新需求的變化能力,一般來說,開發新需求的時候,不修改原代碼或很少修改,即可達到需求開發的能力,通常會預留一些功能擴充點。
-
可複用性 reusability
盡量避免重複造輪子,即能夠沉澱出一些通用的代碼邏輯,保持與上層業務代碼的解耦
-
靈活性 flexibility
這個詞比較寬泛。通常與可維護性、可擴充性以及可複用性類似
-
可測試性
主要反映在寫單測的時候。從兩個方面展現:
1.單元測試是否容易編寫;
2.寫單元測試的時候,不能依賴環境,遠端調用其他服務的借口,盡可能進行mock資料,保持服務之間的解耦。雖然要團隊每人都按這個規範走很難,但我們團隊有一個強制要求,就是每個功能函數不能超過50行代碼,而且要求代碼越短越好。
這幾個次元是評判代碼次元比較重要的幾個名額。
2.2 指導理論
高内聚低耦合幾乎是每個程式員員都會挂在嘴邊的,但這個詞太過于寬泛,太過于正确,是以聰明的程式設計人員們提出了若幹面向對象設計原則來衡量代碼的優劣:
- 開閉原則 OCP (The Open-Close Principle)
- 單一職責原則 SRP (Single Responsibility Principle)
- 依賴倒置原則 DIP (Dependence Inversion Principle)
- 最少知識原則 LKP (Least Knowledge Principle)) / 迪米特法則 (Law Of Demeter)
- 裡氏替換原則 LSP (Liskov Substitution Principle)
- 接口隔離原則 ISP (Interface Segregation Principle)
-
組合/聚合複用原則 CARP (Composite/Aggregate Reuse Principle)
這些理論想必大家都很熟悉了,是我們編寫代碼時的指導方針,按照這些原則開發的代碼具有高内聚低耦合的特性,換句話說,我們可以用這些原則來衡量代碼的優劣。
三、代碼實作技巧
我相信每個工程師都想寫出高品質的代碼,不想一直寫沒有成長、被人吐槽的爛代碼。那如何才能寫出高品質的代碼呢?針對什麼是高品質的代碼,我們剛剛講到了七個最常用、最重要的評價名額。是以,問如何寫出高品質的代碼,也就等同于在問,如何寫出易維護、易讀、易擴充、靈活、簡潔、可複用、可測試的代碼,但要寫好代碼,也不是一蹴而就,需要非常多的實踐與積累,下面簡舉例說明:
3.1 抽像能力
抽象思維是我們工程師最重要的思維能力,因為軟體技術本質上就是一門抽象的藝術。我們工程師每天都要動用抽象思維,對問題域進行分析、歸納、綜合、判斷、推理,進而抽象出各種概念,挖掘概念和概念之間的關系,然後通過程式設計語言實作業務功能,是以,我們大部分的時間并不是在寫代碼,而是在梳理需求,理清概念,對需求有一個全局的認知。而抽像能力讓我及團隊切身感受到,它給我們在編碼和設計上帶來的質的變化。
-
案例一:異步Excel導出
其實導出Excel功能在我們工程裡随處可見,特别是咱們的營運希望一次性導出越多資料越好,為了不給我們系統帶來太大壓力,對于大資料量的導出一般異步進行,針對于這樣一個簡單的功能,那麼應該如何抽像呢?
普通的寫法:
public String exportXXX(參數) throws Exception {
//業務實作
}
public String exportXXX2(參數) throws Exception {
//業務實作
}
抽像寫法:
我們其實可以把每個異步導出看作是一個異步任務,而每個任務可導出的内容是不一樣的,是以完全可以把導出抽像一個方法,由每個具體實作類去實作導出不同的内容,具體如下:
// export excel
public interface IExcelExportTask {
String export(BizCommonExportTask exportTask) throws Exception;
}
//樣例實作類
XXXXExportTask implements IExcelExportTask {
String export(BizCommonExportTask exportTask) throws Exception{
public String export(BizCommonExportTask exportTask) throws Exception {
//組織資料篩選條件
TestReq queryReq = GsonUtils.toObject(exportTask.getInputParams(),TestReq.class);
String fileName = String.format("%s%s%s", exportTask.getUploadFileName(),System.currentTimeMillis(),".xlsx");
String downUrl = excelService.uploadExcel(fileName, null, new Fetcher<PreOccupyModel>(PreOccupyModel.class) {
//循環擷取資料
@Override
public List<TestModel> fetch(int pageNo, int pageSize) throws OspException{
TestQueryResp resp = testFethchLogic.fetchRecord(queryReq);
return pageNo > resp.getPageNum() ? Collections.emptyList() :toExcelModel(resp);
}
});
return downUrl;
}
}
public class XXXXExportTask1 implements IExcelExportTask {
@Override
public String export(BizCommonExportTask exportTask) throws OspException {
TestQuery query = GsonUtils.toObject(exportTask.getInputParams(), TestQuery .class);
String fileName = String.format("%s%s%s", exportTask.getUploadFileName(), System.currentTimeMillis(), ".xlsx");
return excelService.uploadExcel(fileName, null, new Fetcher<ExportItemModel>(TestModel.class) {
@Override
public List<TestModel> fetch(int pageNo, int pageSize) throws OspException {
return XXXXLogic.queryExportItem(query, pageNo, pageSize);
}
});
}
}
//導出任務分發器
public class ExcelTaskDispacther extends ApplicationObjectSupport {
public boolean dispacthTask(Long taskId) throws OspException {
updateTaskStatus(exportTask,CommonExportStatus.CREATING,TransferExportStatus.CREATING,StringUtils.EMPTY);
try {
String beanName = getBeanName();
ExportTaskHandler exportTaskHandler = getApplicationContext().getBean(beanName , IExcelExportTask .class);
if(exportTaskHandler == null) {
log.warn(String.format("任務ID[%s]寫入配置錯誤!", taskId));
return false;
}
updateTaskStatus(exportTask,CommonExportStatus.CREATE_SUCCESS,TransferExportStatus.CREATE_SUCCESS,StringUtils.EMPTY);
log.info(String.format("任務ID[%s]RFID為[%s]處理成功", exportTask.getId(),rfid));
return true;
} catch(BusiException ex) {
log.info("任務ID[{}]失敗,原因:{}", exportTask.getId(),ex.getMessage(),ex);
updateTaskResult();
} catch(Exception ex) {
log.info("任務ID[{}]失敗,原因:{}", exportTask.getId(),ex.getMessage(),ex);
updateTaskResult();
}
return false;
}
}
-
案例二:系統通知
在微服務化流行的今天,為了提升系統吞吐量,系統職責越來越細,各系統子產品需要頻繁互動資料,那麼對于複雜的資料互動場景,比如我們調撥單,調撥單在扭轉的過程中需要與很多系統互動,跟門店、倉庫、庫存子產品有非常多的互動,我們又該如何抽像呢,以下是調撥與各系統互動的代碼示例
//接口定義
public interface BizNotificationHandler {
/**
* 抛異常會當失敗處理
* 是否需要重試由BizNotificationStatus傳回狀态來決定
* @param bizNotification
* @return
* @throws OspException
*/
BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException;
}
//推送調撥差異資料給庫存系統
public class SyncDiffToSimsAndBackQuotaHandler implements BizNotificationHandler {
@Override
public BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {
//業務邏輯實作
return BizNotificationStatus.PROCESS_SUCCESS;
}
}
//占用庫存
public class TransferOccupyInventoryHandler implements BizNotificationHandler {
@Override
public BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {
//業務實作
}
}
//在GPDC生成新條碼
public class GpdcGenerateNewBarcodeHandler implements BizNotificationHandler {
@Override
public BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {
//業務代碼實作
}
}
其實我們在與其它系統互動的時候,我們可以把每一個互動動作抽像成一個通知事件,每次互動的時候,寫一個事件通知事件即可。
3.2 組合/聚合複用原則
關于組合/聚合複用原則,其實我們在項目過程會經常遇到,比如項目裡會經常管理各種單據,像采購單、調撥單、收貨單等,而對于每種單據都會有各種各樣的較驗,我們先來看一段建調撥單代碼,具體如何下:
//接口定義
public interface TransferValidator {
boolean validator(CreateTransferCtx ctx) throws OspException;
}
//接口實作1
public class W2sCrossPoQtyValidator implements TransferValidator {
@Override
public boolean validator(CreateTransferCtx ctx) throws OspException {
//較驗器代碼實作
}
//接口實作2
public class W2sStoreBarcodeSaleLimitValidator implements TransferValidator {
@Override
public boolean validator(CreateTransferCtx ctx) throws OspException {
//較驗器代碼實作
}
}
//較驗器組裝
public class TransferValidators {
public ValidatorChain newChain() {
return new ValidatorChain();
}
public class ValidatorChain {
private final List<TransferValidator> validators = new ArrayList<>();
public ValidatorChain qtyValidator() {
validators.add(qtyValidator);
return this;
}
public ValidatorChain transferRouteCfgValidator() {
validators.add(transferRouteCfgValidator);
return this;
}
public ValidatorChain prodValidator() {
validators.add(prodValidator);
return this;
}
public ValidatorChain w2sWarehouseStoreValidator() {
validators.add(w2sWarehouseStoreValidator);
return this;
}
public ValidatorChain w2sStoreBarcodeSaleLimitValidator() {
validators.add(w2sStoreBarcodeSaleLimitValidator);
return this;
}
public ValidatorChain w2sAssignPoValidator() {
validators.add(w2sAssignPoValidator);
return this;
}
public ValidatorChain w2sCrossPoValidator() {
validators.add(w2sCrossPoValidator);
return this;
}
public ValidatorChain w2sCrossPoQtyValidator() {
validators.add(w2sCrossPoQtyValidator);
return this;
}
public ValidatorChain w2sCross4XupValidator() {
validators.add(w2sCross4XupValidator);
return this;
}
public ValidatorChain repeatLineValidator() {
validators.add(repeatLineValidator);
return this;
}
public ValidatorChain sstradeBarcodeValidator() {
validators.add(sstradeBarcodeValidator);
return this;
}
public ValidatorChain s2wWarehouseStoreValidator() {
validators.add(s2wWarehouseStoreValidator);
return this;
}
public boolean validator(CreateTransferCtx ctx) throws OspException {
for (TransferValidator validator : validators) {
if (!validator.validator(ctx)) {
return false;
}
}
return true;
}
}
}
//業務代碼使用
public interface TransferCreator {
boolean createOrder(CreateTransferCtx ctx) throws OspException;
}
public abstract class DefaultTransferCreator implements TransferCreator {
@Override
public boolean createOrder(CreateTransferCtx ctx) throws OspException {
validator(ctx)
//實作業務邏輯
}
protected abstract boolean validator(CreateTransferCtx ctx) throws OspException;
}
//店倉調撥單
public class S2wRefundCreator extends DefaultTransferCreator {
//較驗器自由組裝
@Override
protected boolean validator(CreateTransferCtx ctx) throws OspException {
return transferValidators.newChain()
.qtyValidator()
.transferRouteCfgValidator()
.prodValidator()
.validator(ctx);
}
}
通過上面的示例,其實抽像并不難,難的是我們要花時間去思考,去了解,隻有自己花足夠的多時間,反複訓練我相信比較容易做到,最近在兩個新項目,我們團隊的部分成員回報做夢都在想如何實作更合理。
四、總結
寫出滿足這些評價标準的高品質代碼,我們需要掌握一些更加細化、更加能落地的程式設計方法論,包括面向對象設計思想、設計原則、設計模式、編碼規範、重構技巧等。而所有這些程式設計方法論的最終目的都是為了編寫出高品質的代碼。
比如,面向對象中的繼承、多态能讓我們寫出可複用的代碼;編碼規範能讓我們寫出可讀性好的代碼;設計原則中的單一職責、DRY、基于接口而非實作、裡式替換原則等,可以讓我們寫出可複用、靈活、可讀性好、易擴充、易維護的代碼;設計模式可以讓我們寫出易擴充的代碼;持續重構可以時刻保持代碼的可維護性等等,以上示例僅供參考,也希望大家更多參與讨論。