上一篇中我們談到在軟件開發(fā)中使用演進(jìn)式設(shè)計(jì)來讓軟件持續(xù)的響應(yīng)變化寓搬。
演進(jìn)式設(shè)計(jì)強(qiáng)調(diào)對(duì)不確定的變化不做提前預(yù)估,優(yōu)先保持設(shè)計(jì)的“簡(jiǎn)單性”,避免過度設(shè)計(jì)吹埠。但是對(duì)于已經(jīng)出現(xiàn)的變化也不能響應(yīng)滯后斩郎,當(dāng)新的變化方向上的“第一顆子彈”出現(xiàn)時(shí)脑融,就立即依照“正交設(shè)計(jì)原則”的指導(dǎo)進(jìn)行設(shè)計(jì)調(diào)整,并"及時(shí)重構(gòu)"代碼缩宜,讓代碼可以在該變化方向上具有彈性肘迎。
接下來甥温,我們會(huì)深入談?wù)劥a中應(yīng)對(duì)變化的具體技巧,以作為對(duì)演進(jìn)式設(shè)計(jì)中缺乏的代碼實(shí)踐部分的補(bǔ)充妓布。
變化分類
引起軟件發(fā)生變化的原因各種各樣姻蚓。需求變更、技術(shù)演進(jìn)或者性能優(yōu)化等等匣沼,都可能會(huì)讓軟件發(fā)生變更狰挡。但是經(jīng)過設(shè)計(jì)后,轉(zhuǎn)化到既有代碼里的變化肛著,可以歸納為以下三類:
- 數(shù)據(jù)變化
- 行為變化
- 類型變化
本篇我們從“數(shù)據(jù)變化”講起圆兵。
數(shù)據(jù)變化
從一個(gè)簡(jiǎn)單的例子開始。
isPowerOverload
是一個(gè)計(jì)算物理器件是否功率過載的函數(shù)枢贿。
它的參數(shù)是器件當(dāng)前的電流(amps)
值殉农,算法是根據(jù)功率公式 “功率(watts) = 電流(amps) * 電壓(volts)
” 算出當(dāng)前的實(shí)際功率,然后和器件的額定功率(rated watts)
進(jìn)行對(duì)比局荚。超過額定功率則返回true
超凳,表示功率過載。
bool isPowerOverload(U32 amps) {
U32 volts = 20;
U32 rated_watts = 240;
U32 watts = volts * amps;
return watts > rated_watts;
}
在上例中耀态,器件的電壓和額定功率是確定的轮傍,分別是20v
和240w
。為了讓示例清晰首装,這里分別用變量來表示不同含義的值创夜。
現(xiàn)在假設(shè)發(fā)生了新的需求,出現(xiàn)一款新器件B仙逻,它的額定功率是360w
驰吓,固定電壓是12v
,算法一樣系奉。怎么設(shè)計(jì)實(shí)現(xiàn)檬贰?
沒錯(cuò),可以復(fù)用上面的代碼缺亮。于是有人將上面的代碼拷貝一份然后修改為:
bool isPowerOverloadForDeviceB(U32 amps) {
const U32 volts = 12;
const U32 rated_watts = 360;
const watts = volts * amps;
return watts > rated_watts;
}
于是我明白了翁涤,很多人理解的代碼“可復(fù)用性”,指的是既有系統(tǒng)里面有大量的可供拷貝修改的代碼萌踱。遺憾的是葵礼,這種“復(fù)用”方式只會(huì)讓我們的代碼重復(fù)率越來越高,最后陷入難以維護(hù)的“焦油坑”并鸵。
事實(shí)上章咧,真正的可復(fù)用代碼
指得是:不對(duì)被復(fù)用的代碼修改任何一行源碼,直接就可以調(diào)用能真。
當(dāng)然,遵循“簡(jiǎn)單設(shè)計(jì)”的系統(tǒng)是不可能一上來就滿足各種未曾出現(xiàn)的復(fù)用性要求的。但是遵循演進(jìn)式設(shè)計(jì)粉铐,就是要讓軟件在響應(yīng)變化的過程中疼约,逐漸的提高可復(fù)用性。
回到這段代碼蝙泼,我們對(duì)比這兩個(gè)函數(shù)程剥,會(huì)發(fā)現(xiàn)發(fā)生變化的只是額定功率
和固定電壓
的數(shù)值,這屬于我們說的第一類代碼變化: 數(shù)據(jù)變化 汤踏。
而解決數(shù)據(jù)變化的方法也很簡(jiǎn)單织鲸,那就是 將變化的數(shù)據(jù)參數(shù)化。
將變化的數(shù)據(jù)參數(shù)化
于是我們將代碼修改如下:
bool isPowerOverload(U32 amps, U32 volts, U32 rated_watts) {
U32 watts = volts * amps;
return watts > rated_watts;
}
這時(shí)原有器件A的調(diào)用處需要改為 bool result = isPowerOverload(amps, 20, 240)
溪胶;
而對(duì)新器件B的調(diào)用則為bool result = isPowerOverload(amps, 12, 360)
搂擦。
可以看到新的isPowerOverload
同時(shí)支持電流、電壓和額定功率的變化哗脖,所以代碼的可復(fù)用性比以前更好了瀑踢。
講到這里,可能會(huì)覺得才避,應(yīng)對(duì)“數(shù)據(jù)變化”的方法不就是“提參數(shù)”嘛橱夭,這不是編程入門課里教的東西嗎? 可是桑逝,別急棘劣,讓我們把初始的程序改一改。
#define RATED_VOLTS 20
#define RATED_WATTS 240
bool isPowerOverload(U32 amps) {
return RATED_VOLTS * amps > RATED_WATTS;
}
如果最初的程序?qū)崿F(xiàn)如上楞遏,這時(shí)對(duì)于同樣的新需求(支持額定功率是360w
茬暇,固定電壓是12v
),會(huì)有不少程序員的實(shí)現(xiàn)如下:
#define RATED_VOLTS_FOR_DEVICE_B 12
#define RATED_WATTS_FOR_DEVICE_B 360
bool isPowerOverloadForDeviceB(U32 amps) {
return RATED_VOLTS_FOR_DEVICE_B * amps > RATED_WATTS_FOR_DEVICE_B;
}
咋看過去橱健,這兩段代碼沒有重復(fù)而钞,事實(shí)上用compare工具比較也顯示沒有重復(fù)。
但是我們看看重復(fù)代碼的定義拘荡。所謂 代碼重復(fù) 臼节,指的是 知識(shí)的重復(fù) , 即 “代碼中對(duì)同一項(xiàng)知識(shí)出現(xiàn)了的重復(fù)的表達(dá)” 珊皿。
在上面的實(shí)現(xiàn)中网缝,對(duì)于 “怎么計(jì)算功率” 以及 “如何認(rèn)定功率過載” 的計(jì)算知識(shí),出現(xiàn)了兩次重復(fù)的表達(dá)蟋定。而一旦知識(shí)發(fā)生了變化粉臊,重復(fù)的表達(dá)必然需要同步修改。例如現(xiàn)在要求所有的 “功率計(jì)算” 和 “過載比較” 支持浮點(diǎn)數(shù)驶兜,上面表面看似沒有重復(fù)的兩處代碼卻都要進(jìn)行修改扼仲。
因此远寸,上面的修改方式仍然是 “拷貝+修改” 式復(fù)用的慣性結(jié)果。究其原因是由于變化的數(shù)據(jù)成了全局常量(宏)屠凶,所以“提參數(shù)”的方法變得稍微不那么顯而易見了驰后。
這里更好的改法依然是將變化的數(shù)據(jù)參數(shù)化。只有將變化的數(shù)據(jù)參數(shù)化了矗愧,計(jì)算的邏輯才能標(biāo)準(zhǔn)化灶芝。
bool isPowerOverload(U32 amps, U32 volts, U32 rated_watts) {
return volts * amps > rated_watts;
}
#define RATED_VOLTS_OF_DEVICE_A 20
#define RATED_WATTS_OF_DEVICE_A 240
#define RATED_VOLTS_OF_DEVICE_B 12
#define RATED_WATTS_OF_DEVICE_B 360
如上修改中,依然鼓勵(lì)了消除魔術(shù)數(shù)字唉韭,將不變的數(shù)字常量化夜涕,但是這和將計(jì)算過程參數(shù)化并不矛盾。
現(xiàn)在使用方可以這樣調(diào)用:
// for device A
bool result = isPowerOverload(amps, RATED_VOLTS_OF_DEVICE_A, RATED_WATTS_OF_DEVICE_A)属愤;
// for device B
bool result = isPowerOverload(amps, RATED_VOLTS_OF_DEVICE_B, RATED_WATTS_OF_DEVICE_B);
將數(shù)據(jù)之間的關(guān)系顯示化
上面修改后的代碼女器,通過將變化的數(shù)據(jù)參數(shù)化,解決了對(duì)計(jì)算方法重復(fù)表達(dá)的問題春塌。
不過上述代碼存在另一個(gè)問題晓避,就是使用方每次調(diào)用isPowerOverload
的時(shí)候,需要負(fù)責(zé)將電壓
和額定功率
的常量值成對(duì)選擇正確只壳,而且傳參的時(shí)候還要確保順序正確俏拱。這不僅麻煩而且容易出錯(cuò),尤其是當(dāng)調(diào)用點(diǎn)比較多的時(shí)候吼句。
對(duì)于上面的問題锅必,我們可以把對(duì)參數(shù)如何選擇的知識(shí)封裝起來,給調(diào)用方更易用的接口惕艳。
static const U32 RATED_VOLTS_OF_DEVICE_A = 20;
static const U32 RATED_WATTS_OF_DEVICE_A = 240;
static const U32 RATED_VOLTS_OF_DEVICE_B = 12;
static const U32 RATED_WATTS_OF_DEVICE_B = 360;
static bool isPowerOverload(U32 amps, U32 volts, U32 rated_watts) {
return volts * amps > rated_watts;
}
bool isPowerOverloadForDeviceA(U32 amps) {
return isPowerOverload(amps, RATED_VOLTS_OF_DEVICE_A, RATED_WATTS_OF_DEVICE_A);
}
bool isPowerOverloadForDeviceB(U32 amps) {
return isPowerOverload(amps, RATED_VOLTS_OF_DEVICE_B, RATED_WATTS_OF_DEVICE_B);
}
上面的修改搞隐,可以讓調(diào)用方變得簡(jiǎn)單。使用者只用選擇調(diào)用isPowerOverloadForDeviceA
或者isPowerOverloadForDeviceB
远搪,避免了重復(fù)且易出錯(cuò)的常量選擇劣纲。
另外,由于使用方不再需要看到電壓和額定功率的常量值谁鳍,所以可以將全局宏常量變成局部于實(shí)現(xiàn)文件內(nèi)的static const
類型常量癞季,這樣進(jìn)一步隱藏了實(shí)現(xiàn)細(xì)節(jié)。甚至如果這些常量只用于計(jì)算功率過載的函數(shù)里倘潜,可以進(jìn)一步將其內(nèi)置于各自的函數(shù)內(nèi)绷柒。
當(dāng)前的實(shí)現(xiàn)中isPowerOverload
也已屬于內(nèi)部實(shí)現(xiàn),所以將其用static
修飾涮因,作用域限定在當(dāng)前的文件內(nèi)废睦。而isPowerOverloadForDeviceA
和isPowerOverloadForDeviceB
是接口,它們分別調(diào)用了isPowerOverload
养泡,共享了一份計(jì)算邏輯的代碼表述嗜湃。
不過在兩個(gè)接口的實(shí)現(xiàn)中奈应,目前依然需要選擇電壓和額定功率,并需要保證與對(duì)應(yīng)的器件類型是一致的购披。
這個(gè)問題的根本原因是由于電壓和額定功率之間本來就是有內(nèi)在聯(lián)系的钥组。而將電壓和功率分別用獨(dú)立的U32
常量進(jìn)行表示,實(shí)際上將它們之間的關(guān)系隱式化了今瀑,需要每個(gè)調(diào)用者保證它們使用時(shí)的一致性。
為了避免出現(xiàn)不一致点把,我們其實(shí)是需要進(jìn)一步把 數(shù)據(jù)之間的關(guān)系用數(shù)據(jù)結(jié)構(gòu)顯示化的表達(dá)出來橘荠。這個(gè)時(shí)候ADT(Abstract Data Type),抽象數(shù)據(jù)類型就有用了郎逃。
struct DeviceConfig {
U32 volts;
U32 watts;
};
enum DeviceType {
DEVICE_A,
DEVICE_B,
DEVICE_MAX,
};
static DeviceConfig DEVICE_CONFIG[DEVICE_MAX] = [
/* volts, watts*/
{ 20 , 240 },
{ 12 , 360 }
];
static bool isPowerOverload(U32 amps, U32 volts, U32 rated_watts) {
return volts * amps > rated_watts;
}
bool isPowerOverloadForDeviceA(U32 amps) {
const DeviceConfig* cfg = DEVICE_CONFIG[DEVICE_A];
return isPowerOverload(amps, cfg->volts, cfg->watts);
}
bool isPowerOverloadForDeviceB(U32 amps) {
const DeviceConfig* cfg = DEVICE_CONFIG[DEVICE_B];
return isPowerOverload(amps, cfg->volts, cfg->watts);
}
上面的實(shí)現(xiàn)使用了C語言的常用技巧:結(jié)構(gòu)體和表驅(qū)動(dòng)哥童。由于結(jié)構(gòu)體
可以把相互關(guān)聯(lián)的數(shù)據(jù)聚合在一起,所以上面代碼中只用根據(jù)DeviceType
就可以一次把所有關(guān)聯(lián)的數(shù)據(jù)全部拿出來褒翰,避免了數(shù)據(jù)分散選擇時(shí)可能出現(xiàn)的不一致贮懈。
使用抽象數(shù)據(jù)類型(ADT)可以表示數(shù)據(jù)的各種關(guān)系。C語言中結(jié)構(gòu)體
可以表示數(shù)據(jù)的聚合關(guān)系优训,數(shù)組
和鏈表
可以表示數(shù)據(jù)間的位置關(guān)系朵你,而哈希表
可以表示數(shù)據(jù)的字典關(guān)系。
善于利用合適的數(shù)據(jù)結(jié)構(gòu)把數(shù)據(jù)之間的關(guān)系顯示化揣非,可以讓算法變得簡(jiǎn)單抡医,甚至性能更高。軟件工程很早就說過的 “軟件設(shè)計(jì)的關(guān)鍵就是設(shè)計(jì)好數(shù)據(jù)結(jié)構(gòu)” 以及 “程序 = 數(shù)據(jù)結(jié)構(gòu) + 算法”早敬。今天來看這些說法有其時(shí)代局限性忌傻,但是至少體現(xiàn)出數(shù)據(jù)結(jié)構(gòu)對(duì)于軟件設(shè)計(jì)的重要性。
通過上面的示例也看到了搞监,隨著封裝水孩,代碼的行數(shù)也在逐漸變多。這是由于我們的示例比較簡(jiǎn)單琐驴,只有兩個(gè)關(guān)聯(lián)數(shù)據(jù)》郑現(xiàn)實(shí)中當(dāng)互相關(guān)聯(lián)的數(shù)據(jù)越多,使用抽象數(shù)據(jù)類型把互相關(guān)聯(lián)的數(shù)據(jù)組織在一起的價(jià)值就越大棍矛。另外安疗,封裝的價(jià)值還在于封閉知識(shí),讓客戶更簡(jiǎn)單够委、更不容易出錯(cuò)荐类。
相反的例子是,在代碼中大量使用全局變量茁帽,而這些全局變量之間的關(guān)系又都是隱式的玉罐,分布在軟件的各個(gè)計(jì)算邏輯的細(xì)節(jié)中屈嗤,最后只能依賴程序員之間口口相傳,或者只能依賴某個(gè)“資深”程序員才能對(duì)其做安全的修改吊输。這樣的程序最終會(huì)變成所有維護(hù)者的噩夢(mèng)饶号。
對(duì)數(shù)據(jù)進(jìn)行建模
繼續(xù)回到上面的代碼示例。最后的版本中使用了表驅(qū)動(dòng)的方式對(duì)不同器件的數(shù)據(jù)進(jìn)行了組織季蚂。表驅(qū)動(dòng)是C語言常用的設(shè)計(jì)技巧茫船,不過需要注意的是,一個(gè)單獨(dú)的二維的表扭屁,只能表達(dá)一個(gè)方向上的變化算谈。當(dāng)出現(xiàn)兩個(gè)及其以上的變化方向時(shí),一個(gè)表必然出現(xiàn)出現(xiàn)數(shù)據(jù)重復(fù)料滥。
我們回到器件和單板的例子上然眼。假如某類器件的靜態(tài)配置數(shù)據(jù)有5項(xiàng),其中3項(xiàng)數(shù)據(jù)隨著器件的自身類型變化葵腹,另外2項(xiàng)數(shù)據(jù)隨著器件所在的單板變化高每。假設(shè)該類器件有3種,一共支持4種單板践宴。如果將所有數(shù)據(jù)項(xiàng)放到一張表里鲸匿,表里的數(shù)據(jù)量會(huì)是所有變化方向的乘積,也即一共有 5 * 3 * 4 = 60 項(xiàng)浴井。 這些數(shù)據(jù)里面一定存在需要同步修改的重復(fù)數(shù)據(jù)晒骇。
這提醒我們需要按照不同的變化方向?qū)?shù)據(jù)進(jìn)行拆分。
在C程序里面容易出現(xiàn)過度使用表驅(qū)動(dòng)的情況磺浙。經(jīng)常見到大量數(shù)據(jù)被塞入到一張張大表中洪囤,表里的數(shù)據(jù)存在高度的重復(fù)。這樣不僅維護(hù)困難撕氧,而且還浪費(fèi)了大量的靜態(tài)內(nèi)存瘤缩。
表的內(nèi)容要消除重復(fù),所遵循的設(shè)計(jì)原則和關(guān)系數(shù)據(jù)庫表設(shè)計(jì)范式一樣伦泥,需要把數(shù)據(jù)按照不同的主鍵(即不同的變化原因)分開剥啤。所以上例中需要把結(jié)構(gòu)體拆成兩個(gè),一個(gè)只包含隨著器件自身變化的數(shù)據(jù)不脯,另一個(gè)只包含和器件所在單板變化的數(shù)據(jù)府怯,兩個(gè)結(jié)構(gòu)體通過一個(gè)鍵值(代碼里對(duì)應(yīng)ID或者指針)建立關(guān)聯(lián)。這樣兩個(gè)結(jié)構(gòu)體各自對(duì)應(yīng)一張表防楷,表里的數(shù)據(jù)就不會(huì)再有重復(fù)牺丙。
這其實(shí)就是數(shù)據(jù)建模的過程。
數(shù)據(jù)建模除了關(guān)注變化原因外,還要關(guān)注數(shù)據(jù)的變化頻率冲簿。假設(shè)我們合理的分表了粟判,表和表之間就會(huì)產(chǎn)生關(guān)聯(lián)。那么數(shù)據(jù)表放在哪些物理代碼文件和目錄下也是有講究的峦剔。這里的基本原則是 數(shù)據(jù)和誰更緊密档礁,經(jīng)常一起變化,就和誰放到一起吝沫。不遵循這個(gè)原則就會(huì)出現(xiàn)關(guān)聯(lián)的數(shù)據(jù)發(fā)生變化后呻澜,代碼里需要散彈式修改的問題。
我們繼續(xù)使用器件和單板的例子惨险。例如每個(gè)器件都有在不同單板上的靜態(tài)配置數(shù)據(jù)易迹。如果經(jīng)常新增器件的話,那么每個(gè)器件在不同單板上的配置數(shù)據(jù)就應(yīng)該和器件的代碼放在一起平道,這樣每增加一個(gè)新器件可以一次內(nèi)聚的把它應(yīng)該支持的所有單板的數(shù)據(jù)都配置好,還不會(huì)影響別的器件的數(shù)據(jù)供炼。然而如果是經(jīng)常新增加單板的話一屋,那么就需要把這些數(shù)據(jù)以單板為維度組織在一起。否則的話袋哼,每增加一個(gè)單板都需要逐一打開每個(gè)器件的代碼去修改冀墨,不僅麻煩還容易遺漏。
數(shù)據(jù)建模的最后一個(gè)考慮點(diǎn)是 數(shù)據(jù)的生命周期涛贯。生命周期一致的數(shù)據(jù)放在一起诽嘉,會(huì)讓數(shù)據(jù)的生命周期管理變得容易。相關(guān)的數(shù)據(jù)同生同滅弟翘,數(shù)據(jù)的一致性容易得到滿足虫腋,會(huì)減少很多訪問過期的數(shù)據(jù)或懸空指針的頭疼問題。
所以總結(jié)一下稀余,數(shù)據(jù)建脑眉剑可以讓數(shù)據(jù)減少重復(fù),更容易修改和維護(hù)睛琳。數(shù)據(jù)建暮畜。可以參考關(guān)系數(shù)據(jù)表建模的設(shè)計(jì)范式要求,但是對(duì)于代碼還要站在數(shù)據(jù)的變化原因师骗、變化頻率和生命周期的角度去思考历等,最后再加上前面提到的顯示合理的表達(dá)數(shù)據(jù)間關(guān)系,這就是數(shù)據(jù)建模的基本設(shè)計(jì)方法了辟癌。
數(shù)據(jù)外置
使用表驅(qū)動(dòng)的方式將靜態(tài)數(shù)據(jù)存在代碼里寒屯,這樣數(shù)據(jù)和使用它們的代碼離得近,改動(dòng)方便愿待,而且技術(shù)棧一致好管理浩螺。
但是當(dāng)靜態(tài)數(shù)據(jù)量變大后靴患,用源碼來描述數(shù)據(jù)和數(shù)據(jù)間關(guān)系的手段就會(huì)捉襟見肘,而且所有數(shù)據(jù)占據(jù)著代碼的靜態(tài)區(qū)要出,增加了版本的體積和內(nèi)存的開銷鸳君。
因此,當(dāng)靜態(tài)數(shù)據(jù)量比較大患蹂,或者數(shù)據(jù)關(guān)系復(fù)雜的情況下或颊,建議將靜態(tài)數(shù)據(jù)外置出去形成配置文件。
根據(jù)數(shù)據(jù)關(guān)系選擇合適的配置文件格式是重要的传于。
常見的配置文件格式有:INI囱挑、XML、JSON沼溜、YAML和TOML等平挑。
INI是種比較簡(jiǎn)單的配置形式,支持對(duì)數(shù)據(jù)進(jìn)行簡(jiǎn)單的分段聚合系草。它最多只能解決一層嵌套通熄,如果需要兩層以上嵌套,需要用到數(shù)組找都,就稍微力不從心了唇辨。
XML則是非常靈活的,支持?jǐn)?shù)據(jù)的多級(jí)樹狀結(jié)構(gòu)能耻。XML有schema的支持赏枚,還有XSLT支持格式轉(zhuǎn)換。但是XML寫起來比較冗余晓猛,閱讀起來噪音相對(duì)比較多饿幅。一般XML很少讓人直接去編輯,大多作為各種工具的中間的文本存儲(chǔ)格式戒职。
JSON是一種非常好的數(shù)據(jù)存放和傳輸?shù)母袷浇氩牵⑶矣袃?nèi)建的數(shù)據(jù)類型支持。但是JSON由于它的多級(jí)大括號(hào)縮進(jìn)帕涌,閱讀起來不方便摄凡。而且JSON不支持注釋(JSON5之前),同級(jí)的數(shù)據(jù)間沒有順序保證蚓曼,將JSON讀入后再導(dǎo)出格式會(huì)亂掉亲澡。
YAML也是一種非常靈活的配置文件格式。YAML靠縮進(jìn)描述數(shù)據(jù)間的關(guān)系纫版,并且有內(nèi)建的數(shù)據(jù)類型床绪,還支持以錨點(diǎn)的方式做相對(duì)復(fù)雜的數(shù)據(jù)引用。YAML在易讀性和靈活性中平衡得相對(duì)較好,我們熟知的Swagger就用YAML描述服務(wù)的API癞己。YAML的語法相對(duì)較多膀斋,稍微有些復(fù)雜,大多數(shù)情況下做配置文件都只用到其一個(gè)子集痹雅。
還有一種推薦的配置文件格式是TOML仰担,這是github覺得YAML不太簡(jiǎn)潔所以自己搞出來的。TOML的目標(biāo)是成為一個(gè)極簡(jiǎn)的配置文件格式绩社,它有自己內(nèi)建的數(shù)據(jù)類型摔蓝,并且被設(shè)計(jì)成可以無歧義地被映射為哈希表,從而被多種語言解析∮浒遥現(xiàn)在TOML被用的越來越廣泛贮尉,RUST語言的包管理器cargo就是采用TOML作為其包描述文件。
上面我們介紹了常見的配置文件格式朴沿。用配置文件可以把數(shù)據(jù)從代碼里面獨(dú)立出來專門維護(hù)猜谚,再借助各種配置文件的解析工具可以讓數(shù)據(jù)的編輯維護(hù)變得更加容易,在需要的時(shí)候也可以為配置文件開發(fā)一些輔助性工具以提高數(shù)據(jù)配置和校驗(yàn)的效率赌渣。
配置文件可以用來參與構(gòu)建階段做代碼生成龄毡,也可以在運(yùn)行時(shí)被二進(jìn)制程序加載。另外锡垄,將數(shù)據(jù)獨(dú)立于配置文件后,我們可以根據(jù)目標(biāo)場(chǎng)景選擇性的交付數(shù)據(jù)祭隔,以降低二進(jìn)制程序的大小和內(nèi)存占用空間货岭。
再進(jìn)一步,當(dāng)數(shù)據(jù)量大到配置文件也不適合的時(shí)候疾渴,這時(shí)可能需要一款數(shù)據(jù)庫的幫忙了千贯。關(guān)于數(shù)據(jù)庫選型的話題超出本文的范疇了,這里推薦我的朋友尉剛強(qiáng)寫的一篇文章《如何為業(yè)務(wù)產(chǎn)品選擇一款合適的數(shù)據(jù)庫搞坝?》搔谴。
當(dāng)選型數(shù)據(jù)庫之后,需要按照數(shù)據(jù)庫的設(shè)計(jì)范式約束做數(shù)據(jù)建模桩撮。遺憾的是敦第,很多人會(huì)遺忘這件事對(duì)于用配置文件保存數(shù)據(jù)同樣重要。有時(shí)甚至需要把數(shù)據(jù)模型的元模型也用數(shù)據(jù)表達(dá)出來店量,以凸顯模型背后的關(guān)系芜果。這里給大家推薦《企業(yè)軟件架構(gòu)模式》一書,里面有很多關(guān)于關(guān)于此方面的最佳實(shí)踐融师。
一點(diǎn)補(bǔ)充
再回到最開始的例子右钾,有一個(gè)初始版本的“判斷器件是否功率過載”的函數(shù)實(shí)現(xiàn)如下:
bool isPowerOverload(U32 amps) {
return amps > 12;
}
這個(gè)版本為了性能優(yōu)化,將20v
的固定電壓和240w
的額定功率做了預(yù)先計(jì)算,最后得出只要電流大于12a
就算過載舀射。
這是常見的性能優(yōu)化手段窘茁。一旦算法涉及到固定的數(shù)字,為了性能優(yōu)化就會(huì)提前做一些計(jì)算脆烟,以降低運(yùn)行時(shí)的計(jì)算開銷山林。
這里存在一個(gè)潛在問題是:當(dāng)我們打開器件手冊(cè),能看到的數(shù)字可能仍然是20v
和240w
浩淘,它們和代碼里的12
之間的關(guān)系并沒有在代碼里顯示化表達(dá)了捌朴。假設(shè)有一天20v
的數(shù)字調(diào)整了,很容易遺漏去更改代碼里面的12
张抄。
所以代碼里的數(shù)據(jù)和數(shù)據(jù)源之間的關(guān)系是需要維護(hù)的砂蔽。盡可能用代碼將這種關(guān)系顯示化的表達(dá)出來,比如上面的問題署惯,可以依賴編譯器的編譯期自動(dòng)計(jì)算能力左驾。
constexpr U32 AMPS_THRESHOLD(U32 watts, U32 volts) {
return watts/volts;
}
const static U32 AMPS_THRESHOLD_OF_DEVICE_A = AMPS_THRESHOLD(240, 20);
const static U32 AMPS_THRESHOLD_OF_DEVICE_B = AMPS_THRESHOLD(360, 12);
static inline bool isPowerOverload(U32 amps, U32 threshold) {
return amps > threshold
}
bool isPowerOverloadForDeviceA(U32 amps) {
return isPowerOverload(amps, AMPS_THRESHOLD_OF_DEVICE_A);
}
bool isPowerOverloadForDeviceB(U32 amps) {
return isPowerOverload(amps, AMPS_THRESHOLD_OF_DEVICE_B);
}
如上,我們不僅在代碼里顯示的維護(hù)了數(shù)據(jù)之間的關(guān)系极谊,還同時(shí)借助編譯器的編譯期計(jì)算能力诡右,幫我們完成了可以提前進(jìn)行的數(shù)值計(jì)算工作,保證了運(yùn)行時(shí)性能轻猖。上面的例子里面借助了C++的constexpr
的能力帆吻。
另外,當(dāng)數(shù)據(jù)量比較大咙边,為了避免編譯時(shí)開銷猜煮,借助腳本自動(dòng)計(jì)算刷新也是可以的,這時(shí)候數(shù)據(jù)之間的關(guān)系就被腳本代碼維護(hù)著败许。
總結(jié)
本文通過一個(gè)非常簡(jiǎn)單的小例子王带,總結(jié)了代碼中應(yīng)對(duì)“數(shù)據(jù)變化”的常用設(shè)計(jì)技巧。
- 將變化的數(shù)據(jù)參數(shù)化市殷,使得計(jì)算邏輯可以被統(tǒng)一愕撰;
- 將數(shù)據(jù)之間的關(guān)系,通過選擇合適的數(shù)據(jù)結(jié)構(gòu)類型進(jìn)行顯示化表達(dá)醋寝;
- 根據(jù)數(shù)據(jù)的變化原因搞挣、變化頻率和生命周期,對(duì)數(shù)據(jù)進(jìn)行劃分和組織音羞;
- 如果靜態(tài)配置數(shù)據(jù)規(guī)模大或者關(guān)系復(fù)雜柿究,建議將配置數(shù)據(jù)外置到配置文件中,甚至數(shù)據(jù)庫中黄选;
- 配置文件和數(shù)據(jù)庫一樣蝇摸,都需要根據(jù)數(shù)據(jù)特征進(jìn)行合理選型婶肩,并對(duì)數(shù)據(jù)進(jìn)行建模;
由于本文主要站在代碼設(shè)計(jì)的角度貌夕,所以選擇了和代碼設(shè)計(jì)緊密相關(guān)的數(shù)據(jù)設(shè)計(jì)話題律歼,不涉及數(shù)據(jù)庫和數(shù)據(jù)處理相關(guān)的話題。另外啡专,示例以C語言為主险毁,沒有刻意采用面向?qū)ο蟮脑O(shè)計(jì)技巧,面向?qū)ο蟮脑掝}會(huì)放在后面“行為變化”中討論们童。
最后想說的是畔况,數(shù)據(jù)設(shè)計(jì)是軟件設(shè)計(jì)的基礎(chǔ)。后面我們會(huì)繼續(xù)探討代碼中的“行為變化”和“類型變化”慧库,會(huì)發(fā)現(xiàn)其中很多設(shè)計(jì)技巧和這里的數(shù)據(jù)設(shè)計(jì)之間存在著千絲萬縷的關(guān)系跷跪。