Если вы всегда передаете минимум необходимых данных в функцию в подобных случаях


81

Допустим, у меня есть функция, IsAdminкоторая проверяет, является ли пользователь администратором. Давайте также скажем, что проверка администратора выполняется путем сопоставления идентификатора пользователя, имени и пароля с каким-то правилом (не важно).

В моей голове есть две возможные функции подписи для этого:

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

Я чаще всего иду на второй тип подписи, думая, что:

  • Функция подписи дает читателю намного больше информации
  • Логика, содержащаяся внутри функции, не должна знать о Userклассе
  • Обычно это приводит к немного меньшему количеству кода внутри функции

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

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

Я программирую как в объектно-ориентированном, так и в функциональном стилях, поэтому вопрос следует рассматривать как относящийся ко всем идиомам.


40
Не по теме: из любопытства, почему статус пользователя "is admin" зависит от его пароля?
stakx

43
@ syb0rg Неверно. В C # это соглашение об именах .
Magnattic

68
@ syb0rg Правильно, он не указал язык, поэтому я считаю странным сказать ему, что он нарушает соглашение определенного языка.
Magnattic

31
@ syb0rg Общее утверждение, что «имена функций не должны начинаться с заглавной буквы» неверно, потому что есть языки, где они должны. Во всяком случае, я не хотел вас обидеть, я просто пытался выяснить это. Это становится в значительной степени не по теме самого вопроса. Я не вижу в этом необходимости, но если вы хотите продолжить, давайте перенесем дискуссию в чат .
Magnattic

6
Точно так же, как правило, отказ от собственной безопасности - это плохо . Большинство языков и сред имеют системы безопасности / разрешений, и есть веские причины использовать их, даже если вы хотите что-то более простое.
Джеймс Снелл

Ответы:


123

Я лично предпочитаю первый метод просто IsAdmin(User user)

Его гораздо проще использовать, и если ваши критерии для IsAdmin изменятся позднее (возможно, на основе ролей или isActive), вам не нужно будет переписывать сигнатуру метода везде.

Это также, вероятно, более безопасно, поскольку вы не рекламируете, какие свойства определяют, является ли пользователь администратором или нет, или везде передаете свойство пароля. И Сирион делает хорошее замечание, что происходит, когда ваш idне соответствует name/ password?

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


3
Я думаю, что вопрос рефакторинга - очень хороший момент - спасибо.
Андерс Арпи

1
Я собирался сделать аргумент подписи метода, если вы этого не сделали. Это особенно полезно, когда есть много зависимостей от вашего метода, которые поддерживаются другими программистами. Это помогает людям не мешать друг другу. +1
jmort253

1
Я согласен с этим ответом, но позвольте мне отметить две ситуации, когда другой подход («минимальные данные») может быть предпочтительным: (1) Вы хотите кэшировать результаты метода. Лучше не использовать весь объект в качестве ключа кеша или проверять свойства объекта при каждом вызове. (2) Вы хотите, чтобы метод / функция выполнялся асинхронно, что может включать сериализацию аргументов и, скажем, их сохранение в таблице. Проще и проще искать целое число, чем объектный BLOB-объект при управлении отложенными заданиями.
GladstoneKeep

Примитивный анти-паттерн одержимости может быть ужасным для юнит-тестирования.
Самуил

82

Первая сигнатура является превосходной, поскольку она позволяет инкапсулировать пользовательскую логику в сам Userобъект. Не имеет смысла иметь логику для построения кортежа «id, name, password» о вашей кодовой базе. Кроме того, это усложняет isAdminфункцию: что произойдет, если кто-то перейдет в, например, idчто не соответствует name? Что если вы хотите проверить, является ли данный пользователь администратором из контекста, в котором вы не должны знать его пароль?

Кроме того, как примечание к вашему третьему пункту в пользу стиля с дополнительными аргументами; это может привести к «меньшему количеству кода внутри функции», но куда девается эта логика? Это не может просто исчезнуть! Вместо этого он распространяется вокруг каждого места, где вызывается функция. Чтобы сохранить пять строк кода в одном месте, вы заплатили пять строк за использование функции!


45
Следуя вашей логике, не user.IsAdmin()будет ли намного лучше?
Андерс Арпи

13
Да, конечно.
asthasr

6
@ AndersHolmström Это зависит от того, тестируете ли вы, является ли пользователь администратором в целом или просто администратором для текущей страницы / формы, на которой вы находитесь. Если в общем, то да, это было бы лучше, но если вы тестируете только IsAdminопределенную форму или функцию, это не должно быть связано с пользователем
Рэйчел

40
@ Андерс: нет, не должно. Пользователь не должен решать, является ли он администратором или нет. Привилегии или Система безопасности должны. Что-то, что тесно связано с классом, не означает, что это хорошая идея, чтобы сделать его частью этого класса
Марьян Венема

11
@MarjanVenema - идея о том, что класс User не должен «решать», является ли экземпляр администратором, кажется мне немного грубой. Вы действительно не можете сделать это обобщение так сильно. Если ваши потребности сложны, то, возможно, полезно заключить их в отдельную «систему», но, ссылаясь на KISS + YAGNI, я бы хотел, чтобы это решение действительно столкнулось с такой сложностью, а не с общим правилом :-). И обратите внимание, что даже если логика не является «частью» пользователя, для практического применения может быть полезно иметь сквозной доступ к пользователю, который просто делегирует более сложную систему.
Имон Нербонн

65

Первый, но не (только) по причинам, приведенным другими.

public bool IsAdmin(User 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 Пользователь ничего не решает. Пользовательский объект / таблица хранит данные о каждом пользователе. Реальные пользователи не могут ничего изменить в себе. Даже когда пользователь изменяет что-то, что ему разрешено, он может вызывать предупреждения безопасности, такие как электронная почта, если они изменяют свой адрес электронной почты, свой идентификатор пользователя или пароль. Используете ли вы систему, в которой пользователям разрешено магически изменять все в определенных типах объектов? Я не знаком с такой системой.
ГленПетерсон

2
@GlenPeterson: вы сознательно меня не понимаете? Когда я говорил о пользователе, я, конечно, имел в виду пользовательский класс.
Марьян Венема

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

3
@MarjanVenema: я не намеренно затрудняюсь. Я думаю, что у нас просто разные взгляды, и я хотел бы лучше понять вашу. Я открыл новый вопрос, где это может быть лучше обсуждено: programmers.stackexchange.com/questions/216443/…
GlenPeterson

6

Если ваш язык выбора не передает структуры по значению, тогда (User user)вариант кажется лучше. Что если однажды вы решите отказаться от имени пользователя и просто использовать идентификатор / пароль для идентификации пользователя?

Кроме того, такой подход неизбежно приводит к длинным и чрезмерным вызовам методов или несоответствиям. Передача 3 аргументов в порядке? Как насчет 5? Или 6? Зачем думать об этом, если передача объекта обходится дешево с точки зрения ресурсов (возможно, даже дешевле, чем передача 3 аргументов)?

И я (вроде) не согласен с тем, что второй подход дает читателю больше информации - интуитивно, вызов метода спрашивает «есть ли у пользователя права администратора», а не «имеет ли данная комбинация идентификатор / имя / пароль права администратора».

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


3

Я бы, наверное, и то и другое. Первый как внешний API, с некоторой надеждой на стабильность во времени. Вторая - частная реализация, вызываемая внешним API.

Если через какое-то время мне придется изменить правило проверки, я просто напишу новую приватную функцию с новой сигнатурой, вызываемой внешней.

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

Что касается названия вопроса, то в обоих случаях вы предоставляете минимально необходимую информацию. Пользователь, кажется, самая маленькая структура данных, связанных с приложением, содержащая информацию. Три поля id, password и name кажутся наименьшими необходимыми данными реализации, но на самом деле они не являются объектами уровня приложения.

Другими словами, если вы имеете дело с базой данных, пользователь будет записью, а id, пароль и логин будут полями в этой записи.


Я не вижу причины для частного метода. Зачем поддерживать оба? Если приватному методу нужны другие аргументы, вам все равно придется изменить публичный. И вы протестируете это через публичный. И нет, это не обычно, что вам нужно и то, и другое через некоторое время. Вы угадаете здесь - ЯГНИ.
Петр Перак

Например, вы предполагаете, что структура данных пользователя не имеет никакого другого поля с самого начала. Обычно это не так. Вторая функция проясняет, какая внутренняя часть пользователя проверяется, чтобы узнать, является ли она администратором. Типичные варианты использования могут быть такими: если время создания пользователя наступает раньше некоторой даты, тогда вызывать старое правило, если не вызывать новое правило (которое может зависеть от других частей пользователя или зависит от того же самого, но с использованием другого правила). Разделив код таким образом, вы получите две минимальные функции: минимальный концептуальный API и минимальную функцию реализации.
Крис

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

Еще одно слово: я, конечно, не буду тестировать внутренний метод через внешний. Это плохая практика тестирования.
Крис

1
Это хорошая практика, и для нее есть название. Это называется «принцип единой ответственности». Может применяться как к классам, так и к методам. Наличие методов с единственной ответственностью - хороший способ создать более модульный и обслуживаемый код. В этом случае одна задача выбирает базовый набор данных из объекта пользователя, а другая задача проверяет этот базовый набор данных, чтобы определить, является ли пользователь администратором. Следуя этому принципу, вы получаете возможность составлять методы и избегать дублирования кода. Это также означает, что, как утверждает @kriss, вы получаете лучшие юнит-тесты.
diegoreymendez

3

Есть один негативный момент для отправки классов (ссылочных типов) в методы, а именно, Уязвимость массового назначения . Представьте, что вместо IsAdmin(User user)метода у меня есть UpdateUser(User user)метод.

Если Userкласс имеет вызываемое логическое свойство 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;}
}
  • Использование типа интерфейса в качестве параметра для этой функции позволяет передавать объекты User, определять фиктивные объекты User для тестирования и дает вам больше гибкости в отношении как использования, так и обслуживания этой функции.

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

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

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


3
IUser здесь бесполезен и только добавляет сложности. Я не понимаю, почему вы не могли использовать реального пользователя в тестах.
Петр Перак

1
Это добавляет гибкости в будущем при минимальных усилиях, поэтому, не добавляя ценности в настоящее время, это не повредит. Лично я предпочитаю всегда делать это, так как это исключает необходимость продумывать будущие возможности для всех сценариев (что практически невозможно из-за непредсказуемого характера требований) и займет намного больше времени, чтобы даже получить частично хороший ответ, чем это было бы втыкать в интерфейс).
JohnLBevan

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

@JohnLBevan: ЯГНИ !!!
Петр Перак

@jk .: если трудно собрать пользователя, значит, что-то не так
Петр Перак

1

Отказ от ответственности:

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

Я также считаю 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.