Как работают вместе Закон Деметры и композиция с коллекциями?

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

Допустим, у вас есть довольно стандартный объект, состоящий из других объектов, например:

public class Customer {
  private String name;
  private ContactInfo primaryAddress;
  private ContactInfo workAddress;
  private Interests hobbies;
  //Etc...

  public getPrimaryAddress() { return primaryAddress; }
  public getWorkAddress() { return workAddress; }
  public getHobbies() { return hobbies; }
  //Etc...
}

private ContactInfo {
  private String phoneNumber;
  private String emailAddress;
  //Etc...

  public getPhoneNumber() { return phoneNumber; }
  public getEmailAddress() { return emailAddress; }
  //Etc...
}

private Interests {
  private List listOfInterests;
}

Следующее будет нарушать Закон Деметры:

System.out.println("Phone: " + customer.getPrimaryAddress().getPhoneNumber());
System.out.println("Hobbies: " + customer.getHobbies().getListOfInterests().toString());

Я думаю, это также нарушит закон Деметры (пояснение?):

ContactInfo customerPrimaryAddress = customer.getPrimaryAddress();
System.out.println("Phone: " + customerPrimaryAddress.getPhoneNumber());

Предположительно, вы затем добавили бы в Customer метод getPrimaryPhoneNumber ():

public getPrimaryPhoneNumber() {
  return primaryAddress.getPhoneNumber();
}

А затем просто позвоните: System.out.println ("Телефон:" + customer.getPrimaryPhoneNumber ());

Но если сделать это со временем, кажется, что на самом деле это создаст много проблем и будет работать вопреки замыслу Закона Деметры. Это превращает класс Customer в огромный набор геттеров и сеттеров, которые слишком много знают о своих внутренних классах. Например, кажется возможным, что однажды объект «Клиент» будет иметь разные адреса (а не только «основной» и «рабочий» адрес). Возможно, даже у класса Customer будет просто список (или другая коллекция) объектов ContactInfo, а не конкретные именованные объекты ContactInfo. Как в таком случае продолжать следовать Закону Деметры? Казалось бы, это противоречит цели абстракции. Например, это кажется разумным в том случае, когда у клиента есть список элементов ContactInfo:

Customer.getSomeParticularAddress(addressType).getPhoneNumber();

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

Customer.getSomeParticularAddress(addressType).getSomePhoneNumber(phoneType).getPhoneNumber();

В этом случае мы не только обращаемся к объектам внутри объектов внутри объектов, но также должны знать, каковы действительные типы addressType и phoneType. Я определенно вижу в этом проблему, но не знаю, как ее избежать. Особенно, когда любой класс, вызывающий это, вероятно, действительно знает, что они хотят вытащить номер «мобильного» телефона для «основного» адреса рассматриваемого клиента.

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


person Scott Shipp    schedule 28.03.2013    source источник
comment
Я думаю, вы понимаете это слишком буквально, это всего лишь ориентир на возможные запахи кода. То, что ты делаешь, нормально.   -  person Esailija    schedule 29.03.2013
comment
Согласитесь с Esailija, закон Деметры действительно не нужно применять к геттерам, они слишком тривиальны. Фактически, выполнение того, что вы предлагаете (Customer.getPrimaryPhoneNumber ()), приведет к тому, что Клиент должен знать внутреннее устройство ContactInfo. Преимущество наличия объектов типа Composition заключается в том, что вы ограничиваете круг лиц, которые должны знать о ContactInfo, например   -  person Taylor    schedule 29.03.2013
comment
Хорошо, я понимаю этот момент, но, на мой взгляд, будут сценарии без получения / установки для материала внутри ContactInfo. Допустим, вы хотите отправить электронное письмо клиенту по ссылке или кнопке. Вы можете написать его как customer.getSomeParticularContactInfo (addressType) .sendEmail () или customer.sendEmail (), который затем (внутри клиента) вызывает getSomeParticularContactInfo (primary) .sendEmail (). Или есть другие варианты, такие как какой-то класс менеджера, который обрабатывает действие. Тогда пример становится не о геттерах или сеттерах, и я не понимаю, как принимать дизайнерские решения в этом контексте.   -  person Scott Shipp    schedule 29.03.2013
comment
@ScottShipp прав: Customer.getSomeParticularAddress (addressType) .getSomePhoneNumber (phoneType) .getPhoneNumber () действительно плохой. Как мне выйти из такой ситуации? Это, безусловно, приведет к излишнему раздуванию класса Customer. Кроме того, у меня есть такие коллекции, как paragraph.getSentences (). GetWords (); как провести рефакторинг в таком случае. Извините, я не понял ни одного комментария или ответа   -  person swapyonubuntu    schedule 23.06.2015


Ответы (3)


По моему опыту, показанный пример Customer не является «стандартным объектом, состоящим из других объектов», потому что в этом примере используются дополнительные шаги по реализации составляющих его частей как внутренних классов и, кроме того, сделать эти внутренние классы закрытыми. Это неплохо.

В целом модификатор частного доступа увеличивает сокрытие информации, что является основой Закона Деметры. Разоблачение частных занятий противоречиво. IDE NetBeans фактически включает предупреждение компилятора по умолчанию для «Экспорт непубличного типа через общедоступный API».

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

Предлагаемое решение добавления метода getPrimaryPhoneNumber() к Customer является допустимым вариантом. Здесь возникает путаница: «Заказчик ... слишком много знает о своих внутренних классах». Это невозможно; и поэтому важно, чтобы этот пример не был стандартным примером композиции.

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

Учитывая нелепый пример класса Foo, у которого есть вложенный класс Bar, у которого есть вложенный класс Baz, у которого есть вложенный класс Qux, вызов bar.baz.qux не будет нарушением Demeter для Foo (внутренне). .method (). Foo уже знает все, что нужно знать о Bar, Baz и Qux; потому что их код находится внутри Foo, поэтому через длинную цепочку методов не передается никаких дополнительных знаний.

Тогда решение, согласно Закону Деметры, состоит в том, чтобы Customer не возвращал промежуточные объекты, независимо от его внутренней реализации; т.е. независимо от того, реализован ли Customer с использованием нескольких вложенных классов или ни одного, он должен возвращать только то, что в конечном итоге нужно его клиентским классам.

Например, последний фрагмент кода можно отредактировать как customer.getPhoneNumber(addressType, phoneType);

или если вариантов мало, customer.getPrimaryMobilePhoneNumber();

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

person jaco0646    schedule 28.06.2015
comment
связанные: javadevguy.wordpress.com / 2017/05/14 / - person jaco0646; 23.06.2017

Как насчет создания другого класса с именем типа CustomerInformationProvider. Вы можете передать свой объект Customer в качестве параметра конструктора его новому классу. А затем вы можете написать все конкретные методы для получения телефонов, адресов и т. Д. Внутри этого класса, при этом оставив класс Customer чистым.

person kumade    schedule 29.06.2015
comment
Это хороший вариант, но приводит к слишком большому количеству классов провайдеров. - person swapyonubuntu; 30.06.2015

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

Цель Закона Деметры - предотвратить доступ посторонних объектов к внутренностям другого объекта. Доступ к внутренним компонентам имеет две проблемы: 1) он дает слишком много информации о внутренней структуре объекта и 2) он также позволяет внешним объектам изменять внутренние компоненты класса.

Правильный ответ на эту проблему - отделить объект, возвращаемый методом Customer, от внутреннего представления. Другими словами, вместо того, чтобы возвращать ссылку на частный внутренний объект типа ContactInfo, мы вместо этого определяем новый класс UnmodifiableContactInfo и заставляем getPrimaryAddress возвращать UnmodifiableContactInfo, создавая его и заполняя по мере необходимости.

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

(На самом деле я бы переименовал внутренний класс и оставил внешний как ContactInfo, но это второстепенный момент)

Таким образом, мы достигаем целей Закона Деметры, но, похоже, мы его нарушаем. Я думаю об этом так, что метод getAddress не возвращает объект ContactInfo, а создает его экземпляр. Это означает, что в соответствии с правилами Demeter мы можем получить доступ к методам ContactInfo, и код, который вы написали выше, не является нарушением.

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

Несколько заметок. Очевидно, что чрезмерно строгое применение закона Деметры приводит к идиотиям, например, запрещающим:

int nameLength = myObject.getName().length()

это техническое нарушение, которое большинство из нас совершает каждый день. Все также делают:

mylist.get(0).doSomething();

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

Резюме

Вот как должен выглядеть ваш код:

public class Customer {
    private class InternalContactInfo {
        public ContactInfo createContactinfo() {
            //creates a ContactInfo based on its own values...
        }
        //Etc....
    }
   private String name;
   private InternalContactInfo primaryAddress;
   //Etc...

   public Contactinfo getPrimaryAddress() { 
      // copies the info from InternalContactInfo to a new object
      return primaryAddress.createContactInfo();
   }
   //Etc...
}

public class ContactInfo {
   // Not modifiable
   private String phoneNumber;
   private String emailAddress;
   //Etc...

   public getPhoneNumber() { return phoneNumber; }
   public getEmailAddress() { return emailAddress; }
   //Etc...
}

}

person DJClayworth    schedule 27.07.2015