В прошлом я обычно делал большую часть своих манипуляций с объектом в основном методе, которым он создается / обновлялся, но в последнее время я нашел другой подход, и мне любопытно, если это плохая практика.
Вот пример. Допустим, у меня есть репозиторий, который принимает User
сущность, но перед вставкой сущности мы вызываем некоторые методы, чтобы убедиться, что все ее поля установлены так, как нам нужно. Теперь вместо того, чтобы вызывать методы и устанавливать значения полей из метода Insert, я вызываю серию методов подготовки, которые формируют объект до его вставки.
Старый метод:
public void InsertUser(User user) {
user.Username = GenerateUsername(user);
user.Password = GeneratePassword(user);
context.Users.Add(user);
}
Новые методы:
public void InsertUser(User user) {
SetUsername(user);
SetPassword(user);
context.Users.Add(user);
}
private void SetUsername(User user) {
var username = "random business logic";
user.Username = username;
}
private void SetPassword(User user) {
var password = "more business logic";
user.Password = password;
}
В принципе, является ли практика установки значения свойства из другого метода плохой практикой?
c#
design
coding-style
JD Davis
источник
источник
user
были переданы по ссылке, код может дергать его из рук вызывающих и заменить его, просто говоря, скажем,user = null;
.Ответы:
Проблема в том, что на
User
самом деле может содержать две разные вещи:Полная сущность пользователя, которую можно передать в ваше хранилище данных.
Набор элементов данных, требуемых от вызывающей стороны, чтобы начать процесс создания объекта User. Система должна добавить имя пользователя и пароль, прежде чем он действительно станет действительным пользователем, как в # 1 выше.
Это включает недокументированный нюанс вашей объектной модели, который вообще не выражен в вашей системе типов. Вам просто нужно «знать» это как разработчика. Это не здорово, и это приводит к странным шаблонам кода, подобным тому, с которым вы сталкиваетесь.
Я хотел бы предложить вам две сущности, например,
User
класс иEnrollRequest
класс. Последний может содержать все, что вам нужно знать, чтобы создать пользователя. Это будет похоже на ваш класс User, но без имени пользователя и пароля. Тогда вы можете сделать это:Вызывающий абонент начинает только с информации о регистрации и получает обратно завершенного пользователя после его вставки. Таким образом, вы избегаете мутирования любых классов, и у вас также есть типобезопасное различие между пользователем, который вставлен, и тем, кто нет.
источник
InsertUser
может иметь побочный эффект от вставки. Я не уверен, что означает "icky" в этом контексте. Что касается использования «пользователя», который не был добавлен, вы, похоже, упустили из виду. Если он не был добавлен, это не пользователь, а просто запрос на создание пользователя. Вы можете работать с запросом, конечно.Побочные эффекты в порядке, пока они не приходят неожиданно. В общем, нет ничего плохого в том, что в хранилище есть метод, принимающий пользователя и изменяющий внутреннее состояние пользователя. Но ИМХО имя метода вроде
InsertUser
не очень ясно говорит об этом , и это делает его подверженным ошибкам. При использовании вашего репо я бы ожидал такой звонокизменить внутреннее состояние репозитория, а не состояние объекта пользователя. Эта проблема существует в обеих ваших реализациях, как
InsertUser
это внутренне не имеет никакого значения.Чтобы решить эту проблему, вы можете либо
отделяйте инициализацию от вставки (так что вызывающая сторона
InsertUser
должна предоставить полностью инициализированныйUser
объект, или,встроить инициализацию в процесс построения пользовательского объекта (как предлагается в некоторых других ответах), или
просто попробуйте найти лучшее имя для метода, который более четко выражает, что он делает.
Так выбрать имя метода , как
PrepareAndInsertUser
,OrchestrateUserInsertion
,InsertUserWithNewNameAndPassword
или все , что вы предпочитаете , чтобы сделать побочный эффект более ясным.Конечно, такое длинное имя метода означает, что метод, возможно, выполняет «слишком много» (нарушение SRP), но иногда вы не хотите или не можете легко это исправить, и это наименее навязчивое, прагматичное решение.
источник
Я смотрю на два варианта, которые вы выбрали, и я должен сказать, что я предпочитаю старый метод, а не предложенный новый метод. Есть несколько причин для этого, хотя они в основном делают то же самое.
В любом случае вы устанавливаете
user.UserName
иuser.Password
. У меня есть оговорки по поводу элемента пароля, но эти оговорки не имеют отношения к данной теме.Последствия изменения ссылочных объектов
Старый метод против нового метода
Старый метод облегчил тестирование:
GenerateUserName()
независимо проверяется Вы можете написать тесты для этого метода и убедиться, что имена генерируются правильноGenerateUserName(User user)
и поддерживать эту тестируемостьНовый метод скрывает мутации:
User
объект меняется, пока вы не пройдете 2 слояUser
объекте более удивительны в этом случаеSetUserName()
делает больше, чем установить имя пользователя. Это не правда в рекламе, из-за которой новым разработчикам становится сложнее понять, как все работает в вашем приложении.источник
Я полностью согласен с ответом Джона Ву. Его предложение хорошее. Но это немного не соответствует вашему прямому вопросу.
Не по своей сути.
Вы не можете зайти слишком далеко, потому что столкнетесь с неожиданным поведением. Например,
PrintName(myPerson)
не следует изменять объект person, поскольку метод подразумевает, что он заинтересован только в чтении существующих значений. Но это другой аргумент, чем в вашем случае, посколькуSetUsername(user)
настоятельно подразумевает, что он собирается устанавливать значения.На самом деле это подход, который я часто использую для модульных / интеграционных тестов, где я создаю метод специально для изменения объекта, чтобы установить его значения в конкретной ситуации, которую я хочу протестировать.
Например:
Я явно ожидаю, что
ArrrangeContractDeletedStatus
метод изменит состояниеmyContract
объекта.Основное преимущество заключается в том, что этот метод позволяет мне тестировать удаление контракта с различными первоначальными контрактами; например, контракт с длинной историей статуса или контракт с предыдущей историей статуса, контракт с преднамеренно ошибочной историей статуса, контракт, который мой тестовый пользователь не может удалить.
Если бы я слился
CreateEmptyContract
иArrrangeContractDeletedStatus
в единый метод; Мне пришлось бы создать несколько вариантов этого метода для каждого контракта, который я хотел бы проверить в удаленном состоянии.И хотя я мог сделать что-то вроде:
Это либо избыточно (так как я все равно меняю объект), либо я заставляю себя сделать глубокий клон
myContract
объекта; что является чрезмерно трудным, если вы хотите охватить каждый случай (представьте, если мне нужны навигационные свойства на несколько уровней. Нужно ли клонировать их все? Только сущность верхнего уровня? ... Так много вопросов, так много неявных ожиданий )Изменение объекта - это самый простой способ получить то, что я хочу, без необходимости делать больше работы, просто чтобы не менять объект в принципе.
Таким образом, прямой ответ на ваш вопрос заключается в том, что по своей сути это не плохая практика, если вы не запутываете, что метод может изменить переданный объект. Для вашей текущей ситуации это совершенно ясно видно из названия метода.
источник
Я нахожу старое и новое проблематичным.
Старый способ:
1)
InsertUser(User user)
Когда я читаю название этого метода, всплывающее в моей IDE, первое, что приходит мне в голову, это
Имя метода должно читаться
AddUserToContext
. По крайней мере, это то, что метод делает в конце.2)
InsertUser(User user)
Это явно нарушает принцип наименьшего удивления. Скажем, я до сих пор выполнял свою работу и создал свежий экземпляр a,
User
дал ему aname
и set apassword
, я был бы удивлен:а) это не только вставить пользователя во что-то
б) это также подрывает мое намерение вставить пользователя как есть; имя и пароль были переопределены.
в) означает ли это, что это также является нарушением принципа единой ответственности.
Новый путь:
1)
InsertUser(User user)
Еще нарушение СРП и принцип наименьшего удивления
2)
SetUsername(User user)
Вопрос
Лучше:
SetRandomName
или что-то, что отражает намерение.3)
SetPassword(User user)
Вопрос
Лучше:
SetRandomPassword
или что-то, что отражает намерение.Предложение:
То, что я предпочел бы читать было бы что-то вроде:
По поводу вашего первоначального вопроса:
Нет, это больше вопрос вкуса.
источник