- 設(shè)計(jì)模式之禪(第2版)
- 秦小波
- 443字
- 2018-12-31 23:28:11
第3章
依賴倒置原則
3.1 依賴倒置原則的定義
依賴倒置原則(Dependence Inversion Principle,DIP)這個(gè)名字看著有點(diǎn)別扭,“依賴”還“倒置”,這到底是什么意思?依賴倒置原則的原始定義是:
High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.
翻譯過來,包含三層含義:
- 高層模塊不應(yīng)該依賴低層模塊,兩者都應(yīng)該依賴其抽象;
- 抽象不應(yīng)該依賴細(xì)節(jié);
- 細(xì)節(jié)應(yīng)該依賴抽象。
高層模塊和低層模塊容易理解,每一個(gè)邏輯的實(shí)現(xiàn)都是由原子邏輯組成的,不可分割的原子邏輯就是低層模塊,原子邏輯的再組裝就是高層模塊。那什么是抽象?什么又是細(xì)節(jié)呢?在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被實(shí)例化的;細(xì)節(jié)就是實(shí)現(xiàn)類,實(shí)現(xiàn)接口或繼承抽象類而產(chǎn)生的類就是細(xì)節(jié),其特點(diǎn)就是可以直接被實(shí)例化,也就是可以加上一個(gè)關(guān)鍵字new產(chǎn)生一個(gè)對象。依賴倒置原則在Java語言中的表現(xiàn)就是:
- 模塊間的依賴通過抽象發(fā)生,實(shí)現(xiàn)類之間不發(fā)生直接的依賴關(guān)系,其依賴關(guān)系是通過接口或抽象類產(chǎn)生的;
- 接口或抽象類不依賴于實(shí)現(xiàn)類;
- 實(shí)現(xiàn)類依賴接口或抽象類。
更加精簡的定義就是“面向接口編程”——OOD(Object-Oriented Design,面向?qū)ο笤O(shè)計(jì))的精髓之一。
3.2 言而無信,你太需要契約
采用依賴倒置原則可以減少類間的耦合性,提高系統(tǒng)的穩(wěn)定性,降低并行開發(fā)引起的風(fēng)險(xiǎn),提高代碼的可讀性和可維護(hù)性。
證明一個(gè)定理是否正確,有兩種常用的方法:一種是根據(jù)提出的論題,經(jīng)過一番論證,推出和定理相同的結(jié)論,這是順推證法;還有一種是首先假設(shè)提出的命題是偽命題,然后推導(dǎo)出一個(gè)荒謬、與已知條件互斥的結(jié)論,這是反證法。我們今天就用反證法來證明依賴倒置原則是多么優(yōu)秀和偉大!
論題:依賴倒置原則可以減少類間的耦合性,提高系統(tǒng)的穩(wěn)定性,降低并行開發(fā)引起的風(fēng)險(xiǎn),提高代碼的可讀性和可維護(hù)性。
反論題:不使用依賴倒置原則也可以減少類間的耦合性,提高系統(tǒng)的穩(wěn)定性,降低并行開發(fā)引起的風(fēng)險(xiǎn),提高代碼的可讀性和可維護(hù)性。
我們通過一個(gè)例子來說明反論題是不成立的。現(xiàn)在的汽車越來越便宜了,一個(gè)衛(wèi)生間的造價(jià)就可以買到一輛不錯(cuò)的汽車,有汽車就必然有人來駕駛,司機(jī)駕駛奔馳車的類圖如圖3-1所示。

圖3-1 司機(jī)駕駛奔馳車類圖
奔馳車可以提供一個(gè)方法run,代表車輛運(yùn)行,實(shí)現(xiàn)過程如代碼清單3-1所示。
代碼清單3-1 司機(jī)源代碼
public class Driver { //司機(jī)的主要職責(zé)就是駕駛汽車 public void drive(Benz benz){ benz.run(); } }
司機(jī)通過調(diào)用奔馳車的run方法開動(dòng)奔馳車,其源代碼如代碼清單3-2所示。
代碼清單3-2 奔馳車源代碼
public class Benz { //汽車肯定會(huì)跑 public void run(){ System.out.println("奔馳汽車開始運(yùn)行..."); } }
有車,有司機(jī),在Client場景類產(chǎn)生相應(yīng)的對象,其源代碼如代碼清單3-3所示。
代碼清單3-3 場景類源代碼
public class Client { public static void main(String[] args) { Driver zhangSan = new Driver(); Benz benz = new Benz(); //張三開奔馳車 zhangSan.drive(benz); } }
通過以上的代碼,完成了司機(jī)開動(dòng)奔馳車的場景,到目前為止,這個(gè)司機(jī)開奔馳車的項(xiàng)目沒有任何問題。我們常說“危難時(shí)刻見真情”,我們把這句話移植到技術(shù)上就成了“變更才顯真功夫”,業(yè)務(wù)需求變更永無休止,技術(shù)前進(jìn)就永無止境,在發(fā)生變更時(shí)才能發(fā)覺我們的設(shè)計(jì)或程序是否是松耦合。我們在一段貌似磐石的程序上加上一塊小石頭:張三司機(jī)不僅要開奔馳車,還要開寶馬車,又該怎么實(shí)現(xiàn)呢?麻煩出來了,那好,我們走一步是一步,我們先把寶馬車產(chǎn)生出來,實(shí)現(xiàn)過程如代碼清單3-4所示。
代碼清單3-4 寶馬車源代碼
public class BMW { //寶馬車當(dāng)然也可以開動(dòng)了 public void run(){ System.out.println("寶馬汽車開始運(yùn)行..."); } }
寶馬車也產(chǎn)生了,但是我們卻沒有辦法讓張三開動(dòng)起來,為什么?張三沒有開動(dòng)寶馬車的方法呀!一個(gè)拿有C駕照的司機(jī)竟然只能開奔馳車而不能開寶馬車,這也太不合理了!在現(xiàn)實(shí)世界都不允許存在這種情況,何況程序還是對現(xiàn)實(shí)世界的抽象,我們的設(shè)計(jì)出現(xiàn)了問題:司機(jī)類和奔馳車類之間是緊耦合的關(guān)系,其導(dǎo)致的結(jié)果就是系統(tǒng)的可維護(hù)性大大降低,可讀性降低,兩個(gè)相似的類需要閱讀兩個(gè)文件,你樂意嗎?還有穩(wěn)定性,什么是穩(wěn)定性?固化的、健壯的才是穩(wěn)定的,這里只是增加了一個(gè)車類就需要修改司機(jī)類,這不是穩(wěn)定性,這是易變性。被依賴者的變更竟然讓依賴者來承擔(dān)修改的成本,這樣的依賴關(guān)系誰肯承擔(dān)!證明到這里,我們已經(jīng)知道反論題已經(jīng)部分不成立了。
注意 設(shè)計(jì)是否具備穩(wěn)定性,只要適當(dāng)?shù)亍八伤赏痢保^察“設(shè)計(jì)的藍(lán)圖”是否還可以茁壯地成長就可以得出結(jié)論,穩(wěn)定性較高的設(shè)計(jì),在周圍環(huán)境頻繁變化的時(shí)候,依然可以做到“我自巋然不動(dòng)”。
我們繼續(xù)證明,“減少并行開發(fā)引起的風(fēng)險(xiǎn)”,什么是并行開發(fā)的風(fēng)險(xiǎn)?并行開發(fā)最大的風(fēng)險(xiǎn)就是風(fēng)險(xiǎn)擴(kuò)散,本來只是一段程序的錯(cuò)誤或異常,逐步波及一個(gè)功能,一個(gè)模塊,甚至到最后毀壞了整個(gè)項(xiàng)目。為什么并行開發(fā)就有這樣的風(fēng)險(xiǎn)呢?一個(gè)團(tuán)隊(duì),20個(gè)開發(fā)人員,各人負(fù)責(zé)不同的功能模塊,甲負(fù)責(zé)汽車類的建造,乙負(fù)責(zé)司機(jī)類的建造,在甲沒有完成的情況下,乙是不能完全地編寫代碼的,缺少汽車類,編譯器根本就不會(huì)讓你通過!在缺少Benz類的情況下,Driver類能編譯嗎?更不要說是單元測試了!在這種不使用依賴倒置原則的環(huán)境中,所有的開發(fā)工作都是“單線程”的,甲做完,乙再做,然后是丙繼續(xù)……這在20世紀(jì)90年代“個(gè)人英雄主義”編程模式中還是比較適用的,一個(gè)人完成所有的代碼工作。但在現(xiàn)在的大中型項(xiàng)目中已經(jīng)是完全不能勝任了,一個(gè)項(xiàng)目是一個(gè)團(tuán)隊(duì)協(xié)作的結(jié)果,一個(gè)“英雄”再牛也不可能了解所有的業(yè)務(wù)和所有的技術(shù),要協(xié)作就要并行開發(fā),要并行開發(fā)就要解決模塊之間的項(xiàng)目依賴關(guān)系,那然后呢?依賴倒置原則就隆重出場了!
根據(jù)以上證明,如果不使用依賴倒置原則就會(huì)加重類間的耦合性,降低系統(tǒng)的穩(wěn)定性,增加并行開發(fā)引起的風(fēng)險(xiǎn),降低代碼的可讀性和可維護(hù)性。承接上面的例子,引入依賴倒置原則后的類圖如圖3-2所示。

圖3-2 引入依賴倒置原則后的類圖
建立兩個(gè)接口:IDriver和ICar,分別定義了司機(jī)和汽車的各個(gè)職能,司機(jī)就是駕駛汽車,必須實(shí)現(xiàn)drive()方法,其實(shí)現(xiàn)過程如代碼清單3-5所示。
代碼清單3-5 司機(jī)接口
public interface IDriver { //是司機(jī)就應(yīng)該會(huì)駕駛汽車 public void drive(ICar car); }
接口只是一個(gè)抽象化的概念,是對一類事物的最抽象描述,具體的實(shí)現(xiàn)代碼由相應(yīng)的實(shí)現(xiàn)類來完成,Driver實(shí)現(xiàn)類如代碼清單3-6所示。
代碼清單3-6 司機(jī)類的實(shí)現(xiàn)
public class Driver implements IDriver{ //司機(jī)的主要職責(zé)就是駕駛汽車 public void drive(ICar car){ car.run(); } }
在IDriver中,通過傳入ICar接口實(shí)現(xiàn)了抽象之間的依賴關(guān)系,Driver實(shí)現(xiàn)類也傳入了ICar接口,至于到底是哪個(gè)型號的Car,需要在高層模塊中聲明。
ICar及其兩個(gè)實(shí)現(xiàn)類的實(shí)現(xiàn)過程如代碼清單3-7所示。
代碼清單3-7 汽車接口及兩個(gè)實(shí)現(xiàn)類
public interface ICar { //是汽車就應(yīng)該能跑 public void run(); } public class Benz implements ICar{ //汽車肯定會(huì)跑 public void run(){ System.out.println("奔馳汽車開始運(yùn)行..."); } } public class BMW implements ICar{ //寶馬車當(dāng)然也可以開動(dòng)了 public void run(){ System.out.println("寶馬汽車開始運(yùn)行..."); } }
在業(yè)務(wù)場景中,我們貫徹“抽象不應(yīng)該依賴細(xì)節(jié)”,也就是我們認(rèn)為抽象(ICar接口)不依賴BMW和Benz兩個(gè)實(shí)現(xiàn)類(細(xì)節(jié)),因此在高層次的模塊中應(yīng)用都是抽象,Client的實(shí)現(xiàn)過程如代碼清單3-8所示。
代碼清單3-8 業(yè)務(wù)場景
public class Client { public static void main(String[] args) { IDriver zhangSan = new Driver(); ICar benz = new Benz(); //張三開奔馳車 zhangSan.drive(benz); } }
Client屬于高層業(yè)務(wù)邏輯,它對低層模塊的依賴都建立在抽象上,zhangSan的表面類型是IDriver,Benz的表面類型是ICar,也許你要問,在這個(gè)高層模塊中也調(diào)用到了低層模塊,比如new Driver()和new Benz()等,如何解釋?確實(shí)如此,zhangSan的表面類型是IDriver,是一個(gè)接口,是抽象的、非實(shí)體化的,在其后的所有操作中,zhangSan都是以IDriver類型進(jìn)行操作,屏蔽了細(xì)節(jié)對抽象的影響。當(dāng)然,張三如果要開寶馬車,也很容易,我們只要修改業(yè)務(wù)場景類就可以,實(shí)現(xiàn)過程如代碼清單3-9所示。
代碼清單3-9 張三駕駛寶馬車的實(shí)現(xiàn)過程
public class Client { public static void main(String[] args) { IDriver zhangSan = new Driver(); ICar bmw = new BMW(); //張三開奔馳車 zhangSan.drive(bmw); } }
在新增加低層模塊時(shí),只修改了業(yè)務(wù)場景類,也就是高層模塊,對其他低層模塊如Driver類不需要做任何修改,業(yè)務(wù)就可以運(yùn)行,把“變更”引起的風(fēng)險(xiǎn)擴(kuò)散降到最低。
注意 在Java中,只要定義變量就必然要有類型,一個(gè)變量可以有兩種類型:表面類型和實(shí)際類型,表面類型是在定義的時(shí)候賦予的類型,實(shí)際類型是對象的類型,如zhangSan的表面類型是IDriver,實(shí)際類型是Driver。
我們再來思考依賴倒置對并行開發(fā)的影響。兩個(gè)類之間有依賴關(guān)系,只要制定出兩者之間的接口(或抽象類)就可以獨(dú)立開發(fā)了,而且項(xiàng)目之間的單元測試也可以獨(dú)立地運(yùn)行,而TDD(Test-Driven Development,測試驅(qū)動(dòng)開發(fā))開發(fā)模式就是依賴倒置原則的最高級應(yīng)用。我們繼續(xù)回顧上面司機(jī)駕駛汽車的例子,甲程序員負(fù)責(zé)IDriver的開發(fā),乙程序員負(fù)責(zé)ICar的開發(fā),兩個(gè)開發(fā)人員只要制定好了接口就可以獨(dú)立地開發(fā)了,甲開發(fā)進(jìn)度比較快,完成了IDriver以及相關(guān)的實(shí)現(xiàn)類Driver的開發(fā)工作,而乙程序員滯后開發(fā),那甲是否可以進(jìn)行單元測試呢?答案是可以,我們引入一個(gè)JMock工具,其最基本的功能是根據(jù)抽象虛擬一個(gè)對象進(jìn)行測試,測試類如代碼清單3-10所示。
代碼清單3-10 測試類
public class DriverTest extends TestCase{ Mockery context = new JUnit4Mockery(); @Test public void testDriver() { //根據(jù)接口虛擬一個(gè)對象 final ICar car = context.mock(ICar.class); IDriver driver = new Driver(); //內(nèi)部類 context.checking(new Expectations(){{ oneOf (car).run(); }}); driver.drive(car); } }
注意粗體部分,我們只需要一個(gè)ICar的接口,就可以對Driver類進(jìn)行單元測試。從這一點(diǎn)來看,兩個(gè)相互依賴的對象可以分別進(jìn)行開發(fā),孤立地進(jìn)行單元測試,進(jìn)而保證并行開發(fā)的效率和質(zhì)量,TDD開發(fā)的精髓不就在這里嗎?測試驅(qū)動(dòng)開發(fā),先寫好單元測試類,然后再寫實(shí)現(xiàn)類,這對提高代碼的質(zhì)量有非常大的幫助,特別適合研發(fā)類項(xiàng)目或在項(xiàng)目成員整體水平比較低的情況下采用。
抽象是對實(shí)現(xiàn)的約束,對依賴者而言,也是一種契約,不僅僅約束自己,還同時(shí)約束自己與外部的關(guān)系,其目的是保證所有的細(xì)節(jié)不脫離契約的范疇,確保約束雙方按照既定的契約(抽象)共同發(fā)展,只要抽象這根基線在,細(xì)節(jié)就脫離不了這個(gè)圈圈,始終讓你的對象做到“言必信,行必果”。
3.3 依賴的三種寫法
依賴是可以傳遞的,A對象依賴B對象,B又依賴C,C又依賴D……生生不息,依賴不止,記住一點(diǎn):只要做到抽象依賴,即使是多層的依賴傳遞也無所畏懼!
對象的依賴關(guān)系有三種方式來傳遞,如下所示。
1.構(gòu)造函數(shù)傳遞依賴對象
在類中通過構(gòu)造函數(shù)聲明依賴對象,按照依賴注入的說法,這種方式叫做構(gòu)造函數(shù)注入,按照這種方式的注入,IDriver和Driver的程序修改后如代碼清單3-11所示。
代碼清單3-11 構(gòu)造函數(shù)傳遞依賴對象
public interface IDriver { //是司機(jī)就應(yīng)該會(huì)駕駛汽車 public void drive(); } public class Driver implements IDriver{ private ICar car; //構(gòu)造函數(shù)注入 public Driver(ICar _car){ this.car = _car; } //司機(jī)的主要職責(zé)就是駕駛汽車 public void drive(){ this.car.run(); } }
2.Setter方法傳遞依賴對象
在抽象中設(shè)置Setter方法聲明依賴關(guān)系,依照依賴注入的說法,這是Setter依賴注入,按照這種方式的注入,IDriver和Driver的程序修改后如代碼清單3-12所示。
代碼清單3-12 Setter依賴注入
public interface IDriver { //車輛型號 public void setCar(ICar car); //是司機(jī)就應(yīng)該會(huì)駕駛汽車 public void drive(); } public class Driver implements IDriver{ private ICar car; public void setCar(ICar car){ this.car = car; } //司機(jī)的主要職責(zé)就是駕駛汽車 public void drive(){ this.car.run(); } }
3.接口聲明依賴對象
在接口的方法中聲明依賴對象,3.2節(jié)的例子就采用了接口聲明依賴的方式,該方法也叫做接口注入。
3.4 最佳實(shí)踐
依賴倒置原則的本質(zhì)就是通過抽象(接口或抽象類)使各個(gè)類或模塊的實(shí)現(xiàn)彼此獨(dú)立,不互相影響,實(shí)現(xiàn)模塊間的松耦合,我們怎么在項(xiàng)目中使用這個(gè)規(guī)則呢?只要遵循以下的幾個(gè)規(guī)則就可以:
●每個(gè)類盡量都有接口或抽象類,或者抽象類和接口兩者都具備
這是依賴倒置的基本要求,接口和抽象類都是屬于抽象的,有了抽象才可能依賴倒置。
●變量的表面類型盡量是接口或者是抽象類
很多書上說變量的類型一定要是接口或者是抽象類,這個(gè)有點(diǎn)絕對化了,比如一個(gè)工具類,xxxUtils一般是不需要接口或是抽象類的。還有,如果你要使用類的clone方法,就必須使用實(shí)現(xiàn)類,這個(gè)是JDK提供的一個(gè)規(guī)范。
●任何類都不應(yīng)該從具體類派生
如果一個(gè)項(xiàng)目處于開發(fā)狀態(tài),確實(shí)不應(yīng)該有從具體類派生出子類的情況,但這也不是絕對的,因?yàn)槿硕际菚?huì)犯錯(cuò)誤的,有時(shí)設(shè)計(jì)缺陷是在所難免的,因此只要不超過兩層的繼承都是可以忍受的。特別是負(fù)責(zé)項(xiàng)目維護(hù)的同志,基本上可以不考慮這個(gè)規(guī)則,為什么?維護(hù)工作基本上都是進(jìn)行擴(kuò)展開發(fā),修復(fù)行為,通過一個(gè)繼承關(guān)系,覆寫一個(gè)方法就可以修正一個(gè)很大的Bug,何必去繼承最高的基類呢?(當(dāng)然這種情況盡量發(fā)生在不甚了解父類或者無法獲得父類代碼的情況下。)
●盡量不要覆寫基類的方法
如果基類是一個(gè)抽象類,而且這個(gè)方法已經(jīng)實(shí)現(xiàn)了,子類盡量不要覆寫。類間依賴的是抽象,覆寫了抽象方法,對依賴的穩(wěn)定性會(huì)產(chǎn)生一定的影響。
●結(jié)合里氏替換原則使用
在第2章中我們講解了里氏替換原則,父類出現(xiàn)的地方子類就能出現(xiàn),再結(jié)合本章的講解,我們可以得出這樣一個(gè)通俗的規(guī)則: 接口負(fù)責(zé)定義public屬性和方法,并且聲明與其他對象的依賴關(guān)系,抽象類負(fù)責(zé)公共構(gòu)造部分的實(shí)現(xiàn),實(shí)現(xiàn)類準(zhǔn)確的實(shí)現(xiàn)業(yè)務(wù)邏輯,同時(shí)在適當(dāng)?shù)臅r(shí)候?qū)Ω割愡M(jìn)行細(xì)化。
講了這么多,估計(jì)大家對“倒置”這個(gè)詞還是有點(diǎn)不理解,那到底什么是“倒置”呢?我們先說“正置”是什么意思,依賴正置就是類間的依賴是實(shí)實(shí)在在的實(shí)現(xiàn)類間的依賴,也就是面向?qū)崿F(xiàn)編程,這也是正常人的思維方式,我要開奔馳車就依賴奔馳車,我要使用筆記本電腦就直接依賴筆記本電腦,而編寫程序需要的是對現(xiàn)實(shí)世界的事物進(jìn)行抽象,抽象的結(jié)果就是有了抽象類和接口,然后我們根據(jù)系統(tǒng)設(shè)計(jì)的需要產(chǎn)生了抽象間的依賴,代替了人們傳統(tǒng)思維中的事物間的依賴,“倒置”就是從這里產(chǎn)生的。
依賴倒置原則的優(yōu)點(diǎn)在小型項(xiàng)目中很難體現(xiàn)出來,例如小于10個(gè)人月的項(xiàng)目,使用簡單的SSH架構(gòu),基本上不費(fèi)太大力氣就可以完成,是否采用依賴倒置原則影響不大。但是,在一個(gè)大中型項(xiàng)目中,采用依賴倒置原則有非常多的優(yōu)點(diǎn),特別是規(guī)避一些非技術(shù)因素引起的問題。項(xiàng)目越大,需求變化的概率也越大,通過采用依賴倒置原則設(shè)計(jì)的接口或抽象類對實(shí)現(xiàn)類進(jìn)行約束,可以減少需求變化引起的工作量劇增的情況。人員的變動(dòng)在大中型項(xiàng)目中也是時(shí)常存在的,如果設(shè)計(jì)優(yōu)良、代碼結(jié)構(gòu)清晰,人員變化對項(xiàng)目的影響基本為零。大中型項(xiàng)目的維護(hù)周期一般都很長,采用依賴倒置原則可以讓維護(hù)人員輕松地?cái)U(kuò)展和維護(hù)。
依賴倒置原則是6個(gè)設(shè)計(jì)原則中最難以實(shí)現(xiàn)的原則,它是實(shí)現(xiàn)開閉原則的重要途徑,依賴倒置原則沒有實(shí)現(xiàn),就別想實(shí)現(xiàn)對擴(kuò)展開放,對修改關(guān)閉。在項(xiàng)目中,大家只要記住是“面向接口編程”就基本上抓住了依賴倒置原則的核心。
講了這么多依賴倒置原則的優(yōu)點(diǎn),我們也來打擊一下大家,在現(xiàn)實(shí)世界中確實(shí)存在著必須依賴細(xì)節(jié)的事物,比如法律,就必須依賴細(xì)節(jié)的定義。“殺人償命”在中國的法律中古今有之,那這里的“殺人”就是一個(gè)抽象的含義,怎么殺,殺什么人,為什么殺人,都沒有定義,只要是殺人就統(tǒng)統(tǒng)得償命,這就是有問題了,好人殺了壞人,還要陪上自己的一條性命,這是不公正的,從這一點(diǎn)看,我們在實(shí)際的項(xiàng)目中使用依賴倒置原則時(shí)需要審時(shí)度勢,不要抓住一個(gè)原則不放,每一個(gè)原則的優(yōu)點(diǎn)都是有限度的,并不是放之四海而皆準(zhǔn)的真理,所以別為了遵循一個(gè)原則而放棄了一個(gè)項(xiàng)目的終極目標(biāo):投產(chǎn)上線和盈利。作為一個(gè)項(xiàng)目經(jīng)理或架構(gòu)師,應(yīng)該懂得技術(shù)只是實(shí)現(xiàn)目的的工具,惹惱了頂頭上司,設(shè)計(jì)做得再漂亮,代碼寫得再完美,項(xiàng)目做得再符合標(biāo)準(zhǔn),一旦項(xiàng)目虧本,產(chǎn)品投入大于產(chǎn)出,那整體就是扯淡!你自己也別想混得更好!
- 常用工具軟件案例教程
- CorelDRAW X6圖形設(shè)計(jì)立體化教程
- Drupal 7 Module Development
- Photoshop CC 實(shí)戰(zhàn)入門
- R Graph Cookbook
- Maya 2019三維動(dòng)畫基礎(chǔ)案例教程
- Instant MuseScore
- Drupal: Creating Blogs, Forums, Portals, and Community Websites
- 中文版3ds Max 2016實(shí)用教程
- Photoshop網(wǎng)店美工實(shí)例教程(第2版 全彩微課版)
- Implementing SugarCRM 5.x
- 中文版Photoshop CS6從新手到高手·全彩版
- 巧學(xué)巧用Flash CS6制作動(dòng)畫
- 用Multisim玩轉(zhuǎn)電路仿真
- Photoshop CC2017圖像處理實(shí)例教程