Возвращение различных типов из метода С#

У меня есть метод:

public ??? AuthManager.Login(Credentials credentials)

Вот набор допустимых выходных значений этого метода:

  • Успех (+ accountId)
  • Отказ: AccountLockedOut
  • Отказ: UsernameNotFound
  • Отказ: InvalidPassword (+ счет неудачных попыток)

В зависимости от типа возврата пользователю отображаются разные представления (да, представление для AccountLockedOut отличается от InvalidPassword).

Я мог бы пойти с:

public class LoginAttemptResult {
    public bool Succeeded { get; set; }
    public AccountId AccountId { get; set; } // for when success
    public LoginAttemptResultEnumType Result { get;set; } // Success, Lockedout, UsernameNotFound, InvalidPassword  
    public int FailedAttemptCount { get; set; } // only used for InvalidPassword
}

Мне это не нравится и вы ищете лучшее решение. Во-первых, это приводит к частично инициализированному объекту, два - к нарушению принципа сегрегации интерфейса, три - к SRP.

ОБНОВЛЕНИЕ: исключения исключений также не являются изящным решением, потому что InvalidPassword, как я вижу, это не исключение. Исключением является неудачное соединение с БД. Исключение составляет аргумент null. InvalidPassword - допустимый ожидаемый ответ.

Я думаю, что лучшим решением является создание иерархии классов:

abstract class LoginAttemptResult
    sealed class LoginSuccess : LoginAttemptResult { AccountId }
    abstract class LoginFailure : LoginAttemptResult
        sealed class InvalidPasswordLoginFailure : LoginFailure { FailedAttemptCount }
        sealed class AccountLockedoutLoginFailure : LoginFailure

вызывающий метод Login тогда должен был бы сделать что-то вроде:

if (result is LoginSuccess) { 
    ..."welcome back mr. account id #" + (result as LoginSuccess).AccountId
}
else if (result is InvalidPasswordLoginFailure ) { 
    ..."you failed " + (result as InvalidPasswordLoginFailure).FailedAttemptCount + " times"
}

Я не вижу ничего неправильного (концептуально) с этим подходом (кроме ряда классов, с которыми он идет).

Что еще не так с этим подходом?

Обратите внимание, что этот подход по существу представляет собой F # дискриминационный союз (DU).

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

Ответ 1

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

/// <summary>
/// Immutable, created by the server
/// </summary>
class LoginResult
{
    /// <summary>
    /// Null in the case of failure
    /// </summary>
    public int? Id { get; private set; }

    /// <summary>
    /// Null in the case of success
    /// </summary>
    public string FailReason { get; private set; }

    /// <summary>
    /// Always >= 1
    /// </summary>
    public int AttemptNumber { get; private set; }

    public LoginResult(int id, int attemptNumber)
    {
        Id = id;
        AttemptNumber = attemptNumber;
    }

    public LoginResult(string reason, int attemptNumber)
    {
        FailReason = reason;
        AttemptNumber = attemptNumber;
    }
}

Я могу себе представить, что ваша логика аутентификации может быть очень сложной, а Id, FailReason и AttemptNumber - это не только свойства, которые вам понадобятся. В этом случае вам нужно представить нам более конкретный пример, мы попытаемся построить абстракции, которые будут соответствовать вашей логике, если это необходимо. В этом конкретном случае - нет смысла для абстракции.

Ответ 2

Сводка: вместо того, чтобы возвращать значение и декодировать его - дать Login набор обработчиков, поэтому Login вызовет соответствующий обратный вызов (think jQuery ajax { success: ..., error: ... })

Потребителю метода Login придется декодировать ответ, используя, по существу, оператор switch. Один из способов реорганизовать этот код, чтобы исключить этот оператор "switch", а также удалить взрыв пользовательских типов, вместо того, чтобы просить метод Login вернуть дискриминированный союз, мы даем методу входа в систему набор thunks - по одному для каждого ответа.

(тонкая точка). Технически мы не избавляемся от пользовательских классов, мы просто заменяем их дженериками, т.е. мы заменили InvalidPasswordFailedLogin { int failedAttemptCount } на Action<int>. Этот подход также представляет некоторые интересные возможности, например, Login можно обрабатывать как можно более естественно. Тестирование с другой стороны становится немного более неясным.

public class LoginResultHandlers {
    public Action<int> InvalidPassword { get; set; }
    public Action AccountLockedout { get; set; }
    public Action<AccountId> Success { get; set; }
}

public class AccountId {}

public class AuthManager {
    public void Login(string username, string password, LoginResultHandlers handler) {
        // if (...
            handler.Success(new AccountId());
        // if (...
            handler.AccountLockedout();
        // if (...
            handler.InvalidPassword(2);
    }
}

public class Application {
    public void Login() {
        var loginResultHandlers = new LoginResultHandlers {
                AccountLockedout = ShowLockedoutView,
                InvalidPassword = (failedAttemptCount) => ShowInvalidPassword(failedAttemptCount),
                Success = (accountId) => RedirectToDashboard(accountId)
        };
        new AuthManager().Login("bob", "password", loginResultHandlers);
    }

    private void RedirectToDashboard(AccountId accountId) {
        throw new NotImplementedException();
    }

    private void ShowInvalidPassword(int failedAttemptCount) {
        throw new NotImplementedException();
    }

    private void ShowLockedoutView() {
        throw new NotImplementedException();
    }
}

Ответ 3

Вы можете вернуть Tuple

public Tuple<T1,T2> AuthManager.Login(Credentials credentials){
//do your stuff here
return new Tuple<T1,T2>(valueOfT1,valueOfT2);
}

Ответ 4

Если вы создадите абстрактный тег LoginAttemptResult, вы можете добавить абстрактное свойство Message, которое заставит ваши дочерние классы реализовать его.

public abstract class LoginAttemptResult
{        
    public abstract string Message { get; }

    // any other base methods/properties and abstract methods/properties here
}

Затем ваши дети могут выглядеть так:

public class LoginSuccess : LoginAttemptResult
{
    public override string Message 
    { 
        get
        {
            return "whatever you use for your login success message";
        }
    }
}

С помощью этого метода Login можно просто вернуть LoginAttemptResult

public LoginAttemptResult AuthManager.Login(Credentials credentials)
{
    // do some stuff
}

И тогда ваш вызывающий абонент просто вызовет ваш LoginAttemptResult.Message (или что-то еще, что вам нужно для этого):

var loginResult = AuthManager.Login(credentials);
var output = loginResult.Message;

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

Ответ 5

Другим возможным подходом является создание класса, который инкапсулирует процесс входа и его результаты, например:

    public interface ILoginContext
    {
        //Expose whatever properties you need to describe the login process, such as parameters and results

        void Login(Credentials credentials);
    }

    public sealed class AuthManager
    {
        public ILoginContext GetLoginContext()
        {
            return new LoginContext(this);
        }

        private sealed class LoginContext : ILoginContext
        {
            public LoginContext(AuthManager manager)
            {
                //We pass in manager so that the context can use whatever it needs from the manager to do its job    
            }
            //...
        }
    }

В основном, что подразумевается в этом дизайне, так это то, что вход в систему стал достаточно сложной операцией, что один метод больше не является подходящей инкапсуляцией. Нам нужно вернуть сложный результат и, возможно, захотите включить более сложные параметры. Поскольку класс теперь несет ответственность за поведение, а не только за данные, он с меньшей вероятностью считается нарушением SRP; это просто сложный класс для несколько сложной операции.

Обратите внимание, что вы также можете сделать реализацию IDInposable LoginContext, если она имеет естественную транзакционную область.

Ответ 6

ваша безопасность api не должна выставлять столько информации. Указанный вами API не предоставляет никакой полезной информации клиенту, кроме как помочь злоумышленнику в попытке захватить учетную запись. Ваш метод входа должен предоставить только информацию о пропуске/сбое и токен, который может быть передан любому механизму авторизации, который вам нужен.

// used by clients needing to authenticate
public interfac ISecurity {
  AuthenticationResponse Login(Credentials credentials);
}

// the response from calling ISecurity.Login
public class AuthenticationResponse {

  internal AuthenticationResponse(bool succeeded, AuthenticationToken token, string accountId) {
    Succeeded = succeeded;
    Token = token;
  }

  // if true then there will be a valid token, if false token is undefined
  public bool Succeeded { get; private set; }

  // token representing the authenticated user.
  // document the fact that if Succeeded is false, then this value is undefined
  public AuthenticationToken Token { get; private set; }

}

// token representing the authenticated user. simply contains the user name/id
// for convenience, and a base64 encoded string that represents encrypted bytes, can
// contain any information you want.
public class AuthenticationToken {

  internal AuthenticationToken(string base64EncodedEncryptedString, string accountId) {
    Contents = base64EncodedEncryptedString;
    AccountId = accountId;
  }

  // secure, and user can serialize it
  public string Contents { get; private set; }

  // used to identify the user for systems that aren't related to security
  // (e.g. customers this user has)
  public string AccountId { get; private set; }

}


// simplified, but I hope you get the idea. It is what is used to authenticate
// the user for actions (i.e. read, write, modify, etc.)
public interface IAuthorization {
  bool HasPermission(AuthenticationToken token, string permission); 
}

Вы заметите, что этот API не имеет попыток входа в систему. Клиент не должен заботиться о правилах, связанных с входом в систему. Разработчик интерфейса ISecurity должен идти в ногу с попытками входа в систему и возвращать сбой при успешном наборе учетных данных, но количество попыток было exceedeed.

Простое сообщение об ошибке должно читать что-то по строкам:

Could not log you on at this time. Check that your username and/or password are correct, or please try again later.

Ответ 7

Вот решение, которое удовлетворяет всем моим требованиям (читаемость, тестируемость, открытость и эстетика).

Код (обратите внимание, что реализация немного отличается от исходной задачи, но концепция остается):

public class AuthResult {
    // Note: impossible to create empty result (where both success and failure are nulls).
    // Note: impossible to create an invalid result where both success and failure exist.
    private AuthResult() {}
    public AuthResult(AuthSuccess success) {
        if (success == null) throw new ArgumentNullException("success");
        this.Success = success;
    }
    public AuthResult(AuthFailure failure) {
        if (failure == null) throw new ArgumentNullException("failure");
        this.Failure = failure;
    }
    public AuthSuccess Success { get; private set; }
    public AuthFailure Failure { get; private set; }
}

public class AuthSuccess {
    public string AccountId { get; set; }
}

public class AuthFailure {
    public UserNotFoundFailure UserNotFound { get; set; }
    public IncorrectPasswordFailure IncorrectPassword { get; set; }
}

public class IncorrectPasswordFailure : AuthResultBase {
    public int AttemptCount { get; set; }
}

public class UserNotFoundFailure : AuthResultBase {
    public string Username { get; set; }
}

Обратите внимание, что AuthResult правильно моделирует гетерогенный и иерархический характер диапазона функций.

И если вы добавите следующий неявный оператор:

public static implicit operator bool(AuthResultBase result) {
    return result != null;
}

вы можете использовать результат следующим образом:

var result = authService.Auth(credentials);
if (result.Success) {
    ...
}

который читает (возможно) лучше, чем:

if (result.Success != null) {
    ...
}