掃二維碼與項目經理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網交流
但是一直都是開發(fā)過程中的一個難題,本文旨在探討如何去從容應對復雜性。

創(chuàng)新互聯(lián)建站科技有限公司專業(yè)互聯(lián)網基礎服務商,為您提供遂寧托管服務器,高防主機,成都IDC機房托管,成都主機托管等互聯(lián)網服務。
熵的概念最早起源于物理學,熱力學第二定律(又稱“熵增定律”),表明了在自然過程中,一個孤立的系統(tǒng)總是從最初的集中、有序的排列狀態(tài),趨向于分散、混亂和無序;當熵達到最大時,系統(tǒng)就會處于一種靜寂狀態(tài)。
軟件系統(tǒng)亦是如此, 在軟件系統(tǒng)的維護過程中。軟件的生命力會從最初的集中、有序的排列狀態(tài),逐步趨向復雜、無序狀態(tài),直到軟件不可維護而被迫下線或重構。
自然界是如何應對這復雜性?
這在物理中被稱為構造定律 (Constructal Law), 是由Adrian Bejan于1995提出的:
For a finite-size system to persist in time (to live), it must evolve in such a way that it provides easier access to the imposed currents that flow through it.
對于一個有限大小的持續(xù)活動的系統(tǒng),它必須以這種方式發(fā)展演進:它提供了一種在自身元素之間更容易訪問的流動方式。
這個定理在自然界中比比皆是,最典型的比如水循環(huán)系統(tǒng),海水蒸發(fā)到大氣,下雨時降落在地面,一部分滲入地面流入江河,一部分繼續(xù)蒸發(fā),不斷循環(huán)。這種自發(fā)性質的設計反映了這一趨勢:他們允許實體或事物更容易地流動 - 以最少的能量消耗到達最遠的地方,就連街道和道路這些人為地構建物體,往往也是有排序的模式,以提供最大的靈活性。
軟件系統(tǒng)的復雜性往往是被低估的。復雜越高,開發(fā)人員會感到不安。對其的理解認知負荷代價就越高,我們就更不快樂。真正的挑戰(zhàn)是在構建我們的系統(tǒng)時要保持其有序以及工程師的生產方式。
Ousterhout教授在《軟件設計的哲學》書中提到:軟件設計的最大目標,就是降低復雜度(complexity)。
就是設計符合業(yè)務的構造定律的演進方式,一種可以以最小的開發(fā)維護成本, 使業(yè)務更快更好的流動發(fā)展的方式。
(1)業(yè)務的不確定性
(2)技術的不確定性
(3)人員流動的不確定性
面對外部的確定性,轉化為內核的確定性。
面對外部的不確定性,找到穩(wěn)定的內核基礎。
當下互聯(lián)網發(fā)展速度是迅猛的, 軟件的形態(tài)也在不斷的變化演進。面對未來的業(yè)務及變化,橫向業(yè)務與縱向業(yè)務的發(fā)展都是不確定性的。
Robert C. Martin提到的BDUF,永遠不要想著在開始就設計好了全部的事情(big design up front),一定要避免過度設計。除非能夠十分確認的可預見變化, 業(yè)務邊界,否則專注解決當前1-2年內業(yè)務變化設計, 講好當下的用戶故事,專注解決眼前的問題域。 面向不確定設計,增量敏捷開發(fā)。
隨著業(yè)務的變化、系統(tǒng)設計也要持續(xù)演進升級。沒有一開始就完美的架構, 好的架構設計一定演化來的,不是一開始就設計出來的。
一個健康公司的成長,業(yè)務橫向、縱向會發(fā)展的會越來越復雜,支持業(yè)務的系統(tǒng)也一定會越來越復雜。
系統(tǒng)演進過程中的成本,會受到最開始的設計、系統(tǒng)最初的內核影響的。面對外部業(yè)務的不確定性, 技術的不確定性,外部依賴的不確定性。一個穩(wěn)定的內核應該盡量把外部的不確定性隔離。
以業(yè)務為核心,分離業(yè)務復雜度和技術復雜度。
系統(tǒng)和代碼像多個線團一樣散落一地一樣,混亂不堪,毫無頭緒。
(1)統(tǒng)一認知(秩序化)
(2)系統(tǒng)清晰明了的結構(結構化)
(3)業(yè)務開發(fā)流程化(標準化)
注:這里說的流程化并非指必須使用類似BPM的流程編排系統(tǒng)。
而是指對于一個需求,業(yè)務開發(fā)有一定的順序, 有規(guī)劃的先做一部分事情,開發(fā)哪一個模塊再去做剩下的工作,是可以流程化的。
業(yè)務規(guī)模的膨脹以及開發(fā)團隊規(guī)模的膨脹,都會帶來系統(tǒng)的復雜性提升。
(1)業(yè)務隔離, 分而治之;
(2)專注產品核心競爭力的發(fā)展;
(3)場景分層。
投入更多的開發(fā)、測試資源、業(yè)務資源(比如單元測試覆蓋率在90%以上)在關鍵場景。
更快,更低成本、更少資源投入地完成普通場景的迭代。
是指開發(fā)人員需要多少知識才能完成一項任務。
在引入新的變化時,要考慮到帶來的好處是否大于系統(tǒng)認知成本的提升,比如:之前提到的BPM流程編排引擎,如果對系統(tǒng)帶來的好處不夠多也是增加認知成本的一種。
不合適的設計模式也是增加認知成本的一種,前臺同學吐槽的中臺架構比較高的學習成本, 也是認知成本的一種。
(1)系統(tǒng)與現(xiàn)實業(yè)務更自然真實的映射,對業(yè)務抽象建模。
軟件工程師實際上只在做一件事情,即把現(xiàn)實中的問題搬到計算機上,通過信息化提升生產力。
(2)代碼的含義清晰,不模糊。
(3)代碼的整潔度。
(4)系統(tǒng)的有序性, 架構清晰。
(5)避免過度設計。
(6)減少復雜、重復概念, 降低學習成本。
(7)謹慎引入會帶來系統(tǒng)復雜性的變化。
DDD是把業(yè)務模型翻譯成系統(tǒng)架構設計的一種方式, 領域模型是對業(yè)務模型的抽象。
不是所有的業(yè)務服務都合適做DDD架構,DDD合適產品化,可持續(xù)迭代,業(yè)務邏輯足夠復雜的業(yè)務系統(tǒng),小規(guī)模的系統(tǒng)與簡單業(yè)務不適合使用,畢竟相比較于MVC架構,認知成本和開發(fā)成本會大不少。但是DDD里面的一些戰(zhàn)略思想我認為還是較為通用的。
清晰語言認知, 比如之前在詳情裝修系統(tǒng)中:
ItemTemplate : 表示當前具體的裝修頁面
ItemDescTemplate、Template,兩個都能表示模板概概念。
剛開始接觸這塊的時候比較難理解這一塊邏輯,之后在負責設計詳情編輯器大融合這個項目時第一件事就是團隊內先重新統(tǒng)一認知。
不將模板和頁面的概念糅雜在一起,含糊不清,避免重復和混亂的概念定義。
1)貧血模型
貧血模型的基本特征是:它第一眼看起來還真像這么回事兒。項目中有許多對象,它們的命名都是根據(jù)領域模型來的。然而當你真正檢視這些對象的行為時,會發(fā)現(xiàn)它們基本上沒有任何行為,僅僅是一堆getter/setter方法。
這些貧血對象在設計之初就被定義為只能包含數(shù)據(jù),不能加入領域邏輯;所有的業(yè)務邏輯是放在所謂的業(yè)務層(xxxService, xxxManager對象中),需要使用這些模型來傳遞數(shù)據(jù)。
@Data
public class Person {
/**
* 姓名
*/
private String name;
/**
* 年齡
*/
private Integer age;
/**
* 生日
*/
private Date birthday;
/**
* 當前狀態(tài)
*/
private Stauts stauts;
}
public class PersonServiceImpl implements PersonService {
public void sleep(Person person) {
person.setStauts(SleepStatus.get());
}
public void setAgeByBirth(Person person) {
Date birthday = person.getBirthday();
if (currentDate.before(birthday)) {
throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");
}
int yearNow = cal.get(Calendar.YEAR);
int dayBirth = bir.get(Calendar.DAY_OF_MONTH);
/*大概計算, 忽略月份等,年齡是當前年減去出生年*/
int age = yearNow - yearBirth;
person.setAge(age);
}
}
}
public class WorkServiceImpl implements WorkService{
public void code(Person person) {
person.setStauts(CodeStatus.get());
}
}
這一段代碼就是貧血對象的處理過程,Person類, 通過PersonService、WorkingService去控制Person的行為,第一眼看起來像是沒什么問題,但是真正去思考整個流程。WorkingService, PersonService到底是什么樣的存在?與真實世界邏輯相比, 過于抽象。基于貧血模型的傳統(tǒng)開發(fā)模式,將數(shù)據(jù)與業(yè)務邏輯分離,違反了 OOP 的封裝特性,實際上是一種面向過程的編程風格。但是,現(xiàn)在幾乎所有的 Web 項目,都是基于這種貧血模型的開發(fā)模式,甚至連 Java Spring 框架的官方 demo,都是按照這種開發(fā)模式來編寫的。
面向過程編程風格有種種弊端,比如,數(shù)據(jù)和操作分離之后,數(shù)據(jù)本身的操作就不受限制了。任何代碼都可以隨意修改數(shù)據(jù)。
2)充血模型
充血模型是一種有行為的模型,模型中狀態(tài)的改變只能通過模型上的行為來觸發(fā),同時所有的約束及業(yè)務邏輯都收斂在模型上。
@Data
public class Person extends Entity {
/**
* 姓名
*/
private String name;
/**
* 年齡
*/
private Integer age;
/**
* 生日
*/
private Date birthday;
/**
* 當前狀態(tài)
*/
private Stauts stauts;
public void code() {
this.setStauts(CodeStatus.get());
}
public void sleep() {
this.setStauts(SleepStatus.get());
}
public void setAgeByBirth() {
Date birthday = this.getBirthday();
Calendar currentDate = Calendar.getInstance();
if (currentDate.before(birthday)) {
throw new IllegalArgumentException("The birthday is before Now,It's unbelievable");
}
int yearNow = currentDate.get(Calendar.YEAR);
int yearBirth = birthday.getYear();
/*粗略計算, 忽略月份等,年齡是當前年減去出生年*/
int age = yearNow - yearBirth;
this.setAge(age);
}
}
3)貧血模型和充血模型的區(qū)別
/**
* 貧血模型
*/
public class Client {
@Resource
private PersonService personService;
@Resource
private WorkService workService;
public void test() {
Person person = new Person();
personService.setAgeByBirth(person);
workService.code(person);
personService.sleep(person);
}
}
/**
* 充血模型
*/
public class Client {
public void test() {
Person person = new Person();
person.setAgeByBirth();
person.code();
person.sleep();
}
}
上面兩段代碼很明顯第二段的認知成本更低, 這在滿是Service,Manage 的系統(tǒng)下更為明顯,Person的行為交由自己去管理, 而不是交給各種Service去管理。
貧血模型相對簡單,模型上只有數(shù)據(jù)沒有行為,業(yè)務邏輯由xxxService、xxxManger等類來承載,相對來說比較直接,針對簡單的業(yè)務,貧血模型可以快速的完成交付,但后期的維護成本比較高,很容易變成我們所說的面條代碼。
充血模型的實現(xiàn)相對比較復雜,但所有邏輯都由各自的類來負責,職責比較清晰,方便后期的迭代與維護。
面向對象設計主張將數(shù)據(jù)和行為綁定在一起也就是充血模型,而貧血領域模型則更像是一種面向過程設計,很多人認為這些貧血領域對象是真正的對象,從而徹底誤解了面向對象設計的涵義。
Martin Fowler 曾經和 Eric Evans 聊天談到它時,都覺得這個模型似乎越來越流行了。作為領域模型的推廣者,他們覺得這不是一件好事,極力反對這種做法。
貧血領域模型的根本問題是,它引入了領域模型設計的所有成本,卻沒有帶來任何好處。最主要的成本是將對象映射到數(shù)據(jù)庫中,從而產生了一個O/R(對象關系)映射層。
只有當你充分使用了面向對象設計來組織復雜的業(yè)務邏輯后,這一成本才能夠被抵消。如果將所有行為都寫入到Service對象,那最終你會得到一組事務處理腳本,從而錯過了領域模型帶來的好處。而且當業(yè)務足夠復雜時, 你將會得到一堆爆炸的事務處理腳本。
限定業(yè)務邊界,對業(yè)務進行與現(xiàn)實更自然的理解和抽象,數(shù)據(jù)模型與業(yè)務模型隔離,把業(yè)務映射成為領域模型沉淀在系統(tǒng)中。
User Interfaces
application
domain
infrastucture
為其他層提供通用的技術能力。如repository的implementation(ibatis,hibernate, nosql),中間件服務等anti-corruption layer的implementation 防腐層實現(xiàn)放在這里。
文檔與注釋可能會失去實時性(文檔、注釋沒有人持續(xù)維護),但是線上生產代碼是業(yè)務邏輯最真實的展現(xiàn),減少代碼中模糊的地方,讓業(yè)務邏輯顯性化體現(xiàn)出來,提升代碼清晰度。
if (itemDO != null && MapUtils.isNotEmpty(itemDO.getFeatures()) && itemDO.getFeatures().containsKey(ITEM_PC_DESCRIPTION_PUSH)) {
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
} else {
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_WL_TEMPLATEID, "" + templateId);
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_WL_PUSH, "" + content.hashCode());
}
比如這一段代碼就把判斷里的業(yè)務邏輯隱藏了起來,這段代碼其實的業(yè)務邏輯是這樣, 判斷商品是否有PC裝修內容。如果有做一些操作, 如果沒有做一些操作,將hasPCContent 這個邏輯表現(xiàn)出來, 一眼就能看出來大概的業(yè)務邏輯,讓業(yè)務邏輯顯現(xiàn)化,能讓代碼更清晰??梢愿膶懗蛇@樣:
boolean hasPCContent = itemDO != null && MapUtils.isNotEmpty(itemDO.getFeatures()) && itemDO.getFeatures().containsKey(ITEM_PC_DESCRIPTION_PUSH);
if (hasPCContent) {
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
} else {
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_PC_TEMPLATEID, "" + templateId);
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_WL_TEMPLATEID, "" + templateId);
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_PC_PUSH, "" + pcContent.hashCode());
itemUpdateBO.getFeatures().put(ItemTemplateConstant.FEATURE_TSP_SELL_WL_PUSH, "" + content.hashCode());
}
只要系統(tǒng)可測試并且越豐富的單元測試越會導向保持類短小且目的單一的設計方案,遵循單一職責的類,測試起來比較簡單。
遵循有關編寫測試并持續(xù)運行測試的簡單、明確規(guī)則,系統(tǒng)就會更貼近OO低偶爾度,高內聚度的目標。編寫測試越多,就越會遵循DIP之類的規(guī)則,編寫最大可測試可改進并走向更好的系統(tǒng)設計。
重復是擁有良好設計系統(tǒng)的大敵。它代表著額外的工作、額外的風險和額外且不必要的復雜度。除了雷同的代碼,功能類似的方法也可以進行包裝減少重復,“小規(guī)模復用”可大量降低系統(tǒng)復雜性。要想實現(xiàn)大規(guī)模復用,必須理解如何實現(xiàn)小規(guī)模復用。
共性的抽取也會使代碼更好的符合單一職責原則。
軟件項目的主要成本在于長期維護,當系統(tǒng)變得越來越復雜,開發(fā)者就需要越來越多的時間來理解他,而且也極有可能誤解。
所以作者需要將代碼寫的更清晰:選用好名稱、保持函數(shù)和類的短小、采用標準命名法、標準的設計模式名,編寫良好的單元測試。用心是最珍貴的資源。清晰:選用好名稱、保持函數(shù)和類的短小、采用標準命名法、標準的設計模式名,編寫良好的單元測試。用心是最珍貴的資源。
如果過度使用以上原則,為了保持類的函數(shù)短小,我們可能會造出太多細小的類和方法。所以這條規(guī)則也主張函數(shù)和類的數(shù)量要少。
如應當為每個類創(chuàng)建接口、字段和行為必須切分到數(shù)據(jù)類和行為類中。應該抵制這類教條,采用更實用的手段。目標是在保持函數(shù)和類短小的同時,保持系統(tǒng)的短小精悍。不過這是優(yōu)先級最低的一條。更重要的是測試,消除重復和清晰表達。
總而言之,做業(yè)務開發(fā)其實一點也不簡單,面對不確定性的問題域,復雜的業(yè)務變化,
如何更好的理解和抽象業(yè)務,如何更優(yōu)雅的應對復雜性,一直都是軟件開發(fā)的一個難題。
在對抗軟件熵增,尋找對抗軟件復雜性,符合業(yè)務的構造定律的演進方式,我們一直都在路上。
[1] 《Domain-Driven Design》 :https://book.douban.com/subject/1629512/
[2] 《Implementing Domain-Driven Design》 :https://book.douban.com/subject/25844633/
[3] 《Clean Code》:https://book.douban.com/subject/4199741/
[4] 《A Philosophy of Software Design》 :https://book.douban.com/subject/30218046/

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網交流