Передача об'єкта методу, який змінює об'єкт, чи це загальна (анти-) модель?


17

Я читаю про поширені запахи коду в книзі "Рефакторинг" Мартіна Фаулера . У цьому контексті мені було цікаво про шаблон, який я бачу в кодовій базі, і тим більше можна об'єктивно вважати його антидіаграмою.

Шаблон - це те, коли об’єкт передається як аргумент одному чи декільком методам, всі вони змінюють стан об'єкта, але жоден з яких не повертає об'єкт. Таким чином, він покладається на пропуск за посилальним характером (у даному випадку) C # /. NET.

var something = new Thing();
// ...
Foo(something);
int result = Bar(something, 42);
Baz(something);

Я вважаю, що (особливо коли методи не названі належним чином), мені потрібно вивчити такі методи, щоб зрозуміти, чи змінився стан об'єкта. Це робить розуміння коду більш складним, оскільки мені потрібно відстежувати кілька рівнів стека викликів.

Я хотів би запропонувати вдосконалити такий код, щоб повернути інший (клонований) об'єкт з новим станом або все, що потрібно для зміни об'єкта на сайті виклику.

var something1 =  new Thing();
// ...

// Let's return a new instance of Thing
var something2 = Foo(something1);

// Let's use out param to 'return' other info about the operation
int result;
var something3 = Bar(something2, out result);

// If necessary, let's capture and make explicit complex changes
var changes = Baz(something3)
something3.Apply(changes);

Мені здається, перша модель обрана на припущеннях

  • що це менше роботи або вимагає менше рядків коду
  • що це дозволяє нам як змінити об’єкт, так і повернути якусь іншу інформацію
  • що це більш ефективно, оскільки у нас менше примірників Thing.

Я ілюструю альтернативу, але щоб її запропонувати, потрібно мати аргументи проти оригінального рішення. Які, якщо такі є, аргументи можна зробити, щоб зробити так, що оригінальне рішення є антидіаграмою?

І що, якщо що, не так з моїм альтернативним рішенням?



1
@DaveHillier Спасибі, я був знайомий з терміном, але не зв’язався.
Міхель ван Остерхаут

Відповіді:


9

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

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

Якщо об'єкти не є невеликими і дещо перехідними (що робить їх хорошими кандидатами на незмінність), я був би схильний просто перенести більше стану переходу в самі об'єкти. Це дозволяє приховати деталі реалізації цих переходів та встановити більш жорсткі вимоги щодо того, хто / що / де відбувається ці переходи.


1
Наприклад, якщо у мене є об'єкт "Файл", я б не намагався перемістити будь-який метод зміни стану у цей об'єкт - це порушило б SRP. Це залишається дійсним навіть тоді, коли у вас є власні класи замість класу бібліотеки типу "Файл" - введення кожної логіки переходу стану в клас об'єкта насправді не має сенсу.
Док Браун

@Tetastyn Я знаю, що це стара відповідь, але у мене виникають проблеми з візуалізацією вашої пропозиції в останньому абзаці конкретно. Чи можете ви детальніше розглянути чи навести приклад?
AaronLS

@AaronLS - замість Bar(something)(і змінити стан something), зробіть Barчлен типу something's. something.Bar(42)більше шансів мутувати something, а також дозволяє використовувати засоби OO (приватний стан, інтерфейси тощо) для захисту свого somethingстану
Теластин

14

коли методи не названі належним чином

На насправді, що це реальний код запах. Якщо у вас є об'єкт, що змінюється, він надає методи зміни свого стану. Якщо у вас є заклик до такого методу, вкладеного у завдання ще декількох висловлювань, цілком непогано переробити це завдання на власний метод - який залишає вас в точно описаній ситуації. Але якщо у вас немає імен методів , як Fooі Bar, але методи , які роблять очевидним , що вони змінюють об'єкт, я не бачу тут проблеми. Подумай

void AddMessageToLog(Logger logger, string msg)
{
    //...
}

або

void StripInvalidCharsFromName(Person p)
{
// ...
}

або

void AddValueToRepo(Repository repo,int val)
{
// ...
}

або

void TransferMoneyBetweenAccounts(Account source, Account destination, decimal amount)
{
// ...
}

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

Якщо ви не хочете побічних ефектів, зробіть свої об'єкти непорушними, вони застосовуватимуть такі методи, як перелічені вище, для повернення зміненого (клонованого) об'єкта без зміни вихідного.


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

2
@michielvoo: якщо іменування методів consise видається неможливим, ваш метод об'єднує неправильні речі замість того, щоб будувати функціональну абстракцію для завдання, яку вона виконує (і це правда з побічними ефектами або без них).
Док Браун

4

Так, дивіться на http://codebetter.com/matthewpodwysocki/2008/04/30/side-effecting-functions-are-code-smells/ один із багатьох прикладів людей, які вказують, що несподівані побічні ефекти є поганими.

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


Я б охарактеризував те, що ОП характеризує як "очікувані побічні ефекти". Наприклад, делегата ви можете передати в движок певного типу, який працює над кожним елементом у списку. Це в основному те, що ForEach<T>робить.
Роберт Харві

@RobertHarvey Скарги на неправильне призначення методів та необхідність читати код, щоб з’ясувати побічні ефекти, змушує їх точно не очікувати побічних ефектів.
btilly

Я вам це дозволю. Але наслідком цього є те, що правильно названий документований метод із очікуваними ефектами на сайті може бути не анти-зразком.
Роберт Харві

@RobertHarvey Я згоден. Ключовим моментом є те, що про важливі побічні ефекти дуже важливо знати, і їх потрібно ретельно документувати (бажано від назви методу).
btilly

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

3

Перш за все, це не залежить від "проходження посилальної природи", це залежить від об'єктів, що є змінними еталонними типами. У нефункціональних мовах це майже завжди буде так.

По-друге, чи це проблема, чи ні, залежить як від об'єкта, так і від того, наскільки тісно зміни в різних процедурах пов'язані між собою - якщо ви не зможете внести зміни в Foo і це призведе до краху Bar, то це проблема. Не обов'язково запах коду, але це проблема або з Foo, або Bar або чимось (можливо, Bar як слід перевірити його введення, але це може бути щось, що перебуває у недійсному стані, що воно повинно перешкоджати).

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


2

Я зауважу, що між A.Do(Something)модифікацією somethingта something.Do()модифікацією невелика різниця something. У будь-якому випадку, це повинно бути зрозуміло з назви виклику методу, який somethingбуде змінено. Якщо з назви методу не зрозуміло, незалежно від того, чи somethingє параметр this, чи частина середовища, він не повинен бути змінений.


1

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

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();

var excludeAdminUsersFilter = new ExcludeAdminUsersFilter();
var filterByAnotherCriteria = new AnotherCriteriaFilter();

excludeAdminUsersFilter.Apply(users);
filterByAnotherCriteria.Apply(users); 

І так, ви можете зробити це досить, перемістивши фільтрування в інший метод, так що ви закінчите щось із рядків:

var users = Dependency.Resolve<IGetUsersQuery>().GetAll();
Filter(users);

Де Filter(users)виконувати вищевказані фільтри.

Я не пам'ятаю, де саме я раніше стикався з цим, але думаю, що це називалося фільтруючим трубопроводом.


0

Я не впевнений, чи запропоноване нове рішення (копіювання об'єктів) є зразком. Проблема, як ви вказали, полягає в поганій номенклатурі функцій.

Скажімо, я записую складну математичну операцію як функцію f () . Задокументувати , що F () є функцією , яка відображає NXNна N, і алгоритм позаду нього. Якщо функція названа неправильно, і не задокументована, і не має супровідних тестових випадків, і вам доведеться зрозуміти код, у такому випадку код марний.

Про ваше рішення, деякі спостереження:

  • Програми розроблені з різних аспектів: коли об’єкт використовується лише для утримування значень або передається через межі компонентів, розумно змінити внутрішню частину об'єкта, а не заповнити його деталями, як змінити.
  • Клонування об'єктів призводить до роздуття пам'яті вимог, а в багатьох випадках призводить до існування еквівалентних об'єктів у несумісних станах ( Xстали Yпісля f(), але Xнасправді є Y) і, можливо, тимчасової невідповідності.

Проблема, яку ви намагаєтеся вирішити, є дійсною; однак, навіть з величезним переобладнанням, проблему обходять, не вирішують.


2
Це буде кращою відповіддю, якби ви пов’язали своє спостереження з питанням ОП. Як це - це скоріше коментар, ніж відповідь.
Роберт Харві

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