對象的協作
面向對象設計的一個重要分析方法是利用對象的職責來驅動設計。對象有了職責,才會成為具體擁有意識的對象,成為對象社區中可以獨立完成,或者發出指令委派別的對象協作完成職責的一員。將對象看成是“具有職責的東西”。對象應該自己負責自己,而且應該清楚地定義職責。這就是對象設計的關鍵。好的軟件設計者應該像牧羊人一般放牧自己的牛羊,只需要給它們一片豐沃的草地,它們就能自己覓食生活。
Rebecca認為:對象在履行職責時有3種選擇,你可以任選其一:(1)親自完成所有的工作。(2)請求其他對象幫忙完成部分工作(和其他對象協作)。(3)將整個服務請求委托給另外的幫助對象。在分析對象職責時,可以考慮“專家”模式,即信奉專業的事情交給專家來完成,既不互相推諉,卻也不能越俎代庖。專家有其擅長的領域,如果把專家錯放在他不熟悉的領域,不僅會降低工作效率,還可能引入潛在危機。所謂“尺有所長,寸有所短”,含義正在于此。
對象的能力總是有限的,正如我們不能將所有的雞蛋放在一個籃子里,我們也不能將所有的職責交給“上帝”。這既避免單一功能點帶來的風險,又能實現職責的分權。Christepher Alexander建議,在遇到設計問題時“盡量少用集權的機制”。軟件設計同樣如此。職責總是可大可小,面對復雜的職責行為,總是需要多個對象的協作才能完成,就像機器中的零部件,按部就班,各司其職。
讓我們來設計一個數據分析器,它通過分析算法對輸入數據進行分析,并將結果保存在輸出集中。根據業務需求的不同,執行的分析算法也不相同。同時,為了提高分析性能,我們還需要采用多線程方式執行分析任務。從調用者的角度來看【即Martin Fowler提到的規約視角】,我們只關心分析需要的數據以及分析后的結果。那么,誰應該來承擔分析的職責呢?毫無疑問,我們可以定義分析器對象來履行這一職責。此外,調用者其實并不會關心分析算法的實現以及分析的過程,他希望分析是易于執行的,這一愿望通過有效的封裝完全可以滿足。我們可以將分析的職責封裝在DataAnalyzer類中,同時隱藏具體的分析算法。
public static void run(InputData input) {
DataAnalyzer analyzer = new DataAnalyzer();
AnalyticResult result = analyzer.analysis(input);
//做其它事情
//處理分析后的數據
handleOutput(analyzer.output(result));
}
}
public class DataAnalyzer {
public AnalyticResult analysis(InputData input) {}
public OutputData output(AnalyticResult result) {}
}
然而,數據的分析并不是一件簡單的事情。分析器雖然是分析的專家,卻不精通多線程的執行。它自身無法提供分析的異步操作,這時就需要其他對象的協作。分析器將這一職責委派給TaskScheduler對象。TaskScheduler是一個任務調度器,可以發起任務的運行,并在運行完畢的時候,通知任務的發起者。對于TaskScheduler而言,并不會關心具體執行了何種任務。這樣的設計能夠遵循Demeter法則,讓對象盡量保持無知,以避免不必要的依賴。
private Task task;
public AnalyticResult beginRun(Task task) {
AnalyticResult result = new AnalyticResultImpl();
task.setResult(result);
this.task = task;
Thread thread = new Thread(this.task);
thread.start();
return result;
}
public OutputData endRun(AnalyticResult result) {
while (!result.beFinished()) {
result.wait();
}
return task.getOutputData();
}
}
TaskScheduler引入AnalyticResult對象的目的是為了判斷任務執行的狀態。它封裝了當前的線程,從而可以獲得當前線程的狀態,并能夠操作線程。
public void setCurrentThread(Thread thread);
public boolean isFinished();
public void finish();
public void wait();
}
DataAnalyzer的內部封裝了對TaskScheduler對象的調用:
private TaskScheduler scheduler = new TaskScheduler();
public AnalyticResult analysis(InputData input) {
Task task = createTask(input);
return scheduler.beginRun(task);
}
public OutputData output(AnalyticResult result) {
return scheduler.endRun(result);
}
}
Task任務對象負責完成分析任務。雖然在之前的分析中,我們認為DataAnalyzer對象承擔了分析數據的職責。不過,這一職責僅就調用者而言是可行的,對于DataAnalyzer的內部實現則不然。這基于兩種原因。其一是分析過程的復雜程度,它需要對輸入數據進行多個步驟的處理,包含數據的轉換、過濾、運算以及存儲。如果讓DataAnalyzer一力承擔,則可能導致職責過重,形成一個龐大的復雜對象。這既有礙于代碼的閱讀性,也不利于處理過程或算法的重用。其二是分析過程的可變性。根據不同的輸入數據,需要不同的分析算法。DataAnalyzer不應該負責對算法的決策與選擇。而且,隨著需求的變化,數據分析器可能需要擴展。將任務抽象出來,可以很好地應對變化。
引入Task對象可以將DataAnalyzer從繁重的分析任務中解放出來,同時又能夠保證它對分析任務的封裝,是很好的對象協作表現。由于Task對象需要支持多線程,我們還需要它實現Runnable接口。至于ExecutionCallback是一個回調對象,注冊該對象可以在任務執行完畢后調用它:
public InputData getInputData();
public void setInputData(InputData input);
public OutputData getOutputData();
public void setOutputData(OutputData output);
public OutputData execute(InputData input);
public void setResult(AnalyticResult result);
public ExecutionCallback getCallback();
public void setCallback(ExecutionCallback callback);
}
public interface ExecutionCallback {
public void callback();
}
例如分析銷售量波動的任務:
private InputData inputData;
private OutputData outputData;
private AnalyticResult result;
public OutputData execute(InputData input) {
inputData = input;
//分析輸入數據,獲得銷售量波動的情況
result.finished();
if (getCallback() != null) {
getCallback().callback();
}
return output;
}
public void run() {
this.setOutputData(execute(getInputData());
}
public ExecutionCallback getCallback() {
return new ExecutionCallback() {
public void callback() {
System.out.println(
"The sale's wave task is completed.");
}
}
}
}
之所以要在Task中為InputData和OutputData定義get、set訪問器,是因為Runnable接口提供的run()方法不允許傳入參數和返回結果。而在TaskScheduler中又只接受Task對象,利用這些方法可以在Task中存儲和傳遞這些數據。
在DataAnalyzer類的定義中,不能忽略的另一個職責是Task對象的創建。雖然,DataAnalyzer的調用者也可以完成對Task對象的創建,并將創建好的對象傳遞給DataAnalyzer。然而,更好的做法是DataAnalyzer能夠根據傳入的InputData來決定創建哪一種Task對象。這樣可以減輕調用者的負擔。然而,一旦這樣定義,DataAnalyzer的職責就顯得混淆不清了。一方面它負責執行分析任務,另一方面又要承擔創建Task對象的職責。這意味著它既是Task對象的使用者,又是Task對象的創建者。此外,DataAnalyzer對象并沒有持有創建Task對象所必須的數據,違背了將數據與行為封裝在一起的原則。因此,我們應該將創建的職責委派給其他對象。還有什么對象比得上“工廠專家”更適宜做創建的工作呢?最簡單的做法是定義一個靜態工廠:
public static Task create(InputData input) {
Task task = null;
if (input.getType().equals(SALE_WAVE)) {
task = new SaleWaveTask();
task.setInputData(input);
}
return task;
}
}
為了更好地應對任務的變化,我們也可以引入配置文件來管理任務。Task對象的創建因配置信息的不同而變化。create()方法能夠讀取配置文件,然后根據配置信息來決定創建哪一種Task對象。
數據的分析任務事實上是按照一定的步驟來完成的。這些步驟可能會采取不同的順序或組合方式來執行。每一種執行步驟就是一個算法,完成數據的收集、篩選、計算、分析和存儲。Task對象負責將這些步驟整合起來,并封裝到接口中,將這些算法實現以及執行算法的順序隱藏起來。為此,我們可以定義一個實現了Task接口的抽象類,統一完成整合分析步驟的工作。
protected AnalyticResult result;
protected List<Algorithm> algos;
public void registerAlgorithm(Algorithm algo) {
algos.add(algo);
}
public OutputData execute(InputData input) {
AnalyticData data = new AnalyticData(input,null);
processAlgorithms(data);
afterExecute();
return data.getOutputData();
}
protected void processAlgorithms(AnalyticData data) {
for (Algorithm algo : algos) {
algo.process(data);
}
}
private void afterExecute() {
result.finished();
if (getCallback() != null) {
getCallback().callback();
}
}
public void run() {
this.setOutputData(execute(getInputData());
}
}
public class AnalyticData {
public AnalyticData(InputData input, OutputData output) {}
public InputData getInputData() {}
public void setInputData(InputData input) {}
public OutputData getOutputData() {}
public void setOutputData(OutputData output) {}
public void prepare() {}
public void complete() {}
}
任務的執行步驟被抽象為Algorithm接口,而AnalyticData則用于協調輸入數據和輸出數據,因為Algorithm對象在Task中形成了一條流水線,上一個Algorithm的輸出是下一個Algorithm的輸入。輸入數據和輸出數據形成了一種不可協調的共扼性,需要提供prepare()方法和complet()方法來完成二者之間的轉換。
public void process(AnalyticData data);
}
public abstract class AbstractAlgorithm implements Algorithm {
public void process(AnalyticData data) {
data.prepare();
process(data.getInputData(), data.getOutputData());
data.complete();
}
protected abstract void process(
InputData input, OutputData output);
}
我們發現Task與AnalyticData都重復定義了InputData與OutputData的get、set方法。在設計和實現時,必須避免這樣的重復代碼。我們發現,這樣的輸入輸出數據體現了數據池的概念,因此可以定義一個抽象的DataSink接口:
public InputData getInputData() {}
public void setInputData(InputData input) {}
public OutputData getOutputData() {}
public void setOutputData(OutputData output) {}
}
AnalyticData類實現了該接口,而Task則可以持有該對象。
public interface Task extends Runnable {
public void setDataSink(DataSink dataSink);
public OutputData execute(InputData input);
public void setResult(AnalyticResult result);
public ExecutionCallback getCallback();
public void setCallback(ExecutionCallback callback);
}
public abstract class AbstractTask implements Task {
protected DataSink dataSink;
protected AnalyticResult result;
protected List<Algorithm> algos;
public OutputData execute(InputData input) {
processAlgorithms(dataSink);
afterExecute();
return dataSink.getOutputData();
}
protected void processAlgorithms(AnalyticData data) {
for (Algorithm algo : algos) {
algo.process(data);
}
}
public void run() {
execute(dataSink.getInputData());
}
}
傳遞InputData對象的方式也發生了變化:
public static Task create(InputData input) {
Task task = null;
DataSink dataSink = new AnalyticData(input, null);
if (input.getType().equals(SALE_WAVE)) {
task = new SaleWaveTask();
task.setDataSink(dataSink);
}
return task;
}
}
通過對分析器職責的分析,我們引入了DataAnalyzer、TaskScheduler、Task以及Algorithm等對象。這些對象的協作順序如下圖所示:
DataAnalyzer相當于分析器的外觀,它總攬全局,管理著各種對象之間的協作,共同實現分析工作。TaskScheduler是任務的調度器,負責啟動任務和結束任務,而它主要的職責則是封裝了對多線程的處理,用以完成任務的異步調用。如果未來需求需要強化任務的調度模式,例如增加任務隊列,以調度和管理多個任務的執行,則可以修改TaskScheduler而不影響它的調用者。Task對象體現了任務的獨立性,同時又利用抽象統一了任務的執行方式,有利于任務的擴展。Algorithm則完成對任務的切分,將任務步驟單獨封裝,有利于各種算法的重用。這些對象的協作以一種層層委派的方式,實現了職責的分離,避免了“集權式”的對象。不同的職責可以分別演化,又能很好地協作,共同完成數據分析的整體職責。