1. 程式人生 > >面向對象的弊端是什麽(轉)

面向對象的弊端是什麽(轉)

linux 所有 特色 固定 應該 它的 ofo sge 和我

作者:invalid s
鏈接:https://www.zhihu.com/question/20275578/answer/26577791
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請註明出處。

弊端是,沒有人還記得面向對象原本要解決的問題是什麽。

1、面向對象原本要解決什麽(或者說有什麽優良特性)
似乎很簡單,但實際又很不簡單:面向對象三要素封裝、繼承、多態

警告:事實上,從業界如此總結出這面向對象三要素的一剎那開始,就已經開始犯錯了!)。

封裝:封裝的意義,在於明確標識出允許外部使用的所有成員函數和數據項,或者叫接口

有了封裝,就可以明確區分內外,使得類實現者可以修改封裝的東西而不影響部調用者;而外部調用者也可以知道自己不可以碰哪裏。這就提供一個良好的合作基礎——或者說,只要接口
這個基礎約定不變,則代碼改變不足為慮。



繼承+多態:繼承和多態必須一起說。一旦割裂,就說明理解上已經誤入歧途了。

先說繼承:繼承同時具有兩種含義:其一是繼承基類的方法,並做出自己的改變和/或擴展——號稱解決了代碼重用問題;其二是聲明某個子類兼容於某基類(或者說,接口上完全兼容於基類),外部調用者可無需關註其差別(內部機制會自動把請求派發[dispatch]到合適的邏輯)。

再說多態:基於對象所屬類的不同,外部對同一個方法的調用,實際執行的邏輯不同。

很顯然,多態實際上是依附於繼承的兩種含義的:“改變”和“擴展”本身就意味著必須有機制去自動選用你改變/擴展過的版本,故無多態,則兩種含義就不可能實現。

所以,多態實質上是繼承的實現細節;那麽讓多態與封裝、繼承這兩個概念並列,顯然是不符合邏輯
的。不假思索的就把它們當作可並列概念使用的人,顯然是從一開始就被誤導了——正是這種誤導,使得大多數人把註意力過多集中在多態這個戰術層面的問題上,甚至達到近乎惡意利用的程度;同時卻忽略了戰略層面的問題,這就致使軟件很容易被他們設計成一灘稀屎(後面會詳細談論這個)。


實踐中,繼承的第一種含義(實現繼承)意義並不很大,甚至常常是有害的。因為它使得子類與基類出現強耦合。

繼承的第二種含義非常重要。它又叫“接口繼承”。
接口繼承實質上是要求“做出一個良好的抽象,這個抽象規定了一個兼容接口,使得外部調用者無需關心具體細節,可一視同仁的處理實現了特定接口的所有對象”——這在程序設計上,叫做歸一化


歸一化使得高層的外部使用者可以不加區分的處理所有接口兼容的對象集合——就好象linux的泛文件概念一樣,所有東西都可以當文件處理,不必關心它是內存、磁盤、網絡還是屏幕(當然,對底層設計者,當然也可以區分出“字符設備”和“塊設備”,然後做出針對性的設計:細致到什麽程度,視需求而定)。

歸一化
的實例:
a、一切對象都可以序列化/toString
b、一切UI對象都是個window,都可以響應窗口事件。

——必須註意,是一切(符合xx條件的)對象皆可以做什麽,而不是“一切皆對象”。後者毫無意義(從信息論角度上說,一切皆xx蘊含的信息量為0)。


顯然,歸一化可以大大簡化使用者的處理邏輯:
這和帶兵打仗是類似的,班長需要知道每個戰士的姓名/性格/特長,否則就不知道該派誰去對付對面山坡上的狙擊手;而連長呢,只需知道自己手下哪個班/排擅長什麽就行了,然後安排他們各自去守一段戰線;到了師長/軍長那裏,他更關註戰場形勢的轉變及預期……沒有這種層層簡化、而是必須直接指揮到每個人的話,累死軍長都沒法指揮哪怕只是一場形勢明朗的沖突——光一個個打完電話就能把他累成啞巴。

反過來也對:軍長壓根就不應該去幹涉某個步兵班裏、幾個大頭兵之間的戰術配合;這不僅耽誤他行使身為軍長的職責,也會幹擾士兵們長久以來養成的默契。他的職責是讓合適的部隊在合適的時機出現在合適的戰場,而不是一天到晚對著幾個小兵指手畫腳、弄的他們無所適從。

約束各單位履行各自的職責、禁止它們越級胡亂指揮,這就是封裝

正是通過封裝和歸一化,我們才可以做到“如果一個師解決不了問題,那就再調兩個師”“如果單憑陸軍解決不了問題,那就讓空軍也過來”——這種靈活性顯然是從良好的部隊編制得來的。在軟件設計裏,我們叫它“通過合理模塊化而靈活應對需求變更”。



軟件設計同樣。比如說,消息循環在派發消息時,只需知道所有UI對象都是個window,都可以響應窗口消息就足夠了;它沒必要知道每個UI對象究竟是什麽(歸一化)、也不應該關心這個UI對象的內部執行細節(封裝)——該對象自己知道收到消息該怎麽做;而且若它出了問題,只需修改該對象即可,不會影響外部。

合理劃分功能層級、適時砍掉不必要的繁雜信息,一層層向上提供簡潔卻又完備的信息/接口,高層模塊才不會被累死——KISS是最難也是最優的軟件設計方法,沒有之一。


可見,封裝和歸一化才是戰略層面、生死攸關的問題。遵循它並不能保證你一定能打勝仗,但違反它你必定死的很難看。

但這兩個問題太大、太難,並且不存在普適性答案。這就使得沒有足夠經驗、缺乏認真思考的外行們根本無從置喙



前面提到過,人們錯誤的把多態這個戰術技巧提到“封裝和歸一化”相同的戰略層面上。這就致使本該談論戰略的設計工作被一群毫無實踐經驗、只會就著淺顯的多態胡扯八道的戰術家攻占和把持,進而使得“以戰術代替戰略”成為普遍現象——因為對他們來說,多態是既容易理解又容易玩出諸多花樣的;而封裝和歸一化就太空泛又太復雜,對他們來說完全無從著手了。
所以,他們把一切精力都用在多態的濫用上,卻從不談封裝和歸一化:即使談到了,也是作為多態的附庸出現的。

這種戰術層面的空談很容易、也很容易出彩,但並不解決問題——反而總是導致簡單問題復雜化。
然而,對於如何解決問題,他們並不在行,也不在乎。因為他們沒有能力在乎。
這就要命了。



總結:面向對象的好處實際就這麽兩點。
一是通過封裝明確定義了何謂接口、何謂接口內部實現、何謂接口的外部調用者,使得大家各司其職,不得越界;
二是通過繼承+多態這種內置機制,在語言的層面支持歸一化的設計,並使得內行可以從代碼本身看到這個設計——但,註意僅僅只是支持歸一化的設計。不懂如何做出這種設計的外行仍然不可能從瞎胡鬧的設計中得到任何好處。


顯然,不用面向對象語言、不用class,一樣可以做歸一化的設計(如老掉牙的泛文件概念、遊戲行業的一切皆精靈),一樣可以封裝(通過定義模塊和接口),只是用面向對象語言可以直接用語言元素顯式聲明這些而已;
而用了面向對象語言,滿篇都是class,並不等於就有了歸一化的設計。甚至,因為被這些花哨的東西迷惑,反而更加不知道什麽才是設計。


2、人們以為面向對象是什麽、以及因此制造出的悲劇以及鬧劇

誤解一、面向對象語言支持用語言元素直接聲明封裝性和接口兼容性,所以用面向對象語言寫出來的東西一定更清晰、易懂

事實上,既然class意味著聲明了封裝、繼承意味著聲明了接口兼容,那麽錯誤的類設計顯然就是錯誤的聲明、盲目定義的類就是無意義的喋喋不休。而錯誤的聲明比沒有聲明更糟;通篇毫無意義的喋喋不休還不如錯誤的聲明

除非你真正做出了漂亮的設計,然後用面向對象的語法把這個設計聲明出來——僅僅聲明真正有設計、真正需要人們註意的地方,而不是到處瞎叫喚——否則不可能得到任何好處。

一切皆對象實質上是在鼓勵堆砌毫無意義的喋喋不休,並且用這種戰術層面都蠢的要命的喋喋不休來代替戰略層面的考量。

大部分人——註意,不是個別人——甚至被這種無意義的喋喋不休搞出了神經質,以至於非要在喋喋不休中找出意義:沒錯,我說的就是設計模式驅動編程,以及如此理解面向對象編程。



誤解二、面向對象三要素是封裝、繼承、多態,所以只要是面向對象語言寫的程序,就一定“繼承”了語言的這三個優良特性

事實上,如前所述,封裝、繼承、多態只是語言層面對良好設計的支持,並不能導向良好的設計。
如果你的設計做不出真正的封裝性、不懂得何謂歸一化,那它用什麽寫出來都是垃圾(不僅如此,因為你的低水平,“面向對象三要素”反而會誤導你,使你更快、更遠、更詭異的偏離目標)。



誤解三、把軟件寫成面向對象的至少是無害的

要了解事實上是什麽,需要先科普幾個概念。


1、什麽是真正的封裝

——回答我,封裝是不是等於“把不想讓別人看到、以後可能修改的東西用private隱藏起來”?

顯然不是
如果功能得不到滿足、或者未曾預料到真正發生的需求變更,那麽你怎麽把一個成員變量/函數放到private裏面的,將來就必須怎麽把它挪出來。

你越瞎搞,越去搞某些華而不實的“靈活性”——比如某種設計模式——真正的需求來臨時,你要動的地方就越多。

真正的封裝是,經過深入的思考,做出良好的抽象,給出“完整且最小”的接口,並使得內部細節可以對外透明(註意:對外透明的意思是外部調用者可以順利的得到自己想要的任何功能,完全意識不到內部細節的存在;而不是外部調用者為了完成某個功能、卻被礙手礙腳的private聲明弄得火冒三丈;最終只能通過怪異、復雜甚至奇葩的機制,才能更改他必須關註的細節——而且這種訪問往往被實現的如此復雜,以至於稍不註意就會釀成大禍)。

一個設計,只有達到了這個高度,才能真正做到所謂的“封裝性”,才能真正杜絕對內部細節的訪問。

否則,生硬放進private裏面的東西,最後還得生硬的被拖出來——當然,這種東西經常會被美化成“訪問函數”之類渣渣(不是說訪問函數是渣渣,而是說因為設計不良、不得不以訪問函數之類玩意兒在封裝上到處挖洞洞這種行為是渣渣)。



一個典型的例子,就是C++的new和過於靈活的內存使用方式之間的耦合。
這個耦合就導致了new[]/delete[]、placement new/placement delete之類怪異的東西:這些東西必須成對使用,怎麽分配就必須怎麽釋放,任何錯誤搭配都可能導致程序崩潰——這是為了兼容C、以及得到更高執行效率的無奈之舉;但,它更是“抽象層次過於復雜,以至於無法做出真正透明的設計”的典型案例:只能說,c++設計者是真正的大師,如此復雜的東西在他手裏,才僅僅付出了如此之小的代價。

(更準確點說,是new/delete和c++的其它語言元素之間是非正交的;於是當同時使用這些語言元素時,就不可避免的出現了彼此扯淡的現象。即new/delete這個操作對其它語言元素非透明:在c++的設計裏,是通過把new/delete分成兩層,一是內存分配、二是在分配的內存上初始化,然後暴露這個分層細節,從而在最大程度上實現了封裝——但比之其它真正能彼此透明的語言元素間的關系,new/delete顯然過於復雜了)

這個案例,可以非常直觀的說明“設計出真正對外透明的封裝”究竟會有多難。


2、接口繼承真正的好處是什麽?是用了繼承就顯得比較高大上嗎?

顯然不是。

接口繼承沒有任何好處。它只是聲明某些對象在某些場景下,可以用歸一化的方式處理而已。

換句話說,如果不存在“需要不加區分的處理類似的一系列對象”的場合,那麽繼承不過是在裝X罷了。




了解了如上兩點,那麽,很顯然:
1、如果你沒有做出好的抽象、甚至完全不知道需要做好的抽象就忙著去“封裝”,那麽你只是在“封”和“裝”而已。
這種“封”和“裝”的行為只會制造累贅和虛假的承諾;這些累贅以及必然會變卦的承諾,必然會為未來的維護帶來更多的麻煩,甚至拖垮整個項目。

正是這種累贅和虛假的承諾的拖累,而不是為了應付“需求改變”所必需的“靈活性”,才是大多數面向對象項目代碼量暴增的元兇。

2、沒有真正的抓到一類事物(在當前應用場景下)的根本,就去設計繼承結構,是必不會有所得的。

不僅如此,請註意我強調了在當前應用場景下
這是因為,分類是一個極其主觀的東西,不存在普適的分類法

舉例來說,我要研究種族歧視,那麽必然以膚色分類;換到法醫學,那就按死因分類;生物學呢,則搞門科目屬種……

想象下,需求是“時尚女裝”,你卻按“窒息死亡/溺水死亡/中毒死亡之體征”來了個分類……你說後面這軟件還能寫嗎?



類似的,我遇到過寫遊戲的卻去糾結“武器裝備該不該從遊戲角色繼承”的神人。你覺得呢?

事實上,遊戲界真正的抽象方法之一是:一切都是個有位置能感受時間流逝的精靈;而某個“感受到時間流逝顯示不同圖片的對象”,其實就是遊戲主角;而“當收到碰撞事件時,改變主角下一輪顯示的圖片組的”,就是遊戲邏輯。


看看它和“武器裝備該不該從遊戲角色繼承”能差多遠。想想到得後來,以遊戲角色為基類的方案會變成什麽樣子?為什麽會這樣?





最具重量級的炸彈則是:正方形是不是一個矩形?它該不該從矩形繼承?如果可以從矩形繼承,那麽什麽是正方形的長和寬?在這個設計裏,如果我修改了正方形的長,那麽這個正方形類還能不能叫正方形?它不應該自然轉換成長方形嗎?如果我有兩個List,一個存長方形,一個存正方形,自動轉換後的對象能否自動遷移到合適的list?什麽語言能提供這種機制?如果不能,“一視同仁的處理某個容器中的所有元素”豈不變成了一句屁話?

造成這顆炸彈的根本原因是,面向對象中的“類”,和我們日常語言乃至數學語言中的“類”根本就不是一碼事。

面向對象中的“類”,意思是“接口上兼容的一系列對象”,關註的只不過是接口的兼容性而已(可搜索 裏氏代換);關鍵放在“可一視同仁的處理”上(學術上叫is-a)。

顯然,這個定義完全是且只是為了應付歸一化的需要。

這個定義經常和我們日常對話中提到的類概念上重合;但,如前所述,根本上卻徹徹底底是八桿子打不著的兩碼事。

就著生活經驗濫用“類”這個術語,甚至依靠這種粗淺認識去做設計,必然會導致出現各種各樣的偏差。這種設計實質上就是在胡說八道。
就著這種胡說八道來寫程序——有人覺得這種人能有好結果嗎?

——但,幾乎所有的面向對象語言、差不多所有的面向對象方法論,卻就是在鼓勵大家都這麽做,完全沒有意識到它們的理論基礎有多麽的不牢靠。
——如此作死,焉能不死?!


——你還敢說面向對象無害嗎?

——在真正明白何謂封裝、何謂歸一化之前,每一次寫下class,就在錯誤的道路上又多走了一步。
——設計真正需要關註的核心其實很簡單,就是封裝和歸一化。一個項目開始的時候,“class”寫的越早,就離這個核心越遠
——過去鼓吹的各種面向對象方法論、甚至某些語言本身,恰恰正是在慫恿甚至逼迫開發者盡可能早、盡可能多的寫class。


重復一遍:封裝可(通過固定接口而)應付需求變更、歸一化可簡化(類的使用者的)設計:以上,就是面向對象最最基本的好處。
——其它一切,都不過是在這兩個基礎上的衍生而已。

換言之,如果得不到這兩個基本好處,那麽也就沒有任何衍生好處——應付需求變更/簡化設計並不是打打嘴炮就能做到的。


誤解四、只有面向對象語言寫的程序才是面向對象的。

事實上,unix系統提出泛文件概念時,面向對象語言根本就不存在;遊戲界的精靈這個基礎抽象,最初是用C甚至匯編寫的;……。

面向對象其實是汲取以上各種成功設計的經驗才提出來的。

所以,面向對象的設計,不必非要c++/java之類支持面向對象的語言才能實現;它們不過是在你做出了面向對象的設計之後,能讓你寫得更愜意一些罷了——但,如果一個項目無需或無法做出面向對象的設計,某些面向對象語言反而會讓你很難受。

用面向對象語言寫程序,和一個程序的設計是面向對象的,兩者是八桿子打不著的兩碼事。純C寫的linux kernel事實上比c++/java之類語言搞出來的大多數項目更加面向對象——只是絕大部分人都自以為自己到處瞎寫class的面條代碼才是面向對象的正統、而死腦筋的linus搞的泛文件抽象不過是過程式思維搞出來的老古董。

——這個誤解之深,甚至達到連wiki詞條裏面,都把OOP定義為“用支持面向對象的語言寫程序”的程度。
——我們提及面向對象時,明明在談論戰略、談論軟件總體設計;但總有人把它歪曲成戰術方面的、漫無目標卻還自我感覺良好的、瑣碎的投機。
——恐怕這也是沒有人說泛文件設計思想是個騙局、而面向對象卻被業界大牛們嚴厲抨擊的根本原因了:真正的封裝、歸一化精髓被拋棄,浮於表面的、喋喋不休的class/設計模式卻成了”正統“!

借用樓下PeytonCai朋友的鏈接:
名家吐槽:面向對象編程從骨子裏就有問題

————————————————————————————

總結: 面向對象其實是對過去成功的設計經驗的總結。但那些成功的設計,不是因為用了封裝/歸一化而成功,而是切合自己面對的問題,給出了恰到好處的設計

讓一個初學者知道自己應該向封裝/歸一化這個方向前進,是好的;用一個面向對象的條條框框把他們框在裏面、甚至使得他們以為寫下class是完全無需思索的、真正應該追求的是設計模式,則是罪惡的——它實質上是把初學者的註意力從真正應該註意的封裝、歸一化方向引開,欺騙他們陷入“近乎惡意的全方位濫用多態”的泥潭。

事實上,class寫的越隨意,才越需要設計模式;就著錯誤的實現寫得越多、特性用得越多,它就越發的死板,以至於必須更加多得多的特性、模式、甚至語法hack,才能勉強完成需求。

只有經過真正的深思熟慮,才有可能做到KISS。


到處鼓噪的面向對象編程的最大弊端,是把軟件設計工作偷換概念,變成了“就著class及相關教條瞎胡鬧,不管有沒有好處先插一杠子”,甚至使得人們忘記去關註“抽象是否真正簡化了面對的問題”——這是猥瑣的投機,不是設計。

一言以蔽之:沒有銀彈。任何寄希望於靠著某種“高大上”的技術——無論是面向對象、數據驅動、消息驅動還是lambda、協程等等等等——就能一勞永逸的使得任何現實問題“迎刃而解”的企圖都是註定要失敗的,都不過是外行的意淫而已;靠意淫來做設計,不掉溝裏才怪。

想要做出KISS的方案,就必須對面對的問題有透徹的了解,有足夠的經驗和能力,並經過深思熟慮,這才能做出簡潔的抽象:至於最終的抽象是面向對象的、面向過程的還是數據驅動/消息驅動的,甚至是大雜燴的,那都無所謂。只要這個設計能做到最重要、也是最難的KISS,它就是個好設計。

在特定領域、特定場景下,的確有成功的經驗、正確/合理的方向:技術無罪,但,沒有銀彈。


————————————————————————————————————————
2016.5.16:

嗯,這個是我很久很久以前在CU上發過的一系列帖子……

當時很多鼓吹“面向對象就是好來就是好的”就著一知半解胡攪蠻纏,這系列帖子是駁斥他們的。所以很多詞句挖苦意味很濃,見諒。

再比如,傳說中的面向對象本該大顯神威的遊戲領域——就說流行的WOW吧。

這個遊戲有10個職業,10個種族,每個種族都有自己的幾個特有種族天賦(這個種族天賦還可能根據職業有所不同,比如血精靈);每個職業有幾十甚至上百種不同的技能/法術,這些技能有近戰技能,有遠程技能;有的技能會對敵方造成傷害或不良狀態,有的技能能給己方隊友加上好的狀態或治療隊友;而且很多這類技能還會根據目標的狀態切換不同的效果;有些技能是單體效果,有些技能是光環效果(又分為對敵方造成光環效果還是對己方兩種,也可能兩者兼備),而另一些技能是地圖範圍效果(如烈焰風暴是一個圓形區域;冰錐術是一個錐形區域;特別的,順劈斬是在當前攻擊目標旁邊不超過5碼的另一個敵對目標——某個boss的順劈斬更強,它會從第一個目標傳遞幾十個目標,總傳遞距離可以達到誇張的幾百碼;並且這個傷害也是各有特色的:戰士的順劈斬是每個目標傷害固定,有些boss的則是同時挨打的人越多傷害越低,但還有個變態boss卻是被打的人越多傷害越高……);大多數技能還可以通過天賦雕文強化/改變的面目全非(比如插一個雕文,法師的火球就不會造成持續傷害但施法速度增加;點一個天賦,法師的冰冷減速效果就會降低對方受到的治療效果;點某個天賦,盜賊的某些技能攻擊就會延長自身提升攻擊速度這個狀態的持續時間,等等);還有很多技能是因為學習了某個專業或裝備/持有某個物品而得到(比如,學了采藥,就可以得到生命之血這個技能,每3分鐘可用,能夠在若幹秒內回復你若幹生命值——這個技能和采藥技能等級掛鉤,但很可能接下來的某個版本,就會再和玩家的生命上限值掛鉤,以避免它像現在一樣,被玩家斥為廢柴技能);另外,不同等級的技能可能有施法時間甚至額外特效方面的差別;此外,每個技能會造成不同屬性的傷害/效果(神聖、暗影、元素、物理等等),甚至一個技能同時造成多種類型傷害效果,更有冰火球這樣根據目標抵抗力而智能選擇更大殺傷效果類型的變態魔法……

最後,最最重要的是,這所有職業上千個技能(或許加上NPC特有的一些技能,數目會達到幾千種)並不穩定,常常會因為某個技能或某些技能的組合過於強大/弱小而加以修改(比如加一個額外的負面狀態如無敵/聖療;甚至全面修改“抗性”“破甲”概念的定義)——玩過wow的都知道,這事幾乎每個月都有發生。

好吧,你打算怎麽設計這數千個技能/效果?
或者,你就這樣把這些概念用class這個筐一裝,然後到處開特例、特例都解決不了就搞23個模式使勁往一塊粘,管他整體結構如何,淌哪算哪?

扯淡。


有個故事說的好:
有人送幾個瞎子一條魚,瞎子們高興壞了,決定熬魚湯喝。魚湯熬好了,瞎子甲嘗了一口,真鮮啊;瞎子乙趕緊也喝一口,太鮮了,太好喝了。幾個瞎子一邊喝一邊贊美——忽然瞎子丙叫了起來:魚跳我腳上了,它不在鍋裏!
眾瞎子大驚:這魚都沒放到鍋裏,湯就鮮成這樣了;要是放進鍋裏,還不得把我們都鮮死啊!

眾面向對象原教旨主義者把事情攪得一團糟,同樣也會大驚:天哪,用了面向對象都復雜成這樣,這要不用面向對象,這軟件就不能寫了吧!


想想看,假如讓那些面向對象原教旨主義者來設計,會出現什麽情況:

定義一個基類叫技能;然後一個繼承類叫法術技能,另一個叫物理技能;然後神聖法術從法術技能繼承,疾病法術也從法術技能繼承;由於聖騎士一個技能同時具備物理和法術兩種效果,於是必須多重繼承神聖法術和物理技能;多重繼承太危險,於是不得不把神聖法術搞成接口類,引入接口繼承甚至帶實現的純虛函數等等高端概念;然後,活該槍斃的暴雪設計師又想出了讓某個技能同時對目標加上神聖持續傷害效果的奇怪點子——於是不得不再加個繼承層次,使得神聖法術是神聖持續傷害法術的子集:僅立刻造成一次持續傷害的DOT(damage of time)技能……

那麽,點一個天賦,一個技能就會有dot,否則就沒有怎麽辦?

設計模式是靈丹妙藥,不是嗎 ^_^


等到把這所有幾千個技能全部搞定,起碼也是一個數萬個類、幾十層的恐怖繼承樹,並且會用完23個設計模式(甚至再發明幾個新模式出來,我也不會感到奇怪),精巧復雜到沒有任何人願意去碰它。


但,請註意,天殺的暴雪設計師,在最開始的設計方案裏規定DOT不能暴擊;後來又添加約定說某某某職業的某個dot可以暴擊;另一個職業的某個dot在點天賦後可暴擊;至於死亡騎士,在他穿了T9套裝中的其中四件裝備時,他的某個瘟疫類型的dot可以暴擊——但另一個瘟疫dot永遠不能暴擊。


嗯嗯嗯,太好解決了——這不就是策略模式嗎?

好吧,你再填幾十幾百個類體系,然後把舊的幾十層繼承樹中的數萬個類一個個都策略化吧。反正不是我在維護……



哎呀不好,那個槍斃了幾百次都還沒死的暴雪設計師又出餿主意了,他要求:當死亡騎士點了邪惡系的某個天賦時,不光給他增加一個新的dot、並且在這個新dot的存在期間,還要保護他的兩個dot性疾病和1個debuf性疾病不被驅散!


繼續補充:在WLK裏面,那個腦袋都被子彈打成篩子了的暴雪設計師又跳出來了,用他滿是漏洞的腦子出了個該殺的主意:他要求添加載具概念,當玩家坐上載具時,臨時刪除他的所有技能,替換為載具的技能;或者當他坐在特定載具的特定位置時,防止他受到任何傷害、並且允許他釋放自己的所有技能!
更該死的是,他要求,一些技能本來不允許在移動中施放;但現在,當玩家坐在載具上某個位置時,要臨時允許他移動施法!

還有,為了平衡某個野外戰場,他還要求,在某方人數較少時,臨時根據提高他們的生命值和所有技能的攻擊力和治療能力——這個改變必須根據進入戰場的人數實時進行;在一方連續在某個戰場失敗時,同樣要給他們一定補償!



嗯嗯,看看這些不斷改變的刁鉆需求吧,如果沒有面向對象,沒有以策略模式為首的28個設計模式(我有理由相信你們需要至少28個設計模式而不是23個)的英明領導,我們這些沒接觸過大項目、不懂面向對象的傻B們,就是哭的拿眼淚把長城溶解掉都沒辦法吧?——我當然知道搭建長城的材料極難溶與水。

可憐的瞎子,你們的魚湯很鮮吧?




嗯,到這裏,希望讀者們也能停下來,好好思考一下,看看這個問題該如何解決。














想到了沒有?
這裏是答案,看看你的想法是否能不謀而合吧:

這個問題暴雪在Diablo 2時代已經完美解決了: 法術/技能數據庫化


所謂數據庫化,其實等同於表格化,例如這個隨便杜撰出來的簡化方案,是設計一個有如下字段的數據表格:

法術ID 動畫效果 作用範圍 作用類型 屬性 特殊限制 強化類型 特殊設定


其中,特殊設定字段可以是一段LUA代碼,可以在其中搜索、設置極其特殊的傷害類型,或者查詢順劈斬/治療鏈等奇特技能的傳遞目標等等。

特殊限制字段設定法術的施法或/和生效條件,如驅散限定為只能作用於魔法性buf/debuf(根據職業不同,可能有進攻性驅散和防守性驅散之一,也可能同時具備——這就體現在可否驅散敵方/友方目標的debuf)



在這個方案下,釋放一個法術/技能,就成為一種查表運算——找到此法術ID,找到它的作用類型和傷害屬性,計算特殊設定(包括但不限於順劈斬模式的判斷、天賦加成和天賦效果、雕文加成和雕文效果等等)。

於是,到最後,整個法術體系被分為一組組的魔法buf/debuf、物理buf/debuf,這些buf/debuf會影響傷害公式中的某個因子或者造成傷害效果;而傷害效果又分為立即傷害/立即治療和持續傷害/持續治療;最後則是一套影響範圍判定機制。


舉例來說,騎士開聖盾,他同時得到一個buf和一個debuf。
buf是“無敵”,效果相當於設置傷害公式 a*(....) 前面的a因子為0(沒有無敵時此因子為1),於是所有傷害無效。
debuf則是“自律”,因為他的聖盾、聖療技能判斷條件裏都有“有自律debuf,則不允許使用”的設定,於是禁止他在短時間內再次使用這些無賴技能。

敵方法師對他釋放寒冰箭,系統受理,但查詢騎士狀態,發現他處於無敵狀態,返回大大的兩個字“免疫”。

然後,有一個敵方牧師對他使用驅散,查詢牧師的驅散術發現,在驅散術的可驅散列表裏沒有聖盾術,於是提示無法驅散或驅散了另外的可驅散(魔法)效果。
敵方牧師迅速反應過來,再次對他使用強力驅散;查詢牧師強力驅散術,發現該牧師在不久前使用過強力驅散,提示無法施法。
等待3秒後,敵方牧師發現自己的強力驅散冷卻(cool down),再次使用強力驅散,查詢發現強力驅散可驅散聖盾術,於是成功移除騎士的無敵狀態。

現在,敵方法師再次對他釋放寒冰箭,騎士切換冰抗光環,系統查詢騎士狀態,發現冰抗光環,又查詢法師穿透等級,和暴擊等級,根據公式計算能否命中、能否造成全額傷害以及能否暴擊;然後提取法師和騎士雙方裝備、天賦數據代入公式計算傷害加成、減免數據,最後給出騎士受到的傷害數字(包括部分抵抗了多少)。



在暴雪設計師的整理之下,如上種種最終構成了幾個表格;只要查詢並代入相應的數據,即可計算出傷害/治療數值以及類型;特殊效果可以用存儲在數據庫中的LUA代碼補充完成。

最終的設計效果就好像內嵌了一個解釋器,這個解釋器會根據法術ID解釋執行數據庫內部的相關內容。


這樣一來,只要傷害公式、傷害/buf類型、動畫效果等等就位,那麽新增一個法術就只不過是在數據庫中新增一條記錄;讓某個角色學會一個新法術,則只需在它的可使用法術列表裏添加法術名稱(或法術ID);釋放法術是根據法術ID在數據庫中提取動畫;計算傷害是根據法術ID讀取傷害公式,然後代入相關字段並求值。

而這一切,甚至可以通過內部實現的編輯器,用圖形界面完成。


如何?無與倫比的擴展性和便利性,是吧?


這麽一整套東西,核心代碼很可能只有數千甚至數百行。這是因為看似復雜的光環、buf等等東西,其實都已經抽象到和其他法術同樣的流程上來了。最終,所有這些全部歸一為解釋執行傷害公式、提取執行指定動畫之類寥寥幾個通用過程——這顯然同樣是封裝和歸一化思想結出的另一顆果實。但為什麽你就是想不到封裝和歸一化還能這樣用?很簡單,因為你被那些只會就著淺顯的多態喋喋不休的笨蛋徹底引偏方向了。

我並沒有親自實現過這個,所以不敢斷定這玩意兒靠幾百行代碼真的就能全部實現;但根據我在其它項目上的經驗,這套東西應該就是數百行代碼就可以寫出來的——但寫出並調試好這數百行代碼所需的時間可能是一個星期甚至一個月。

相比於不假思索的寫下class所必然導致的龐大、復雜的類層次,以及扯來扯去蛋疼無比的復雜的設計模式大網,這玩意兒的實現、維護、修改、擴展的便利程度,顯然不是一個量級的:前者可能數百人努力數年、弄出幾百萬行代碼都不能正確實現需求,而且必然bug滿天飛;而後者,一個人,個把月,千把行代碼,完成。如果實現水平足夠的話,寫完就再不用碰代碼,而是去寫圖形編輯工具了。之後,擴展、維護都不過是用自己實現的工具拖來拖去再改改屬性、數值,然後點存盤寫入數據庫,完事。


所以說,萬不可死板的傻抱著面向對象不放。你所面對的問題才是最重要的。
你必須隨機應變給出合適的方案——至於最後的設計方案會是什麽流派,那玩意兒根本無關緊要。拿出一個簡單、有效、可靠的方案,比什麽都重要。

最後,還是我在前文總結的那句話:

封裝可(通過固定接口而)應付需求變更、歸一化可簡化(類的使用者的)設計:以上,就是面向對象最最基本的好處。其它一切,都不過是在這兩個基礎上的衍生而已。


換言之,如果得不到這兩個基本好處,那麽也就沒有任何衍生好處——應付需求變更/簡化設計並不是打打嘴炮就能做到的。


再強調一遍,應付需求變更/簡化設計並不是空洞的宣傳口號。

封裝和歸一化類似軍隊制度建設,目標是搞出一個標準化、立體、多變、高效的指揮體系,從而獲得打大戰、打硬戰的能力,然後再去輕松碾壓問題。此所謂戰略。

而那些堆砌無用的所謂“設計模式”的家夥,其實是在每個零件表面粘上掛鉤——據他們說,這樣會增加靈活性、應對需求變更、簡化設計:比如說你帶了個包,就可以掛他們在飛輪上粘的那個勾子上。

但實際上,你永遠不會把包掛飛輪上(但你還是不得不為那些”聰明絕頂“的家夥“為了避免飛輪上的鉤子脫落、掛住其它零件、離心力太大破壞掛在上面的包”等等而衍生出的”傑出“設計買單)。
幸運的是,除了某些企業項目(或其他類似性質的項目),你並不會用到這麽爛的東西。因為這些笨蛋到處亂粘的鉤子會不可避免的導致整個項目變成黏糊糊的一團,從而在曠日持久的拖延後自殺。

這種做法,顯然是和面向對象的初心——通過封裝和歸一化獲得高效指揮體系——背道而馳,從而使得每個中了這種毒的家夥參與的項目不可避免的成為一灘稀屎。

所以,很遺憾,只有殺馬特設計師才會這樣做。真正的設計師壓根不會在設計發動機時考慮“飛輪上掛包”這樣的需求(這就叫“以不知所謂的戰術投機代替戰略布局”)。他會幹凈利落的在整車設計時加個後備箱。




請註意,這並不是個比喻。

如你所見,在”每個零件上粘上掛鉤“這種事情實在太過瘋狂,所以在其他行業連玩笑都不是,因為再傻的人都不會這麽做。

然而在軟件設計時……這種事情是如此多見,多見到面向對象的領軍人物會推薦別人這樣做(如此理解面向對象編程);多見到業內很多大佬都不得不站出來,怒斥”面向對象是個騙局“。

名家吐槽:面向對象編程從骨子裏就有問題
“面向對象編程是一個極其糟糕的主意,只有矽谷裏的人能幹出這種事情。” — Edsger Dijkstra(圖靈獎獲得者)

Edsger W. Dijkstra

如此沈重的心智負擔,這顯然是面向對象的原罪。

面向對象的弊端是什麽(轉)