Архитектура: изменение модели по-разному

Описание проблемы

У меня есть класс модели, который выглядит примерно так (чрезвычайно упрощен, некоторые элементы и многие, многие методы опущены для ясности):

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

    QString text() const;

    ItemState state() const;

private:
    QString _text;

    ItemState _state;
}

Это основной элемент приложения и используется во многих разных частях кода:

  • Сериализован/десериализован в/из различных форматов файлов
  • Его можно записать в базу данных или прочитать из нее
  • Он может быть обновлен с помощью "импорта", который считывает файл и применяет изменения к текущей загруженной модели в памяти.
  • Он может быть обновлен пользователем через различные функции графического интерфейса.

Проблема в том, что этот класс вырос за эти годы и теперь имеет несколько тысяч строк кода; это стало ярким примером того, как нарушить принцип Одиночной ответственности.

Он имеет методы для установки "текст", "состояние" и т.д. непосредственно (после десериализации) и один и тот же набор методов для их установки из пользовательского интерфейса, который имеет побочные эффекты, такие как обновление "lastChangedDate" и "lastChangedDate", lastChangedUser и т.д. Некоторые методы или группы методов существуют даже более двух раз, причем каждый из них делает в основном одно и то же, но немного отличается.

При разработке новых частей приложения вы, вероятно, используете неправильные пять различных способов манипулирования MyModelItem, что делает его чрезвычайно трудоемким и расстраивающим.

Требования

Учитывая этот исторически сложившийся и чрезмерно сложный класс, цель состоит в том, чтобы разделить все различные проблемы его на разные классы, оставив в нем только основные элементы данных.

В идеале я бы предпочел решение, в котором объект MyModelItem имеет ничего, кроме const для доступа к данным, а модификации могут быть сделаны только с помощью специальных классов.

Каждый из этих специальных классов мог бы затем содержать фактическую конкретную реализацию бизнес-логики (сеттер "текста" мог бы сделать что-то вроде "если текст, который будет установлен, начинается с определенной подстроки, а состояние равно" State1 ", установите его в" State2").

Первая часть решения

Для загрузки и хранения всей модели, состоящей из многих объектов MyModelItem и некоторых других, шаблон посетителя выглядит многообещающим решением. Я мог бы реализовать несколько классов посетителей для разных форматов файлов или схем баз данных и иметь метод save и load в MyModelItem, который принимает каждый объект-посетитель каждый.

Открытый вопрос

Когда пользователь вводит определенный текст, я хочу проверить его. Та же проверка должна быть сделана, если вход поступает из другой части приложения, что означает, что я не могу перенести проверку в пользовательский интерфейс (в любом случае проверка только на основе UI часто является плохой идеей). Но если проверка выполняется в самом MyModelItem, у меня снова есть две проблемы:

  • Разделение проблем, с которых была начата цель, отрицается. Весь код бизнес-логики по-прежнему "сбрасывается" в плохую модель.
  • При вызове другими частями приложения эта проверка должна выглядеть по-другому. Реализация различных методов проверки-настройки - это то, как это делается прямо сейчас, у которого плохой запах кода.

Теперь ясно, что валидация должна быть перемещена за пределы UI и модели, в какой-то контроллер (в смысле MVC) или в набор классов. Затем они должны украсить/посетить/etc фактический немой модельный класс с его данными.

Какой шаблон проектирования программного обеспечения лучше всего подходит для описанного случая, чтобы разрешить различные способы изменения экземпляров моего класса?

Я спрашиваю, потому что ни один из шаблонов, которые я знаю, полностью решает мою проблему, и я чувствую, что здесь что-то не хватает...

Большое спасибо за ваши идеи!

Ответ 1

Хотя вы не указываете это явно, рефакторинг тысяч строк кода является сложной задачей, и я предполагаю, что какой-то инкрементный процесс предпочтительнее всего или ничего.

Кроме того, компилятор должен максимально помогать обнаруживать ошибки. Если сейчас много работы и разочарования, чтобы выяснить, какие методы следует вызывать, это будет еще хуже, если API будет унифицирован.

Поэтому я бы предложил использовать "Фасад" , главным образом по этой причине:

обернуть плохо разработанную коллекцию API с помощью одного хорошо разработанного API (в соответствии с задачами)

Потому что это в основном то, что у вас есть: коллекция API в одном классе, которую нужно разделить на разные группы. Каждая группа получала свой собственный Фасад со своими собственными вызовами. Итак, текущий 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);
}

Это больше зависит от ограничений на память, создания объектов, concurrency и т.д. Дело в том, что до сих пор все прямо (я не буду говорить "поиск и замена" ), а компилятор помогает каждый шаг на пути к сбою ошибок.

Хорошая вещь в Facade заключается в том, что он может сформировать интерфейс для нескольких библиотек/классов. После разделения вещей бизнес-правила находятся на нескольких фасадах, но вы можете реорганизовать их дальше:

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

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

и код GUI не нужно менять один бит.

После всего этого вы можете выбрать нормализацию фасадов, чтобы все они имели одинаковые имена методов. Однако это не обязательно, и для ясности это может быть даже лучше, если бы названия были разными. Несмотря на это, еще раз компилятор поможет проверить любой рефакторинг.

(Я знаю, что сильно подчеркиваю компилятор, но, по моему опыту, когда все имеет одно и то же имя и работает через один или несколько уровней косвенности, становится больно узнать, где и когда что-то действительно происходит неправильно.)

Во всяком случае, так я бы это сделал, так как он позволяет довольно быстро распределять большие куски кода, контролируя его, не задумываясь слишком много. Это обеспечивает хороший шаг для дальнейшей настройки. Я предполагаю, что в какой-то момент класс MyModelItem должен быть переименован в MyModelItemMediator.

Удачи вам в вашем проекте.

Ответ 2

Обычный strategy шаблон кажется лучшей стратегией для меня.

Из вашего утверждения я понимаю:

  • Модель изменена.
  • мутация может произойти с помощью другого источника. (т.е. разные классы)
  • модель должна проверять каждое усилие мутации.
  • В зависимости от источника усилия проверка процесса отличается.
  • модель не обращает внимания на источник и процесс. его главной задачей является состояние объекта, который он моделирует.

Предложение:

  • пусть Источник - это классы, которые каким-то образом мутируют модель. это могут быть десериализаторы, пользовательский интерфейс, импортеры и т.д.
  • пусть валидатор - это интерфейс/суперкласс, который содержит основную логику проверки. он может иметь такие методы, как: validateText(String), validateState(ItemState)...
  • Каждый Источник имеет валидатор. Этот валидатор может быть экземпляром базового валидатора или может наследовать и переопределять некоторые из его методов.
  • Каждый валидатор имеет ссылку на модель.
  • Источник сначала устанавливает свой собственный валидатор, затем выполняет попытку мутации.

Теперь,

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

поведение разных валидаторов может быть различным.

Ответ 3

Если я правильно понимаю вашу проблему, тогда я бы не решил, какой шаблон дизайна выбрать. Я думаю, что я видел такой код несколько раз раньше, и основная проблема в моей точке зрения всегда заключалась в том, что изменения основывались на изменениях, связанных с изменениями. Класс потерял свою первоначальную цель и теперь выполняет несколько целей, которые все четко не определены и не установлены. Результатом является большой класс (или большая база данных, код спагетти и т.д.), Который, кажется, незаменим, но является кошмаром для обслуживания.

Большой класс является симптомом процесса, который вышел из-под контроля. Здесь вы можете увидеть, как это происходит, но я предполагаю, что когда этот класс будет восстановлен, многие другие классы будут первыми, кто перепроектирует. Если я прав, то есть также много искажений данных, потому что во многих случаях определение данных неясно.

Мой совет будет возвращаться к вашему клиенту, говорить о бизнес-процессах, реорганизовать управление проектами приложения и попытаться выяснить, хорошо ли приложение все еще служит бизнес-процессу. Это может быть не так - я несколько раз был в этом виде ситуации в разных организациях. Если понимать бизнес-процесс и преобразовать модель данных в соответствие с новой моделью данных, то вы можете заменить приложение новым дизайном, который намного проще создать. Большой класс, который сейчас существует, больше не нуждается в реорганизации, потому что его причина существования исчезла. Это стоит денег, но обслуживание теперь стоит и денег. Хорошим показателем для редизайна является то, что новые функции больше не реализованы, потому что они стали слишком дорогими или подвержены ошибкам.

Ответ 4

Я постараюсь дать вам другую точку зрения на ситуацию, которая у вас есть. Обратите внимание, что объяснения написаны моими словами для простоты. Однако упомянутые термины относятся к шаблонам архитектуры корпоративного приложения.

Вы разрабатываете бизнес-логику приложения. Таким образом, MyModelItem должен быть своего рода бизнес-объектом. Я бы сказал, что у вас есть Active Record.

Активная запись: бизнес-объект, который может сам CRUD, и может управлять бизнес-логика, связанная с самим собой.

Бизнес-логика, содержащаяся в Active Record, увеличилась и стала трудно управлять. Эта очень типичная ситуация с Active Records. Здесь вы должны перейти от шаблона активной записи к шаблону Data Mapper.

Data Mapper: механизм (обычно класс), управляющий отображением (обычно между объектом и данными, которые он переводит с/на). Это начинается, когда проблемы с отображением Active Record что они должны быть помещены в отдельный класс. Сопоставление становится логикой само по себе.

Итак, мы пришли к очевидному решению: создадим Data Mapper для объекта MyModelItem. Упростите объект так, чтобы он не обрабатывал отображение самого себя. Перенесите управление сопоставлением в Data Mapper.

Если MyModelItem принимает участие в наследовании, подумайте о создании абстрактного Data Mapper и конкретных Data Mappers для каждого конкретного класса, который вы хотите сопоставить по-другому.

Несколько замечаний о том, как я его реализую:

  • Сделать сущность осведомленной о картере.
  • Mapper - это поисковик объекта, поэтому приложение всегда начинается с mapper.
  • Сущность должна раскрывать функциональность, которая естественна для нее.
  • И субъект использует (абстрактный или конкретный) сборщик для выполнения конкретных вещей.

В общем, вы должны моделировать свое приложение без учета данных. Затем, проектор mapper для управления преобразованиями из объектов в данные и вице verca.

Теперь о проверке

Если валидация одинакова во всех случаях, тогда реализуйте ее в сущности, как это кажется мне естественным. В большинстве случаев этого подхода достаточно.

Если проверка отличается и зависит от чего-то, отвлеките что-нибудь и вызовите проверку через абстракцию. Один из способов (если это зависит от наследования) заключается в том, чтобы поставить валидацию в mapper или иметь ее в том же семействе объектов, что и mapper, созданный общей темой Factory.