近兩年設(shè)計(jì)了幾個(gè)系統(tǒng),不管是直接使用傳統(tǒng)設(shè)計(jì)ER圖只搁,還是使用4C建模音比,但在做架構(gòu)評(píng)審時(shí),ER卻都是重中之重,讓人不得不深思氢惋,編程思想經(jīng)過(guò)了一代代發(fā)展洞翩,為什么還在圍繞ER,在遠(yuǎn)古時(shí)代焰望,沒(méi)有OO,沒(méi)有DDD骚亿,但為什么延續(xù)至今的偉大軟件也比比皆是
帶著這個(gè)問(wèn)題,需要回頭看看熊赖,結(jié)構(gòu)化編程為什么不行来屠?面向?qū)ο笠蚝味穑降捉鉀Q了什么問(wèn)題震鹉?
《架構(gòu)整潔之道》也特別介紹了面向?qū)ο缶幊叹愕眩嫦驅(qū)ο缶烤故鞘裁矗蠖鄰娜筇匦裕悍庋b传趾、繼承迎膜、抽象說(shuō)起,但其實(shí)這三種特性并不是面向?qū)ο笳Z(yǔ)言特有
結(jié)構(gòu)化編程
提到結(jié)構(gòu)化編程就自然想到其中的順序結(jié)構(gòu):代碼按照編寫(xiě)的順序執(zhí)行浆兰,選擇結(jié)構(gòu): if/else磕仅,而循環(huán)結(jié)構(gòu): do/while
雖然這些對(duì)每個(gè)程序員都很熟悉,但其實(shí)在結(jié)構(gòu)化編程之間還有非結(jié)構(gòu)化編程簸呈,也就是goto語(yǔ)句時(shí)代榕订,沒(méi)有if else、while蜕便,一切都通過(guò)goto語(yǔ)句對(duì)程序控制劫恒,它可以讓程序跑到任何地方執(zhí)行,這樣當(dāng)代碼規(guī)模變大之后玩裙,就幾乎難以維護(hù)
編程是一項(xiàng)難度很大的活動(dòng)兼贸。因?yàn)橐粋€(gè)程序會(huì)包含非常多的細(xì)節(jié),遠(yuǎn)超一個(gè)人的認(rèn)知能力范圍吃溅,任何一個(gè)細(xì)微的錯(cuò)誤都會(huì)導(dǎo)致整個(gè)程序出現(xiàn)問(wèn)題溶诞。因此需要將大問(wèn)題拆分成小問(wèn)題,逐步遞歸下去决侈,這樣螺垢,一個(gè)大問(wèn)題就會(huì)被拆解成一系列高級(jí)函數(shù)的組合,而這些高級(jí)函數(shù)各自再拆分成一系列低一級(jí)函數(shù)赖歌,一步步拆分下去枉圃,每一個(gè)函數(shù)都需要按照結(jié)構(gòu)化編程方式進(jìn)行開(kāi)發(fā),這也是現(xiàn)在常被使用的模塊功能分解開(kāi)發(fā)方式
結(jié)構(gòu)化編程中庐冯,各模塊的依賴關(guān)系太強(qiáng)孽亲,不能有效隔離開(kāi)來(lái),一旦需求變動(dòng)展父,就會(huì)牽一發(fā)而動(dòng)全身返劲,關(guān)聯(lián)的模塊由于依賴關(guān)系都得變動(dòng),那么組織大規(guī)模程序就不是它的強(qiáng)項(xiàng)
面向?qū)ο?/h1>
正因?yàn)榻Y(jié)構(gòu)化編程的弊端栖茉,所以有了面向?qū)ο缶幊汤郝蹋梢愿玫慕M織程序,相對(duì)結(jié)構(gòu)局部性思維吕漂,我們有了更宏觀視角:對(duì)象
封裝
把一組相關(guān)聯(lián)的數(shù)據(jù)和函數(shù)圈起來(lái)亲配,使圈外的代碼只能看見(jiàn)部分函數(shù),數(shù)據(jù)則完全不可見(jiàn)惶凝;如類中的公共函數(shù)和私有成員變量
提取一下關(guān)鍵字:
- 數(shù)據(jù)吼虎,完全不可見(jiàn)
- 函數(shù),只能看見(jiàn)
- 相關(guān)聯(lián)
這些似乎就是我們追求的高內(nèi)聚苍鲜,也是常提的充血模型鲸睛,如此看,在實(shí)踐中最基本的封裝都沒(méi)有達(dá)成
到處是貧血模型坡贺,一個(gè)整體卻分成兩部分:滿是大方法的上帝類service與只有g(shù)etter和setter的model
service對(duì)外提供接口官辈,model傳輸數(shù)據(jù),數(shù)據(jù)庫(kù)固化數(shù)據(jù)遍坟,哪有封裝性拳亿,行為與數(shù)據(jù)割裂了
怎么才能做到一個(gè)高內(nèi)聚的封裝特性呢?
設(shè)計(jì)一個(gè)類愿伴,先要考慮其對(duì)象應(yīng)該提供哪些行為肺魁。然后,我們根據(jù)這些行為提供對(duì)應(yīng)的方法隔节,最后才是考慮實(shí)現(xiàn)這些方法要有哪些字段
并且對(duì)于這些字段盡可能不提供getter 和 setter鹅经,尤其是 setter
暴露getter和setter寂呛,一是把實(shí)現(xiàn)細(xì)節(jié)暴露出來(lái)了;二是把數(shù)據(jù)當(dāng)成了設(shè)計(jì)核心
方法的命名瘾晃,體現(xiàn)的是你的意圖贷痪,而不是具體怎么做
// 修改密碼
public void setPassword(final String password) {
this.password = password;
}
// 修改密碼
public void changePassword(final String password) {
this.password = password;
}
把setter改成具體的業(yè)務(wù)方法名,把意圖體現(xiàn)出來(lái)蹦误,將意圖與實(shí)現(xiàn)分離開(kāi)來(lái)劫拢,這是一個(gè)優(yōu)秀設(shè)計(jì)必須要考慮的問(wèn)題
構(gòu)建一個(gè)內(nèi)聚的單元,我們要減少這個(gè)單元對(duì)外的暴露强胰,也就是定義中的【只能看到的函數(shù)】
這句話的第一層含義是減少內(nèi)部實(shí)現(xiàn)細(xì)節(jié)的暴露舱沧,它還有第二層含義,減少對(duì)外暴露的接口
最小化接口暴露偶洋。也就是熟吏,每增加一個(gè)接口,你都要找到一個(gè)合適的理由玄窝。
總結(jié):
基于行為進(jìn)行封裝分俯,不要暴露實(shí)現(xiàn)細(xì)節(jié),最小化接口暴露
繼承
先看繼承定義:
繼承(英語(yǔ):inheritance)是面向?qū)ο筌浖夹g(shù)當(dāng)中的一個(gè)概念哆料。這種技術(shù)使得復(fù)用以前的代碼非常容易缸剪,能夠大大縮短開(kāi)發(fā)周期,降低開(kāi)發(fā)費(fèi)用
繼承就是子類繼承父類的特征和行為东亦,使得子類對(duì)象(實(shí)例)具有父類的屬性和方法杏节,或子類從父類繼承方法,使得子類具有父類相同的行為
從定義看典阵,繼承就是為了復(fù)用奋渔,把一些公共代碼放到父類,之后在實(shí)現(xiàn)子類時(shí)壮啊,可以少寫(xiě)一些代碼嫉鲸,消除重復(fù),代碼復(fù)用
繼承分為兩類:實(shí)現(xiàn)繼承與接口繼承
Child object = new Child();
Parent object = new Child();
但有個(gè)設(shè)計(jì)原則:組合優(yōu)于繼承Composition-over-inheritance
為什么不推薦使用繼承呢歹啼?
繼承意味著強(qiáng)耦合玄渗,而高內(nèi)聚低耦合才符合我們的道,但其實(shí)并不是說(shuō)不能使用繼承狸眼,對(duì)于行為需要使用組合藤树,而數(shù)據(jù)還得使用繼承
這樣解釋似乎不夠形象,再進(jìn)一步講拓萌,繼承也違背了《SOLID》中的OCP,繼承雖然可以通過(guò)子類擴(kuò)展新的行為岁钓,但因?yàn)樽宇惪赡苤苯右蕾嚫割悓?shí)現(xiàn),導(dǎo)致一個(gè)變更可能會(huì)影響所有子類。也就是講繼承雖然能Open for extension屡限,但很難做到Closed for modification
借用阿里大牛的示例:
有個(gè)游戲品嚣,基本規(guī)則就是玩家裝備武器去攻擊怪物
- 玩家(Player)可以是戰(zhàn)士(Fighter)、法師(Mage)钧大、龍騎(Dragoon)
- 怪物(Monster)可以是獸人(Orc)翰撑、精靈(Elf)、龍(Dragon)拓型,怪物有血量
- 武器(Weapon)可以是劍(Sword)额嘿、法杖(Staff)瘸恼,武器有攻擊力
- 玩家可以裝備一個(gè)武器劣挫,武器攻擊可以是物理類型(0),火(1)东帅,冰(2)等压固,武器類型決定傷害類型
public abstract class Player {
Weapon weapon
}
public class Fighter extends Player {}
public class Mage extends Player {}
public class Dragoon extends Player {}
public abstract class Weapon {
int damage;
int damageType; // 0 - physical, 1 - fire, 2 - ice etc.
}
public Sword extends Weapon {}
public Staff extends Weapon {}
攻擊規(guī)則如下:
- 獸人對(duì)物理攻擊傷害減半
- 精靈對(duì)魔法攻擊傷害減半
- 龍對(duì)物理和魔法攻擊免疫,除非玩家是龍騎靠闭,則傷害加倍
public class Player {
public void attack(Monster monster) {
monster.receiveDamageBy(weapon, this);
}
}
public class Monster {
public void receiveDamageBy(Weapon weapon, Player player) {
this.health -= weapon.getDamage(); // 基礎(chǔ)規(guī)則
}
}
public class Orc extends Monster {
@Override
public void receiveDamageBy(Weapon weapon, Player player) {
if (weapon.getDamageType() == 0) {
this.setHealth(this.getHealth() - weapon.getDamage() / 2); // Orc的物理防御規(guī)則
} else {
super.receiveDamageBy(weapon, player);
}
}
}
public class Dragon extends Monster {
@Override
public void receiveDamageBy(Weapon weapon, Player player) {
if (player instanceof Dragoon) {
this.setHealth(this.getHealth() - weapon.getDamage() * 2); // 龍騎傷害規(guī)則
}
// else no damage, 龍免疫力規(guī)則
}
}
如果此時(shí)帐我,要增加一個(gè)武器類型:狙擊槍,能夠無(wú)視一切防御愧膀,此時(shí)需要修改
- Weapon,擴(kuò)展狙擊槍Gun
- Player和所有子類(是否能裝備某個(gè)武器)
- Monster和所有子類(傷害計(jì)算邏輯)
public class Monster {
public void receiveDamageBy(Weapon weapon, Player player) {
this.health -= weapon.getDamage(); // 老的基礎(chǔ)規(guī)則
if (Weapon instanceof Gun) { // 新的邏輯
this.setHealth(0);
}
}
}
public class Dragon extends Monster {
public void receiveDamageBy(Weapon weapon, Player player) {
if (Weapon instanceof Gun) { // 新的邏輯
super.receiveDamageBy(weapon, player);
}
// 老的邏輯省略
}
}
由此可見(jiàn)拦键,增加一個(gè)規(guī)則,幾乎鏈路上的所有類都得修改一遍檩淋,越往后業(yè)務(wù)越復(fù)雜芬为,每一次業(yè)務(wù)需求變更基本要重寫(xiě)一次,這也是為什么建議盡量不要違背OCP蟀悦,最核心的原因就是現(xiàn)有邏輯的變更可能會(huì)影響一些原有代碼媚朦,導(dǎo)致一些無(wú)法預(yù)見(jiàn)的影響。這個(gè)風(fēng)險(xiǎn)只能通過(guò)完整的單元測(cè)試覆蓋來(lái)保障日戈,但在實(shí)際開(kāi)發(fā)中很難保障UT的覆蓋率
也由此可見(jiàn)繼承的確不是代碼復(fù)用的好方式
從設(shè)計(jì)原則角度看询张,繼承不是好的復(fù)用方式;從語(yǔ)言特性看浙炼,也不是鼓勵(lì)的做法份氧。一是像Java,只能單繼承弯屈,一旦被繼承就再也無(wú)法被其他繼承半火,而且java中有Variable Hiding的局限性
比如現(xiàn)在添加一個(gè)業(yè)務(wù)規(guī)則:
- 戰(zhàn)士只能裝備劍
- 法師只能裝備法杖
@Data
public class Fighter extends Player {
private Sword weapon;
}
@Test
public void testEquip() {
Fighter fighter = new Fighter("Hero");
Sword sword = new Sword("Sword", 10);
fighter.setWeapon(sword);
Staff staff = new Staff("Staff", 10);
fighter.setWeapon(staff);
assertThat(fighter.getWeapon()).isInstanceOf(Staff.class); // 錯(cuò)誤了
}
其實(shí)只是修改了父類的weapon,并沒(méi)有修改子類的季俩;由此編程語(yǔ)言的強(qiáng)類型無(wú)法承載業(yè)務(wù)規(guī)則钮糖。
繼承并不是復(fù)用的唯一方法,如ruby中有mixin機(jī)制
多態(tài)
多態(tài)(Polymorphism)按字面的意思就是“多種狀態(tài)”。在面向?qū)ο笳Z(yǔ)言中店归,接口的多種不同的實(shí)現(xiàn)方式即為多態(tài)
在上一講阎抒,接口繼承更多是多態(tài)特性
只使用封裝和繼承的編程方式,稱之為基于對(duì)象編程消痛,而只有把多態(tài)加進(jìn)來(lái)且叁,才能稱之為面向?qū)ο缶幊蹋辛硕鄳B(tài)秩伞,才將基于對(duì)象與面向?qū)ο髤^(qū)分開(kāi)逞带;有了多態(tài),軟件設(shè)計(jì)才有了更大的彈性
多態(tài)雖好纱新,但想要運(yùn)用多態(tài)展氓,需要構(gòu)建出一個(gè)抽象,構(gòu)建抽象需要找出不同事物的共同點(diǎn)脸爱,這也是最有挑戰(zhàn)地方遇汞。在構(gòu)建抽象上,接口扮演著重要角色:一接口將變的部分和不變部分隔離開(kāi)來(lái)簿废,接口是約定空入,約定是不變的,變化的是各自的實(shí)現(xiàn)族檬;二接口是一個(gè)邊界歪赢,系統(tǒng)模塊間通信重要的就是通信協(xié)議,而接口就是通信協(xié)議的表達(dá)
ArrayList<> list = new ArrayList();
List<> list = new ArrayList();
二者之間的差別就在于變量的類型单料,是面向一個(gè)接口埋凯,還是面向一個(gè)具體的實(shí)現(xiàn)類;看似沒(méi)什么意義,但在《SOLID》中可以發(fā)現(xiàn)看尼,幾乎所有原則都需要基于接口編程递鹉,才能達(dá)到目的
而這也就是多態(tài)的威力
就java這門(mén)語(yǔ)言,繼承與多態(tài)相互依存藏斩,但對(duì)于其他語(yǔ)言并不是如此
總結(jié)
除了結(jié)構(gòu)化編程和面向?qū)ο缶幊条锝幔F(xiàn)在還有函數(shù)式編程,然通過(guò)上面的闡述狰域,回到開(kāi)篇的問(wèn)題媳拴,我應(yīng)該是把編程語(yǔ)言與編程范式搞混了,像結(jié)構(gòu)化編程兆览、面向?qū)ο缶幊淌且环N編程范式屈溉,而具體的C、Java其實(shí)是編程語(yǔ)言抬探,對(duì)于編程語(yǔ)言是年輕的子巾,的確在很多偉大軟件之后才誕生帆赢,但編程范式是一直存在的,面向?qū)ο蠓妒讲⒉皇莏ava之后才有
更不是C語(yǔ)言不能創(chuàng)造偉大軟件线梗,語(yǔ)言不過(guò)是工具椰于,而最最重要的是思維方式,最近思考為什么TDD仪搔,DDD這些驅(qū)動(dòng)式開(kāi)發(fā)都很難瘾婿,關(guān)鍵還是思維方式的轉(zhuǎn)變
為什么都要看ER圖呢,這里面又常被混淆的概念:數(shù)據(jù)模型與領(lǐng)域模型烤咧,下一篇再分解
Reference
《架構(gòu)整潔之道》
《軟件之美》