12

问题陈述

我有一个模型类,看起来像(非常简化;为清楚起见,省略了一些成员和许多许多方法):

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

    QString text() const;

    ItemState state() const;

private:
    QString _text;

    ItemState _state;
}

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

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

问题是,这个类多年来一直在增长,现在有几千行代码;它已成为如何违反单一责任原则的典型例子。

它具有直接设置“文本”、“状态”等的方法(在反序列化之后)以及从 UI 中设置它们的相同方法集,这些方法具有更新“lastChangedDate”和“lastChangedUser”等副作用. 有些方法或方法组的存在甚至不止两次,每个人做的事情基本相同,但略有不同。

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

要求

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

理想情况下,我更喜欢这样的解决方案,其中MyModelItem对象只有const用于访问数据的成员,并且只能使用特殊类进行修改。

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

解决方案的第一部分

对于加载和存储由许多MyModelItem对象和更多对象组成的整个模型,访问者模式看起来是一个很有前途的解决方案。我可以为不同的文件格式或数据库模式实现几个访问者类,并有一个saveandload方法MyModelItem,每个都接受这样的访问者对象。

开放式问题

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

  • 最初的目标是关注点分离被否定了。所有的业务逻辑代码仍然被“倾倒”到糟糕的模型中。
  • 当应用程序的其他部分调用此验证时,该验证的外观必须有所不同。实现不同的验证设置方法是现在的方式,这有一种不好的代码味道。

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

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

我在问,因为我所知道的模式都不能完全解决我的问题,而且我觉得我在这里遗漏了一些东西......

非常感谢您的想法!

4

4 回答 4

6

简单的策略模式对我来说似乎是最好的策略。

我从你的陈述中了解到:

  1. 模型是可变的。
  2. 突变可能通过不同的来源发生。(即不同的类)
  3. 该模型必须验证每个突变工作。
  4. 根据工作的来源,验证过程会有所不同。
  5. 模型忽略了来源和过程。它主要关心的是它正在建模的对象的状态。

提议:

  1. Source成为以某种方式改变模型的类。它可能是反序列化器、UI、导入器等。
  2. 验证器成为一个接口/超类,它包含验证的基本逻辑。它可以有如下方法:validateText(String), validateState(ItemState)...
  3. 每个Source 都有一个 验证器。该验证器可能是基本验证器的一个实例,也可能继承和覆盖它的一些方法。
  4. 每个验证器 都有一个模型的引用。
  5. 源首先设置自己的验证器,然后进行变异尝试。

现在,

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

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

于 2013-06-05T17:51:31.143 回答
3

尽管您没有明确说明,但重构数千行代码是一项艰巨的任务,我认为一些增量过程比全有或全无的过程更可取。

此外,编译器应尽可能帮助检测错误。如果现在要弄清楚应该调用哪些方法需要大量的工作和挫折,那么如果 API 已经统一起来,那就更糟了。

因此,我建议使用Facade 模式,主要是因为这个原因:

用一个设计良好的 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 的好处是它可以形成多个库/类的接口。拆分后,业务规则都在几个 Facades 中,但您可以进一步重构它们:

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

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

并且 GUI 代码不必更改一点。

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

(我知道我对编译器的压力很大,但根据我的经验,一旦所有东西都具有相同的名称,并且通过一层或多层间接工作,找出实际出错的地点和时间会变得很痛苦。)

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

祝你的项目好运。

于 2013-06-11T21:19:37.897 回答
2

如果我正确理解您的问题,那么我是否还不能决定选择哪种设计模式。我认为我之前已经多次看到过这样的代码,在我看来,主要问题始终是变化是建立在变化之上的。该类失去了最初的目的,现在服务于多个目的,这些目的都没有明确定义和设置。结果是一个大类(或一个大数据库、意大利面条代码等),这似乎是必不可少的,但却是维护的噩梦。

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

我的建议是回到你的客户那里,讨论业务流程,重新组织应用程序的项目管理,并尝试找出应用程序是否仍然很好地服务于业务流程。可能不是——我在不同的组织中多次遇到这种情况。如果业务流程被理解并且数据模型被转换为符合新的数据模型,那么您是否可以用新的设计替换应用程序,这样更容易创建。现在存在的大类,不必再重组了,因为它存在的理由已经没有了。它要花钱,但现在维护也要花钱。重新设计的一个很好的迹象是是否不再实现新功能,因为它变得过于昂贵或执行起来容易出错。

于 2013-06-11T23:13:03.703 回答
1

我会尝试给你一个不同的视角来看待你所面临的情况。请注意,为简单起见,解释都是用我自己的话写的。但是,提到的术语来自企业应用程序架构模式。

您正在设计应用程序的业务逻辑。所以,MyModelItem一定是某种商业实体。我会说这是Active Record你的。

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

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

数据映射器:管理映射的机制(通常是一个类)(通常在实体和它转换的数据之间)。当 Active Record 的映射关注点非常成熟以至于需要将它们放入单独的类中时,它就开始存在了。映射本身就成为一种逻辑。

因此,我们来到了显而易见的解决方案:为MyModelItem实体创建一个数据映射器。简化实体,使其不处理自身的映射。将映射管理迁移到 Data Mapper。

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

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

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

通常,您必须在不考虑数据的情况下对应用程序进行建模。然后,设计映射器来管理从对象到数据的转换,反之亦然。

现在关于验证

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

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

于 2013-06-12T23:03:01.600 回答