编程范式;想知道是否需要重写/重构

时间:2012-11-15 11:28:03

标签: c++ refactoring paradigms

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

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

  

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

enter image description here

现在这种方法运作得非常好,我原本以为“嘿,我可能会通过qt或Windows更改命令提示符 - 甚至可以使用外部(C#)并简单地调用此程序”。

然而随着节目的增长,它变得越来越无聊。最重要的是,数据以不同的方式显示,具体取决于数据是什么,更重要的是 - 它位于何处。所以我回到了树&添加了一些“跟踪”父线是什么“。然后普通观众将搜索最具体的实际小部件。 它使用有一个列表[location;小部件]值,并找到最佳匹配位置。

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

现在我可以完全交换它。而不是调用通用查看器的树数据结构。我会使用OO“内部”树功能。节点将是孩子(并且当需要新的观察者或保存机制时,形成新的孩子)。

这将删除困难的检查机制,我在其中检查树中的位置。然而,它可能会打开另外一堆蠕虫。 我想对此发表一些意见吗?我应该让观众完全分开 - 难以检查数据吗?或者新方法更好,但它结合了数据和执行到单个节点。 (所以,如果我想从qt改为cli / C#,那几乎是不可能的)

enter image description here

我最终应该采用什么方法?还有什么我可以做的吗?为了使观众保持独立,还要防止必须进行检查以查看应该显示哪个小部件?

编辑,只是为了显示一些“代码”以及我的程序是如何工作的。不确定这是否有任何好处,正如我所说的那样,它已成为一种非常集中的方法论。

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

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

无论如何,所以“general viewer”(AskGUIFn)有以下typedef来处理这个问题:

    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 个答案:

答案 0 :(得分:2)

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

<强>建筑
看起来你正朝着Model-View-Controller建筑迈进,这对IMO来说是一件好事。您的树结构及其节点是模型,其中查看器和“窗口小部件”是视图,并且根据节点选择窗口小部件的逻辑将是控制器的一部分。

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

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

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

存在替代方案,例如每当触摸祖父传真的父节点时保持子节点上升。或者您可以定义一个Node::findParentOf(node)函数,知道某些节点只能(或经常)被发现为某些节点的子节点。这是粗野的,但对于小树来说效果会相当好,只是不能很好地扩展。

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

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

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

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

重写与重构
如果你重写,你可能会很好地利用Qt等现有框架,以便专注于你的程序,而不是重写轮子。将编写良好的程序从框架移植到另一个框架可能比将每个平台的特性抽象出来更容易。 Qt是一个很好的框架,用于获得经验和对MVC架构的良好理解。

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

答案 1 :(得分:1)

欢迎来到编程世界! 您所描述的是应用程序的典型生命周期,从一个简单的小应用程序开始,然后它将获得越来越多的功能,直到它不再可维护。你无法想象我在最后一个崩溃阶段看过多少个项目! 你需要重构吗?你当然可以!每时每刻!你需要重写一切吗?不太确定。
实际上,良好的解决方案是循环工作:您设计需要编码的代码,编写代码,需要更多功能,设计此新功能,重构代码以便集成新代码等。如果您不要这样做,那么你将达到重写然后重构的成本。得到这本书:重构 - 马丁福勒。如果你喜欢它,那么得到这个:重构为模式。

答案 2 :(得分:0)

正如Pedro NF所说,Martin Fowler "Refactoring"是熟悉它的好地方。

答案 3 :(得分:0)

我建议购买罗伯特·马丁斯的“C#敏捷原则,模式和实践”的副本。他介绍了一些非常实用的案例研究,展示了如何克服这样的维护问题。