// Результат аутентификации
enum LoginStatus
{
// Успех
Ok = 0,
// Провал
Fail = (2 << 1),
// Уточняем, в чем провал:
LoginIncorrect = (2 << 2),
PasswordIncorrect = (2 << 3)
}
На этом можно было бы и закончить, но возможно, Вы возразите мне: "А как связаны статусы: Fail и PasswordIncorrect ? Интуитивно понятно, что связь между статусами есть.". Проблема решается просто на уровне битовых полей и флагов, давайте свяжем эти статусы следующим образом: статус PasswordIncorrect, это тот же статус Fail, но с небольшим уточнением в виде одного бита (2 << 3):
// Результат аутентификации
[Flags]
enum LoginStatus
{
// Успех
Ok = 0,
// Провал
Fail = (2 << 1),
// Уточняем, в чем провал:
LoginIncorrect = (2 << 2) | Fail,
PasswordIncorrect = (2 << 3) | Fail
}
Вот это уже намного лучше. Осталось привести пример использования:
if ((LoginStatus.Fail & status) == LoginStatus.Fail)
{
// Аутентификация завершилась неудачей
// и не важно, почему.
}
if ((LoginStatus.LoginIncorrect & status) == LoginStatus.LoginIncorrect)
{
// Аутентификация завершилась неудачей:
// как минимум логин пользователя не существует.
}
if ((LoginStatus.PasswordIncorrect & status) == LoginStatus.PasswordIncorrect)
{
// Аутентификация завершилась неудачей:
// как минимум пароль указан неверно.
}
Допустим, мы ввели еще один статус "Пароль имеет пустое значение" (PasswordIsEmpty), тогда мы с легкостью сможем расширить наш Enum следующим образом:
// Результат аутентификации
[Flags]
enum LoginStatus
{
// Успех
Ok = 0,
// Провал
Fail = (2 << 1),
// Уточняем, в чем провал:
LoginIncorrect = (2 << 2) | Fail,
PasswordIncorrect = (2 << 3) | Fail,
// Пустой пароль
PasswordIsEmpty = (2 << 4) | PasswordIncorrect
}
Достоинства:
- Никаких magic-constants.
- Простота и легкость расширения.
- Установление взаимосвязей за счет комбинирования.
- Ограничение числа возможных статусов разрядностью Enum.
Ссылки:
Еще один недостаток - сложность использования и чтения кода, который перевешивает все достоинства. Мне кажется более подходящим вариант с исключениями, там наследование позволяет более очевидно связать Fail и PasswordIncorrect.
ОтветитьУдалитьНе совсем согласен с тезисом "Исключение как результат", исключения должны возникать если что то пошло не так. А ведь Fail у нас запланированный результат. Да и исключения более затратны по ресурсам нежели вернуть просто Enum.
ОтветитьУдалитьА вот со сложностью чтения кода я не совсем понял почему.
Кстати, есть ли open-source проекты построенные на архитектуре "Исключение как результат" или может личный опыт?
Всё зависит от сигнатуры метода и стиля библиотеки\программы.
ОтветитьУдалитьЕсли метод называется Login, то он подразумевает "one way" запрос. Его ошибки вполне могут и не ожидать или просто не интересоваться её подробностями. В этом случае он должен кидат ьисключение, которое ползет вверх по стеку, в отличие от возвращаемого значения. В framework-е много так устроенных методов, например по работе с файлами.
А вот если метод называется TryLogin или типа того, то тут уже программист явно вызывает его с намериниями узнать результат этой операции и эксепшены ему не нужны.
А вообще ничего нового то во флагах нет, в C и С++ так всегда и писали.
я пользуюсь таким вариантом
ОтветитьУдалитьbool TryLogin(LoginParams params);
bool TryLogin(LoginParams params, out LoginResult result);
Возвращает true - если получилось, false - если не получилось
Если нужны подробности - передаем out-параметр
Продолжу мысль @anksto и @Dmitry Golubets.
ОтветитьУдалитьНапример вот так:
// Выбрасываем исключение если не удалось
Account Login(LoginParams params);
Или
// Никаких исключений, все в результате.
LoginResult TryLogin(LoginParams params, out Account result);
у меня бы Account был внутри объекта LoginResult
ОтветитьУдалитьВ четвертом .NET (LoginStatus.Fail & status) == LoginStatus.Fail можно написать status.HasFlag(LoginStatus.Fail)
ОтветитьУдалитьАбсолютно поддерживаю точку зрения romchick про исключительные ситуации.
Вариант ankstoo с out параметром мне не совсем нравится.
@hazzik
ОтветитьУдалитьНо ведь в нашем случае, if...else конструкции позволяют гораздо гибче, обработать результат, нежели catch(Specific)...catch(Common).
class E1 : Exception { }
class E2 : E1 { }
class E3 : E1 { }
try
{
throw new E3();
}
catch (E3 e3)
{
}
catch (E2 e2)
{
}
catch (E1 e1)
{
// Сюда не попадем
}
catch (Exception e)
{
// И сюда не попадем
}
А если просто вернуть byte ?
ОтветитьУдалить(00-OK
01 - LiginIncorrect
10 - PasswordIncorrect
@GLeBaTi
ОтветитьУдалитьТогда в коде у нас получатся magic-constants. Например, чтобы узнать что означает код 10 придется смотреть документацию.
@GLeBaTi читабельность будет 0
ОтветитьУдалитьВот один в один ваш пример на Exception.
ОтветитьУдалитьclass LoginFail : Exception { }
class LoginIncorrect : LoginFail { }
class PasswordIncorrect : LoginFail { }
try
{
throw new PasswordIncorrect();
}
catch (LoginFail loginEx)
{
if (loginEx.GetType() == typeof(LoginIncorrect))
{
}
if (loginEx.GetType() == typeof(PasswordIncorrect))
{
// попадаем сюда!
}
}
catch (Exception ex)
{
// common ex
}
@Vadim Sentyaev ваш пример с исключениями ужасный и неправильный - проверять через GetType моветон и, к тому же, медленно. Пример Ильи более правильный.
ОтветитьУдалитьА вообще пример не очень хорошо иллюстрирует использование перечислений. В данном примере логичней кидать всегда одно и то же исклчение с разным сообщением. Сообщение возвращать пользователю.
@hazzik
ОтветитьУдалитьenum LoginStatus
{
// статусы ошибок
}
class LoginFail : Exception
{
public LoginStatus Status {get; protected set;}
}
Lucky Club Casino Site » Review & Bonus up to ₹80000 + 200 FS
ОтветитьУдалитьLucky Club Casino provides an excellent site luckyclub which offers a wide range of banking options and offers a unique user interface which is unique