思路
最初我拿到一個問題的時候,首先想的就是他怎么實現,具體的實現
,而面向接口編程需要先考慮好流程,明白變動點可能在哪里,系統的邊界在哪里,邊界的劃分決定了模塊和服務的拆分,變動點決定了需要抽象和隔離的地方。想清楚再下手。
提煉一下就是:
案例
代碼重構方案
//原來的代碼
for (ModuleEnum module : modules) {
switch (module) {
case ALBUM:
Album albumData = getAlbumData(key, pageSize, pageNumber);
if (albumData.getContent().size() == 0) {
searchData.setAlbum(null);
} else {
searchData.setAlbum(albumData);
}
break;
case ACTIVITY:
Activity activityData = getActivityData(key, pageSize, pageNumber);
if (activityData.getContent().size() == 0) {
searchData.setActivity(null);
} else {
searchData.setActivity(activityData);
}
break;
case COURSE:
Course courseData = getCourseData(key, pageSize, pageNumber);
if (courseData.getContent().size() == 0) {
searchData.setCourse(null);
} else {
searchData.setCourse(courseData);
}
break;
case BLOG:
Blog blogData = getBlogData(key, pageSize, pageNumber);
if (blogData.getBlogContents().size() == 0) {
searchData.setBlog(null);
} else {
searchData.setBlog(blogData);
}
break;
default:
}
}
這里我們看到了非常多的代碼冗余,邏輯完全一樣,但是寫了四遍.再看下面:
//這里4個方法對應上面的調用(邏輯完全一樣,僅僅是返回值和其中一些調用的方法不一樣,我就只列出方法名了)
private Album getAlbumData(String key, int pageSize, int pageNumber) throws IOException;
private Activity getActivityData(String key, int pageSize, int pageNumber) throws IOException;
private Course getCourseData(String key, int pageSize, int pageNumber) throws Exception;
private Blog getBlogData(String key, int pageSize, int pageNumber) throws IOException;
重構思路
對這里的重構我考慮用如下的方法:
1.使用工廠模式
調用不同的實現,替換switch.
2.在接口和實現之間增加抽象方法
,提取公共邏輯
上碼
1 枚舉實現工廠方法
//枚舉類
public enum ModuleEnum {
//在線課程
COURSE("0", "course", "courseindex"){
@Override
public ISearchService build() {
return new CourseSearch();
}
},
//培訓活動
ACTIVITY("1", "activity", "activityindex") {
@Override
public ISearchService build() {
return new AlbumSearch();
}
},
//知識庫
ALBUM("2", "album", "albumindex") {
@Override
public ISearchService build() {
return new AlbumSearch();
}
},
//Q記
BLOG("3", "blog", "blogindex") {
@Override
public ISearchService build() {
return new BlogSearch();
}
};
public abstract ISearchService build();
}
這里使用枚舉
來實現工廠方法,因為考慮使用反射效率較低.
可以看到,我定義了一個抽象的build方法,返回實現類的接口,而在每個枚舉類中又實現了build方法 返回不同的實現類.
//調用者
ISearchService isearchservice = ModuleEnum.build();
if (isearchservice == null) {
//防御性判斷
throw new RuntimeException("不支持的模塊類型" + ModuleEnum.getName());
}
return isearchservice;
這里通過調用build方法來實現工廠方法的調用
2 增加抽象方法
public abstract class AbstractSearchService implements ISearchService {
private static final Logger log = LoggerFactory.getLogger(AbstractSearchService.class);
@Override
public SearchData getSearchData(String key, int pageNumber, int pageSize, SearchData searchData) throws Exception {
AbstractlModulBase modulData = getModulData(key, pageSize, pageNumber);
if (modulData.getContent().size() == 0) {
searchData.setAlbum(null);
} else {
//根據具體的子類調用具體的實現
searchData = setData(searchData, modulData);
}
return searchData;
}
private AbstractlModulBase getModulData(String key, int pageSize, int pageNumber) {
Page page = new Page(pageSize, pageNumber);
//從ES中查詢基礎數據
List<ParentBean> activityBeanList = getModulBeanList(key, page);
//獲取模塊Id
List<Integer> masterIdList = getParentIdList(activityBeanList);
log.info("搜索到的模塊id = {}", masterIdList);
//從bubbo中查詢實時數據 之后對ES中的數據和Dubbo中的實時數據進行整合 并判斷是否有數據未同步
return getModulBeenAndMix(masterIdList, activityBeanList, page);
}
/**
* 將基類轉換成不同的子類 并賦值
* @param searchData 統一存儲查詢出的所有內容
* @param modulData 基類
* @return SearchData
*/
protected abstract SearchData setData(SearchData searchData, AbstractlModulBase modulData);
/**
* ES查詢
* @param key ES查詢關鍵詞
* @param page 分頁信息
* @return ES查詢結果Been
*/
protected abstract List<ParentBean> getModulBeanList(String key, Page page);
/**
* 從bubbo中查詢實時數據 之后對ES中的數據和Dubbo中的實時數據進行整合 并判斷是否有數據未同步
* @param masterIdList IdList
* @param beanList ES查詢集合
* @param page 分頁信息
* @return 查詢總結果
*/
protected abstract AbstractlModulBase getModulBeenAndMix(List<Integer> masterIdList, List<ParentBean> beanList, Page page);
/**
* 提取IdList
* @param parentBeans been
* @return IdList
*/
private List<Integer> getParentIdList(List<? extends ParentBean> parentBeans) {
Preconditions.checkNotNull(parentBeans);
return Lists.transform(parentBeans, new Function<ParentBean, Integer>() {
@Override
public Integer apply(ParentBean input) {
return input.getId();
}
});
}
}
這里針對具體的邏輯進行拆分 并將具體的實現交由子類完成
一些設計思路
功能設計
- 模塊功能的改變、增加、移除;系統流程的變化
- 適應業務發展的一種常態(頻繁的,不可抗拒的)
- 設計系統時考慮:模塊解耦(避免牽一發而動全身); 面向接口編程(盡量減少對調用方影響)
- 修改系統時考慮:擴展 > 修改 (遵循“開閉原則”) → 功能兼容
程序設計
1. 問題拆分,劃分模塊 明確輸入輸出,暫時忽略代碼細節(切忌代碼堆疊) → 定義接口(面向接口編程)
參數校驗 → 命令識別與解析 → 文件讀取 → 命令執行 → 結果輸出
變化:
- 參數校驗模塊(功能改變):不再校驗源文件目錄參數
- 文件讀取模塊(新流程不再使用):不再需要使用文件讀取模塊
- 結果輸出模塊(功能改變):由輸出到結果文件 → 通過日志到控制臺 [LocalFileOutputService → LogOutputService]
模塊功能變化后的幾種改動方式(不分優劣,取決于具體場景):
- 直接修改原模塊邏輯:核心邏輯未變,局部微調,bugfix
- 繼承原有模塊:優勢:復用父類邏輯;局限性:單繼承
- 新增模塊實現:核心邏輯變化,入參未變化(如結果輸出:輸出文件 → 輸出日志)
- 擴展接口方法:核心邏輯未變,入參變化 (不同方法適用于不同的場景,方法重載,類比構造方法)
優勢:功能聚合,適用不同場景;公共邏輯可以復用
2. 數據模型的建立 → 抽象(關注當前問題域的屬性)、繼承
- 命令實體:關注:(類型,選項,參數)
- 文件數據:關注:(內容,文件名) 忽略(創建時間,訪問權限等)
- 不再代表原始文件數據:而是代表中間處理結果
3. 過程抽象 → 分離變與不變(模板方法模式)
- 流程發生變化
- 對外接口不變
4. 各個模塊代碼的實現 → 代碼規范
- 變量命名:見名知意、駝峰命名、常量大寫下劃線分隔等
- 異常處理:
- 拋出or處理
- 自定義異常:類型、原因、執行現場信息、異常棧(便于定位問題代碼)
- 日志記錄:遠離System.out.println(性能:加鎖+阻塞當前線程)
5. 性能優化 → 可選
多文件流式處理(緩解內存壓力),線程池執行(提高吞吐量)等等。
最后
這里我的實現還不是很完善,但思路基本就是這樣,重構完之后代碼可擴展性更好,代碼冗余減少.