Feel Good.

03 июня 2010

Enum как результат

Иногда, бывает очень удобно возвращать результат работы сервис-метода в качестве Enum значения. Представим ситуацию: Вы пишите сервис-метод, который возвращает два возможных состояния true/false (область значений), как бы Вы поступили, ограничились бы стандартным типом Bool или все-таки ввели бы новый Enum? Идея оставить Bool не совсем удачна, если вдруг придется расширить область значений сервис-метода. Например, в задаче аутентификации пользователя, где помимо информации об удаче/неудаче необходимо указать, в чем состоит неудача аутентификации, в итоге получим 4 возможных результата: "успех" (Ok), "провал" (Fail), "логин не верен" (LoginIncorrect), "пароль не верен" (PasswordIncorrect):

// Результат аутентификации

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

}


Достоинства:
  1. Никаких magic-constants.
  2. Простота и легкость расширения.
  3. Установление взаимосвязей за счет комбинирования.
Недостатки:
  1. Ограничение числа возможных статусов разрядностью Enum.

Ссылки:
  1. Enumeration Types (C# Programming Guide)

Progg it

14 комментариев:

  1. Еще один недостаток - сложность использования и чтения кода, который перевешивает все достоинства. Мне кажется более подходящим вариант с исключениями, там наследование позволяет более очевидно связать Fail и PasswordIncorrect.

    ОтветитьУдалить
  2. Не совсем согласен с тезисом "Исключение как результат", исключения должны возникать если что то пошло не так. А ведь Fail у нас запланированный результат. Да и исключения более затратны по ресурсам нежели вернуть просто Enum.
    А вот со сложностью чтения кода я не совсем понял почему.
    Кстати, есть ли open-source проекты построенные на архитектуре "Исключение как результат" или может личный опыт?

    ОтветитьУдалить
  3. Всё зависит от сигнатуры метода и стиля библиотеки\программы.
    Если метод называется Login, то он подразумевает "one way" запрос. Его ошибки вполне могут и не ожидать или просто не интересоваться её подробностями. В этом случае он должен кидат ьисключение, которое ползет вверх по стеку, в отличие от возвращаемого значения. В framework-е много так устроенных методов, например по работе с файлами.

    А вот если метод называется TryLogin или типа того, то тут уже программист явно вызывает его с намериниями узнать результат этой операции и эксепшены ему не нужны.

    А вообще ничего нового то во флагах нет, в C и С++ так всегда и писали.

    ОтветитьУдалить
  4. я пользуюсь таким вариантом
    bool TryLogin(LoginParams params);
    bool TryLogin(LoginParams params, out LoginResult result);

    Возвращает true - если получилось, false - если не получилось
    Если нужны подробности - передаем out-параметр

    ОтветитьУдалить
  5. Продолжу мысль @anksto и @Dmitry Golubets.
    Например вот так:
    // Выбрасываем исключение если не удалось
    Account Login(LoginParams params);
    Или
    // Никаких исключений, все в результате.
    LoginResult TryLogin(LoginParams params, out Account result);

    ОтветитьУдалить
  6. у меня бы Account был внутри объекта LoginResult

    ОтветитьУдалить
  7. В четвертом .NET (LoginStatus.Fail & status) == LoginStatus.Fail можно написать status.HasFlag(LoginStatus.Fail)

    Абсолютно поддерживаю точку зрения romchick про исключительные ситуации.

    Вариант ankstoo с out параметром мне не совсем нравится.

    ОтветитьУдалить
  8. @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)
    {
    // И сюда не попадем
    }

    ОтветитьУдалить
  9. А если просто вернуть byte ?
    (00-OK
    01 - LiginIncorrect
    10 - PasswordIncorrect

    ОтветитьУдалить
  10. @GLeBaTi

    Тогда в коде у нас получатся magic-constants. Например, чтобы узнать что означает код 10 придется смотреть документацию.

    ОтветитьУдалить
  11. @GLeBaTi читабельность будет 0

    ОтветитьУдалить
  12. Вот один в один ваш пример на 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
    }

    ОтветитьУдалить
  13. @Vadim Sentyaev ваш пример с исключениями ужасный и неправильный - проверять через GetType моветон и, к тому же, медленно. Пример Ильи более правильный.

    А вообще пример не очень хорошо иллюстрирует использование перечислений. В данном примере логичней кидать всегда одно и то же исклчение с разным сообщением. Сообщение возвращать пользователю.

    ОтветитьУдалить
  14. @hazzik

    enum LoginStatus
    {
    // статусы ошибок
    }

    class LoginFail : Exception
    {
    public LoginStatus Status {get; protected set;}
    }

    ОтветитьУдалить