1. 程式人生 > >嘗試用AOP解決OOP和設計模式問題(1)

嘗試用AOP解決OOP和設計模式問題(1)

2005.11.14  來自:Csdn dotnet blog 專家群  jgtm

在展開問題之前我們首先設定一個例子,在這個示例中我將使用盡可能簡單的邏輯實現所有功能需求,這將更突出我們所要解決的核心問題。例子是一個簡單計算器類:

public class Calculator
{
public int Add(int x, int y) { return x + y; }
}

測試程式碼如下(你可以使用NUnit與我們一起完成對這個例子的研究):

public void Test()
{
Calculator calculator = new Calculator();

Assert.IsNotNull(calculator);
Assert.AreEqual(5, calculator.Add(2,3));
}

這個類再簡單不過了,不過若你將它想象為一個可能更復雜的業務處理類的時候,你將面臨除了核心功能實現之外的更多處理細節,比如說:許可權控制、審計日誌、效能監測、緩衝處理、事務環境等等。為簡單起見,我們首先為該類增加記錄日誌的功能,該功能要求將對每個方法的呼叫和處理結果輸出到Console中,如下:

public class Calculator
{
public int Add(int x, int y)
{
Console.Write("Add({0},{1})", x, y);
int result = x + y;
Console.WriteLine(" = {0}", result);
return result;
}
}

再簡單不過了,對吧?現在我們需要為該方法實現效能監測,如下:

public class Calculator
{
public int Add(int x, int y)
{
Console.Write("Add({0},{1})", x, y);
DateTime startTime = PreciseTimer.Now;
int result = x + y;
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Console.Write(" [{0}] ", elapsedTime);

Console.WriteLine(" = {0}", result);
return result;
}
}

這裡暫時不要去管PreciseTimer這個類,它只是一個用來計時的工具而已,跟我們的核心問題沒什麼大關係。此時你已經感覺到,雖然我們實現了所需的功能,但是在一個方法中堆疊了處理各類事宜(Aspect! I heard you! :)的不同程式碼。雖然在這個簡單例子中不會感覺有什麼不爽,但是請你想象一下如果我們將為該類新增第二個方法時會發生什麼事情:

public class Calculator
{
public int Add(int x, int y)
{
Console.Write("Add({0},{1})", x, y);
DateTime startTime = PreciseTimer.Now;
int result = x + y;
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Console.Write(" [{0}] ", elapsedTime);
Console.WriteLine(" = {0}", result);
return result;
}

public int Subtract(int x, int y)
{
Console.Write("Subtract({0},{1})", x, y);
DateTime startTime = PreciseTimer.Now;
int result = x - y;
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Console.Write(" [{0}] ", elapsedTime);

Console.WriteLine(" = {0}", result);
return result;
}

}

此時的單元測試程式碼補充如下:

public void Test()
{
ICalculator calculator = new Calculator();

Assert.IsNotNull(calculator);
Assert.AreEqual(5, calculator.Add(2,3));
Assert.AreEqual(5, calculator.Subtract(8,3));
}

在兩個方法中已經明顯出現重複程式碼了,這可不是一個好的smell——想想一下如果我們的計算器有10個方法呢?如果我們還有類似於計算器類的另外數十個類呢?如果我們還有更多的方法級功能要實現呢(許可權控制、事務管理……)?在企業級應用開發中,這可是一個經常會遇的問題。為清楚起見,我們將問題分解成兩部分,首要的問題是程式碼職責混淆,其次則是同樣的程式碼邏輯反覆多次——這些問題都將導致開發管理、程式碼編寫與維護的各種困難。

為了解決程式碼職責混淆的問題,我們首先考慮的是使用DECORATOR設計模式將不同職責的程式碼分解到若干個DECORATOR中,並繼而使用一種物件構造模式(如ABSTRACT FACTORYFACTORY METHODBUILDER等等,都可以)在執行時將DECORATOR與核心實現組裝起來。為了實現DECORATOR模式,我們首先需要使用Extract Interface的重構技術提取介面,如下:

public interface ICalculator
{
int Add(int x, int y);
int Subtract(int x, int y);
}


public class Calculator: ICalculator
{
public int Add(int x, int y)
{
Console.Write("Add({0},{1})", x, y);
DateTime startTime = PreciseTimer.Now;
int result = x + y;
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Console.Write(" [{0}] ", elapsedTime);
Console.WriteLine(" = {0}", result);
return result;
}

public int Subtract(int x, int y)
{
Console.Write("Subtract({0},{1})", x, y);
DateTime startTime = PreciseTimer.Now;
int result = x - y;
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Console.Write(" [{0}] ", elapsedTime);
Console.WriteLine(" = {0}", result);
return result;
}
}

測試程式碼相應修改如下:

public void Test()
{
ICalculator calculator = new Calculator();

Assert.IsNotNull(calculator);
Assert.AreEqual(5, calculator.Add(2,3));
Assert.AreEqual(5, calculator.Subtract(8,3));
}

執行測試程式碼可以確信現在Calculator的行為未受到任何影響(這是正確使用重構技術的重要標誌噢),那麼我們開始剝離負責效能監測職責的程式碼並將其作為一個DECORATOR實現:

public class Calculator: ICalculator
{
public int Add(int x, int y)
{
Console.Write("Add({0},{1})", x, y);
DateTime startTime = PreciseTimer.Now;
int result = x + y;
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Console.Write(" [{0}] ", elapsedTime);

Console.WriteLine(" = {0}", result);
return result;
}

public int Subtract(int x, int y)
{
Console.Write("Subtract({0},{1})", x, y);
DateTime startTime = PreciseTimer.Now;
int result = x - y;
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Console.Write(" [{0}] ", elapsedTime);

Console.WriteLine(" = {0}", result);
return result;
}
}

public class CalculatorTimer: ICalculator
{
public int Add(int x, int y)
{
DateTime startTime = PreciseTimer.Now;
int result =
Decoratee.Add(x, y);
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
return result;
}

public int Subtract(int x, int y)
{
DateTime startTime = PreciseTimer.Now;
int result =
Decoratee.Subtract(x, y);
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
return result;
}
}

可以看到,在負責效能監測(在這個簡單例子中我們還不如就叫它timer吧:)的DECORATOR中,程式碼只關注計時的功能,而將核心的工作交由decoratee(自造詞,別介意——caller/calleetester/testeeassigner/assigneebomber/bombee……:)去處理。這裡的decoratee也是一個ICalculator型別的介面引用,我們需要在構造decorator的時候為其設定好decoratee,這個工作將在物件工廠中實現。這裡我一次到位的為所有可能的DECORATOR實現一個基類,該類實現一個新的介面ICalculatorDecorator,在這個介面中我們宣告一個支援讀寫decoratee的語義,如下:

public interface ICalculatorDecorator
{
void InitDecorator(ICalculator decoratee);
ICalculator Decoratee { get; }
}

然後我們為所有的Decorator提供該介面的一個基本實現:

public abstract class CalculatorDecorator: ICalculatorDecorator
{
private ICalculator decoratee;


void ICalculatorDecorator.InitDecorator(ICalculator decoratee)
{
this.decoratee = decoratee;
}

// FIXED: to use implicit interface implementation instead explicit way
// so that derived classes could access this property. Thanks
greatqn!
ICalculator ICalculatorDecorator.Decoratee
public ICalculator Decoratee
{
get { return this.decoratee; }
}
}

再由此派生出之前的CalculatorTimer

public class CalculatorTimer: CalculatorDecorator, ICalculator
{
public int Add(int x, int y)
{
DateTime startTime = PreciseTimer.Now;
int result = Decoratee.Add(x, y);
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
return result;
}

public int Subtract(int x, int y)
{
DateTime startTime = PreciseTimer.Now;
int result = Decoratee.Subtract(x, y);
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
return result;
}
}

為了將這個負責效能監測職能的DECORATOR與其被裝飾者(decoratee)組合起來,我們需要一個物件工廠類(它同時可以將呼叫程式碼邏輯與物件建立邏輯解耦):

public class CalculatorFactory
{
private CalculatorFactory() {}

private static Type[] GetObjectGraph()
{
ArrayList objectGraphTypes = new ArrayList();
// Add decoratee as 1st type to be created

objectGraphTypes.Add(typeof(Calculator));

// and then add all decorators
objectGraphTypes.Add(typeof(CalculatorTimer));
return (Type[])objectGraphTypes.ToArray(typeof(Type));

}

public static ICalculator CreateInstance()
{
Type[] objectGraphTypes = GetObjectGraph();


ICalculator result = null;

foreach (Type calcType in objectGraphTypes)
{
ICalculator calcImpl = (ICalculator)Activator.CreateInstance(calcType);

if (calcImpl is ICalculatorDecorator)
{
((ICalculatorDecorator)calcImpl).InitDecorator(result);
}

result = calcImpl;
}

return result;

}
}

相應修改單元測試使其使用這個工廠的CreateInstance()方法(這實際是設計模式中所謂的FACTORY METHOD),如下:

public void Test()
{
ICalculator calculator = CalculatorFactory.CreateInstance();

Assert.IsNotNull(calculator);
Assert.AreEqual(5, calculator.Add(2,3));
Assert.AreEqual(5, calculator.Subtract(8,3));
}

在這個類裡面我們提供了一個靜態方法CreateInstance()用來返回ICalculator的一個實現。在這個實現邏輯中,我們首先構造一個物件組裝圖(object graph),其實就是一個型別列表,按照從內到外的順序依次新增若干ICalculatorICalculatorDecorator的實現型別。接下來,一個foreach迴圈按照構造列表依次建立每一個型別,並在建立後判斷新建立的型別是否是一個ICalculatorDecorator,如果是的話,我們將已然建立好的最後一個ICalculator實現(即result)作為decoratee賦予它以實現物件組合。迴圈完成時,result所引用的ICalculator實現就是最外面的一個decorator,而整個呼叫鏈其實是通過每一個DECORATOR類的方法中的Decoratee.XXX()來構成的。

有了這個改進的物件結構,我們可以很容易的新增新的DECORATOR為物件增加新的職責,如將之前的日誌記錄邏輯實現如下:

public class CalculatorLogger: CalculatorDecorator, ICalculator
{
public int Add(int x, int y)
{
Console.Write("Add({0}, {1})", x, y);
int result = Decoratee.Add(x, y);
Console.WriteLine(" = {0}", result);
return result;
}

public int Subtract(int x, int y)
{
Console.Write("Subtract({0}, {1})", x, y);
int result = Decoratee.Subtract(x, y);
Console.WriteLine(" = {0}", result);
return result;
}
}

接下來只要在物件工廠的CreateInstance()方法中將其加入物件構造圖中即可:

public class CalculatorFactory
{

private static Type[] GetObjectGraph()
{
ArrayList objectGraphTypes = new ArrayList();
// Add decoratee as 1st type to be created

objectGraphTypes.Add(typeof(Calculator));

// and then add all decorators
objectGraphTypes.Add(typeof(CalculatorTimer));
objectGraphTypes.Add(typeof(CalculatorLogger));return (Type[])objectGraphTypes.ToArray(typeof(Type));

}

public static ICalculator CreateInstance() …
}

不過,以我有限的設計經驗來看,讓所有的DECORATOR顯式去呼叫Decoratee的方法以形成物件關係鏈是繁瑣和易出錯的(不過也是效率最好的)。因為如果某一個方法實現沒有去呼叫Decoratee的相應方法的話,整個物件鏈將被切斷。同時我們還發現,隨著元件介面的日益複雜,介面中方法的數目越來越多,因此對於每一個新增的DECORATOR而言都需要實現所有這些介面方法,這對於一些並非施加於所有介面方法的DECORATOR而言就顯得有些累贅了(如僅對少數方法有意義的緩衝策略DECORATOR等等)。這些現象都提示我們可以使用TEMPLATE METHOD設計模式來為所有的DECORATOR提供一個統一的、靈活的基礎實現邏輯,如下:

public abstract class CalculatorDecorator: ICalculatorDecorator, ICalculator
{
private ICalculator decoratee;


void ICalculatorDecorator.InitDecorator(ICalculator decoratee)
{
this.decoratee = decoratee;
}

public ICalculator Decoratee
{
get { return this.decoratee; }
}

int ICalculator.Add(int x, int y)
{
DoPreAdd(ref x, ref y);
int result = decoratee.Add(x, y);
DoPostAdd(x, y, ref result);
return result;
}

int ICalculator.Subtract(int x, int y)
{
DoPreSubtract(ref x, ref y);
int result = decoratee.Subtract(x, y);
DoPostSubtract(x, y, ref result);
return result;
}

protected virtual void DoPreAdd(ref int x, ref int y) {}
protected virtual void DoPostAdd(int x, int y, ref result) {}

protected virtual void DoPreSubtract(ref int x, ref int y) {}
protected virtual void DePostSubtract(int x, int y, ref result) {}
}

在這個基於TEMPLATE METHODS設計模式的DECORATOR基類中,我們顯式的實現了ICalculator介面所有方法的模板邏輯,模板中可變的部分委託給若干protected virtual的模板方法實現,這些模板方法繼而可被派生類可選擇的override(因為是virtual的)。當然,如果你希望所有的DECORATOR一定要提供某一個環節的模板方法實現,你也可以使用protected abstract來修飾這個模板方法,這樣一來派生類就必須要實現這個方法才能夠通過編譯了。另外,雖然我們在基類上使用了顯式介面實現,但是派生類仍然可以重新提供相關介面的實現,這種靈活度足以滿足各種應用場合了(參見C#語言規範之13.4.113.4.4)。

還有一個問題需要解決,即DECORATOR之間的相互協作在目前的設計中是不可以完成的。為什麼要DECORATOR之間要協作?舉個簡單例子,如果我們需要在Logger中順便記錄方法執行的時間,而這個時間是由Timer來測定的話,我們就需要在Logger的實現中取得由Timer記錄下來的時長資訊(當然,這同時要求Logger位於Timer的外層,這樣當Timer返回後Logger才可能獲取到Timer記錄的資訊)。為了達到這個目的,需要為所有組成物件組合的物件例項引入一個公共的資訊容器(比如一個使用Hashtable實現的資料字典),不妨稱其為Context。我們希望所有的DECORATOR都可以取得這個共享的容器,因此首先我們擴充ICalculatorDecorator介面及其實現:

public interface ICalculatorDecorator
{
void InitDecorator(ICalculator decoratee, IDictionary context);
ICalculator Decoratee { get; }
IDictionary Context { get; }
}

public abstract class CalcuatorDecorator: ICalculatorDecorator
{
private ICalculator decoratee;
private IDictionary context;

public void InitDecorator(ICalculator decoratee, IDictionary context)
{
this.decoratee = decoratee;
this.context = context;
}

public ICalculator Decoratee
{
get { return this.decoratee; }
}

public IDictionary Context
{
get { return this.context; }
}

}

接下來,我們在物件工廠建立物件的時候為複合物件建立並設定context

public class CalculatorFactory
{

public static ICalculator CreateInstance()
{
Type[] objectGraphTypes = GetObjectGraph();


ICalculator result = null;
IDictionary context = new Hashtable();

foreach (Type calcType in objectGraphTypes)
{
ICalculator calcImpl = (ICalculator)Activator.CreateInstance(calcType);

if (calcImpl is ICalculatorDecorator)
{
((ICalculatorDecorator)calcImpl).InitDecorator(result, context);
}

result = calcImpl;
}

return result;

}
}

這樣,在DECORATOR(也即CalculatorDecorator的派生類)的內部就可以使用Context屬性訪問整個物件組合中各個DECORATOR的共享儲存環境:

public class CalculatorTimer: CalculatorDecorator, ICalculator
{
public int Add(int x, int y)
{
DateTime startTime = PreciseTimer.Now;
int result = Decoratee.Add(x, y);
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Context["Add.ElapsedTime"] = elapsedTime;
return result;
}

public int Subtract(int x, int y)
{
DateTime startTime = PreciseTimer.Now;
int result = Decoratee.Subtract(x, y);
TimeSpan elapsedTime = PreciseTimer.ElapsedSince(start);
Context["Subtract.ElapsedTime"] = elapsedTime;
return result;
}
}

public class CalculatorLogger: CalculatorDecorator, ICalculator
{
public int Add(int x, int y)
{
Console.Write("Add({0}, {1})", x, y);
int result = Decoratee.Add(x, y);
Console.WriteLine(" = {0} {1}", result, Context["Add.ElapsedTime"]);
return result;
}

public int Subtract(int x, int y)
{
Console.Write("Subtract({0}, {1})", x, y);
int result = Decoratee.Subtract(x, y);
Console.WriteLine(" = {0} {1}", result, Context["Subtract.ElapsedTime"]);
return result;
}
}

本例進展至此已經形成了一定的物件結構,讓我們小結一下吧。首先,我們提出了一個問題,即在企業系統開發中因為既要滿足功能性需求,也需要照顧很多不同方面的非功能性需求,這使得物件方法的實現變得越來越複雜、繁瑣、僵硬和難以維護(雖然從執行效率的角度看還是最優化的);接下來我們引入物件結構設計模式DECORATOR將物件必須實現的各方面程式碼分散到以介面為基礎的核心實現物件和若干修飾物件,並進而引入物件建立設計模式將這些物件的構造過程封裝起來,通過介面保持與呼叫程式碼的鬆散耦合;最後,我們對已經形成的物件結構進行小的修正與擴充,使其允許各個DECORATOR通過一個共享的Context相互通訊。

經過這個設計過程,我們最終形成了一個比起最初模型而言更易於擴充套件的、鬆散耦合的、較易維護的物件模型,然而我們也面臨一個問題:如果我們有很多這樣的業務元件都需要同樣的一些DECORATOR的話,我們仍然會面臨很多重複程式碼,而且這些程式碼在面向物件的範疇內是無法消除的。什麼意思?以Logger為例,如果給幾十個元件的數百個方法都去實現loggingDECORATOR,無疑仍然是個巨大的開發和維護工作。為什麼不可以通過面嚮物件的技術消除?因為我們希望消除的重複是在方法的層次上,而方法已經是物件內部封裝的範疇,很難為不同的型別的每個方法提供一個統一的實現(如果要有也應該在所有物件的一個公共基類中——Object?顯然進了死衚衕……)。那麼這個問題該如何解決呢(尤其是在.NET的環境中)?我將在本文的下篇中結合CLR中提供的相關機制探討在.NET環境下進行AOPAspect-Oriented Programming,面向方面的程式設計思想)編碼的相關技術,包括CLR中的TranspantProxy/RealProxyMarshalByRefObject/ContextBoundObjectContext Attribute/Context Property/IMessageSink等等。