重构一个繁琐的数据结构

简介:

  在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)。

image 

接口定义

    在原有代码的基础上,我先增加了以下几个接口,目的在于先把复杂的概念清晰化:

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。


部分实现代码

    首先,有树的遍历操作,自然先实现这个。这里使用两个静态方法对已经建立关系的树进行遍历,一个深度遍历使用栈,一个广度遍历使用队列。代码就不贴了,太占空间。

image

    下面这个GBusinessTreeListBase<T,T>类,继承自原来的GBusinessListBase<T, C>类。考虑到继承层次过深对性能的影响,所以在这个类中直接全部实现了前面的所有接口。但是却是逻辑分离的。先看代码:

image

    注意到,前面的接口,继承层次是这样的:

image

    但是GBusinessTreeListBase<T,T>类在实现全部接口时,逻辑上,可以简单理解为以下形式:

image

    其中,箭头方向,即是逻辑中的继承方向,也是实现中的依赖方向。在GBusinessTreeListBase<T,T>中每一个接口实现的Region中,都可以看成是一个简单的类。只不过是把它们整合到了一个类里。如下:

ISimpleMovableCollection<C> 实现:

image

IOrderedObjectCollection<C> 实现:

image

ITreeNodeCollection<C> 实现:

image

IOrderedTreeNodeCollection<C> 实现:

image

 


后话

    细心的读者可能会发现:IOrderedObjectCollection<T>实现中的的MoveNode方法调用了IOrderedTreeNodeCollection<T>实现中的MoveNode方法;本应该出来在ITreeNodeCollection<T>实现中CreateNode方法,却被放在了IOrderedObjectCollection<T>实现。

    谁能说出这是为什么吗?这还算是单向依赖的吗?:)


本文转自BloodyAngel博客园博客,原文链接:http://www.cnblogs.com/zgynhqf/archive/2009/12/02/1615450.html,如需转载请自行联系原作者

相关文章
|
10月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
862 9
|
10月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
217 59
|
3月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
47 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
8月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
332 77
|
7月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
154 11
|
7月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
8月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
241 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
8月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
132 9
|
8月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
186 7