什么是面向?qū)ο?/h1>

近兩年設(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)鍵字:

  1. 數(shù)據(jù)吼虎,完全不可見(jiàn)
  2. 函數(shù),只能看見(jiàn)
  3. 相關(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í)需要修改

  1. Weapon,擴(kuò)展狙擊槍Gun
  2. Player和所有子類(是否能裝備某個(gè)武器)
  3. 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)整潔之道》

《軟件之美》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者

  • 序言:七十年代末偏陪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子煮嫌,更是在濱河造成了極大的恐慌笛谦,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件立膛,死亡現(xiàn)場(chǎng)離奇詭異揪罕,居然都是意外死亡梯码,警方通過(guò)查閱死者的電腦和手機(jī)宝泵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)轩娶,“玉大人儿奶,你說(shuō)我怎么就攤上這事■悖” “怎么了闯捎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)许溅。 經(jīng)常有香客問(wèn)我瓤鼻,道長(zhǎng),這世上最難降的妖魔是什么贤重? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任茬祷,我火速辦了婚禮,結(jié)果婚禮上并蝗,老公的妹妹穿的比我還像新娘祭犯。我一直安慰自己,他們只是感情好滚停,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布沃粗。 她就那樣靜靜地躺著,像睡著了一般键畴。 火紅的嫁衣襯著肌膚如雪最盅。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音涡贱,去河邊找鬼挂签。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盼产,可吹牛的內(nèi)容都是我干的饵婆。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼戏售,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼侨核!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起灌灾,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤搓译,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后锋喜,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體些己,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年嘿般,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了段标。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炉奴,死狀恐怖逼庞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞻赶,我是刑警寧澤赛糟,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站砸逊,受9級(jí)特大地震影響璧南,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜师逸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一司倚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧字旭,春花似錦对湃、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至屈暗,卻和暖如春拆讯,著一層夾襖步出監(jiān)牢的瞬間脂男,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工种呐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宰翅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓爽室,卻偏偏與公主長(zhǎng)得像汁讼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阔墩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容