對象的自治和行為的擴展與適配

作者: 張逸  來源: 博客園  發布時間: 2011-02-28 21:38  閱讀: 567 次  推薦: 0   原文鏈接   [收藏]  

  在壞的設計中,數據往往是分散的,甚至是雜亂的,這就好像一群失去意識的猛獸,我們無法控制、協調以及管理它們。這種漫無頭緒的散亂數據,猶如猛獸的肆意妄為,會給系統帶來無盡的災難。隨著系統的演化,這種災難會逐漸蔓延至系統的各個角落。因此,在面向對象設計過程中,對數據分類是識別對象的一個前提。但是,僅僅封裝了數據的對象,如果沒有操作數據的行為,仍舊是沒有意識的死亡對象。

  我始終認為,對象在擁有自己數據的情況下,應該是自治的。這種“自治”類似于SOA中服務自治的概念,但由于對象應該保持足夠合理的細粒度,因此這種自治是有限度的自治;或者說它體現的是專家的自治。如果對象擁有足夠的數據信息,就必須樹立這些信息的權威,這些信息的處理就應該由對象自己來完成。如果它擁有的信息量不夠,或者根本不具備,則可以委派給其他對象。此時,行為即對象的意識,是對象能夠自治的前提。

  對象自治依賴于面向對象設計的一個重要原則,即對象的數據與行為應該封裝在一起。Craig Larman提出的“信息專家模式”正是說明了這一點,該模式認為擁有信息的對象才是處理這些信息的專家。

  對象自治是一個很有趣的概念,我們把對象擬人化,使得對象成為組成社區的基本元素。在這個社區里,每個對象的行動都應該由自己來控制。無論是完成某個操作,還是發出請求,或者響應事件,對象都應該有自己的判斷。判斷的合理性來自于它掌握的信息量,以及我們賦予它的意識的靈性。在構建軟件系統時,我們的目標就是要搭建這樣一個由自治對象組成的社區,而不是無序的混沌世界。每當我們在操作數據時,發現數據開始具有發散、混亂、模糊、蔓延等特征時,就是封裝數據的信號。不管這些數據的數量,還是大小,它都應該作為對象存在于系統,同時該對象應具備操作該數據的能力。

  例如在報表系統中,我們試圖將構建好的報表整體導出為Excel文件。我們為導出功能定義了專門的接口ExcelTableExporter,它接收一個報表對象和工作薄對象,導出報表到Excel文件中:

 
public interface ExcelTableExporter {
public void export(ReportTable table, WritableWorkbook workbook);
}

  這一接口的定義并無不妥之處。然而,當我們在實現export()接口方法時,事情開始變得難以控制。我們需要在export()方法中遍歷整個報表,獲得報表的行頭、列頭以及數據單元格,然后計算它們的坐標,獲得它們的格式,再寫入到Excel單元格中。顯然,ExcelTableExporter要做的事情太多了,而它所要處理的報表數據也開始變得發散而混亂。雖然我們對報表進行了合理的分解與封裝,但坐標依舊是散亂的,格式也沒有和報表對象封裝在一起。組成報表的元素對象僅僅擁有展現的數據值,卻不知道自己該放在哪個位置,又該以什么面貌展現。換言之,這些組成報表的對象都不具備充分的自主意識,使得操作它們的ExcelTableExporter心力憔悴。它需要觀察每個報表元素對象的數據,元素之間的依賴關系,考慮如何計算它們的坐標,獲得符合客戶要求的格式。如果我們將這種展現和導出報表的功能看做是將報表數據繪制在Excel畫布上,那么ExcelTableExporter就好似一位不太高明的畫師,奔忙于全局的掌控與細節的刻畫,卻因為能力不夠而無法二者兼顧。如果我們讓這些組成報表的元素對象擁有繪制自身的能力,境況是否煥然一新呢?此時,ExcelTableExporter只需要取出元素對象,放在Excel畫布上,它們自己就知道該往哪兒去,該怎么繪制,根本不用ExcelTableExporter來操心。

  根據單一職責原則(SRP),報表元素對象與報表直接相關,本身不應該承擔繪制的責任,但放在導出報表這個場景來看,卻又是合乎情理的。而且,與繪制相關的數據本身就與報表數據直接相關,例如報表元素的坐標,就依賴于報表數據的個數,以決定它占用的行數和列數。報表的格式同樣設置在報表元數據中。不過,從抽象的角度來看,我們應該為其定義不同的接口,這也符合接口隔離原則(ISP)。同時,我們還需要考慮繪制行為的擴展。例如,在未來我們可能需要考慮將報表繪制為HTML網頁。因此,我們可以定義一個繪制元素的接口:

 
public interface DrawingElement {
public void draw(ReportCanvas canvas);
public object getElement();
}

  draw()方法負責將報表元素繪制到ReportCanvas對象中。ReportCanvas體現了“畫布”的隱喻,作為載體用來添加繪制出來的報表元素。

 
public interface ReportCanvas {
public void addElement(DrawingElement element);
}

  對于Excel而言,實現draw()方法就是在內部創建單元格對象。如果使用開源項目jxl來完成excel文件的生成,則該單元格對象可以是Label對象,也可以是jxl.write.Number對象。不過,ReportCanvas是不關心這些的,它只需要能夠添加DrawingElement即可。這里就體現出了抽象DrawingElement的好處。當報表元素對象在實現該接口時,如果是針對Excel的導出,就可以把諸如Label和Number這樣的單元格對象封裝到實現類中。例如報表中的行頭對象就可以實現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;
}
}

  倘若將來需要支持Html,可以定義RowHeaderHtmlElement類實現DrawingElement接口。如果二者之間存在一些共同邏輯,則可以提取一個共同的基類RowHeaderElementBase。

  因為引入了DrawingElement接口,報表元素對象就將繪制元素對象的數據與行為都封裝了起來,使其成為了自治的對象。由于報表元素對象自身具備繪制功能,使得ExcelTableExporter的工作變得輕松自如,只需發出繪制的請求即可:

 
for (DrawingElement element : table.getReportUnits()) {
element.draw(canvas);
}

  在實現上,我們還有一個問題需要解決。ExcelTableExporter的export()方法實現使用了jxl,DrawingElement類封裝的Label或Number對象事實上需要繪制到jxl的WritableSheet中,而不是我們自己抽象的ReportCanvas。為了保證DrawingElment接口的抽象性,以及未來的可擴展性,draw()方法的輸入參數必須是與實現無關的抽象類型。如果修改方法的定義為接受WritableSheet對象,就會限定為jxl,無法輕易變更,這是絕對不可取的。它違背了“供應商綁定”的反模式。

  由于WritableSheet對象與ReportCanvas之間沒有任何關系,強制的類型轉換也無法保證將WritableSheet對象傳遞給DrawingElement對象的draw()方法。除非我們修改WritableSheet的定義,使其實現ReportCanvas接口。但這是不可能的,因為WritableSheet是第三方提供的公開接口,我們不能修改。這時,就需要考慮二者之間的適配。通過運用Adapter模式,我們可以引入一個間接對象WritableSheetAdapter,讓其實現ReportCanvas接口,同時重用WritableSheet提供的職責。在jxl中,WritableSheet被定義為接口,通過WritableWorkbook創建。所以,我們可以考慮將WritableWorkbook創建的對象傳遞給WritableSheetAdapter:

 
public class WritableSheetAdapter implements ReportCanvas {
private WritableSheet sheet;
public WritableSheetAdapter(WritableSheet sheet) {
this.sheet = sheet;
}
@Override

public void addElement(DrawingElement element) {
sheet.addCell((WritableCell)element.getElement());
}
}

  WritableSheetAdapter既實現了ReportCanvas接口,同時又組合了WritableSheet對象,完成了WritableSheet到ReportCanvas的適配,使得DrawingElement對象可以接受它:

 
WritableSheet sheet = workbook.createSheet(sheetName, 0);
WritableSheetAdapter sheetAdapter
= new WritableSheetAdapter(sheet);

//遍歷報表元素,以數據單元格為例
for (DrawingElement element : table.getCellGroups()) {
element.draw(sheetAdapter);
}
0
0
 
標簽:設計
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()