10

很长一段时间以来,我一直在研究一个应用程序。由于编程只是一种爱好,这个项目已经花费了太长时间,但这不是重点。我现在正处于每个“问题”都变得非常难以解决的地步。而且我正在考虑重构代码,但这会导致“完全”重写。

让我解释一下这个问题,以及我目前是如何解决的。基本上我有数据,我让事情发生在这些数据上(好吧,我描述了每个程序,不是吗?)。会发生什么:

数据 -> 要求查看器显示 -> 查看器根据实际数据显示数据 查看器返回用户输入 -> 数据 -> 要求“执行者”执行它 -> 新数据

在此处输入图像描述

现在这曾经工作得很好,我最初在想“嘿,我可能会通过 qt 或 windows 更改命令提示符 - 甚至可以使用外部 (C#) 并简单地调用这个程序”。

然而,随着程序的发展,它变得越来越令人厌烦。最重要的是,数据以不同的方式显示,具体取决于数据的内容以及 - 更重要的是 - 它所在的位置。所以我回到树并添加了某种方式来“跟踪”父行是什么”。然后一般查看器将搜索最具体的实际小部件。它使用具有 [location; widget] 值的列表,并找到最佳匹配位置。

问题在更新新“数据”时开始 - 我必须检查所有资产 - 查看器,保护程序等。更新检查机制给了我很多错误..诸如“嘿为什么它显示错误的小部件现在又来了?”。

现在我可以完全交换这个了。而不是调用通用查看器的树数据结构。我会使用面向对象的“内部”树功能。这些节点将是子节点(并且当需要新的查看器或保存机制时,会形成一个新的子节点)。

这将消除困难的检查机制,在那里我检查树中的位置。然而,它可能会打开另一罐蠕虫。我想对此发表一些评论?我是否应该将查看器完全分开 - 难以检查数据?或者新方法更好,但它将数据和执行组合到一个节点中。(因此,如果我想从 qt 更改为 cli/C#,这几乎是不可能的)

在此处输入图像描述

我到底应该追求什么方法?还有什么我可以做的吗?为了使查看器保持独立,但又避免必须检查以查看应显示的小部件?

编辑,只是为了展示一些“代码”以及我的程序是如何工作的。不确定这是否有什么好处,正如我已经说过的那样,它已经变成了一堆方法论。

它旨在将几个“游戏制作者项目”合并在一起(因为 GM:studio 奇怪地缺少该功能)。Gamemaker 项目文件只是一组 xml 文件。(主 xml 文件仅包含指向其他 xml 文件的链接,以及每个资源(对象、精灵、声音、房间等)的 xml 文件)。然而,有一些“怪癖”使得无法使用诸如 boost 属性树或 qt 之类的东西进行阅读:1) 属性/子节点的顺序在文件的某些部分非常重要。2) 空白经常被忽略,但在其他方面,保留它非常重要。

话虽如此,节点也有很多点完全相同。就像背景可以有<width>200</width>,房间也可以有。然而,对于用户来说,他所谈论的宽度非常重要。

无论如何,因此“通用查看器”(AskGUIFn)具有以下类型定义来处理此问题:

    typedef int (AskGUIFn::*MemberFn)(const GMProject::pTree& tOut, const GMProject::pTree& tIn, int) const;
    typedef std::vector<std::pair<boost::regex, MemberFn> > DisplaySubMap_Ty;
    typedef std::map<RESOURCE_TYPES, std::pair<DisplaySubMap_Ty, MemberFn> > DisplayMap_Ty;

其中“GMProject::pTree”是一个树节点,RESOURCE_TYPES 是一个常量,用于跟踪我目前的资源类型(精灵、对象等)。这里的“memberFn”只是加载小部件的东西。(虽然 AskGUIFn 当然不是唯一的通用查看器,但只有在其他“自动”-覆盖、跳过、重命名-处理程序失败时才打开这个查看器)。

现在展示如何初始化这些映射(命名空间“MW”中的所有内容都是一个 qt 小部件):

AskGUIFn::DisplayMap_Ty AskGUIFn::DisplayFunctionMap_INIT() {
    DisplayMap_Ty t;
        DisplaySubMap_Ty tmp;

        tmp.push_back(std::pair<boost::regex, AskGUIFn::MemberFn> (boost::regex("^instances "), &AskGUIFn::ExecuteFn<MW::RoomInstanceDialog>));
        tmp.push_back(std::pair<boost::regex, AskGUIFn::MemberFn> (boost::regex("^code $"), &AskGUIFn::ExecuteFn<MW::RoomStringDialog>));
        tmp.push_back(std::pair<boost::regex, AskGUIFn::MemberFn> (boost::regex("^(isometric|persistent|showcolour|enableViews|clearViewBackground) $"), &AskGUIFn::ExecuteFn<MW::ResourceBoolDialog>));
        //etc etc etc
    t[RT_ROOM] = std::pair<DisplaySubMap_Ty, MemberFn> (tmp, &AskGUIFn::ExecuteFn<MW::RoomStdDialog>);

        tmp.clear();
        //repeat above
    t[RT_SPRITE] = std::pair<DisplaySubMap_Ty, MemberFn>(tmp, &AskGUIFn::ExecuteFn<MW::RoomStdDialog>);
    //for each resource type.

然后,当树数据结构告诉一般查看器它希望显示时,查看器执行以下功能:

AskGUIFn::MemberFn AskGUIFn::FindFirstMatch() const {
    auto map_loc(DisplayFunctionMap.find(res_type));
    if (map_loc != DisplayFunctionMap.end()) {
        std::string stack(CallStackSerialize());
        for (auto iter(map_loc->second.first.begin()); iter != map_loc->second.first.end(); ++iter) {
            if (boost::regex_search(stack, iter->first)) {
                return iter->second;
            }
        }
        return map_loc->second.second;
    }

    return BackupScreen;
}

这就是问题开始变得坦率的地方。CallStackSerialize()函数依赖于调用堆栈。但是,call_stack 存储在“处理程序”中。我将它存储在那里是因为一切都从处理程序开始。我不确定我应该将这个“call_stack”存储在哪里。引入另一个跟踪正在发生的事情的对象?我尝试了将父节点与节点本身一起存储的路线。(防止需要调用堆栈)。然而,这并不像我希望的那样顺利:每个节点只是有一个包含其子节点的向量。所以使用指针指向父注释是不可能的......(PS:也许我应该在另一个问题中对此进行修改......)

4

4 回答 4

2

将这种复杂的位置检查机制从查看器中重构/重写为专用类是有意义的,因此您可以在不影响程序其余部分的情况下改进您的解决方案。让我们称之为NodeToWidgetMap

架构
似乎您正朝着模型-视图-控制器架构迈进,这是 IMO 的一件好事。您的树结构及其节点是模型,其中查看器和“小部件”是视图,而根据节点选择小部件的逻辑将是控制器的一部分。

主要问题仍然是何时以及如何为给定节点 N 选择小部件 w N以及如何存储此选择。

NodeToWidgetMap:何时选择
如果您可以假设 w N在其生命周期内即使节点被移动也不会改变,您可以在创建节点时正确选择它。否则,您将需要知道位置(或通过 XML 的路径),因此,在请求节点时要找到它的父节点。

查找父节点
我的解决方案是存储指向而不是节点实例本身的指针,可能使用boost::shared_ptr. 这有缺点,例如复制节点会迫使您实现自己的复制构造函数,该构造函数使用递归来创建子树的深层副本。(但移动不会影响子节点。)

存在替代方案,例如每当接触父节点各自的祖父向量时保持子节点为最新。或者您可以定义一个Node::findParentOf(node)函数,知道某些节点只能(或经常)作为某些节点的子节点找到。这很粗鲁,但对于小树来说效果相当好,只是不能很好地扩展。

NodeToWidgetMap:如何选择试着在一张纸上
写下如何选择 w N的规则,也许只是部分地。然后尝试将这些规则翻译成 C++。这在代码方面可能会稍长一些,但更容易理解和维护。

您当前的方法是使用正则表达式来匹配 XML 路径(堆栈)。

我的想法是创建一个查找图,其边缘由 XML 元素名称标记,其节点指示应使用哪个小部件。这样,您的 XML 路径(堆栈)描述了通过图表的路线。那么问题就变成了是否显式地建模一个图,或者是否可以使用一组函数调用来镜像这个图。

NodeToWidgetMap:存储选择
的位置 将唯一的数字 id 关联到每个节点,使用从节点 id 到 NodeToWidgetMap 内的小部件的映射记录小部件选择。

重写与重构
如果您重写,您可能会获得与现有框架(如 Qt)绑定的良好杠杆作用,以便专注于您的程序而不是重写轮子。将编写良好的程序从一个框架移植到另一个框架比围绕每个平台的特性进行抽象更容易。Qt 是一个很好的框架,用于获得经验和对 MVC 架构的良好理解。

完全重写让你有机会重新思考一切,但意味着你有从头开始的风险,并且在很长一段时间内都没有新版本。谁知道你是否有足够的时间来完成?如果您选择重构现有结构,您将逐步改进它,在每一步之后都有一个可用的版本。但是,停留在旧的思维方式中的风险很小,因为重写几乎迫使你重新思考一切。所以这两种方法都有其优点,如果你喜欢编程,我会重写。更多的编程,更多的快乐。

于 2012-11-23T09:37:16.703 回答
1

欢迎来到编程世界!
您描述的是应用程序的典型生命周期,从一个小的简单应用程序开始,然后它获得越来越多的功能,直到它不再可维护。你无法想象我在最后一个崩溃阶段看到了多少项目!
你需要重构吗?你当然知道!每时每刻!您需要重写所有内容吗?不太确定。
事实上,好的解决方案是按周期工作:你设计你需要编码的东西,你编码它,你需要更多功能,你设计这个新功能,你重构代码以便你可以集成新代码,等等。如果你不要这样做,那么您将达到重写然后重构成本更低的地步。获取这本书:重构 - Martin Fowler。如果你喜欢它,那么就得到这个:重构到模式。

于 2012-11-16T14:36:07.893 回答
0

我建议购买一份 Robert Martins “C# 中的敏捷原则、模式和实践”的副本,他回顾了一些非常实用的案例研究,展示了如何克服此类维护问题。

于 2012-11-22T00:36:20.580 回答
0

如前所述Pedro NF,Martin Fowler “重构”是熟悉它的好地方。

于 2012-11-21T23:49:08.333 回答