Разве Информационный Эксперт и Скажи, Не спрашивай в противоречии с принципом Единой Ответственности?
Information-Expert, Tell-Don't-Ask и SRP часто упоминаются вместе как лучшие практики. Но я думаю, что они расходятся. Вот о чем я говорю.
Код, который поддерживает SRP, но нарушает принцип "говорите, не спрашивайте" и "Info-Expert":
Customer bob = ...;
// TransferObjectFactory has to use Customer's accessors to do its work,
// violates Tell Don't Ask
CustomerDTO dto = TransferObjectFactory.createFrom(bob);
Код, который поддерживает функцию "говорите, не спрашивайте" и "эксперт по информации", но нарушает SRP:
Customer bob = ...;
// Now Customer is doing more than just representing the domain concept of Customer,
// violates SRP
CustomerDTO dto = bob.toDTO();
Пожалуйста, напишите мне, как эти практики могут мирно сосуществовать.
Определения терминов,
Информационный эксперт: объекты, которые имеют данные, необходимые для операции, должны содержать операцию.
Скажите не спрашивайте: не запрашивайте у данных данные для выполнения работы; сказать объектам, чтобы сделать работу.
Принцип единой ответственности: каждый объект должен иметь узко определенную ответственность.
6 ответов
Я не думаю, что они так сильно расходятся, поскольку подчеркивают разные вещи, которые причинят вам боль. Один из них касается структурирования кода, чтобы прояснить, в чем заключаются конкретные обязанности, и уменьшения связи, а другой - уменьшения причин изменения класса.
Мы все должны каждый день принимать решения о том, как структурировать код и какие зависимости мы хотим внести в дизайн.
Мы разработали множество полезных рекомендаций, принципов и шаблонов, которые могут помочь нам принимать решения.
Каждый из них полезен для обнаружения различных типов проблем, которые могут присутствовать в наших проектах. Для любой конкретной проблемы, на которую вы можете смотреть, где-то найдется сладкое место.
Различные рекомендации противоречат друг другу. Простое применение каждого руководства, которое вы услышали или прочитали, не улучшит ваш дизайн.
Для решения конкретной проблемы, с которой вы сталкиваетесь сегодня, вам необходимо решить, каковы наиболее важные факторы, которые могут причинить вам боль.
Вы можете говорить о "Скажите не спрашивать", когда вы спрашиваете состояние объекта, чтобы заставить объект что-то делать.
В вашем первом примере TransferObjectFactory.createFrom просто конвертер. Он не говорит объекту-клиенту что-либо делать после проверки его состояния.
Я думаю, что первый пример правильный.
Те классы не в ссоре. DTO просто служит каналом передачи данных из хранилища, которое предназначено для использования в качестве тупого контейнера. Это, конечно, не нарушает ПСП.
С другой стороны, метод.toDTO сомнителен - почему Заказчик должен нести эту ответственность? Ради "чистоты" у меня был бы другой класс, задачей которого было создание DTO из бизнес-объектов, таких как Customer.
Не забывайте, что эти принципы являются принципами, и когда вы можете отказаться от более простых решений, пока изменение требований не вызовет проблемы, сделайте это. Излишняя сложность - это определенно то, чего следует избегать.
Я настоятельно рекомендую, кстати, Agile Patterns, Практики и принципы Роберта К. Мартина для более углубленного изучения этого предмета.
Крейг Ларман обсуждал это, когда представил GRASP в разделе "Применение UML и шаблонов к объектно-ориентированному анализу, проектированию и итеративной разработке" (2004):
В некоторых ситуациях решение, предложенное экспертом, нежелательно, как правило, из-за проблем сцепления и сплоченности (эти принципы обсуждаются далее в этой главе).
Например, кто должен нести ответственность за сохранение продажи в базе данных? Конечно, большая часть информации, которая будет сохранена, находится в объекте Sale, и, таким образом, эксперт может утверждать, что ответственность лежит на классе Sale. И, благодаря логическому расширению этого решения, каждый класс будет иметь свои собственные службы для сохранения себя в базе данных. Но действия на основе этих рассуждений приводят к проблемам сплоченности, связи и дублирования. Например, класс Sale теперь должен содержать логику, связанную с обработкой базы данных, например, связанную с SQL и JDBC (Java Database Connectivity). Класс больше не фокусируется только на чистой логике приложения "быть продажей". Теперь другие виды ответственности снижают его сплоченность. Этот класс должен быть связан со службами технической базы данных другой подсистемы, такими как службы JDBC, а не просто связан с другими объектами на уровне домена программных объектов, поэтому его связь увеличивается. И вполне вероятно, что подобная логика базы данных будет дублирована во многих постоянных классах.
Все эти проблемы указывают на нарушение основного архитектурного принципа: проектирование для разделения основных проблем системы. Храните логику приложения в одном месте (например, объекты программного обеспечения домена), сохраняйте логику базы данных в другом месте (например, отдельную подсистему служб персистентности) и т. Д., А не смешивайте различные системные проблемы в одном и том же компоненте.[11]
Поддержка разделения основных проблем улучшает сцепление и сплоченность в конструкции. Таким образом, даже несмотря на то, что с помощью Expert мы могли найти какое-то оправдание для того, чтобы возложить ответственность за службы баз данных на класс Sale, по другим причинам (обычно сплоченности и связности) у нас получился бы плохой дизайн.
Таким образом, SRP, как правило, превосходит Information Expert.
Тем не менее, принцип инверсии зависимости может хорошо сочетаться с экспертом. Аргументом здесь будет то, что Customer не должен зависеть от CustomerDTO (от общего к подробному), а наоборот. Это будет означать, что CustomerDTO является экспертом и должен знать, как построить себя, учитывая клиента:
CustomerDTO dto = new CustomerDTO(bob);
Если у вас аллергия на новые, вы можете зайти в тупик:
CustomerDTO dto = CustomerDTO.buildFor(bob);
Или, если вы ненавидите и то и другое, мы возвращаемся к AbstractFactory:
public abstract class DTOFactory<D, E> {
public abstract D createDTO(E entity);
}
public class CustomerDTOFactory extends DTOFactory<CustomerDTO, Customer> {
@Override
public CustomerDTO createDTO(Customer entity) {
return new CustomerDTO(entity);
}
}
DTO с сестринским классом (как у вас) нарушают все три изложенных вами принципа и инкапсуляцию, вот почему у вас возникают проблемы.
Для чего вы используете это CustomerDTO, и почему вы не можете просто использовать Customer и иметь данные DTO внутри клиента? Если вы не будете осторожны, CustomerDTO понадобится Customer, а Customer - CustomerDTO.
TellDontAsk говорит, что если вы основываете решение на состоянии одного объекта (например, клиента), то это решение должно выполняться внутри самого класса клиента.
Например, если вы хотите напомнить клиенту о необходимости оплатить оставшиеся счета, позвоните
List<Bill> bills = Customer.GetOutstandingBills();
PaymentReminder.RemindCustomer(customer, bills);
это нарушение. Вместо этого вы хотите сделать
Customer.RemindAboutOutstandingBills()
(и, конечно, вам нужно будет передать в PaymentReminder как зависимость от конструкции клиента).
Информационный эксперт говорит то же самое в значительной степени.
Принцип единой ответственности можно легко понять неправильно - он говорит, что на класс клиента должна быть возложена одна ответственность, а также на то, что ответственность за группирование данных, методов и других классов, соответствующих концепции "Клиент", должна быть заключена только в один класс. Чрезвычайно сложно точно определить, что представляет собой отдельная ответственность, и я рекомендовал бы больше читать по этому вопросу.
Я не согласен на 100% с вашими двумя примерами как с репрезентативными, но с общей точки зрения вы, кажется, исходите из предположения о двух объектах и только двух объектах.
Если вы еще больше отделите проблему и создадите один (или несколько) специализированных объектов для выполнения ваших индивидуальных обязанностей, а затем попросите управляющий объект передать экземпляры других объектов, которые он использует, в специализированные объекты, которые вы вырезали, вы должен быть в состоянии наблюдать счастливый компромисс между SRP (каждая ответственность обрабатывается специализированным объектом) и Tell Don't Ask (контролирующий объект сообщает специализированным объектам, которые он создает, чтобы делать то, что они делают, чтобы друг с другом).
Это композиционное решение, которое использует какой-то контроллер для координации и делегирования между другими объектами, не вдаваясь в их внутренние детали.