開發框架搭建考量
本文梳理了在搭建開發框架時的一些考量及具體的處理方法。
框架搭建目標
-
整體代碼規範化
-
重複代碼自動化
-
複雜關係精簡化
-
公共代碼統一化
儘量保證開發人員的核心關注點在業務邏輯。
儘量避免非業務問題影響開發進度。
整體代碼規範化
規範的作用不是爲了規範而規範,也不是對不按規範的人做出懲罰。是爲了方便溝通。
-
方便新員工能根據規範文檔快速熟悉代碼結構
-
方便接手其他人的代碼時,只需要瞭解業務就可以
-
方便和其他人溝通時,不需要關注業務之外的內容
可以從下面幾個方面做出規範:
-
統一格式化
-
一種方案是使用相同的 IDE,一種方案是使用相同的格式化配置
-
保證在代碼合併或代碼 review 時,不會因爲代碼格式化問題導致衝突或難以對比
-
統一三方庫的使用
-
統一使用一種功能的庫,比如:持久化框架選定了 Mybatis 就不要再用 Hibernate,安全框架選定了 SpringSecurity 就不要使用 Shiro
-
這會增加團隊成員的學習成本,以及後續的維護成本
-
統一代碼風格
-
命名方式的統一:命名實際上是個很重要但是一直不被重視的工作。好的命名能極大的降低溝通成本。至少要保證包層級的名稱的語義。我接觸的項目裏,對 DTO 來說,有的項目裏叫 PO,有的叫 VO,導致一個開發人員接收另一個項目時,理解上就有了難度。
-
職責區分:SRP 是一個看起來簡單,但是很難做好的設計原則。比如:還是拿 DTO 來說,有的項目直接會直接將 Entity 作爲 DTO 來使用,可以避免 DTO 與 Entity 直接的數據字段處理。實際上這裏的 DTO 即做了 DTO 的工作,又做了 Entity 的工作。邏輯簡單時是比較爽,但是因爲兩個對象的職責不一樣,進化頻率也不一樣。當 DTO 字段調整時,就會對後面的 DAO 操作產生影響。
-
RESTful 規範化:現在大部分的項目都會使用 RESTful。如果使用了 RESTful,那就按照 RESTful 的規範來。儘量提高代碼語義,不要使用個四不像。比如:就使用 POST 和 GET。
-
非業務字段獨立:業務相關對象和非業務相關對象區分開。例如:對於查詢來說,可能需要進行分頁。對於分頁控制字段建議整合爲 PageInfo 對象來統一處理,而不要作爲獨立的字段加到 DTO 對象裏。一是不方便管理,二是將不同場景的字段混合到了一起,不易於區分。
-
接口攜帶版本號:接口攜帶版本號,可以基於版本來進行平滑升級。例如:可以保留 / api/user/v1/login,同時發佈 / api/user/v2/login,待 / api/user/v1/login 不再使用後,在刪除 / api/user/v1/login
-
代碼架構匹配
-
架構設計時,實際觸及到的是系統、子系統、層級、模塊;而具體到代碼,只體現了系統(一個個的服務)和層級(Controller,Service,DAL)。模塊的映射關係消失了。
-
爲了提高代碼與架構的匹配度,降低溝通成本。可以在代碼層面通過添加一層包的方式,來映射模塊關係。
-
接口與實現的層級關係(推薦)
-
傳統 MVC 框架是按照 Controller、Service 和 DAO 的方式來分層的。如果有接口,一般情況下接口定義與實現是在同一層的,比如 Service 和 ServiceImpl,都在 Service 層。實際上此種結構是有問題的。
-
假設現在有兩套 Service 的實現,代碼在項目中如何存儲?
-
實際上 Service 和 ServiceImpl 分屬不同的層,Service 是接口層,ServiceImpl 是實現層(雖然可能感覺不符合常識,不過確實如此)。兩套 ServiceImpl 對應到 Maven 項目中,就是兩個模塊。當要替換實現時,通過 Maven 的項目結構調整依賴即可。
重複代碼自動化
在代碼規範化的前提下,基於代碼生成工具(比如 IDEA 的 EasyCode)構建一套完整的代碼模板,基於代碼模板快速的生成對應的代碼。提高開發效率。
對於庫表結構調整後對代碼的影響,可以基於代碼結構的調整來儘量減少對現有代碼的影響。假設使用 Mybatis 作爲持久化框架,有三個方案:
-
基於 MybatisPlus(其實就是接口繼承,具體參考 MybatisPlus 文檔)
-
基於 MybatisProvider
-
基於 Mapper 接口繼承
基於 MybatisProvider
-
自動生成 Mapper 和 Provider,Mapper 不使用 XML,而使用註解的方式來使用 SQL
-
Provider 中的方法對參數進行反射來處理字段信息,構建對應的 sql
-
當表結構調整後,只需要調整對應的 Bean 的字段即可
// Mapper
@SelectProvider(type = IssueProvider.class, method = "list")
Page list(IssueVO issue);
// Provider
// Issue 是對應的 Bean,字段調整後,在 Issue 中添加對應的字段即可(可以生成,可以手動添加)
public String list(Issue issue) {
SQL sql = new SQL();
sql.SELECT("*");
sql.FROM("issue");
BeanMap beanMap = BeanMap.create(issue);
for (Object key : beanMap.keySet()) {
Object val = beanMap.get(key);
if (val != null) {
if (val instanceof String && ((String) val).contains("%")) {
sql.WHERE("
" + FieldUtils.camelToLine(key + "") +"
"+"like #{"+ key +"}");} else {
sql.WHERE("
" + FieldUtils.camelToLine(key + "") +"
"+"=#{"+ key +"}");}
}
}
return sql.toString();
}
-
優勢:
-
改動量比較小
-
劣勢:
-
反射對性能的影響
-
基於註解的方式,習慣性問題
-
IDEA 對 Provider 的方式的支持沒有 XML 高,XML 支持自動完成,Provider 中不支持
基於 Mapper 接口繼承
-
Mybatis 生成的 Mapper 和 XML 文件分別爲獨立的目錄
-
自定義的 Mapper 繼承生成的 Mapper,在自定義 Mapper 中新增自定義接口方法,對應的 sql 添加到對應的自定義 XML 文件中
-
自定義 XML 文件可以基於 NameSpace 來使用生成的 XML 文件的 Result
-
當表結構調整後,重新生成對應的 Mapper 和 XML 即可
-
優勢:
-
符合習慣
-
劣勢:
-
改動量相對多一點,不過是生成的,所以基本可以忽略
複雜關係精簡化
一般項目中的庫表設計基本都是按照關聯表的方式進行設計的:
-
主表 + 子表的關聯關係
-
表中冗餘其它表的字段
可以結合場景考慮,做一些結構簡化和結構化。簡化爲對單表的操作,可以基於上面的模板來自動生成代碼。
主表 + 子表的關聯關係
此種方式適用於主表和子表需要單獨修改的場景。如果主表和子表是聚合關係,即子表依賴於主表存在,且需要一起調整,甚至子表不需要調整,實際可以簡化此種關聯關係。
因爲此種關聯關係涉及到了聯表查詢,聯表查詢是無法基於工具生成的。通過簡化此種關係,可以基於工具來提高開發效率。
表中冗餘其它表的字段
有些情況下,可能會將兩個邏輯上分離的對象整合爲一個對象來處理,簡化操作負責度。例如訂單中可能直接就包含購買的應用信息。此種情況實際是爲了操作的便利性,同時兼顧數據庫特性,將結構化的數據扁平化了。
數據扁平化本身問題不大,不過弱化了代碼語義。在正常理解裏,訂單包含訂單明細,訂單明細中是商品信息。
可以通過下面的方法來解決上面提到的兩個問題。
解決方案
Mysql5.7 開始支持 Json。上述兩種情況,都可以基於 Json 的方式來處理。即數據庫字段可以使用 json 類型。
// 支持基於 json 的查詢,請自行 google
create table order
(
......
item_info_json json not null comment 'json',
);
public class Order {
private ItemInfo itemInfoJson;
}
在 Mybatis 層面,通過 TypeHandler 來處理 Json 與對象之間的自動轉換。
public interface OrderDao {
// 配置轉換 handler
@Results(id = "jsonResult", value = {
@Result(property = "itemInfoJson", column = "item_info_json", typeHandler = JsonTypeHandler.class)
})
@Select("select * from order where rec_id = #{recId}")
Order selectByPrimaryKey(@Param("recId") String recId);
@InsertProvider(type = OrderProvider.class, method = "insert")
void insert(Order model);
......
}
public class OrderProvider {
public String insert(Order order) {
SQL sql = new SQL();
sql.INSERT_INTO("order");
BeanMap beanMap = BeanMap.create(order);
for (Object key : beanMap.keySet()) {
Object val = beanMap.get(key);
if (val != null) {
if ((key + "").endsWith("Json")) { // 根據後綴判定是否需要轉換
sql.VALUES("
" + FieldUtils.camelToLine(key + "") +"
","#{"+ key +", typeHandler=com.iwhalecloud.common.mybatis.JsonTypeHandler}");} else {
sql.VALUES("
" + FieldUtils.camelToLine(key + "") +"
","#{"+ key +"}");}
}
}
return sql.toString();
}
轉換類處理邏輯:
public class JsonTypeHandler extends BaseTypeHandler {
private static final Gson gson = new Gson();
private Class clazz;
public JsonTypeHandler(Class clazz) {
if (clazz == null) {
throw new IllegalArgumentException("Type argument cannot be null");
} else {
this.clazz = clazz;
}
}
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, this.toJson(parameter));
}
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return this.toObject(rs.getString(columnName), this.clazz);
}
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return this.toObject(rs.getString(columnIndex), this.clazz);
}
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return this.toObject(cs.getString(columnIndex), this.clazz);
}
private String toJson(T object) {
try {
return object instanceof String ? (String)object : gson.toJson(object);
} catch (Exception var3) {
throw new RuntimeException(var3);
}
}
private T toObject(String content, Class<?> clazz) {
if (content != null && !content.isEmpty()) {
try {
return clazz == String.class ? content : gson.fromJson(content, clazz);
} catch (Exception var4) {
throw new RuntimeException(var4);
}
} else {
return null;
}
}
}
公共代碼統一化
對於多個項目中使用到的代碼,需要根據場景進行公共化統一管理,避免公共代碼散落在各個服務中,難以維護。
根據場景的不同,可以有幾種管理方式:
-
基於公共 jar 包的管理
-
基於 git 源代碼的管理
-
基於公共服務的管理
基於公共 jar 包的管理
此方案是最常規的方案,如果幾個服務中使用到了公共的代碼,比如:一些工具類。這種情況下就可以將這些類獨立爲公共項目,通過 jar 包的方式來進行管理。
基於 git 源代碼的管理
如果公共的代碼修改頻率比較高,可以基於 git 的 subtree 來處理公共代碼的管理問題。
-
創建一個項目,用於存放需要公用的代碼,正常創建即可
-
在需要使用同步代碼的項目中執行如下命令(只需要執行一次):
module 是取的別名
git remote add -f module ${上面的項目 git 地址}
將這個項目拉取到 src/module 目錄下
git subtree add --prefix=src/module module master --squash
-
在公共項目中修改模塊代碼後,正常 push 即可
-
需要同步的項目,執行如下命令(如果需要同步共享代碼,則執行):
git subtree pull --prefix=src/module module master
◆ 基於公共服務的管理
如果公用的邏輯是一個獨立的功能,後續可以作爲服務對外提供服務。那可以考慮將這些代碼獨立爲服務來對外提供服務。
總結
本文從幾個維度來考量在搭建一個項目框架時需要考慮的問題,以及對應的解決方案。
來源:
https://www.toutiao.com/i6876823730878677516/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pbHkRrKwE873ZTrDDY_Dkw