Как разбить высокосвязную модель на разные классы - PullRequest
0 голосов
/ 20 июня 2020

У меня контейнер в котором проживает 8 реле. Теперь я хочу позволить клиенту включать / выключать разные реле. Пока все хорошо.

Одно из возможных решений:

container.tryTurnOn(1, true);
container.turnRandomOn(true); //turn relais random to on or not.

1 означает relais 1, а истинное означает, попробуйте включить его; false означало бы, попробуйте выключить его. Если бы я добавил туда еще несколько методов (изменение цвета, самоуничтожение или некоторые другие творческие вещи ^^), относящиеся к самому реле, а не к контейнеру, интерфейс контейнера вырос бы.

Поэтому я думаю, что хорошей идеей было бы иметь для каждого реле собственный класс, чтобы вы могли сделать что-то вроде этого

container.getRelais(1).tryTurnOn(true);
container.turnRandomOn(true); //this stay the same

Теперь у меня также есть другие правила: Допускается только включение 6 из 8 реле. (Задача контейнера обеспечить это).

Итак, в этом случае у меня есть несколько проблем:

Relais, который я получаю от getRelais, - anemi c , он содержит только состояние relais, но функциональность находится в контейнере, потому что только контейнер знает, разрешен tryTurnOn (true) или нет. (Или я должен поместить поведение в реле, получая доступ к другому реле через контейнер?) У меня есть вторая проблема: циклическая c зависимость: Relais отправляет wi sh включения на контейнер, поэтому он должен знать контейнер. Контейнер также должен знать свои дочерние элементы (relais), чтобы проверить, не было ли больше реле, чем 6. Также у меня здесь больше нет Tell-Dont-Ask , потому что контейнер запрашивает Relais для их состояния, и на основе результата он получает доступ к состоянию напрямую из реле и изменяет его.

  • Так есть ли другое решение?
  • Как исправить эту cycli c, инкапсуляцию, проблему Tell-Dont-Ask
  • Что я должен сделать, получив здесь OO-Aproach об этом

Одно расширение вопроса:

Возможно, я хочу добавить туда еще несколько правил:

  • Администратору разрешено включать 7 реле, обычному пользователю разрешено включать только 6 реле.
  • Если возможное солнце в моей модели светит ярче 80%, только 3 реле для обоих (администратор и обычный пользователь) могут включаться.
  • Если солнце светит ярче 95%, контроллер отключает все lais.

Как бы вы смоделировали их в ОО-стиле? Проблемная область выглядит очень связной, потому что все должны знать все. Где бы вы поместили поведение, вы бы разбили его на разные компоненты? Я не знаю, как это сделать в объектно-ориентированном стиле, мне кажется, что это все равно приводит к некоторому процедурному коду. Но именно поэтому я спрашиваю ^^

1 Ответ

1 голос
/ 21 июня 2020

В рамках этого ответа я буду говорить о лампах, потому что я считаю, что это легче визуализировать.

Включение и выключение, «говори-не-спрашивай» и растущий интерфейс.

Прежде всего, лампа не имеет права голоса по этому поводу. Точнее, мы можем позволить лампе выключиться, но лампа не узнает, можно ли ее включить.

Попытка включить и выключить модель в лампе приводит к лампа спрашивает другие лампы об их состоянии. И это просто не лучший дизайн. Даже если вы не верите в принцип «скажи-не-спрашивай», сделать потокобезопасным без глобальной блокировки такой дизайн был бы кошмаром. включено или выключено как внешнее c к лампе. У нас будет коллекция горящих ламп. Включение лампы добавляет ее в коллекцию. Выключение - это удаление из коллекции. Коллекция может иметь установленную максимальную емкость, и там у вас есть возможность говорить-не-спрашивать. Вы говорите коллекции добавить лампу, и она знает, можно ли и как это сделать. Вам не нужно спрашивать, сколько в нем предметов, и принимать решение на основании этого.

Вы можете решить, должны ли другие атрибуты быть внешними c. Например, вы можете сделать цвет атрибутом лампы. Или у вас может быть служба цвета, которая содержит словарь / карту от ламп до цветов.

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

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

Администратор

Коллекция не несет ответственности за то, чтобы узнать, является ли пользователь администратором.

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

Однако набор горящих ламп - это тем же. Таким образом, мы собираемся разделить этот класс на два: во-первых, класс хранилища без ограничений, а во-вторых, класс представления, который отвечает за проверки и является единственным способом доступа к классу хранилища. Теперь контроллер может запрашивать представление, настроенное с различной максимальной емкостью, в зависимости от того, обрабатываем ли мы запрос от администратора или нет.

Кто делает эти запросы? Вид, конечно. Должен быть какой-то пользовательский интерфейс, который будет использовать пользователь, который может быть администратором или нет. Кроме того, должна быть какая-то система управления сеансом, потому что пользователь должен аутентифицироваться, иначе… как бы у вас были роли?

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

Солнце

Оказывается, Солнце - актер. Солнце начнет операции. Когда яркость солнца изменится, нам, возможно, придется выключить лампы.

Независимо от того, как вы моделируете солнце, нам нужен солнечный адаптер, который будет обрабатывать изменение яркости солнца. И когда эта яркость станет больше порогового значения (95%), адаптер вызовет контроллер для выключения всех ламп.

Точно так же мы сможем запросить у адаптера последнее значение яркости. Я не уверен, что контроллеры должны это делать или маршрутизатор должен это делать. Несмотря на это… Это должно привести к тому, что при создании представления коллекции включенных ламп будет запрошена другая мощность.

Надеюсь, вы видите, как вы можете расширить эту идею для более сложных правил относится к Солнцу.

Композиция Root, Созидание и Разрушение

Нам не нужны синглтоны, и нам не нужны жестко запрограммированные лампы.

Будет композиция root. По умолчанию композиция root - это класс, содержащий точку входа в приложение. Хотя это может быть делегировано другому классу, когда ограничение не позволяет вам писать точку входа (например, вы делаете плагин для чего-то другого). Вы можете думать о композиции root как о своем клее .

Композиция root создаст экземпляры различных классов, необходимых для начального состояния приложения. И это можно сделать, вызвав фабрики. И свяжет эти экземпляры вместе.

Таким образом, композиция root создаст первые лампы и добавит их в контейнер, содержащий все лампы. И этот контейнер не одноэлементный, и лампы не имеют жесткого кода.

Композиция root также создаст коллекцию включенных ламп.

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

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

Это означает, что их может быть несколько. И нам может быть интересно создать несколько из них за одно выполнение для целей тестирования. Это также означает, что мы можем добавить лампу к нескольким из них. Это означает, что мы не должны думать о буквальном разрушении объекта. Но удаление из контейнера.

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

Здесь контейнер и коллекция полностью разделены, но связаны во время выполнения композицией root. Вот что я имею в виду, когда считаю композицию root вашим клеем.

Самоуничтожающиеся лампы, конечно, можно реализовать, вынув лампу из контейнера. Может, по таймеру? Я не знаю.

Разрушенная лампа - это лампа, которая все еще жива, но уже не находится в контейнере контекста, в котором она была создана.

Простота использования

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

Как указано здесь, на и выключение не является атрибутом ламп. Мы могли бы притвориться, что это так. Это означало бы ссылку на коллекцию ламп, которые горят в лампах. И это не только зависимость формы cycli c, но, что более важно, это означает, что лампа имеет доступ для выключения других ламп. Это не годится.

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

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

Таким образом, лампы не зависят от коллекции. Они просто не знают, что коллекция существует. Слушатели зависят от обоих, но не от слушателя. Точно так же фабрика будет зависеть от слушателя и класса лампы, но они не зависят от фабрики. И, наконец, от всего зависит состав root, а от состава root ничего не зависит. Здесь нет зависимости cycli c.

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

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

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

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

В нескольких контекстах

В зависимости от ваших требований вы можете рассмотреть следующее необязательное.

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

К сожалению, мы говорим Java. Итак, решение, которое я предлагаю, - запретить добавление лампы в несколько контейнеров и коллекций.

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

Подождите ... У этого контекста будет контейнер и коллекция ... контекст - это композиция root! Ах, но это означало бы, что у контекста есть контейнер, у которого есть контекст. И в контейнере есть лампы, у которых есть контекст, в котором есть контейнер. Зависимость Cycli c наносит ответный удар!

Передавать композицию root нельзя. Нам просто нужно его идентифицировать. И для этого подойдет простое значение id. Мы передаем id, сравниваем id, и все.

Дополнение к выводу

Допустим, вы хотите отрендерить лампы. Чтобы не усложнять, у вас будет пользовательский интерфейс, в котором есть виджет для каждой лампы, который должен быть ярким или темным в зависимости от того, включена лампа или нет. И это может также изменить цвет и другие вещи.

Я дам вам три решения.

  1. Этот вывод не предназначен для автоматического обновления. Пользователь может запросить, чтобы увидеть состояние ламп. Возможно, можно опросить его, чтобы обновить его. В этом случае будет контроллер, который обрабатывает запрос от пользователя, и этот контроллер будет иметь код, который считывает лампы и коллекцию ламп, которые горят, и создает объект ответа (который не имеет поведения) и передает этот объект ответа в пользовательский интерфейс. Затем пользовательский интерфейс должен представить объект ответа. См. Также «Просмотр модели» и «Презентатор».

  2. Мы хотим, чтобы вывод обновлялся автоматически. Но изменений немного, поэтому мы не хотим, чтобы пользовательский интерфейс работал часто. Вы можете разрешить слушателям перейти к коллекции на лампах. Затем композиция root будет внедрять слушателей, которые (предпочтительно асинхронно) будут вызывать UI для обновления своей модели представления.

  3. Мы хотим, чтобы вывод обновлялся автоматически. Изменения происходят постоянно. Мы заботимся о том, чтобы сделать это быстро. Попросите поток пользовательского интерфейса прочитать контейнер и коллекцию напрямую, один раз за кадр, и выполнить рендеринг на основе этого.

Не знаете, что использовать? Используйте переходник.

Дополнение по тестированию и одиночным тестам

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

Часто это означает, что тесты не должны иметь побочных эффектов (очевидно, что если у них нет побочных эффектов, они не могут влиять друг на друга). Что привело бы к насмешкам и т. Д. но давайте не будем go там.

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

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

Вместо этого мы не хотим, чтобы они были одноэлементными. Мы хотим, чтобы каждый тест создавал новый контейнер, лампы и т. Д. c ... Мы хотим, чтобы каждый синглтон происходил в отдельном контексте с отдельными объектами.

Дополнение к адаптеру и контроллеру

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

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

Что делать, если у вас есть внешняя система, которая требует обоих? Вы делаете адаптер, и этот адаптер может вызывать контроллеры.

Например. Мы делаем только текстовый пользовательский интерфейс, возможно, мы захотим заменить его в будущем каким-нибудь GUI, или, возможно, сделать веб-версию или что-то в этом роде. Выполните рефакторинг кода, чтобы все операции ввода-вывода пользовательского интерфейса были в одном месте. Извлеките это в класс с интерфейсом под вашим контролем. Это ваш адаптер пользовательского интерфейса. Затем вы можете реализовать тот же интерфейс по-другому (например, с GUI), и это позволит вам легко заменить пользовательский интерфейс. Это форма полиморфизма.

Конечно, пользовательский интерфейс должен позволять пользователю взаимодействовать с системой. Таким образом, этот адаптер пользовательского интерфейса должен иметь возможность вызывать контроллеры для выполнения различных операций. Включение и выключение ламп и т. Д. Контроллер должен беспокоиться о том, как это сделать.

Иногда вам нужно вставить что-то посередине, что предоставит другой контроллер адаптеру в зависимости от состояния системы (независимо от того, аутентифицирован пользователь или нет, если роль администратора, и т. д. c.) - Это маршрутизатор.

Это процедурно?

Пожалуй, да. И мы, вероятно, можем сказать то же самое о любом реальном OO-решении. Если присмотреться, это, в конечном счете, процедурное. Однако, что важно, мы будем говорить-не-спрашивать. Тем не менее, в любой паре геттер-сеттер (запрашивающая и не совсем инкапсуляционная пара) есть бастион процедурного кода, так что, возможно, есть место для улучшения. Нам нужно понять, что не каждый объект должен быть сущностью. Некоторые объекты являются службами и имеют только поведение, и внутри они будут очень процедурными. А некоторые другие объекты, к сожалению, являются ценностями и не имеют поведения. Для них мы, вероятно, не использовали бы объекты, если бы были на другой платформе (отличной от Java).

Обратите внимание, что значения необходимы. Вот почему у нас есть int, bool и т. Д.

Думайте о них как о маленьких компьютерах, передающих сообщения. Иногда сообщения слишком сложны для нативных типов. Затем мы делаем тип для этих сообщений. Это должно быть значение ... Вы знаете, как int, но более сложное ... Но Java - это способ, это была бы структура на некоторых других языках. Кроме того, не каждый компьютер в сети требуется состояние, это могут быть серверы, которые просто получают некоторый ввод, обрабатывают его и отправляют сообщение по линии.

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

Мы создали систему, которая сама по себе ведет себя более или менее как один большой объект. Контроллеры - это методы, модель предметной области - это состояние. В компьютере их может быть несколько. Фактически, если вы используете контроллеры, которые обмениваются данными по сети с другими системами, разработанными с аналогичной архитектурой, у вас может быть несколько компьютеров, работающих как один большой компьютер. Некоторые из них могут быть выделены государству, мы называем их серверами баз данных. Другие могут быть предназначены для обработки информации, они могут быть серверами приложений (или аналогичными). Это компьютеры с маленькими компьютерами внутри, с маленькими компьютерами внутри ...

Однако, как только вы достигнете нижних уровней, в какой-то момент вам понадобится некоторый процедурный код.

Давайте посмотрим на столбы: Абстракция, проверка, это делают контроллеры. Инкапсуляция, проверка, хотя мы переворачиваем ее с ног на голову с включением и выключением, это не может привести к недопустимому состоянию (мы хотим предотвратить включение ламп в нескольких контейнерах, чтобы сделать это правильно). Наследование, оно вам не нужно (ну, у нас есть наследование интерфейсов). Полиморфизм, проверка, все инъекции зависимостей.

Давайте посмотрим на принципы SOLID: единственная ответственность, проверка, на самом деле вы, кажется, жалуетесь Я слишком много ломаю, однако проблема с одним жирным контроллером в том, что он слишком много причин, чтобы измениться. Открыть-закрыть, проверить… более или менее, я не предлагал никаких точек расширения, кроме добавления нового контроллера или адаптера. Замена Лисков, проверьте, мы не нарушаем, хотя и не учебный пример, предлагаю заменить переходники. Принцип сегрегации интерфейсов, проверьте? хм… Я не рассматривал это, но хорошо, следуйте за ним, где это имеет смысл при реализации. Инверсия зависимостей, проверка, полностью, я говорю вам не жестко кодировать зависимости, вместо этого вводите их с композицией root, а также без синглтонов.

Дополнение к примеру замысловатой игрушки

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

Для начала предположим, что у автомобилей есть модели и цвета.

Должны ли мы создать класс для каждой модели? Возможно, если функциональные отличия одной модели от другой актуальны для системы. В противном случае мы можем создать атрибут для модели.

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

Что, если мы хотим поговорить о владельце автомобиля? Должно ли это быть атрибутом машины? Возможно. Однако класс автомобилей на нем не действует. Вам нужны данные с поведением. Право собственности c на автомобиль. Для базы данных мы можем поместить поле владельца, которое очень ориентировано на данные. Однако объектно-ориентированный дизайн предполагает, что у людей есть список автомобилей, которыми они владеют ... Подождите, вы можете указать машину как принадлежащую нескольким людям. Имеет ли это смысл в системе? Если несколько владельцев не имеют смысла, это недопустимое состояние. Ошибка инкапсуляции.

Мы можем справиться с этим, используя словарь, который сопоставляет автомобили с владельцами, поскольку автомобиль является ключом, у каждой машины есть только один владелец. Чтобы упростить получение списка автомобилей человека, мы создаем класс, который содержит словарь от автомобилей до владельцев, а также словарь от владельцев до списков автомобилей, и этот новый класс несет ответственность за поддержание инварианта, что владелец автомобиля имеет этот автомобиль в своем списке, и наоборот.

На самом деле, перемещение атрибутов в словари обычно является жизнеспособным. С недостатком обработки разрушения. Учитывая, что словарь, содержащий ссылку на объект, предотвращает сборку мусора. . Не нарушение инкапсуляции, а наоборот. Это противоречит всем методологиям объектно-ориентированного проектирования ... Тем не менее, это может быть лучшим решением, если у вас есть много взаимозависимых правил, требующих обхода традиционной инкапсуляции.

А как насчет того, включена машина или выкл? Это intrinsi c, верно? Поведение в машине. До тех пор, пока вы не дадите мне инвариант, который находится за пределами машины, например, требование, чтобы на ней могло быть только шесть машин. Инвариант, который нельзя сохранить внутри класса, предполагает, что нам нужен новый класс. Какой класс может естественным образом представлять инвариант множества объектов, ограниченных их количеством? Я знаю, что это коллекция (набор, список или вектор).

Теперь нам нужно, чтобы автомобиль взаимодействовал с коллекцией, и у нас есть зависимость cycli c. Этот ответ - моя попытка решить эту проблему.

* Между прочим, в предыдущей попытке этого ответа я собирался использовать полный Model-View-Controller.

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

- Перефразируя цитату, приписываемую Дэвиду Уиллеру.

...