Не нарушает ли добавление типа возврата в метод обновления «Принцип единой ответственности»?

37

У меня есть метод, который обновляет данные сотрудников в базе данных. EmployeeКласс неизменен, так что «обновление» средства объекта на самом деле для создания экземпляра нового объекта.

Я хочу, чтобы Updateметод возвращал новый экземпляр Employeeс обновленными данными, но теперь я могу сказать, что ответственность за метод заключается в обновлении данных сотрудника и получении из базы данных нового Employeeобъекта , нарушает ли он принцип единой ответственности ?

Сама запись БД обновляется. Затем создается новый объект для представления этой записи.

Sipo
источник
5
Небольшой псевдокод может иметь большое значение: действительно ли создана новая запись в БД или обновлена ​​сама запись в БД, но в «клиентском» коде создается новый объект, потому что класс моделируется как неизменяемый?
Мартин Ба
В дополнение к тому, что спросил Мартин Бра, почему вы возвращаете экземпляр Employee при обновлении базы данных? Значения экземпляра Employee не изменились в методе Update, поэтому зачем его возвращать (вызывающие уже имеют доступ к экземпляру ...). Или метод update также извлекает (потенциально разные) значения из базы данных?
Торсал
1
@Thorsal: если ваши сущности данных являются неизменяемыми, возвращение экземпляра с обновленными данными в значительной степени является СОП, так как в противном случае вам придется самостоятельно создавать экземпляры измененных данных.
Миколак
21
Compare-and-swapи test-and-setявляются фундаментальными операциями в теории многопоточного программирования. Оба они являются методами обновления с возвращаемым типом, и не могли работать иначе. Это нарушает такие понятия, как разделение команд и запросов или принцип единой ответственности? Да, и в этом весь смысл . SRP не является универсально хорошей вещью и на самом деле может быть активно вредным.
MSalters
3
@MSalters: точно. Разделение команд / запросов говорит о том, что должна быть возможность выдавать запросы, которые могут быть распознаны как идемпотентные, и выдавать команды без ожидания ответа, но атомарное чтение-изменение-запись должно быть распознано как третья категория операций.
суперкат

Ответы:

16

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

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

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

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

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

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

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

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

сойка
источник
Спасибо за все ответы, но этот действительно помог мне больше всего.
Сипо
Если вы не хотите отправлять без переформатирования, и нет смысла переформатировать, кроме отправки данных позже, тогда это одно, а не два.
Joker_vD
public function sendDataInClientFormat() { formatDataForClient(); sendDataToClient(); } private function formatDataForClient() {...} private function sendDataToClient() {...}
CJ Деннис
@CJDennis Конечно. И на самом деле это то, как я это сделал: функция для форматирования, функция для фактической отправки и две другие функции, которые я не буду здесь описывать. Затем одна функция верхнего уровня для вызова их всех в правильной последовательности. Вы могли бы сказать, что «форматирование и отправка» - это одна логическая операция и, таким образом, может быть правильно объединена в одну функцию Если вы настаиваете на том, что их два, хорошо, тогда все же рациональнее всего иметь одну функцию верхнего уровня, которая делает все это.
Джей
67

Концепция SRP заключается в том, чтобы запретить модулям выполнять две разные вещи, если их выполнение по отдельности способствует лучшему обслуживанию и сокращению времени спагетти. Как говорит СРП, «одна причина для перемен».

В вашем случае, если у вас была подпрограмма, которая «обновляет и возвращает обновленный объект», вы все равно меняете объект один раз, давая ему 1 причину для изменения. То, что вы возвращаете объект обратно, ни здесь, ни там, вы все еще работаете с этим единственным объектом. Кодекс отвечает за одну и только одну вещь.

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

gbjbaanb
источник
7
В качестве альтернативы речь идет не о «попытке свести все операции к одному вызову», а о том, как много разных случаев нужно учитывать при использовании рассматриваемого элемента.
jpmc26
46

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

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

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

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

Килиан Фот
источник
15
Если вам нужно вызвать второй метод, чтобы увидеть, успешно ли первый, разве вы не должны вызывать третий метод, чтобы получить результат второго? Тогда, если вы действительно хотите получить результат, хорошо ...
3
Я могу добавить, что чрезмерное усердие в применении ПСП ведет к множеству маленьких классов, что само по себе является обузой. Насколько большая нагрузка зависит от вашей среды, компилятора, IDE / вспомогательных инструментов и т. Д.
Эрик Алапяя
8

Нарушает ли это принцип единой ответственности?

Не обязательно. Во всяком случае, это нарушает принцип разделения команд и запросов .

Ответственность

обновить данные сотрудника

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

Опять же, все это зависит от степени и субъективного суждения.


Так что насчет разделения команд и запросов?

Ну, этот принцип существует, но возвращение результата обновления на самом деле очень распространено.

(1) Java Set#add(E)добавляет элемент и возвращает его предыдущее включение в набор.

if (visited.add(nodeToVisit)) {
    // perform operation once
}

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

if (!visited.contains(nodeToVisit)) {
    visited.add(nodeToVisit);
    // perform operation once
}

(2) Сравнение-и-замена , выборка-и-добавление и проверка-и-набор - это общие примитивы, которые позволяют параллельному программированию существовать. Эти шаблоны часто появляются, от низкоуровневых инструкций ЦП до высокоуровневых параллельных коллекций.

Пол Дрейпер
источник
2

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

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

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

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

Но то, что метод Update возвращает то, что он только что обновил, прекрасно.

изогнутый
источник
2

Wrt. конкретный случай:

Employee Update(Employee, name, password, etc) (на самом деле использую Builder, так как у меня много параметров).

Это кажетсяUpdate метод принимает существующий в Employeeкачестве первого параметра для идентификации (?) Существующего сотрудника и набора параметров для изменения на этом сотруднике.

Я действительно думаю , что это может быть сделано чище. Я вижу два случая:

(а) Employee действительно содержит базу данных / уникальный идентификатор, по которому его всегда можно идентифицировать в базе данных. (То есть вам не нужно устанавливать все значения записи, чтобы найти ее в БД.

В этом случае я бы предпочел void Update(Employee obj)метод, который просто находит существующую запись по ID, а затем обновляет поля из переданного объекта. Или может бытьvoid Update(ID, EmployeeBuilder values)

Вариант этого, который я нашел полезным, состоит в том, чтобы иметь только void Put(Employee obj)метод, который вставляет или обновляет, в зависимости от того, существует ли запись (по идентификатору).

(б) Полная существующая запись необходима для DB поиска, в этом случае это может еще больше смысла иметь: void Update(Employee existing, Employee newData).

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

Упомянутые параллельные требования в других ответах (атомарный набор-извлечение / сравнение-своп и т. Д.) Не были проблемой в коде БД, над которым я работал до сих пор. Когда я говорю с БД, я думаю, что это следует обрабатывать на уровне транзакций, а не на уровне отдельных операторов. (Это не значит, что не может быть дизайна, в котором «атомарность» Employee[existing data] Update(ID, Employee newData)не имеет смысла, но с доступом к БД это не то, что я обычно вижу.)

Мартин Ба
источник
1

Пока что все здесь говорят о занятиях. Но подумайте об этом с точки зрения интерфейса.

Если интерфейсный метод объявляет тип возвращаемого значения и / или неявное обещание о возвращаемом значении, то каждая реализация должна возвращать обновленный объект.

Так что вы можете спросить:

  • Можете ли вы подумать о возможных реализациях, которые не хотят возвращать новый объект?
  • Можете ли вы подумать о компонентах, которые зависят от интерфейса (путем внедрения экземпляра), которым не требуется обновленный сотрудник в качестве возвращаемого значения?

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

На основании этих соображений вы можете принять решение.

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

Дон Кихот
источник
0

Ваш подход в порядке. Неизменность является сильным утверждением. Единственное, что я хотел бы спросить: есть ли другое место, где вы строите объект. Если бы ваш объект не был неизменным, вам пришлось бы отвечать на дополнительные вопросы, потому что введено «State». И изменение состояния объекта может происходить по разным причинам. Тогда вы должны знать свои дела, и они не должны быть избыточными или разделенными.

oopexpert
источник