Чи модифікація об'єкта, переданого посиланням, є поганою практикою?


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;
}

По суті, чи є практика встановлення вартості властивості за допомогою іншого методу поганою практикою?


6
Просто так сказано ... ви тут нічого не пройшли через посилання. Передача посилань за значенням - це зовсім не одне і те ж.
cHao

4
@cHao: Це різниця без різниці. Поведінка коду однакова незалежно.
Роберт Харві

2
@JDDavis: По суті, єдиною реальною різницею двох ваших прикладів є те, що ви дали значуще ім’я встановленому імені користувача та встановили дії пароля.
Роберт Харві

1
Усі ваші дзвінки тут наведені за посиланням. Для передачі значення потрібно використовувати тип значення в C #.
Френк Хілеман

2
@FrankHileman: Тут усі дзвінки мають значення. Це за замовчуванням у C #. Передача в посилання і переходячи по посиланню різні звірі, і різниця має значення. Якщо userбули передані по посиланню, код може смикати його з рук викликають і замінити його, просто кажучи, скажімо, user = null;.
cHao

Відповіді:


10

Проблема тут полягає в тому, що балончик Userможе містити дві різні речі:

  1. Повна особа Користувача, яка може бути передана до вашого сховища даних.

  2. Набір елементів даних, необхідних для абонента, щоб розпочати процес створення об'єкта Користувача. Система повинна додати ім’я користувача та пароль, перш ніж він буде дійсно дійсним Користувачем, як у №1 вище.

Це включає незадокументований нюанс вашої об'єктної моделі, який зовсім не виражений у вашій системі типу. Вам просто потрібно "знати" це як розробник. Це не чудово, і це призводить до дивних моделей коду, як той, з яким ви стикаєтесь.

Я б запропонував, що вам потрібні два об'єкти, наприклад, Userклас та EnrollRequestклас. Останній може містити все, що потрібно знати, щоб створити Користувача. Це виглядатиме як ваш клас користувача, але без імені користувача та пароля. Тоді ви могли зробити це:

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імовірно, матиме побічний ефект від вставки. Я не впевнений, що в такому контексті означає «неприємний». Що стосується використання "користувача", який не був доданий, то, здається, ви пропустили суть. Якщо його не додано, це не користувач, а лише запит на створення користувача. Ви, звичайно, можете працювати з проханням.
Джон Ву

@candiedorange Це не побічні ефекти, вони бажані ефекти виклику методу. Якщо ви не хочете одразу додавати, ви створюєте інший метод, який задовольняє випадок використання. Але це не той випадок використання, який передається у питанні, тому говорять, що турбота не важлива.
Енді

@candiedorange Якщо з'єднання полегшує код клієнта, я б сказав, що це добре. Про те, що можуть подумати в майбутньому, не має значення. Якщо цей час настає, ви змінюєте код. Ніщо не є марнішим, ніж намагатися створити код, який може працювати з будь-яким можливим майбутнім випадком використання.
Енді

5
@CandiedOrange Я пропоную вам спробувати не чітко поєднувати точно визначену тип системи впровадження з концептуальними, просто англійськими суб'єктами бізнесу. Концепція бізнесу "користувача", безумовно, може бути реалізована в двох класах, якщо не більше, щоб представити функціонально значущі варіанти. Користувачеві, який не зберігається, не гарантується унікальне ім’я користувача, не має первинного ключа, до якого транзакції можуть бути прив'язані, і навіть не може ввійти, тому я б сказав, що він містить вагомий варіант. Миряни, звичайно, і EnrollRequest, і Користувач є "користувачами" на своїй розпливчастій мові.
Джон Ву

5

Побічні ефекти в порядку, якщо вони не набувають несподіваних. Тому в цілому немає нічого поганого, коли сховище має метод прийому користувача та зміна внутрішнього стану користувача. Але ім'я методу IMHO подібне InsertUser не чітко повідомляє про це , і саме це робить його схильним до помилок. Під час користування вашим репо, я очікував би подібного дзвінка

 repo.InsertUser(user);

щоб змінити внутрішній стан репозиторіїв, а не стан об'єкта користувача. Ця проблема існує в обох ваших реалізаціях, тому InsertUserщо це внутрішньо абсолютно не має значення.

Вирішити це ви могли

  • відокремити ініціалізацію від вставки (так що абонент InsertUserповинен надати повністю ініціалізований Userоб'єкт, або,

  • вбудувати ініціалізацію в процес побудови об'єкта користувача (як це запропоновано в деяких інших відповідях), або

  • просто спробуйте знайти кращу назву методу, який чіткіше виражає те, що він робить.

Так вибрати ім'я методу , як PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordабо все , що ви віддаєте перевагу , щоб зробити побічний ефект більш ясним.

Звичайно, така довга назва методу вказує на те, що метод, можливо, робить "занадто багато" (порушення SRP), але іноді ви цього не хочете або не можете легко виправити, і це найменш нав'язливе, прагматичне рішення.


3

Я дивлюся на два обрані вами варіанти, і я повинен сказати, що я напевне віддаю перевагу старому методу, а не запропонованому новому методу. Для цього є кілька причин, хоча вони принципово роблять те саме.

В будь-якому випадку ви встановлюєте user.UserNameі user.Password. У мене є застереження щодо елемента пароля, але ці застереження не залежать від теми.

Наслідки модифікації опорних об'єктів

  • Це ускладнює одночасне програмування - але не всі програми багатопотокові
  • Ці модифікації можуть викликати подив, особливо якщо нічого про метод не говорить про те, що це відбудеться
  • Ці сюрпризи можуть ускладнити обслуговування

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

Старий метод спростив тестування:

  • GenerateUserName()незалежно перевіряється. Ви можете написати тести проти цього методу і переконатися, що імена генеруються правильно
  • Якщо ім'я вимагає інформації від об'єкта користувача, то ви можете змінити підпис GenerateUserName(User user)і підтримувати цю перевірку

Новий метод приховує мутації:

  • Ви не знаєте, що Userоб’єкт змінюється, поки ви не заглибитеся в два шари
  • Зміни Userоб’єкта в цьому випадку більш дивовижні
  • SetUserName()робить більше, ніж встановити ім’я користувача. Це не правда в рекламі, яка ускладнює новим розробникам виявити, як все працює у вашій програмі

1

Я повністю згоден з відповіддю Джона Ву. Його пропозиція хороша. Але це трохи не вистачає вашого прямого питання.

По суті, чи є практика встановлення вартості властивості за допомогою іншого методу поганою практикою?

Не по суті.

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


Це фактично підхід, який я часто використовую для тестів одиниці / інтеграції, де я створюю метод спеціально для зміни об'єкта з метою встановлення його значень для конкретної ситуації, яку я хочу перевірити.

Наприклад:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Я явно очікую, що ArrrangeContractDeletedStatusметод змінить стан myContractоб’єкта.

Основна перевага полягає в тому, що метод дозволяє перевірити вилучення контракту з різними початковими контрактами; наприклад, контракт, який має довгу історію статусу, або той, який не має попередньої історії статусу, контракт, який має навмисно помилкову історію статусу, контракт, який мій користувач-тестувач не може видаляти.

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

І хоча я могла зробити щось на кшталт:

myContract = ArrrangeContractDeletedStatus(myContract);

Це або зайве (оскільки я все-таки змінюю об'єкт), або зараз змушую себе зробити глибокий клон myContractоб'єкта; що надмірно складно, якщо ви хочете висвітлити кожен випадок (уявіть, чи я хочу навігаційних властивостей на кілька рівнів. Чи потрібно мені їх клонувати? Тільки сутність найвищого рівня? ... Стільки питань, стільки неявних очікувань )

Зміна об'єкта - це найпростіший спосіб отримати те, що я хочу, без того, щоб робити більше роботи, щоб не змінити об'єкт як принципова.


Тож пряма відповідь на ваше запитання полягає в тому, що це не є по суті поганою практикою, доки ви не заперечуєте, що метод може змінити переданий об'єкт. Для вашої поточної ситуації це зрозуміло через назву методу.


0

Я вважаю проблематику старого та нового.

Старий шлях:

1) InsertUser(User user)

Під час читання цього методу, що спливе в моєму IDE, перше, що мені спадає на думку

Куди вставляється користувач?

Назва методу повинна читати AddUserToContext. Принаймні, це те, що робить метод зрештою.

2) InsertUser(User user)

Це явно порушує принцип найменшого здивування. Скажіть, я робив свою роботу до цих пір, і створив свіжий екземпляр Userа, дав йому nameта встановив 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);
}

Щодо вашого початкового запитання:

Чи модифікація об'єкта, переданого посиланням, є поганою практикою?

Ні. Це більше питання смаку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.