Проектування класу для прийняття цілих класів як параметрів, а не окремих властивостей


30

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

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

Клас, який потребує ідентифікатора користувача, просто вимагатиме GUID userIdяк параметр, іноді може знадобитися і ім’я користувача, так що він передається як окремий параметр. У деяких випадках це передається індивідуальним методам, тому значення взагалі не утримуються на рівні класу.

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

Користувач - лише один приклад. Це широко практикується в нашому коді.

Я маю рацію, думаючи, що це порушення принципу "Відкрито / закрито"? Не просто акт зміни існуючих класів, але їх налаштування в першу чергу, щоб в майбутньому, швидше за все, були потрібні широкі зміни?

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

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

Чи порушуються інші, несолідні принципи, такі як основні принципи оборонного програмування?

Чи повинен мій конструктор виглядати так:

MyConstructor(GUID userid, String username)

Або це:

MyConstructor(User theUser)

Редагування публікації:

Запропоновано відповісти на запитання у "Передати ідентифікатор чи об'єкт?". Це не дає відповіді на питання про те, як рішення піти в будь-якому випадку впливає на спробу слідувати принципам SOLID, що лежить в основі цього питання.


11
@gnat: Це точно не дублікат. Можливий дублікат стосується методу ланцюга, щоб потрапити вглиб ієрархії об'єктів. Схоже, це питання взагалі не задають цього питання.
Грег Бургхардт

2
Друга форма часто використовується, коли кількість параметрів, що передаються, стала непростим.
Роберт Харві

12
Єдине, що мені не подобається у першому підписі, це те, що немає гарантії того, що userId та ім’я користувача насправді походять від одного користувача. Це потенційна помилка, якої можна уникнути, обходячи всюди користувача. Але рішення дійсно залежить від того, що покликані методи роблять з аргументами.
17 з 26

9
Слово "розбір" не має сенсу в контексті, в якому ви його використовуєте. Ви замість цього мали на увазі "пройти"?
Конрад Рудольф

5
Що про Iв SOLID? MyConstructorв основному каже зараз "мені потрібно Guidі string". То чому б не мати інтерфейс, що забезпечує a Guidі a string, нехай Userреалізує цей інтерфейс і нехай MyConstructorзалежить від екземпляра, що реалізує цей інтерфейс? І якщо потреби MyConstructorзмінити, змінити інтерфейс. - Це мені дуже допомогло думати про інтерфейси, щоб «належати» споживачеві, а не постачальнику . Тому подумайте, "як споживач мені потрібен щось, що робить це і те", а не "як постачальник, я можу робити це і те".
Корак

Відповіді:


31

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

Передавати прості типи даних приємно, поки вони означають щось інше, ніж те, що вони є. Розглянемо цей приклад:

public class Foo
{
    public void Bar(int userId)
    {
        // ...
    }
}

І приклад використання:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user.Id);

Чи можете ви помітити дефект? Компілятор не може. "Ідентифікатор користувача", який передається, є лише цілим числом. Ми називаємо змінну, userале ініціалізуємо її значення з blogPostRepositoryоб'єкта, який, імовірно, повертає BlogPostоб'єкти, а не Userоб’єкти - все-таки код компілюється, і ви закінчуєтеся з невмілою помилкою виконання.

Тепер розглянемо цей змінений приклад:

public class Foo
{
    public void Bar(User user)
    {
        // ...
    }
}

Можливо, Barметод використовує лише "ідентифікатор користувача", але для підпису методу потрібен Userоб'єкт. Тепер повернемося до того ж прикладу використання, що і раніше, але внесемо зміни, щоб передати всього "користувача" у:

var user = blogPostRepository.Find(32);
var foo = new Foo();

foo.Bar(user);

Тепер у нас є помилка компілятора. blogPostRepository.FindМетод повертає BlogPostоб'єкт, який ми називаємо хитро «користувач». Потім ми передаємо цього "користувача" Barметоду і негайно отримуємо помилку компілятора, тому що ми не можемо передати BlogPosta методу, який приймає a User.

Система типу мови використовується для швидшого запису правильного коду та виявлення дефектів під час компіляції, а не часу виконання.

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


6
Я б сказав, що ваше міркування саме по собі насправді вказує на проходження полів, але наявність полів є тривіальними обгортками навколо реальної величини. У цьому прикладі у користувача є поле типу UserID, а у UserID є одне ціле поле з цілим числом. Тепер декларація Bar відразу говорить вам, що Bar не використовує всю інформацію про Користувача, лише їх ідентифікатор, але ви все одно не можете робити жодних нерозумних помилок, як-от передача цілого числа, яке не прийшло з UserID, у Bar.
Ян

(Проти.) Звичайно, такий стиль програмування є досить стомлюючим, особливо мовою, яка не має приємної синтаксичної підтримки для нього (Haskell приємний для цього стилю, наприклад, оскільки ви можете просто відповідати "UserID id") .
Ян

5
@Ian: Я думаю, що обертання Id власних типів ковзанів навколо оригінальної проблеми, піднятої ОП, яка, завдяки структурним змінам класу User, потребує рефакторації багатьох підписів методів. Проходження всього об’єкта User вирішує цю проблему.
Грег Бургхардт

@Ian: Хоча, чесно кажучи, навіть працюючи в C #, я дуже спокусився загортати ідентифікатори та сорти в Struct лише для того, щоб надати трохи більше ясності.
Грег Бургхардт

1
"немає нічого поганого в тому, щоб перевести вказівник на його місці." Або посилання, щоб уникнути всіх проблем із покажчиками, на які ви могли зіткнутися.
Yay295

17

Я маю рацію, думаючи, що це порушення принципу "Відкрито / закрито"?

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

Чи порушено цю практику якісь інші принципи? Інверсія залежності залежна від можливостей?

Ні. Те, що ви описуєте - лише вводячи потрібні частини об'єкта користувача у кожен метод - це навпаки: це чиста інверсія залежності.

Чи порушуються інші, несолідні принципи, такі як основні принципи оборонного програмування?

Ні. Цей підхід є цілком коректним способом кодування. Це не порушує таких принципів.

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

Щоб відповісти на ваш коментар:

Існують проблеми з необхідністю розбору нового значення вниз на п’ять рівнів ланцюга, а потім змінити всі посилання на всі п’ять цих існуючих методів ...

Частина питання тут полягає в тому, що вам, очевидно, не подобається такий підхід, відповідно до коментаря "зайво [пройти] ...". І це досить справедливо; тут немає правильної відповіді. Якщо ви вважаєте це обтяжливим, тоді не робіть цього так.

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

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

Але тоді ти блукаєш на територію ЯГНІ, і це все одно буде ортогональним принципом. Якщо у вас є метод, Fooякий містить ім’я користувача, а потім ви також хочете Fooвизначити дату народження, дотримуючись принципу, ви додаєте новий метод; Fooзалишається незмінним. Знову ж, це хороша практика для публічних API, але це внутрішній код - це нісенітниця.

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


Існують проблеми з необхідністю розбору нового значення вниз на п’ять рівнів ланцюга, а потім змінити всі посилання на всі п’ять цих існуючих методів. Чому принцип Open / Closed застосовується лише до класу User, а не до класу, який я зараз редагую, який також споживається іншими класами? Я знаю, що принцип полягає саме у тому, щоб уникнути змін, але, безумовно, план дотримання цього принципу, наскільки це можливо, включав би стратегію зменшення потреби в майбутніх змінах?
Джимбо

@Jimbo, я оновив свою відповідь, щоб спробувати вирішити ваш коментар.
Девід Арно

Я ціную ваш внесок. До речі. Навіть Роберт Мартін не приймає принципу "Відкритого / закритого", яке має жорстке правило. Це правило, яке неминуче буде порушено. Застосування принципу - це вправа намагатися дотримуватися його настільки, наскільки це можливо. Тому я раніше вживав слово "практичний".
Джимбо

Це не інверсія залежності для передачі параметрів Користувача, а не самого Користувача.
Джеймс Елліс-Джонс

@ Джеймс Елліс-Джонс, інверсія залежності перевертає залежності від "запитувати" до "розповідати". Якщо ви передаєте Userекземпляр, а потім запитаєте цей об'єкт, щоб отримати параметр, ви лише частково інвертуєте залежності; все ще виникають запитання. Справжня інверсія залежності становить 100% "скажи, не питай". Але це за ціною складності.
Девід Арно

10

Так, зміна існуючої функції - це порушення принципу відкритого / закритого типу. Ви змінюєте щось, що має бути закрито до модифікації через зміну вимог. Кращим дизайном (не змінюватись, коли вимоги змінюються) було б передати Користувачеві речі, які повинні працювати на користувачів.

Але це може зіткнутися з принципом розбиття інтерфейсу, оскільки ви можете передавати по шляху більше інформації, ніж функція повинна виконувати свою роботу.

Отже, як і у більшості речей - це залежить .

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

Використання всього Користувача робить його більш зрозумілим щодо використання та укладає більш чіткий договір зі своїми абонентами. Він забезпечує стійкість до змін, якщо ви думаєте, що буде потрібно більше Користувача.


+1, але я не впевнений у вашій фразі "ви могли б передавати більше інформації". При передачі (Користувач користувача) ви передаєте самий мінімум інформації, посилання на один об'єкт. Правда, ця посилання може бути використана для отримання додаткової інформації, але це означає, що код виклику не повинен її отримувати. Під час передачі (GUID userid, рядкове ім'я користувача) викликаний метод завжди може викликати User.find (userid), щоб знайти загальнодоступний інтерфейс об'єкта, тому ви нічого не хочете.
декор

5
@dcorking, " Коли ви передаєте (Користувач користувач), ви передаєте самий мінімум інформації, посилання на один об'єкт ". Ви передаєте максимум інформації, що стосується цього об'єкта: всього об’єкта. " викликаний метод завжди може викликати User.find (userid) ...". У добре розробленій системі це було б неможливо, оскільки відповідний метод не мав би доступу до нього User.find(). Насправді навіть не повинно бутиUser.find . Пошук користувача ніколи не несе відповідальності User.
Девід Арно

2
@dcorking - enh. Те, що ви передаєте посилання, яке, мабуть, є невеликим - це технічний збіг. Ви з'єднуєте ціле Userз функцією. Можливо, це має сенс. Але, можливо, ця функція повинна піклуватися лише про ім'я користувача - а також передавати такі речі, як дата приєднання користувача або адреса неправильна.
Теластин

@DavidArno, можливо, це і є ключовим для чіткої відповіді на ОП. Чия відповідальність повинна знаходити користувача? Чи існує назва дизайнерського принципу відділення пошуку / фабрики від класу?
декор

1
@dcorking Я б сказав, що це одне із наслідків Принципу єдиної відповідальності. Знання, де користувачі зберігаються та як їх отримати за допомогою ідентифікатора, є окремими обов'язками, які User-клас не повинен мати. Можливо, є щось UserRepositoryподібне, яке займається подібними речами.
Халк

3

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

Я маю рацію, думаючи, що це порушення принципу "Відкрито / закрито"?

Ні. Застосування цього шаблону дає можливість відкрити / закрити (OCP). Наприклад, похідні класи класу Userможуть бути надані як параметр, який індукує різну поведінку у споживчому класі.

Чи порушено цю практику якісь інші принципи?

Це може статися. Дозвольте пояснити на основі принципів SOLID.

Принцип єдиної відповідальності (SRP) може бути порушений, якщо він має дизайн, як ви пояснили:

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

Проблема полягає у всій інформації . Якщо Userклас має багато властивостей, він стає величезним об'єктом передачі даних, який переносить непов’язану інформацію з точки зору споживчих класів. Приклад: з точки зору споживчого класу UserAuthenticationвластивість User.Idта User.Nameрелевантність, але ні User.Timezone.

Принцип сегрегації інтерфейсу (ISP) також порушується з подібними міркуваннями, але додає іншої точки зору. Приклад: Припустимо, що споживчий клас UserManagementвимагає User.Nameрозділити властивість до, User.LastNameі User.FirstNameклас також UserAuthenticationповинен бути змінений для цього.

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

class User : IUserAuthenticationInfo, IUserLocationInfo { ... }

Кожен інтерфейс повинен виявляти підмножину пов'язаних властивостей Userкласу, необхідних для споживчого класу для повного заповнення його роботи. Шукайте кластери властивостей. Спробуйте повторно використовувати інтерфейси. У випадку споживчого класу UserAuthenticationвикористовувати IUserAuthenticationInfoзамість User. Тоді, якщо можливо, розбийте Userклас на кілька конкретних класів, використовуючи інтерфейси як "трафарет".


1
Коли користувач ускладнюється, відбувається комбінаторний вибух можливих підінтерфейсів, наприклад, якщо у користувача є лише 3 властивості, є 7 можливих комбінацій. Ваша пропозиція звучить приємно, але нездійсненна.
user949300

1. Аналітично ви праві. Однак залежно від способу моделювання домену біти пов'язаної інформації мають тенденцію до кластеризації. Тому практично не варто мати справу з усіма можливими комбінаціями інтерфейсів та властивостей. 2. Окреслений підхід не мав бути універсальним рішенням, але, можливо, я маю додати у відповідь ще кілька можливих та «можливих».
Тео Лендорфф

2

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

Поширеним прикладом може бути модель сховища. Часто під час запитів до бази даних через сховища багато методів у сховищі приймають багато однакових параметрів.

Мої правила щодо сховищ:

  • Якщо більше одного методу приймає однакові 2 або більше параметрів, параметри повинні бути згруповані як об'єкт моделі.

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

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


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

І так, цілком прекрасно мати дві різні моделі з однаковими властивостями, які обслуговують різні шари / цілі (тобто ViewModels проти POCO).


2

Давайте просто перевіримо окремі аспекти SOLID:

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

Одне, що має тенденцію заплутати дизайнерські інстинкти - це те, що клас по суті є глобальними об'єктами, а по суті лише для читання. У такій ситуації порушення абстракцій не дуже шкодить: просто читання даних, які не модифікуються, створює досить слабку зв'язок; тільки коли вона стає величезною ворсом, біль стає помітною.
Щоб відновити інстинкти дизайну, просто припустіть, що об'єкт не дуже глобальний. Який контекст потребує функції, якщо Userоб'єкт можна було б мутувати будь-коли? Які компоненти об'єкта, ймовірно, мутували б разом? Їх можна розділити User, будь то посилання на суб'єкт або як інтерфейс, який відкриває лише "фрагмент" суміжних полів, не так важливо.

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

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

Розщеплення Userнасправді вийде некрасивим, якщо суб’єкти перекриваються, тоді люди будуть плутати, яке саме вибрати, якщо всі необхідні поля будуть із перекриття. Якщо ви розділите ієрархічно (наприклад, у вас є, UserMarketSegmentщо, серед іншого, є UserLocation), люди будуть не впевнені, на якому рівні знаходиться функція, про яку вони пишуть: чи це стосується даних користувачів на Location рівні чи на MarketSegmentрівні? Це не зовсім допомагає, що це може змінитися з часом, тобто ви знову змінюєте підписи функцій, іноді в цілому ланцюзі викликів.

Іншими словами: Якщо ви дійсно не знаєте свого домену та не маєте чіткого уявлення про те, який модуль має справу з якими аспектами User, покращувати структуру програми насправді не варто.


1

Це справді цікаве питання. Це все залежить.

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

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

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


1

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

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

interface IIdentifieable
{
    Guid ID { get; }
}

або

interface INameable
{
    string Name { get; }
}

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


1

Ось, з чим я стикався час від часу:

  • Метод бере аргумент типу User(або Productбудь-якого іншого), який має безліч властивостей, хоча метод використовує лише декілька з них.
  • Чомусь деяка частина коду потребує виклику цього методу, навіть якщо він не має повністю заселеного Userоб'єкта. Він створює екземпляр та ініціалізує лише ті властивості, які насправді потребує метод.
  • Це трапляється купа разів.
  • Через деякий час, коли ви стикаєтесь з методом, який має Userаргумент, вам виникає необхідність знаходити дзвінки до цього методу, щоб знайти, звідки Userпоходить, щоб ви дізналися, які властивості заселяються. Це "справжній" користувач з адресою електронної пошти, чи він просто створений для передачі ідентифікатора користувача та деяких дозволів?

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

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

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

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

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

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