對(duì)象的自治和行為的擴(kuò)展與適配
在壞的設(shè)計(jì)中,數(shù)據(jù)往往是分散的,甚至是雜亂的,這就好像一群失去意識(shí)的猛獸,我們無法控制、協(xié)調(diào)以及管理它們。這種漫無頭緒的散亂數(shù)據(jù),猶如猛獸的肆意妄為,會(huì)給系統(tǒng)帶來無盡的災(zāi)難。隨著系統(tǒng)的演化,這種災(zāi)難會(huì)逐漸蔓延至系統(tǒng)的各個(gè)角落。因此,在面向?qū)ο笤O(shè)計(jì)過程中,對(duì)數(shù)據(jù)分類是識(shí)別對(duì)象的一個(gè)前提。但是,僅僅封裝了數(shù)據(jù)的對(duì)象,如果沒有操作數(shù)據(jù)的行為,仍舊是沒有意識(shí)的死亡對(duì)象。
我始終認(rèn)為,對(duì)象在擁有自己數(shù)據(jù)的情況下,應(yīng)該是自治的。這種“自治”類似于SOA中服務(wù)自治的概念,但由于對(duì)象應(yīng)該保持足夠合理的細(xì)粒度,因此這種自治是有限度的自治;或者說它體現(xiàn)的是專家的自治。如果對(duì)象擁有足夠的數(shù)據(jù)信息,就必須樹立這些信息的權(quán)威,這些信息的處理就應(yīng)該由對(duì)象自己來完成。如果它擁有的信息量不夠,或者根本不具備,則可以委派給其他對(duì)象。此時(shí),行為即對(duì)象的意識(shí),是對(duì)象能夠自治的前提。
對(duì)象自治依賴于面向?qū)ο笤O(shè)計(jì)的一個(gè)重要原則,即對(duì)象的數(shù)據(jù)與行為應(yīng)該封裝在一起。Craig Larman提出的“信息專家模式”正是說明了這一點(diǎn),該模式認(rèn)為擁有信息的對(duì)象才是處理這些信息的專家。
對(duì)象自治是一個(gè)很有趣的概念,我們把對(duì)象擬人化,使得對(duì)象成為組成社區(qū)的基本元素。在這個(gè)社區(qū)里,每個(gè)對(duì)象的行動(dòng)都應(yīng)該由自己來控制。無論是完成某個(gè)操作,還是發(fā)出請(qǐng)求,或者響應(yīng)事件,對(duì)象都應(yīng)該有自己的判斷。判斷的合理性來自于它掌握的信息量,以及我們賦予它的意識(shí)的靈性。在構(gòu)建軟件系統(tǒng)時(shí),我們的目標(biāo)就是要搭建這樣一個(gè)由自治對(duì)象組成的社區(qū),而不是無序的混沌世界。每當(dāng)我們?cè)诓僮鲾?shù)據(jù)時(shí),發(fā)現(xiàn)數(shù)據(jù)開始具有發(fā)散、混亂、模糊、蔓延等特征時(shí),就是封裝數(shù)據(jù)的信號(hào)。不管這些數(shù)據(jù)的數(shù)量,還是大小,它都應(yīng)該作為對(duì)象存在于系統(tǒng),同時(shí)該對(duì)象應(yīng)具備操作該數(shù)據(jù)的能力。
例如在報(bào)表系統(tǒng)中,我們?cè)噲D將構(gòu)建好的報(bào)表整體導(dǎo)出為Excel文件。我們?yōu)閷?dǎo)出功能定義了專門的接口ExcelTableExporter,它接收一個(gè)報(bào)表對(duì)象和工作薄對(duì)象,導(dǎo)出報(bào)表到Excel文件中:
- public interface ExcelTableExporter {
- public void export(ReportTable table, WritableWorkbook workbook);
- }
這一接口的定義并無不妥之處。然而,當(dāng)我們?cè)趯?shí)現(xiàn)export()接口方法時(shí),事情開始變得難以控制。我們需要在export()方法中遍歷整個(gè)報(bào)表,獲得報(bào)表的行頭、列頭以及數(shù)據(jù)單元格,然后計(jì)算它們的坐標(biāo),獲得它們的格式,再寫入到Excel單元格中。顯然,ExcelTableExporter要做的事情太多了,而它所要處理的報(bào)表數(shù)據(jù)也開始變得發(fā)散而混亂。
雖然我們對(duì)報(bào)表進(jìn)行了合理的分解與封裝,但坐標(biāo)依舊是散亂的,格式也沒有和報(bào)表對(duì)象封裝在一起。組成報(bào)表的元素對(duì)象僅僅擁有展現(xiàn)的數(shù)據(jù)值,卻不知道自己該放在哪個(gè)位置,又該以什么面貌展現(xiàn)。換言之,這些組成報(bào)表的對(duì)象都不具備充分的自主意識(shí),使得操作它們的ExcelTableExporter心力憔悴。它需要觀察每個(gè)報(bào)表元素對(duì)象的數(shù)據(jù),元素之間的依賴關(guān)系,考慮如何計(jì)算它們的坐標(biāo),獲得符合客戶要求的格式。
簡(jiǎn)言之,職責(zé)的控制權(quán)應(yīng)該是擁有相關(guān)數(shù)據(jù)的報(bào)表對(duì)象,而不應(yīng)該是ExcelTableExporter。
如果我們將這種展現(xiàn)和導(dǎo)出報(bào)表的功能看做是將報(bào)表數(shù)據(jù)繪制在Excel畫布上,那么ExcelTableExporter就好似一位不太高明的畫師,奔忙于全局的掌控與細(xì)節(jié)的刻畫,卻因?yàn)槟芰Σ粔蚨鵁o法二者兼顧。
如果我們讓這些組成報(bào)表的元素對(duì)象擁有繪制自身的能力,境況是否煥然一新呢?此時(shí),ExcelTableExporter只需要取出元素對(duì)象,放在Excel畫布上,它們自己就知道該往哪兒去,該怎么繪制,根本不用ExcelTableExporter來操心。
根據(jù)單一職責(zé)原則(SRP),報(bào)表元素對(duì)象與報(bào)表直接相關(guān),本身不應(yīng)該承擔(dān)繪制的責(zé)任,但放在導(dǎo)出報(bào)表這個(gè)場(chǎng)景來看,卻又是合乎情理的。而且,與繪制相關(guān)的數(shù)據(jù)本身就與報(bào)表數(shù)據(jù)直接相關(guān),例如報(bào)表元素的坐標(biāo),就依賴于報(bào)表數(shù)據(jù)的個(gè)數(shù),以決定它占用的行數(shù)和列數(shù)。報(bào)表的格式同樣設(shè)置在報(bào)表元數(shù)據(jù)中。不過,從抽象的角度來看,我們應(yīng)該為其定義不同的接口,這也符合接口隔離原則(ISP)。同時(shí),我們還需要考慮繪制行為的擴(kuò)展。
例如,在未來我們可能需要考慮將報(bào)表繪制為HTML網(wǎng)頁。因此,我們可以定義一個(gè)繪制元素的接口:
- public interface DrawingElement {
- public void draw(ReportCanvas canvas);
- public object getElement();
- }
draw()方法負(fù)責(zé)將報(bào)表元素繪制到ReportCanvas對(duì)象中。ReportCanvas體現(xiàn)了“畫布”的隱喻,作為載體用來添加繪制出來的報(bào)表元素。
- public interface ReportCanvas {
- public void addElement(DrawingElement element);
- }
對(duì)于Excel而言,實(shí)現(xiàn)draw()方法就是在內(nèi)部創(chuàng)建單元格對(duì)象。如果使用開源項(xiàng)目jxl來完成excel文件的生成,則該單元格對(duì)象可以是Label對(duì)象,也可以是jxl.write.Number對(duì)象。不過,ReportCanvas是不關(guān)心這些的,它只需要能夠添加DrawingElement即可。這里就體現(xiàn)出了抽象DrawingElement的好處。
當(dāng)報(bào)表元素對(duì)象在實(shí)現(xiàn)該接口時(shí),如果是針對(duì)Excel的導(dǎo)出,就可以把諸如Label和Number這樣的單元格對(duì)象封裝到實(shí)現(xiàn)類中。例如報(bào)表中的行頭對(duì)象就可以實(shí)現(xiàn)DrawingElement接口:
- public class RowHeaderExcelElement implements DrawingElment{
- private object cell;
- @Override
- public void draw(ReportCanvas canvas) {
- canvas.addElement(this);
- }
- @Override
- public object getElement() {
- if (isNumber()) {
- cell = createNumberCell();
- } else {
- cell = createLabelCell();
- }
- return cell;
- }
- }
這里的RowHeaderExcelElement類就體現(xiàn)了“自治”思想,因?yàn)樗约褐涝撊绾螌⒆约簱碛械臄?shù)據(jù)繪制到ReportCanvas。
而從功能擴(kuò)展的角度講,如果將來需要支持Html,就可以定義新的RowHeaderHtmlElement類實(shí)現(xiàn)DrawingElement接口。
因?yàn)橐肓薉rawingElement接口,報(bào)表元素對(duì)象就將繪制元素對(duì)象的數(shù)據(jù)與行為都封裝了起來,使其成為了自治的對(duì)象。由于報(bào)表元素對(duì)象自身具備繪制功能,使得ExcelTableExporter的工作變得輕松自如,只需發(fā)出繪制的請(qǐng)求即可:
- for (DrawingElement element : table.getReportUnits()) {
- element.draw(canvas);
- }
通過合理地將職責(zé)進(jìn)行轉(zhuǎn)移,盡可能站在每個(gè)對(duì)象自身的角度進(jìn)行合理的職責(zé)分配,從原則上實(shí)現(xiàn)各個(gè)對(duì)象的“自治”,就能夠各司其職,避免出現(xiàn)一個(gè)龐大的無所不能的“上帝”對(duì)象。
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)聯(lián)系原作者】