Якщо вам завжди потрібно передавати мінімум необхідних даних у функції у таких випадках


81

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

Тоді в моїй голові є два можливі підписи функції для цього:

public bool IsAdmin(User user);
public bool IsAdmin(int id, string name, string password);

Я найчастіше берусь за другий тип підпису, думаючи, що:

  • Підпис функції дає читачеві набагато більше інформації
  • Логіка, що міститься у функції, не повинна знати про Userклас
  • Зазвичай це призводить до дещо меншого коду всередині функції

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

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

Я програмую як в об'єктно-орієнтованому, так і у функціональному стилях, тому питання слід розглядати як стосовно будь-яких ідіом.


40
Поза темою: чому цікаво, чому статус користувача "адміністратора" залежить від їхнього пароля?
stakx

43
@ syb0rg Неправильно. У C # це умовне позначення .
магнатський

68
@ syb0rg Правильно, він не вказав мову, тому я вважаю, що досить дивно говорити йому, що він порушує конвенцію певної мови.
магнатський

31
@ syb0rg Загальне твердження про те, що "назви функцій не повинні починатися з великої літери" є помилковим, оскільки є мови, де вони повинні. У всякому разі, я не мав на увазі образити вас, я просто намагався прояснити це. Це стає майже поза темою до самого питання. Я не бачу потреби, але якщо ви хочете продовжити, давайте перенесіть дискусію в чат .
магнатський

6
Як загальний момент, прокрутка власної безпеки - це зазвичай погана річ (тм) . Більшість мов та фреймворків мають системи безпеки / дозволу, і є вагомі причини використовувати їх, навіть коли ви хочете щось простіше.
Джеймс Снелл

Відповіді:


123

Я особисто віддаю перевагу першому методу справедливого IsAdmin(User user)

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

Це також, ймовірно, більш безпечно, оскільки ви не рекламуєте, які властивості визначають, користувач є адміністратором чи ні, або проходять повсюдно навколо властивості пароля. І сиріон гарно говорить про те, що відбувається, коли ваш текст idне відповідає name/ password?

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


3
Я думаю, що питання рефакторингу - це дуже хороший момент - дякую.
Андерс Арпі

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

1
Я погоджуюся з цією відповіддю, але дозвольте позначити дві ситуації, коли інший підхід ("мінімальні дані") може бути кращим: (1) Ви хочете кешувати результати методу. Краще не використовувати цілий об'єкт як кеш-ключ або не перевіряти властивості об'єкта при кожному виклику. (2) Ви хочете, щоб метод / функція виконувалася асинхронно, що може включати серіалізацію аргументів і, скажімо, зберігання їх у таблиці. Легше та простіше шукати ціле число, ніж об’єктна крапка під час управління відкладеними завданнями.
GladstoneKeep

Примітивна анти-модель одержимості може бути жахливою для тестування одиниць.
Самуїл

82

Перший підпис є вищим, оскільки він дозволяє інкапсулювати пов'язану з користувачем логіку в самому Userоб'єкті. Недоцільно мати логіку для побудови кордону "id, ім'я, пароль", розповсюдженого на базі коду. Крім того, це ускладнює isAdminфункцію: що станеться, якщо хтось переходить у id, наприклад, не відповідає name? Що робити, якщо ви хочете перевірити, чи є даний користувач адміністратором із контексту, коли ви не повинні знати їх пароль?

Крім того, як примітка до вашого третього пункту на користь стилю з додатковими аргументами; це може призвести до "меншого коду всередині функції", але куди йде ця логіка? Це не може просто зникнути! Натомість він поширюється навколо кожного окремого місця, де викликається функція. Щоб зберегти п'ять рядків коду в одному місці, ви заплатили п’ять рядків за використання функції!


45
Виходячи з вашої логіки, не user.IsAdmin()було б набагато краще?
Андерс Арпі

13
Так, абсолютно.
asthasr

6
@ AndersHolmström Це залежить, ви тестуєте, чи користувач взагалі адміністратор, чи просто адміністратор поточної сторінки / форми, на якій ви перебуваєте. Якщо взагалі, то так, це було б краще, але якщо ви лише тестуєте IsAdminпевну форму чи функцію, це не має відношення до користувача
Рейчел

40
@Anders: ні, це не повинно. Користувач не повинен вирішувати, чи це Адміністратор чи ні. Привілеї чи система безпеки повинні. Що те, що щільно поєднане з класом, не означає, що це гарна ідея зробити його частиною цього класу
Мар'ян Венема

11
@MarjanVenema - ідея про те, що клас користувача не повинен "вирішувати", чи є екземпляр адміністратором, вражає мене як трохи вантажно-культовим. Ви дійсно не можете зробити таке узагальнення настільки сильно. Якщо ваші потреби складні, можливо, вигідно їх інкапсулювати в окрему "систему", але, посилаючись на KISS + YAGNI, я хотів би, щоб це рішення насправді зіткнулося з цією складністю, а не як загальне правило :-). І зауважте, що навіть якщо логіка не є "частиною" Користувача, це все-таки може бути корисним у практичному відношенні для проходження користувачем, який просто делегує більш складній системі.
Еймон Нербонна

65

Перший, але не (тільки) з тих причин, які наводили інші.

public bool IsAdmin(User user);

Це безпечний тип. Тим більше, що користувач - це тип, який ви самі визначили, мало можливостей переносити або перемикати аргументи або взагалі немає. Він чітко працює з користувачами і не є якоюсь загальною функцією, яка приймає будь-який int та два рядки. Це велика частина сенсу використання безпечної для мови мови, як Java або C #, як виглядає ваш код. Якщо ви запитуєте про певну мову, ви можете додати тег для цієї мови до свого питання.

public bool IsAdmin(int id, string name, string password);

Тут буде виконано будь-який int та два рядки. Що заважає вам перенести ім’я та пароль? Друга - це більше роботи та використання більше можливостей для помилок.

Імовірно, обидві функції вимагають викликати user.getId (), user.getName () та user.getPassword () або всередині функції (у першому випадку), або перед викликом функції (у другому), тому кількість з’єднання однаково в будь-якому випадку. Насправді, оскільки ця функція дійсна лише для користувачів, у Java я би усунув усі аргументи і зробив цей метод екземпляра на об’єктах користувача:

user.isAdmin();

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

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


11
Користувач не повинен вирішувати, чи це Адміністратор чи ні. Привілеї чи система безпеки повинні. Що те, що щільно поєднане з класом, не означає, що це гарна ідея зробити його частиною цього класу.
Мар'ян Венема

6
@MarjanVenema Користувач нічого не вирішує. Об'єкт / таблиця користувача зберігає дані про кожного користувача. Фактичні користувачі не можуть змінити все про себе. Навіть коли користувач змінює щось, що їм дозволено, воно може викликати попередження щодо безпеки, як-от електронний лист, якщо він змінить свою адресу електронної пошти або свій ідентифікатор або пароль користувача. Ви використовуєте систему, де користувачі магічно дозволяють змінювати все про певні типи об'єктів? Я не знайомий з такою системою.
GlenPeterson

2
@GlenPeterson: ти навмисно не розумієш мене? Коли я сказав користувачеві, я, звичайно, мав на увазі клас користувача.
Мар'ян Венема

2
@GlenPeterson Зовсім поза темою, але кілька раундів хешування і криптографічних функцій послаблять дані.
Гусдор

3
@MarjanVenema: Мені навмисне не важко. Я думаю, що у нас просто дуже різні перспективи, і я хотів би зрозуміти вашу краще. Я відкрив нове питання, де це можна краще обговорити: programmers.stackexchange.com/questions/216443/…
GlenPeterson

6

Якщо ваша обрана мова не відповідає структурам за значенням, то (User user)варіант здається кращим. Що робити, якщо коли-небудь ви вирішите зірвати ім’я користувача та просто використаєте ідентифікатор / пароль для ідентифікації користувача?

Крім того, такий підхід неминуче призводить до тривалих і роздутих викликів методів або невідповідностей. Чи добре передавати 3 аргументи? Як щодо 5? Або 6? Навіщо думати про це, якщо здача об'єкта дешева з точки зору ресурсів (навіть, дешевше, ніж передача 3 аргументів, напевно)?

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

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


3

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

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

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

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

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


Я не бачу причини приватного методу. Чому підтримувати обох? Якщо для приватного методу потрібні різні аргументи, вам все одно доведеться змінювати публічний. І ви протестуєте це через публічний. І ні, це не звичайно, що вам потрібно і те, і інше пізніше. Ви тут здогадуєтесь - ЯГНІ.
Пьотр Перек

Ви припускаєте, наприклад, що структура даних користувача не має іншого поля з самого початку. Зазвичай це неправда. Друга функція дає зрозуміти, яка внутрішня частина користувача перевіряється, чи є він адміністратором. Типові випадки використання можуть мати такий вигляд: якщо час створення користувача закінчується до певної дати, тоді зателефонуйте по старому правилу, якщо не викличте нове правило (яке може залежати від інших частин Користувача, або залежати від того самого, але за допомогою іншого правила). Розбивши код таким чином, ви отримаєте дві мінімальні функції: мінімальний концептуальний API та мінімальну функцію реалізації.
kriss

Іншими словами, таким чином з внутрішньої підпису функції буде видно, які частини Користувача використовуються чи ні. У виробничому коді це не завжди очевидно. Зараз я працюю над великою базою кодів з декількома історіями технічного обслуговування, і такий спосіб розщеплення дуже корисний для тривалого технічного обслуговування. Якщо ви не збираєтесь підтримувати код, він справді марний (але навіть надання стабільного концептуального API також, ймовірно, марно).
kriss

Інше слово: я, звичайно, не буду перевіряти внутрішній метод через зовнішній. Це погана практика тестування.
kriss

1
Це хороша практика, і там є назва. Це називається "принцип єдиної відповідальності". Його можна застосувати як до класів, так і до методів. Наявність методів з єдиною відповідальністю - це хороший спосіб зробити більш модульний і доступний код. У цьому випадку одне завдання - це вибір основного набору даних з об’єкта користувача, а інше завдання - перевірка базового набору даних, щоб побачити, чи є користувач адміністратором. Дотримуючись цього принципу, ви отримуєте бонус за можливість складання методів та уникнення дублювання коду. Це також означає, що, як заявляє @kriss, ви отримаєте кращі одиничні тести.
diegoreymendez

3

Є один негативний момент для надсилання класів (типових типів) на методи, а це - вразливість масового призначення . Уявіть, що замість IsAdmin(User user)методу у мене є UpdateUser(User user)метод.

Якщо Userклас має властивість Boolean IsAdmin, і якщо я не перевіряю його під час виконання мого методу, я вразливий до масового призначення. Цим самим підписом GitHub піддався ще в 2012 році.


2

Я б пішов з таким підходом:

public bool IsAdmin(this IUser user) { /* ... */ }

public class User: IUser { /* ... */ }

public interface IUser
{
    string Username {get;}
    string Password {get;}
    IID Id {get;}
}
  • Використання типу інтерфейсу в якості параметра для цієї функції дозволяє передавати об'єкти користувача, визначати макетні об'єкти користувача для тестування та надає більшу гнучкість як щодо використання, так і для підтримки цієї функції.

  • Я також додав ключове слово, thisщоб зробити функцію методом розширення для всіх класів IUser; таким чином ви можете писати код більш зручним для OO способом і використовувати цю функцію в запитах Linq. Ще простіше було б визначити цю функцію в IUser та User, але я припускаю, що є певна причина, чому ви вирішили поставити цей метод поза цим класом?

  • Я використовую користувальницький інтерфейс IIDзамість цього, intоскільки це дозволяє переглядати властивості вашого ідентифікатора; наприклад , якщо вам коли - небудь знадобиться , щоб перейти від intдо long, Guidабо що - то інше. Це, мабуть, надмірно, але завжди добре намагатися зробити такі гнучкими, щоб ви не зациклювалися на ранніх рішеннях.

  • Примітка: Ваш об’єкт IUser може знаходитися в іншому просторі імен та / або збірці до класу User, тому у вас є можливість зберегти свій код користувача повністю відокремленим від вашого коду IsAdmin, де вони мають лише одну бібліотеку, на яку посилається. Саме тому, що ви робите, є дзвінок про те, як ви підозрюєте, що ці предмети будуть використані.


3
IUser тут марний і лише додає складності. Я не бачу, чому ви не могли використовувати реального користувача в тестах.
Піотр Перек

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

@Peri справжній клас користувача може бути складним для створення, оскільки інтерфейс дозволяє знущатися над ним, щоб ви могли просто провести тести
jk.

@JohnLBevan: YAGNI !!!
Пьотр Перак

@jk.: Якщо важко побудувати Користувача, то щось не так
Piotr Perak

1

Відмова від відповідальності:

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

Я також вважаю user.isAdmin()рівнозначним IsAdmin(User user). Це вибір для вас.

Відповідь:

Моя рекомендація - будь-якому користувачеві:

public bool IsAdmin(User user);

Або використовуйте обидва за рядками:

public bool IsAdmin(User user); // calls the private method below
private bool IsAdmin(int id, string name, string password);

Причини публічного методу:

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

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

Причини приватного методу:

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

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

public bool IsAdmin(Localuser user); // Just call private method
public bool IsAdmin(RemoteUser user); // Check if setting is on, then call private method
private bool IsAdmin(int id, string name, string password);

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

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


0
public bool IsAdmin(int id, string name, string password);

погано з перерахованих причин. Це не означає

public bool IsAdmin(User user);

автоматично добре. Зокрема , якщо користувач є об'єктом , що має такі методи , як: getOrders(), getFriends(), getAdresses(), ...

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

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