在GIX4項目的開發過程中,遇到一個比較複雜的資料結構。複雜,是因為它有許多限制條件。我的工作是在現有系統中,添加新的功能,並在過程中重構部分舊代碼。
約束及需求
以下約束是系統中已經存在的必要的約束,不可繞開這些約束而進行代碼的開發。
1.項目中,有許多的實體類,都含有一種多叉樹的關係和邏輯。
2.這些實體的樹型關係,在運行時,只有鍵的關係,而沒有對應的實體參考關聯性。
由於GIX4是資料分析軟體,資料量比較大。建立關係需要的時間比較久,所以伺服器端只負責給資料。這樣用戶端得到的資料,只是一個簡單的對象集合。
3.實體集合所有的更改對象位置只能使用一個特定的操作來實現排序:void Move(Object Item, Int32 index)。
這個約束產生的主要是原因是:一:使用了CSLA作為實現分布式應用的架構,所有實體集合,都需要繼承BusinessListBase。而對這個集合中的實體進行操作,經常會引起該實體的狀態的改變;二:目前的OpenExpressApp架構中,要求實體直接綁定到展示層,而不能對它進行轉換,如使用“ViewModel”。當然,大量的資料處理,也要求了最好不要每次都對這些資料進行轉換。
4.業務上要求,實體是可以進行排序操作的。(以下稱為“邏輯序號”和“邏輯排序”。)這些序號將會持久化到資料庫中。
5.集合中的對象,應該按照“邏輯序號”進行排序。(以下稱為“物理序號”和“物理排序”)
6.樹的每一個結點的孩子結點集合,也應該是按照“邏輯序號”進行物理排序。
7.以上的操作,全部在OpenExpressApp架構中實現,而非應用程式層。
原有代碼
一、樹的結構的定義,已經在老系統中定義並被廣泛使用。屬於固化因素,不可修改。定義如下:
/// <summary>/// 樹形節點/// </summary>public interface ITreeNode{ /// <summary> /// 所有的孩子節點 /// </summary> IList<ITreeNode> ChildrenNodes { get; } /// <summary> /// 這個節點對應的父級節點 /// </summary> ITreeNode ParentNode { get; set; } /// <summary> /// 當前節點的鍵 /// </summary> object Id { get; } /// <summary> /// 父節點的鍵 /// </summary> object Pid { get; }}
這個介面表示的資料結構是樹的結點,但是它受到上文中第2點約束的限制:當得到一個這個介面的執行個體時,它的Pid有值並指示出父節點的ID值,但是同時ParentNode卻可能因為沒有引用到實體,而為null。同樣的,雖然這個節點有許多孩子節點,但是ChildrenNodes得到的集合卻有可能是空集合。
二、實體集合對象繼承自GBusinessListBase<T,C>泛型集合,對它的元素的操作只有一個:Move(C item,int index)。
介面定義
在原有代碼的基礎上,我先增加了以下幾個介面,目的在於先把複雜的概念清晰化:
ISimpleMovableCollection.cs
/// <summary>/// 一個有簡單Move操作的集合/// </summary>internal interface ISimpleMovableCollection : ICollection{ /// <summary> /// 把oldIndex上對應位置的元素移動到newIndex上去。 /// 目的位置上的和中間的所有元素,都向oldIndex的方向移動一個位置。 /// </summary> /// <param name="oldIndex"></param> /// <param name="newIndex"></param> void Move(int oldIndex, int newIndex); /// <summary> /// 把item移動到newIndex上去。 /// 目的位置上的和中間的所有元素,都向oldIndex的方向移動一個位置。 /// </summary> /// <param name="item">本集合中的某一個元素</param> /// <param name="newIndex"></param> void Move(object item, int newIndex);}internal interface ISimpleMovableCollection<T> : ISimpleMovableCollection{ /// <summary> /// 把item移動到newIndex上去。 /// 目的位置上的和中間的所有元素,都向oldIndex的方向移動一個位置。 /// </summary> /// <param name="item"></param> /// <param name="newIndex"></param> void Move(T item, int newIndex);}
介面中的Move方法的定義,主要是來自原系統中的方法:GBusinessListBase<T,C>.Move()。 ISimpleMovableCollection 這個集合不同於下面列出的其它集合,它並不繼承自IList。這是因為它除了Move以外,沒有任何別的排序操作。而如果繼承自IList,則同時會擁有Insert,Remove等方法。這裡需要注意的是,雖然IList介面有IsReadOnly屬性來判斷是否是一個唯讀集合,但是如果這個值為false,而Move操作卻可以執行的話,邏輯上是不對的。所以該集合直接繼承自ICollection。
泛型的ISimpleMovableCollection同樣也沒有實現ICollection<T>,原因也是因為泛型的ICollection<T>也有更改集合的操作。
另外,我在這裡定義的這些集合,都是一個泛型和一個非泛型配合。這是因為代碼的實現是在OpenExpressApp架構中,而在架構中實體類的操作有時候是針對泛型實體,有時候卻針對非泛型實體。所以這裡只好也把非泛型版本也一起定義了。
IOrderedObject.cs
/// <summary>/// 有邏輯排序號的對象/// </summary>public interface IOrderedObject{ /// <summary> /// gs 邏輯排序號 /// LogicIndex /// </summary> int OrderNo { get; set; } //event EventHandler OrderNoChanged;}
IOrderedObject 表示一個可以被邏輯排序的實體,可以直接設定它的邏輯排序號。這個序號也是最後會被持久化到資料庫的值。這樣在下次從資料庫中取出時,可簡單的根據邏輯號進行物理排序,減少了時間消耗。
IOrderedObjectCollection.cs
/// <summary>/// 按排序號OrderNo進行邏輯排序的對象集合/// </summary>/// <typeparam name="T"></typeparam>public interface IOrderedObjectCollection : IList{ /// <summary> /// g 下一個新的對象,可以使用這個排序號 /// </summary> int NextOrderNo { get; } /// <summary> /// 把指定的節點上/下移 /// </summary> /// <param name="node"></param> /// <param name="isUp"></param> void MoveNode(IOrderedObject node, bool isUp); /// <summary> /// 按照OrderNo進行排序 /// </summary> void SortByOrderNo(); /// <summary> /// 按照物理位置把所有的OrderNo設定好。 /// </summary> void SetOrderNoByIndex();}public interface IOrderedObjectCollection<T> : IOrderedObjectCollection, IList<T> where T : IOrderedObject{ }
IOrderedObjectCollection<T>表示一IOrderedObject的集合。它不但負責維護邏輯序號,也負責維護物理位置。需要注意的是,這個集合的定義,與樹的操作是沒有任何關係的。
ITreeNodeCollection.cs
/// <summary>/// 一般的遍曆類型/// </summary>public enum TreeNodeTravelType{ /// <summary> /// 深度優先 /// </summary> DepthFirst, /// <summary> /// 廣度優先 /// </summary> ScopeFirst}
/// <summary>/// TreeNode的集合/// /// 裡面的元素是貧血的樹節點(ITreeNode:雖然有pid,卻不一定有ParentNode對象。也就是說不一定有實體參考關聯性……)/// </summary>public interface ITreeNodeCollection : IList{ /// <summary> /// 是深度排序還是廣度排序 /// </summary> TreeNodeTravelType SortType { get; set; } /// <summary> /// 是否集合中的TreeNode已經按照關係進行排序 /// </summary> bool Sorted { get; } /// <summary> /// 是否集合中的Node都已經按照Pid關聯了ParenNode和ChildrenNodes /// </summary> bool ObjectRelationsRuilt { get; } /// <summary> /// 重建立立ParenNode和ChildrenNodes關聯 /// </summary> void RebuildObjectRelations(); /// <summary> /// 保證建立了ParenNode和ChildrenNodes關聯 /// </summary> void EnsureObjectRelations(); /// <summary> /// 按照SortType進行排序 /// </summary> void SortByTree(); /// <summary> /// 保證已經排序 /// </summary> void EnsureSorted(); /// <summary> /// 把指定的節點升/降級 /// </summary> /// <param name="node"></param> /// <param name="isUp"></param> void ChangeNodeLevel(ITreeNode node, bool isUp); /// <summary> /// 以指定的節點為位置標準,添加一個新的節點。 /// </summary> /// <param name="targetNode">作為位置標準的節點</param> /// <param name="insertAsChild">新的節點是否是targetNode的孩子節點。(如果不是,就算兄弟/同級節點。)</param> /// <param name="createAsChild"> /// 如果是添加兄弟節點,true表示放在targetNode的前面,false則表示放在後面。 /// 如果是添加孩子節點,true表示新的節點作為第一個孩子,false表示作為第後一個績點 /// </param> /// <returns></returns> ITreeNode CreateNode(ITreeNode targetNode, bool createAsChild, bool beforeFirst); /// <summary> /// 把指定的節點和它所有的子節點都加入集合中。 /// </summary> /// <param name="node"></param> void AddNode(ITreeNode node, bool recursivlyAddChildren); /// <summary> /// 在列表中尋找前一個兄弟節點 /// </summary> /// <param name="node"></param> /// <returns></returns> ITreeNode FindPrevSibling(ITreeNode node); /// <summary> /// 在列表中尋找後一個兄弟節點 /// </summary> /// <param name="node"></param> /// <returns></returns> ITreeNode FindNextSibling(ITreeNode node); /// <summary> /// 尋找所有根節點(沒有父親的節點) /// </summary> /// <returns></returns> ITreeNode[] FindRoots();
}public interface ITreeNodeCollection<T> : ITreeNodeCollection, IList<T> where T : ITreeNode{}
ITreeNodeCollection<T>集合是ITreeNode的集合。類似IOrderedObjectCollection<T>,這裡也與IOrderedObject沒任何關係。
這個集合中的每一個ITreeNode,都可以在這個集合中找到它的所有的關聯節點。在集合裡面尋找,是因為ITreeNode.ParentNode和ITreeNode.ChildrenNodes並不一定會有實體引用。 看到方法FindRoots,大家應該想到,這個集合中並不一定只包含一棵樹。
IOrderedTreeNodeCollection.cs
/// <summary>/// 一個元素集合/// /// 整合兩套不同的模式:ITreeNodeCollection和IOrderedObjectCollection/// </summary>public interface IOrderedTreeNodeCollection : ITreeNodeCollection, IOrderedObjectCollection, IList{ /// <summary> /// 按照樹的順序重新給OrderNo賦值 /// 這裡對每個根節點使用深度遍曆設定OrderNo。 /// </summary> void SetOrderNoByTree(); /// <summary> /// 保證 邏輯Index、物理Index 相等。 /// 並且和樹的遞迴順序是一樣的 /// </summary> void EnsureIndices();}public interface IOrderedTreeNodeCollection<T> : IOrderedTreeNodeCollection, ITreeNodeCollection<T>, IOrderedObjectCollection<T>, IList<T> where T : IOrderedObject, ITreeNode{}
IOrderedTreeNodeCollection<T>才是系統中要應用到的集合。集合中的元素一定是同時實現了IOrderedObject和ITreeNode介面。所以我們可以根據樹來設計邏輯序號:SetOrderNoByTree。也有了操作:EnsureIndices。
部分實現代碼
首先,有樹的遍曆操作,自然先實現這個。這裡使用兩個靜態方法對已經建立關係的樹進行遍曆,一個深度遍曆使用棧,一個廣度遍曆使用隊列。代碼就不貼了,太占空間。
下面這個GBusinessTreeListBase<T,T>類,繼承自原來的GBusinessListBase<T, C>類。考慮到繼承層次過深對效能的影響,所以在這個類中直接全部實現了前面的所有介面。但是卻是邏輯分離的。先看代碼:
注意到,前面的介面,繼承層次是這樣的:
但是GBusinessTreeListBase<T,T>類在實現全部介面時,邏輯上,可以簡單理解為以下形式:
其中,箭頭方向,即是邏輯中的繼承方向,也是實現中的依賴方向。在GBusinessTreeListBase<T,T>中每一個介面實現的Region中,都可以看成是一個簡單的類。只不過是把它們整合到了一個類裡。如下:
ISimpleMovableCollection<C> 實現:
IOrderedObjectCollection<C> 實現:
ITreeNodeCollection<C> 實現:
IOrderedTreeNodeCollection<C> 實現:
後話
細心的讀者可能會發現:IOrderedObjectCollection<T>實現中的的MoveNode方法調用了IOrderedTreeNodeCollection<T>實現中的MoveNode方法;本應該出來在ITreeNodeCollection<T>實現中CreateNode方法,卻被放在了IOrderedObjectCollection<T>實現。
誰能說出這是為什麼嗎?這還算是單向依賴的嗎?:)