Как устранить нарушения закона Деметры? - PullRequest
36 голосов
/ 22 января 2009

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

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

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

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

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

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

Внутри BLL

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

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) для интерфейса нашей системы, на мой взгляд, этот уровень в настоящее время имеет худшие нарушения. Привязка данных вида сетки (столбец, который должен отображать Дисциплину протокола, должен быть привязан к Kind.Discipline.Name), которые являются строками , поэтому нет ошибок времени компиляции .

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

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

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

Ответы [ 10 ]

30 голосов
/ 22 января 2009

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

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

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

  1. О себе
  2. Параметры М
  3. любые объекты, созданные / созданные в M
  4. Объекты прямого компонента O

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

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

Kitchen.GetCeilingColour()

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

Если метод за пределами Kitchen передан кухне, по строгому Demeter он также не может вызывать какие-либо методы в результате получения 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, но это часто приводит к хрупкому соединению.

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

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

21 голосов
/ 22 января 2009

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

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

действительно ужасны, но учтите это:

Kitchen.Ceiling.Coulour

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

Kitchen.GetCeilingColour()

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

РЕДАКТИРОВАТЬ: Позвольте мне перефразировать мою точку зрения:

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

11 голосов
/ 22 января 2009

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

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

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

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

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

4 голосов
/ 17 июля 2012

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

"Не ищи вещей"

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

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

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

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

2 голосов
/ 22 января 2009

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

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

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

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

Medicine
{
  applyRuleCompilator(RuleCompilator c) {
    c.lookAt(this);
    AdministrationProtocol.applyRuleCompilator(c);
  }
}
1 голос
/ 19 октября 2013

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

1 голос
/ 08 августа 2013

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

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

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

UML Diagram of Domain model

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

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

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

Для рефакторинга второй задачи (репозитории), внутренний код

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

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

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

1 голос
/ 22 января 2009

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

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

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

private:
    AdministrationRoute _adminRoute;
};

Тогда

if (Medicine.AdministrationRoute.Soluble) ...

становится

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

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

1 голос
/ 22 января 2009

По поводу первого примера со свойством «разрешимое» у меня есть несколько замечаний:

  1. Что такое «AdministrationRoute» и почему разработчик рассчитывает получить от него растворимые свойства лекарства? Две концепции кажутся совершенно не связанными. Это означает, что код не очень хорошо взаимодействует, и, возможно, разложение уже имеющихся классов может быть улучшено. Изменение декомпозиции может привести к поиску других решений ваших проблем.
  2. Растворимый не является непосредственным участником медицины по определенной причине. Если вы обнаружите, что вам нужен прямой доступ к нему, возможно, он должен быть прямым участником. Если требуется дополнительная абстракция, верните эту дополнительную абстракцию из лекарства (либо напрямую, либо по доверенности, либо через фасад). Все, что нуждается в свойстве растворимости, может работать с абстракцией, и вы можете использовать ту же абстракцию для нескольких дополнительных типов, таких как субстраты или витамины.
1 голос
/ 22 января 2009

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

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

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

...