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

12

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

Вот пример. Допустим, у меня есть репозиторий, который принимает 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;
}

В принципе, является ли практика установки значения свойства из другого метода плохой практикой?

JD Davis
источник
6
Просто так сказано ... Вы не передали ничего по ссылке здесь. Передача ссылок по значению - это совсем не одно и то же.
Цао
4
@cHao: это различие без разницы. Поведение кода одинаково независимо.
Роберт Харви
2
@JDDavis: По сути, единственная реальная разница между вашими двумя примерами состоит в том, что вы дали значимое имя заданному имени пользователя и задали действия с паролем.
Роберт Харви
1
Все ваши звонки по ссылке здесь. Вы должны использовать тип значения в C # для передачи по значению.
Фрэнк Хайлман
2
@FrankHileman: Все звонки здесь по значению. Это по умолчанию в C #. Передача в ссылку и переходя по ссылке разные звери, и различие имеет значение. Если userбыли переданы по ссылке, код может дергать его из рук вызывающих и заменить его, просто говоря, скажем, user = null;.
Цао

Ответы:

10

Проблема в том, что на Userсамом деле может содержать две разные вещи:

  1. Полная сущность пользователя, которую можно передать в ваше хранилище данных.

  2. Набор элементов данных, требуемых от вызывающей стороны, чтобы начать процесс создания объекта User. Система должна добавить имя пользователя и пароль, прежде чем он действительно станет действительным пользователем, как в # 1 выше.

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

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

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

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

Джон Ву
источник
2
Метод с именем наподобие InsertUserможет иметь побочный эффект от вставки. Я не уверен, что означает "icky" в этом контексте. Что касается использования «пользователя», который не был добавлен, вы, похоже, упустили из виду. Если он не был добавлен, это не пользователь, а просто запрос на создание пользователя. Вы можете работать с запросом, конечно.
Джон Ву
@candiedorange Это не побочные эффекты, а желаемые эффекты вызова метода. Если вы не хотите сразу добавлять, вы создаете другой метод, который удовлетворяет сценарию использования. Но это не тот случай использования, который был передан в вопросе, так что говорите, что проблема не важна.
Энди
@candiedorange Если связывание делает код клиента проще, я бы сказал, что это нормально. То, что разработчики или pos могут думать в будущем, не имеет значения. Если это время наступит, вы измените код. Нет ничего более расточительного, чем пытаться создать код, который может обработать любой возможный вариант использования в будущем.
Энди
5
@CandiedOrange Я предлагаю вам не пытаться тесно связать четко определенную систему типов реализации с концептуальными, простыми английскими сущностями бизнеса. Бизнес-концепция «пользователя», безусловно, может быть реализована в двух классах, если не больше, для представления функционально значимых вариантов. Пользователю, который не является постоянным, не гарантируется уникальное имя пользователя, у него нет первичного ключа, к которому могут быть привязаны транзакции, и он даже не может войти в систему, поэтому я бы сказал, что он содержит существенный вариант. Для непрофессионала, конечно, и EnrollRequest, и Пользователь являются «пользователями» на их расплывчатом языке.
Джон Ву,
5

Побочные эффекты в порядке, пока они не приходят неожиданно. В общем, нет ничего плохого в том, что в хранилище есть метод, принимающий пользователя и изменяющий внутреннее состояние пользователя. Но ИМХО имя метода вроде InsertUser не очень ясно говорит об этом , и это делает его подверженным ошибкам. При использовании вашего репо я бы ожидал такой звонок

 repo.InsertUser(user);

изменить внутреннее состояние репозитория, а не состояние объекта пользователя. Эта проблема существует в обеих ваших реализациях, как InsertUserэто внутренне не имеет никакого значения.

Чтобы решить эту проблему, вы можете либо

  • отделяйте инициализацию от вставки (так что вызывающая сторона InsertUserдолжна предоставить полностью инициализированный Userобъект, или,

  • встроить инициализацию в процесс построения пользовательского объекта (как предлагается в некоторых других ответах), или

  • просто попробуйте найти лучшее имя для метода, который более четко выражает, что он делает.

Так выбрать имя метода , как PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordили все , что вы предпочитаете , чтобы сделать побочный эффект более ясным.

Конечно, такое длинное имя метода означает, что метод, возможно, выполняет «слишком много» (нарушение SRP), но иногда вы не хотите или не можете легко это исправить, и это наименее навязчивое, прагматичное решение.

Док Браун
источник
3

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

В любом случае вы устанавливаете user.UserNameи user.Password. У меня есть оговорки по поводу элемента пароля, но эти оговорки не имеют отношения к данной теме.

Последствия изменения ссылочных объектов

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

Старый метод против нового метода

Старый метод облегчил тестирование:

  • GenerateUserName()независимо проверяется Вы можете написать тесты для этого метода и убедиться, что имена генерируются правильно
  • Если имя требует информацию от объекта пользователя, вы можете изменить подпись GenerateUserName(User user)и поддерживать эту тестируемость

Новый метод скрывает мутации:

  • Вы не знаете, что Userобъект меняется, пока вы не пройдете 2 слоя
  • Изменения в Userобъекте более удивительны в этом случае
  • SetUserName()делает больше, чем установить имя пользователя. Это не правда в рекламе, из-за которой новым разработчикам становится сложнее понять, как все работает в вашем приложении.
Берин Лорич
источник
1

Я полностью согласен с ответом Джона Ву. Его предложение хорошее. Но это немного не соответствует вашему прямому вопросу.

В принципе, является ли практика установки значения свойства из другого метода плохой практикой?

Не по своей сути.

Вы не можете зайти слишком далеко, потому что столкнетесь с неожиданным поведением. Например, PrintName(myPerson)не следует изменять объект person, поскольку метод подразумевает, что он заинтересован только в чтении существующих значений. Но это другой аргумент, чем в вашем случае, поскольку SetUsername(user)настоятельно подразумевает, что он собирается устанавливать значения.


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

Например:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Я явно ожидаю, что ArrrangeContractDeletedStatusметод изменит состояние myContractобъекта.

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

Если бы я слился CreateEmptyContractи ArrrangeContractDeletedStatusв единый метод; Мне пришлось бы создать несколько вариантов этого метода для каждого контракта, который я хотел бы проверить в удаленном состоянии.

И хотя я мог сделать что-то вроде:

myContract = ArrrangeContractDeletedStatus(myContract);

Это либо избыточно (так как я все равно меняю объект), либо я заставляю себя сделать глубокий клон myContractобъекта; что является чрезмерно трудным, если вы хотите охватить каждый случай (представьте, если мне нужны навигационные свойства на несколько уровней. Нужно ли клонировать их все? Только сущность верхнего уровня? ... Так много вопросов, так много неявных ожиданий )

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


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

Flater
источник
0

Я нахожу старое и новое проблематичным.

Старый способ:

1) InsertUser(User user)

Когда я читаю название этого метода, всплывающее в моей IDE, первое, что приходит мне в голову, это

Куда вставляется пользователь?

Имя метода должно читаться AddUserToContext. По крайней мере, это то, что метод делает в конце.

2) InsertUser(User user)

Это явно нарушает принцип наименьшего удивления. Скажем, я до сих пор выполнял свою работу и создал свежий экземпляр a, Userдал ему a nameи set a password, я был бы удивлен:

а) это не только вставить пользователя во что-то

б) это также подрывает мое намерение вставить пользователя как есть; имя и пароль были переопределены.

в) означает ли это, что это также является нарушением принципа единой ответственности.

Новый путь:

1) InsertUser(User user)

Еще нарушение СРП и принцип наименьшего удивления

2) SetUsername(User user)

Вопрос

Установить имя пользователя? К чему?

Лучше: SetRandomNameили что-то, что отражает намерение.

3) SetPassword(User user)

Вопрос

Установка пароля? К чему?

Лучше: SetRandomPasswordили что-то, что отражает намерение.


Предложение:

То, что я предпочел бы читать было бы что-то вроде:

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

По поводу вашего первоначального вопроса:

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

Нет, это больше вопрос вкуса.

Томас Джанк
источник