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

Architecture: Modifying the model in different ways

本文关键字:方式 修改 模型 架构      更新时间:2023-10-16

问题说明

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

class MyModelItem
{
public:
    enum ItemState
    {
        State1,
        State2
    };
    QString text() const;
    ItemState state() const;
private:
    QString _text;
    ItemState _state;
}

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

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

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

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

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

需求

对于这个历史上成长的、过于复杂的类,目标是将它的所有不同关注点分离到不同的类中,只留下核心数据成员。

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

这些特殊类中的每一个都可以包含业务逻辑的实际具体实现('text'的setter可以做类似的事情,"如果要设置的文本以某个子字符串开始,并且状态等于'State1',则将其设置为'State2'")。

解决方案的第一部分

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

悬而未决的问题

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

  • 关注点分离,这是开始的目标被否定了。所有的业务逻辑代码仍然被"倾倒"到糟糕的模型中。
  • 当被应用程序的其他部分调用时,这个验证必须看起来不同。实现不同的验证设置方法是目前的做法,这有一种不好的代码味道。

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

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

我问,因为我所知道的模式都不能完全解决我的问题,我觉得我在这里错过了一些东西…

非常感谢你的想法!

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

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

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

建议:

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

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

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

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

此外,编译器应该尽可能地帮助检测错误。如果现在要弄清楚应该调用哪些方法需要大量的工作和挫折,那么如果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的好处是它可以形成多个库/类的接口。拆分之后,业务规则都在几个facade中,但是您可以进一步重构它们:

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。

祝你项目顺利。

如果我正确理解了你的问题,那么我还不会决定选择哪种设计模式吗?我想我以前见过几次这样的代码,在我看来,主要的问题总是在变化之上的变化之上的变化。班级已经失去了最初的目的,现在服务于多种目的,这些目的都没有明确的定义和设置。其结果是一个大类(或一个大数据库,意大利面条式代码等),这似乎是必不可少的,但却是维护的噩梦。

大班是进程失控的征兆。这是您可以看到它发生的地方,但我的猜测是,当这个类被恢复后,许多其他类将首先被重新设计。如果我是正确的,那么也有很多数据损坏,因为在很多情况下是数据的定义不清楚。

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

我会给你一个不同的角度来看待你的情况。请注意,为了简单起见,解释是用我自己的话写的。然而,这里提到的术语来自企业应用程序体系结构模式。

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

活动记录:业务实体可以CRUD自己,并可以管理与自身相关的业务逻辑

活动记录中包含的业务逻辑增加了,并且变得难以管理。这是活动记录中非常典型的情况。这就是您必须从活动记录模式切换到Data Mapper模式的地方。

Data Mapper:管理映射(通常在实体和它转换的数据之间)的机制(通常是一个类)。它当活动记录的映射关系存在时,开始存在他们需要被分到单独的班级。映射成为一个独立的逻辑。

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

如果MyModelItem参与了继承,考虑为你想以不同方式映射的每个具体类创建一个抽象的Data Mapper和具体的Data Mapper。

关于如何实现它的几个注意事项:

  • 让实体意识到映射器
  • 映射器是实体的查找器,所以应用程序总是从映射器开始。
  • 实体应该公开其上自然存在的功能。
  • 实体使用(抽象的或具体的)映射器来做具体的事情。
通常,您必须在不考虑数据的情况下对应用程序建模。然后,设计映射器来管理对象到数据的转换,反之亦然。 现在关于验证

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

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