架构:以不同方式修改模型

时间:2013-06-05 09:34:48

标签: c++ qt oop design-patterns refactoring

问题陈述

我有一个类似于(简化的模型类;为清晰起见,省略了一些成员和许多方法):

class MyModelItem
{
public:
    enum ItemState
    {
        State1,
        State2
    };

    QString text() const;

    ItemState state() const;

private:
    QString _text;

    ItemState _state;
}

它是应用程序的核心元素,用于代码的许多不同部分:

  • 将其序列化/反序列化为各种文件格式
  • 可以写入数据库或从数据库中读取
  • 可以通过'import'更新,它可以读取文件并将更改应用于当前加载的内存模型
  • 用户可以通过各种GUI功能进行更新

问题是,这个课程已经发展多年,现在有几千行代码;它已经成为如何违反Single responsibility principle的主要例子。

它有直接设置'text','state'等的方法(反序列化后)和用于在UI中设置它们的同一组方法,这些方法有更新'lastChangedDate'和'的副作用lastChangedUser'等等。一些方法或方法组甚至存在两次以上,其中每个方法基本上都做同样的事情,但略有不同。

在开发应用程序的新部分时,您很可能使用了操纵MyModelItem的五种不同方法的错误,这使得它非常耗时且令人沮丧。

要求

鉴于这个历史悠久且过于复杂的类,目标是将其所有不同的关注点分成不同的类,只留下核心数据成员。

理想情况下,我更倾向于一种解决方案,MyModelItem对象只有const个成员才能访问数据,只能使用特殊类进行修改。

这些特殊类中的每一个都可以包含业务逻辑的实际具体实现(如果要设置的文本以某个子字符串开头并且状态等于,则“文本”的设置器可以执行类似的操作'State1',将其设置为'State2'“)。

解决方案的第一部分

对于加载和存储整个模型(包括许多MyModelItem个对象等),Visitor pattern看起来像是一个很有前景的解决方案。我可以为不同的文件格式或数据库模式实现多个访问者类,并在save中使用loadMyModelItem方法,每个方法都接受这样的访问者对象。

打开问题

当用户输入特定文本时,我想验证该输入。如果输入来自应用程序的另一部分,则必须进行相同的验证,这意味着我无法将验证移动到UI中(在任何情况下,仅UI验证通常是个坏主意)。但如果验证发生在MyModelItem本身,我又有两个问题:

  • 以开始为目标的关注点分离被否定了。所有业务逻辑代码仍被“转储”到穷人模型中。
  • 当应用程序的其他部分调用时,此验证必须具有不同的外观。实现不同的验证设置器方法就是它现在如何完成,它具有糟糕的代码味道。

现在很清楚,验证必须在UI和模型之外移动到某种控制器(在MVC意义上)类或类集合中。然后,这些应该用它的数据装饰/访问/等实际的哑模型类。

哪种软件设计模式最适合所描述的情况,以允许以不同的方式修改我的班级实例?

我在问,因为我所知道的模式都没有完全解决我的问题而且我觉得我在这里错过了一些东西......

非常感谢你的想法!

4 个答案:

答案 0 :(得分:6)

普通strategy模式对我来说似乎是最好的策略。

我从你的陈述中理解的是:

  1. 模型是可变的。
  2. 突变可能通过不同的来源发生。 (即不同的班级)
  3. 模型必须验证每次突变工作。
  4. 根据努力的来源,验证流程会有所不同。
  5. 模型忽略了源和流程。它的主要关注点是它正在建模的对象状态。
  6. 提案:

    1. 来源成为以某种方式改变模型的类。它可能是反序列化器,UI,进口商等。
    2. 验证器成为一个接口/超类,它拥有验证的基本逻辑。它可以有类似的方法:validateText(String), validateState(ItemState) ...
    3. 每个来源 都有一个 验证器。该验证器可以是base-validator的一个实例,也可以继承并覆盖它的一些方法。
    4. 每个验证器 都有对模型引用。
    5. 源首先设置自己的验证器然后进行变异尝试。
    6. 现在,

      Source1                   Model                  Validator
         |     setText("aaa")     |                        |
         |----------------------->|    validateText("aaa") |
         |                        |----------------------->|
         |                        |                        |
         |                        |       setState(2)      |
         |          true          |<-----------------------|
         |<-----------------------|                        |
      

      不同验证器的行为可能不同。

答案 1 :(得分:3)

虽然你没有明确说明,但是重构成千上万行代码是一项艰巨的任务,我认为有些增量过程比全有或全无的过程更受欢迎。

此外,编译器应尽可能地帮助检测错误。如果现在要确定应该调用哪些方法需要大量工作和挫折,如果API已经统一,那将更加糟糕。

因此,我建议使用Facade pattern,主要是出于这个原因:

  

使用一个设计良好的API(根据任务需要)包装设计糟糕的API集合

因为这基本上就是你所拥有的:一个类中的API集合,需要分成不同的组。每个小组都会拥有自己的Facade,并拥有自己的电话。所以当前的MyModelItem,多年来所有精心设计的不同方法调用:

...
void setText(String s);
void setTextGUI(String s); // different name
void setText(int handler, String s); // overloading
void setTextAsUnmentionedSideEffect(int state);
...

变为:

class FacadeInternal {
    setText(String s);
}
class FacadeGUI {
    setTextGUI(String s);
}
class FacadeImport {
    setText(int handler, String s);
}
class FacadeSideEffects {
    setTextAsUnmentionedSideEffect(int state);
}

如果我们将MyModelItem中的当前成员移除到MyModelItemData,那么我们得到:

class MyModelItem {
    MyModelItemData data;

    FacadeGUI& getFacade(GUI client) { return FacadeGUI::getInstance(data); }
    FacadeImport& getFacade(Importer client) { return FacadeImport::getInstance(data); }
}

GUI::setText(MyModelItem& item, String s) {
    //item.setTextGUI(s);
    item.getFacade(this).setTextGUI(s);
}

当然,这里存在实施变体。它同样可能是:

GUI::setText(MyModelItem& item, String s) {
    myFacade.setTextGUI(item, s);
}

这更依赖于对内存,对象创建,并发等的限制。重点是到目前为止,它都是直截了当(我不会说搜索和替换),编译器帮助每一个抓住错误的方法。

Facade的优点在于它可以形成多个库/类的接口。在拆分之后,业务规则都在几个Facade中,但您可以进一步重构它们:

class FacadeGUI {
    MyModelItemData data;
    GUIValidator validator;
    GUIDependentData guiData;

    setTextGUI(String s) {
        if (validator.validate(data, s)) {
            guiData.update(withSomething)
            data.setText(s);
        }
    }
}

并且不需要改变GUI代码。

毕竟,您可以选择规范化Facades,以便它们都具有相同的方法名称。但是,没有必要,为了清楚起见,保持名称不同可能更好。无论如何,编译器将再次帮助验证任何重构。

(我知道我强调编译器位很多,但根据我的经验,一旦所有东西都具有相同的名称,并通过一个或多个间接层工作,找出实际发生的地方和时间变得很痛苦错了。)

无论如何,我就是这样做的,因为它允许以受控的方式相当快速地分割大块代码,而不必过多考虑。它为进一步调整提供了一个很好的垫脚石。我想在某些时候MyModelItem类应该重命名为MyModelItemMediator。

祝你的项目好运。

答案 2 :(得分:2)

如果我理解你的问题,那么我还不会决定选择哪种设计模式。我认为我之前已经多次看过这样的代码,而我的观点中的主要问题始终是变更建立在变更的基础上。 失去的课程是最初的目的,现在服务于多种目的,这些目的都没有明确定义和设定。结果是一个大班(或大型数据库,意大利面条代码等),这似乎是不可或缺的,但却是维护的噩梦。

大班是一个失控的过程的症状。这是你可以看到它发生的地方,但我的猜测是,当这个类被恢复时,很多其他类将是第一个重新设计的。如果我是正确的,那么还有很多数据损坏,因为在很多情况下数据的定义不清楚。

我的建议是回到您的客户,谈论业务流程,重新组织应用程序的项目管理,并尝试找出应用程序是否仍然很好地服务于业务流程。可能不是 - 我在不同的组织中多次遇到过这种情况。 如果理解业务流程并且数据模型按照新数据模型进行转换,那么您是否可以使用新设计替换应用程序,这样更容易创建。现在存在的大班,不再需要重组,因为它的存在理由已经消失。 这需要花钱,但现在的维护也需要花钱。重新设计的一个很好的迹象是,如果新功能不再实现,因为它已经变得太昂贵或者易于执行。

答案 3 :(得分:1)

我会尽量让你对你所处的情况有不同的看法。请注意,为了简单起见,解释用我自己的语言写成。但是,提到的术语来自企业应用程序体系结构模式。

您正在设计应用程序的业务逻辑。因此,MyModelItem必须是某种商业实体。我会说这是你的Active Record

  

Active Record :可以自行CRUD并且可以管理的业务实体   与自身相关的业务逻辑。

Active Record中包含的业务逻辑已经增加,并且变得难以管理。这是Active Records的非常典型的情况。这是您必须从Active Record模式切换到Data Mapper模式的地方。

  

数据映射器:管理映射的机制(通常是一个类)(通常在实体和它之间转换的数据之间)。它   当Active Record的映射问题是这样时,它就会启动   成熟,他们需要被分配到单独的班级。映射本身就是一种逻辑。

因此,我们在这里找到了明显的解决方案:为MyModelItem实体创建数据映射器。简化实体,使其不处理自身的映射。将映射管理迁移到Data Mapper。

如果MyModelItem参与继承,请考虑为要以不同方式映射的每个具体类创建一个抽象的数据映射器和具体的数据映射器。

关于如何实施它的几点说明:

  • 让实体知道映射器。
  • Mapper是实体的查找器,因此应用程序始终从映射器开始。
  • 实体应该公开可以在其上找到的自然功能。
  • 实体利用(抽象或具体)映射器来做具体的事情。

通常,您必须建模您的应用程序而不考虑数据。然后,设计mapper来管理从对象到数据和副verca的转换。

现在关于验证

如果验证在所有情况下都相同,那么在实体中实现它,因为这听起来很自然。在大多数情况下,这种方法就足够了。

如果验证不同并依赖于某些东西,那就抽象出某些东西并通过抽象调用验证。一种方法(如果它取决于继承)将验证放在映射器中,或者将它放在与映射器相同的对象族中,由公共抽象工厂创建。