一個典型的web應用中可能會有很多DAO接口(也被稱作mapper,以下皆稱mapper),在某些業務場景中,我們會對不同的mapper進行高度相似的業務操作,這種情況下如果仍然裸用mapper進行CRUD,就可能制造出大批高資訊熵的代碼,久而久之,難以維護.
例如在筆者之前所開發的項目,它是一個雲産品監控系統,通過組合阿裡雲的blink、datahub、TableStore以及一些java微服務,實作了對幾十種阿裡雲産品的數百個監控項的統一配置實時監控和按規則報警.
為了便于blink服務分監控項計算受監控執行個體的某項名額是否突破門檻值,我們存儲執行個體門檻值配置的表結構被設計以執行個體ID作為唯一辨別,橫向擴充監控項的橫表結構:
執行個體ID | 監控項1 | 監控項2 | 監控項3 | 更多監控項... |
執行個體1 | 55 | 66 | 77 | 88 |
執行個體2 | ||||
更多執行個體... |
并且,讓每一種産品都對應一張門檻值橫表,便于維護,也避免了給不同産品的同名監控項起别名(比方說,ECS和RDS都有記憶體使用率這個監控項)維護的成本.
在這個場景中,負責報警配置的業務開發人員事實上并不需要關心每個監控項的具體業務含義,他可以對監控項作一個統一的抽象:
// 由于曆史原因,我們的項目為每一個監控項都建立了對應的VO,
// 這樣很繁瑣,但是對于分類維護監控項或在domain層次針對性地擴充某個監控項則是有利的
// 如果你偏好貧血模型,那麼可以嘗試用一個CommonMonitorConfig描述所有
public abstract class CommonMonitorConfig {
// 超門檻值狀态超過此時間,則進行報警
String keepTime;
// 自上一次發送報警後的沉默周期
String silenceTime;
// 此監控項是否啟用報警
Boolean isOpen;
// 監控項對應的字段名稱
String monitorName;
// 配置門檻值
Double value;
// 産品類型,貧血模型需要此字段
String productType;
// 充血模型可以定義此方法,讓處于産品及的監控類例如EcsMonitorConfig實作此方法
abstract TypeEnum getTypeEnum();
public CommonMonitorConfig(){
// 如果使用充血模型,你可以分門别類的在構造方法中按類型初始化一些預設配置
}
}
前端可以根據此抽象制作操作報警配置的表單界面并送出報警配置資料.在限制好CommonMonitorConfig的monitorName與門檻值配置執行個體類相應字段的映射關系後,後端接口中可以使用反射友善規整地将VO對象的相應值映射統一到對應的DO對象中.
當然,在真正實作絲滑流暢無污染的反射之前還需要作一點點合理封裝.
以執行個體門檻值配置的存儲和查詢為例,首先給所有産品的entity類定義一個公共父類,并提取公共業務字段到父類中,使得子類實體隻存放門檻值:
public class CommonInsAlarmRule {
@TableId
public Long id;
/**
* 規則編碼
*/
public String ruleUUID;
/**
* 執行個體ID
*/
public String instanceId;
public String moduleCode;
}
然後,寫一個ConfigUtil維護一些靜态映射關系并封裝常用方法:
/**
* k: 産品類别
* v: 報警規則實體類
*/
public static final Map<TypeEnum, Class<? extends CommonInsAlarmRule>> insClassMap = new ImmutableMap
.Builder<CloudProductTypeEnum, Class<? extends CommonInsAlarmRule>>()
.put(ECS, EcsAlarmRule.class)
.put(EIP, EipAlarmRule.class)
.put(RDS, RdsAlarmRule.class)
// ....
.build();
// 比方說,從上面的字典中擷取insType,然後将其執行個體化為被标記為CommonInsAlarmRule的具體子類對象
public static <P, S extends P> P newInstance(Class<S> insType) {
try {
return insType.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
}
}
// 這個方法直接定義在TypeEnum這個枚舉類裡
public static TypeEnum findEnumByNameIgnoreCase(String displayName){
return Arrays.stream(TypeEnum .values()).filter(e -> e.displayName.equalsIgnoreCase(displayName)).findFirst().orElse(null);
}
同樣地,在ConfigUtil中注入并初始話各個産品mapper對象(這裡以MybatisPlus為例)的映射關系:
private static Map<String, BaseMapper<? extends CommonInsAlarmRule>> instanceAlarmRuleMapperMap;
private final EcsAlarmRuleMapper ecsAlarmRuleMapper;
private final RdsAlarmRuleMapper rdsAlarmRuleMapper;
private final EipAlarmRuleMapper eipAlarmRuleMapper;
//....
@PostConstruct
void init(){
instanceAlarmRuleMapperMap = new ImmutableMap.Builder<TypeEnum, BaseMapper<? extends CommonInsAlarmRule>>()
.put(EcsAlarmRule.class.getName(), ecsAlarmRuleMapper)
.put(EipAlarmRule.class.getName(), eipAlarmRuleMapper)
.put(RdsAlarmRule.class.getName(), rdsAlarmRuleMapper)
// .....
.build();
}
為了更好地利用MybatisPlus的LambdaQueryWrapper靜态字段緩存進行代碼中相關SQL的靜态檢查,需要為CommonInsAlarmRule建立一個無需對應任何資料庫表的形式化mapper:
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface AbstractInsMapper extends BaseMapper<CommonInsAlarmRule> {
}
最後,在ConfigUtil中封裝增删查改的常用方法:
public static <T extends CommonInsAlarmRule> BaseMapper<T> getInsAlarmRuleMapperByInsType(TypeEnum productType) {
Class<T> clazz = (Class<T>) insClassMap.get(productType);
return (BaseMapper<T>) instanceAlarmRuleMapperMap.get(clazz.getName());
}
public static List<CommonInsAlarmRule> selectList(CloudProductTypeEnum productType, LambdaQueryWrapper<CommonInsAlarmRule> wrapper) {
return getInsAlarmRuleMapperByInsType(productType).selectList(wrapper);
}
public static CommonInsAlarmRule selectOne(CloudProductTypeEnum productType, LambdaQueryWrapper<CommonInsAlarmRule> wrapper) {
return getInsAlarmRuleMapperByInsType(productType).selectOne(wrapper);
}
public int insertOne(CloudProductTypeEnum productType, CommonInsAlarmRule rule){
return getInsAlarmRuleMapperByInsType(productType).insert(rule);
}
舉個例子,下圖是一段根據執行個體ID及産品類型查詢指定執行個體的配置門檻值的代碼,可以明顯看出,這段代碼的資訊熵會随着産品數量的增加而逐漸升高:

使用統一封裝之後的接口來替代原先的這段代碼,隻需三行,且可在後續疊代中保持資訊密度不變: