Подтвердить что ты не робот

Как разрешить нарушения Закона Деметры?

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

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

class Discipline {}
class ProtocolKind 
{ 
   Discipline; 
}
class Protocol
{
   ProtocolKind;
   ProtocolMedication; //1..*
}
class ProtocolMedication
{
   Medicine;
}
class Medicine
{
   AdministrationRoute;
}
class AdministrationRoute {}

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

Я нашел следующие места, в которых у нас будет нарушение Закона Деметры:

Нарушения закона Деметры

Внутри BLL

Например, внутри бизнес-логики ProtocolMedication существуют правила, которые зависят от свойства AdministrationRoute.Soluble лекарства. Код станет

if (!Medicine.AdministrationRoute.Soluble)
{
   //validate constrains on fields
}

Внутри репозиториев

Метод, который будет перечислять все протоколы в определенной Дисциплине, будет записан как:

public IQueryable<Protocol> ListQueryable(Discipline discipline)
{
    return ListQueryable().Where(p => (p.Kind.Discipline.Id == discipline.Id)); // Entity Frameworks needs you to compare the Id...
}

Внутри пользовательского интерфейса

Мы используем ASP.NET(без MVC) для интерфейса нашей системы, на мой взгляд, этот слой в настоящее время имеет самые серьезные нарушения. Конфигурация привязки gridview (столбец, который должен отображать Дисциплину протокола, должен связываться с Kind.Discipline.Name), которые являются строками, , поэтому ошибки времени компиляции.

<asp:TemplateField HeaderText="Discipline" SortExpression="Kind.Discipline.Name">
   <ItemTemplate>
      <%# Eval("Kind.Discipline.Name")%>
   </ItemTemplate>
</asp:TemplateField>

Итак, я думаю, что реальный вопрос может быть, когда было бы лучше смотреть на него скорее как предложение Деметры, и что можно сделать, чтобы разрешить нарушения Закона Деметры?

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

4b9b3361

Ответ 1

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

В Википедии есть правило как

Более формально, Закон Деметры для функции требует, чтобы метод М объект O может ссылаться только на методы следующих видов объекты:

  • O сам
  • Параметры M
  • любые объекты, созданные/созданные в M
  • O объекты прямого компонента

Если у вас есть метод, который принимает "кухню" в качестве параметра, Деметра говорит, что вы не можете проверять компоненты кухни, а не только проверять непосредственные компоненты.

Написание кучи функций только для того, чтобы удовлетворить Закон Деметры, подобный этому

Kitchen.GetCeilingColour()

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

Если метод за пределами кухни передается на кухне, по строгому Деметру он не может вызывать никаких методов по результату GetCeilingColour() на нем.

Но в любом случае задача состоит в том, чтобы удалить зависимость от структуры, а не перемещать представление структуры из последовательности цепочечных методов в имя метода. Создание таких методов, как MoveTheLeftHindLegForward() в классе Dog, ничего не делает для выполнения Demeter. Вместо этого вызовите dog.walk() и дайте собаке обработать собственные ноги.

Например, что, если требования меняются, и мне также потребуется высота потолка?

Я бы реорганизовал код так, чтобы вы работали с комнатой и потолками:

interface RoomVisitor {
  void visitFloor (Floor floor) ...
  void visitCeiling (Ceiling ceiling) ...
  void visitWall (Wall wall ...
}

interface Room { accept (RoomVisitor visitor) ; }

Kitchen.accept(RoomVisitor visitor) {
   visitor.visitCeiling(this.ceiling);
   ...
}

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

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

Тем не менее, Demeter применяется к OO-системам, где данные инкапсулируются внутри объектов, которые работают с данными, а не о системе, о которой вы говорите, которая имеет разные уровни данных, передаваемых между слоями в немых, навигационных структурах, Я не вижу, что Demeter может быть применим к таким системам так же легко, как к монолитным или основанным на сообщениям. (В системе на основе сообщений вы не можете перейти к чему-либо, что не находится в графе сообщения, поэтому вы застряли с Demeter, нравится вам это или нет)

Ответ 2

Я знаю, что меня убьют до полного уничтожения, но я должен сказать, что я не люблю Закон Деметры. Конечно, такие вещи, как

dictionary["somekey"].headers[1].references[2]

действительно уродливы, но учтите следующее:

Kitchen.Ceiling.Coulour

У меня нет ничего против этого. Написание кучи функций только для того, чтобы удовлетворить Закон Деметры, подобный этому

Kitchen.GetCeilingColour()

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

EDIT: Позвольте мне перефразировать мою мысль:

Является ли этот уровень абстрагирования настолько важным, что я буду тратить время на сочинение 3-4-5 уровней геттеров/сеттеров? Это облегчает обслуживание? Получает ли конечный пользователь что-нибудь? Стоит ли мое время? Я так не думаю.

Ответ 3

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

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

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

Где я думаю, что LoD ломается, это когда вы работаете с объектом, представляющим данные, без каких-либо действий.

В этом случае IMO, пересекающий граф объектов, не отличается от применения выражения XPath в DOM. И добавление таких методов, как "isThisMedicationWarranted()", является более сложным, потому что теперь вы распространяете бизнес-правила среди своих объектов, что затрудняет их понимание.

Ответ 4

Я боролся с LoD, как и многие из вас, пока я не просмотрел сеанс "Чистый код":

"Не смотрите на вещи"

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

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

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

[Я опаздываю на несколько лет, я знаю, что этот ответ, вероятно, не поможет составителю, но это не то, почему я публикую это]

Ответ 5

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

Другая возможность (возможно, переполнение и неполное решение в одно и то же время) - представить бизнес-правило как собственный объект и использовать шаблон двойной отправки/посетителя:

RuleCompilator
{
  lookAt(Protocol);
  lookAt(Medicine);
  lookAt(AdminstrationProcedure) 
}

MyComplexRuleCompilator : RuleCompilator
{
  lookaAt(Protocol)
  lookAt(AdminstrationProcedure)
}

Medicine
{
  applyRuleCompilator(RuleCompilator c) {
    c.lookAt(this);
    AdministrationProtocol.applyRuleCompilator(c);
  }
}

Ответ 6

Для BLL моя идея заключалась в том, чтобы добавить свойство на Медицину следующим образом:

public Boolean IsSoluble
{
    get { return AdministrationRoute.Soluble; } 
}

Это то, что я думаю, описано в статьях о Законе Деметры. Но насколько это будет помешать классу?

Ответ 7

Что касается первого примера с "разрешимым" свойством, у меня есть несколько замечаний:

  • Что такое "AdministrationRoute" и почему разработчик рассчитывает получить от него лекарство? Эти две концепции кажутся совершенно несвязанными. Это означает, что код не очень хорошо общается и, возможно, может быть улучшено разложение классов, которые у вас уже есть. Изменение разложения может привести вас к другим решениям для ваших проблем.
  • Растворимый не является прямым членом медицины по какой-то причине. Если вы обнаружите, что вам нужно получить к нему доступ напрямую, возможно, это должен быть прямой член. Если требуется дополнительная абстракция, верните эту дополнительную абстракцию из лекарственного средства (либо напрямую, либо через прокси или фасад). Все, что нуждается в разрешимом свойстве, может работать над абстракцией, и вы можете использовать ту же абстракцию для нескольких дополнительных типов, таких как субстраты или витамины.

Ответ 8

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

например. в С++:

class Medicine {
public:
    AdministrationRoute()& getAdministrationRoute() const { return _adminRoute; }

private:
    AdministrationRoute _adminRoute;
};

Тогда

if (Medicine.AdministrationRoute.Soluble) ...

становится

if (Medicine.getAdministrationRoute().Soluble) ...

Это дает вам гибкость в изменении getAdministrationRoute() в будущем, например. выберете AdministrationRoute из таблицы DB по запросу.

Ответ 9

Я думаю, что это помогает запомнить смысл существования LoD. То есть, если детали изменятся в цепочках отношений, ваш код может сломаться. Поскольку у вас есть абстракции, близкие к проблемному домену, тогда отношения вряд ли будут меняться, если проблема остается прежней, например, протокол использует Discipline для выполнения своей работы, но абстракции являются высокими и вряд ли будут изменение. Подумайте о скрытии информации, и невозможно, чтобы Протокол игнорировал существование Дисциплины, не так ли? Возможно, я отключен от понимания модели домена...

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

Я думаю, что если бы вы сделали модель домена, вы увидите больше связей, чем в вашей диаграмме классов С#. [Edit] Я добавил, что, как я подозреваю, отношения в вашей проблемной области с пунктирными линиями на следующей диаграмме:

UML Diagram of Domain model

С другой стороны, вы всегда можете реорганизовать свой код, применив Tell, не спрашивайте метафоры:

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

Вы уже реорганизовали первую проблему (BLL) с ответом . (Еще один способ абстрагирования BLL дальше - с механизмом правил.)

Чтобы реорганизовать вторую проблему (репозитории), внутренний код

    p.Kind.Discipline.Id == discipline.Id

может быть заменен каким-то вызовом .equals() с использованием стандартного API для коллекций (я больше программист на Java, поэтому я не уверен в точном эквиваленте С#). Идея состоит в том, чтобы скрыть детали того, как определить соответствие.

Чтобы рефакторировать третью проблему (внутри пользовательского интерфейса), я также не знаком с ASP.NET, но если есть способ сообщить объекту Kind вернуть имена Дисциплины (вместо того, чтобы запрашивать его для деталей как в Kind.Discipline.Name), что способ пойти на уважение LoD.

Ответ 10

Третья проблема очень проста: Discipline.ToString() должна оценить свойство Name Таким образом вы вызываете только Kind.Discipline