从C#方法返回不同的类型

时间:2013-05-03 15:25:00

标签: c# oop

我有一个方法:

public ??? AuthManager.Login(Credentials credentials)

以下是此方法的一组有效输出值:

  1. 成功(+ accountId)
  2. 失败:AccountLockedOut
  3. 失败:UsernameNotFound
  4. 失败:InvalidPassword(+尝试次数失败)
  5. 根据返回类型,向用户显示不同的视图(是的,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。

    UPDATE:抛出异常也不是一个优雅的解决方案,因为我认为InvalidPassword不是例外。数据库连接失败是一个例外。空参数是一个例外。 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#的discriminated union (DU)

    有更好的方法对此进行建模吗?我已经有了几个有效的解决方案 - 现在我想要一个有效的优雅解决方案。

7 个答案:

答案 0 :(得分:4)

我认为如果结果类显着不同并且每个都需要一个单独的类,那么您的解决方案就可以了。但我不确定。为每个结果尝试此类:

/// <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;
    }
}

我可以想象,您的身份验证逻辑非常复杂,IdFailReasonAttemptNumber不仅是您需要的属性。在这种情况下,您需要向我们展示更具体的示例,如果需要,我们将尝试构建适合您逻辑的抽象。在这种特殊情况下 - 没有抽象意义。

答案 1 :(得分:1)

总结:不是返回一个值并解码它 - 给Login一组处理程序,以便Login调用适当的回调(想想jQuery的ajax { success: ..., error: ... }

Login方法的使用者必须使用基本上的switch语句解码响应。重构此代码以消除“switch”语句并消除自定义类型爆炸的一种方法是不要求Login方法返回一个有区别的联合 - 我们给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();
    }
}

答案 2 :(得分:0)

您可以返回Tuple

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

答案 3 :(得分:0)

如果您将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相关联,则可以将其定义为基类中的抽象方法,在子类中实现它,然后调用它完全一样。

答案 4 :(得分:0)

另一种可能的方法是创建一个封装Login进程及其结果的类,如下所示:

    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;对于有些复杂的操作来说,这只是一个有点复杂的类。

请注意,如果LoginContext具有自然的事务范围,您也可以使其实现IDisposable。

答案 5 :(得分:0)

您的安全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接口的实现者应该跟上登录尝试,并且在传递成功的凭证集时返回失败,但是尝试次数已被超越。

关于失败的简单信息应该读取以下内容:

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

答案 6 :(得分:0)

这是一个满足我所有要求(可读性,可测试性,可发现性和美学性)的解决方案。

代码(请注意,实现与原始任务略有不同,但概念仍然存在):

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) {
    ...
}