zoukankan      html  css  js  c++  java
  • 使用策略者模式减少switch case 语句

    策略者模式

    很简单的一个定义:抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。

    场景

    在这之前,你需要看这个文章SPA+.NET Core3.1 GitHub第三方授权登录 ,了解如何实现第三方授权登录。

    我们这里使用策略者模式应用实践,实现第三方授权登录,支持QQ,Gitee,GitHub登录,并且如何把switch case的逻辑判断去掉。

    我们先按正常的思路写代码,引用如下类库

    • AspNet.Security.OAuth.Gitee
    • AspNet.Security.OAuth.GitHub
    • AspNet.Security.OAuth.QQ

    我们会创建一个Service,这个Service包含了保存Github,QQ,Gitee信息的接口。由于三者之间,数据都是以Claims的情况存到ClaimsPrincipal中,键各不相同,只能独立处理

    public  interface IUserIdentityService
     {
        Task<long> SaveGitHubAsync(ClaimsPrincipal principal, string openId);	
        Task<long> SaveQQAsync(ClaimsPrincipal principal, string openId);	
        Task<long> SaveGiteeAsync(ClaimsPrincipal principal, string openId);
     }
    

    实现,保存登录后的授权信息,生成账号,并返回生成的用户id,伪代码如下

      public class UserIdentityService :ApplicationService, IUserIdentityService
      {
        
            public async Task<long> SaveGitHubAsync(ClaimsPrincipal principal, string openId)	
            {   
                return userId;	
            }	
            
             public async Task<long> SaveQQAsync(ClaimsPrincipal principal, string openId)	
             {
                 return userId;	
             }	
            public async Task<long> SaveGiteeAsync(ClaimsPrincipal principal, string openId)	
             {	
                  return userId;	
             }
      }
    

    这时候我们怎么调用 呢,provider为GitHub,QQ,Gitee这种字符串,登录成功后,会回调到此地址,这时,根据provider选择不同的方法进行保存用户数据

    Oauth2Controller

    
    [HttpGet("signin-callback")]
    public async Task<IActionResult> Home(string provider, string redirectUrl = "")
    {
        AuthenticateResult authenticateResult = await _contextAccessor.HttpContext.AuthenticateAsync(provider);
        if (!authenticateResult.Succeeded) return Redirect(redirectUrl);
        
        var openIdClaim = authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier);
        if (openIdClaim == null || string.IsNullOrWhiteSpace(openIdClaim.Value))
            return Redirect(redirectUrl);
            
        long id = 0;
        switch (provider)
        {
            case LinUserIdentity.GitHub:
                id = await _userCommunityService.SaveGitHubAsync(authenticateResult.Principal, openIdClaim.Value);
                break;
    
            case LinUserIdentity.QQ:
                id = await _userCommunityService.SaveQQAsync(authenticateResult.Principal, openIdClaim.Value);
                break;
    
            case LinUserIdentity.Gitee:
                id = await _userCommunityService.SaveGiteeAsync(authenticateResult.Principal, openIdClaim.Value);
                break;
            default:
                _logger.LogError($"未知的privoder:{provider},redirectUrl:{redirectUrl}");
                throw new LinCmsException($"未知的privoder:{provider}!");
        }
        
        //xxx更多参考 https://github.com/luoyunchong/lin-cms-dotnetcore/issues/9
        string token ="";
    
        return Redirect($"{redirectUrl}#login-result?token={token}");
    }
    

    一看上面的代码,也没毛病,原本也没想要再优化,但后来,我想实现账号绑定。比如,我先用QQ登录,退出后,再用gitee登录,这时就是二个账号了。我们可以在QQ登录的情况下,点击绑定账号,实现二者之间的绑定。如下表结构也是支持此功能的。只要他们的create_userid是一个,就是同一个账号。

    按上面的思路,绑定也是lin_user_identity表的数据操作,我们还放到IUserIdentityService服务中。这时就带来新的问题,这个接口在膨胀,他的实现类就更膨胀了。

    public  interface IUserIdentityService
     {
        Task<long> SaveGitHubAsync(ClaimsPrincipal principal, string openId);	
        Task<long> SaveQQAsync(ClaimsPrincipal principal, string openId);	
        Task<long> SaveGiteeAsync(ClaimsPrincipal principal, string openId);
        
         Task<UnifyResponseDto>  BindGitHubAsync(ClaimsPrincipal principal, string openId, long userId);
         Task<UnifyResponseDto>  BindQQAsync(ClaimsPrincipal principal, string openId, long userId);
         Task<UnifyResponseDto>  BindGiteeAsync(ClaimsPrincipal principal, string openId, long userId);
     }
    

    实现类多了一些方法,也能通过私有方法减少一些重复方法,但总感觉这样的设计实在是太挫了。

    这样代码中包含了不同的处理逻辑,一看就是违反了职责单一原则。

       public async Task<UnifyResponseDto> BindGitHubAsync(ClaimsPrincipal principal, string openId, long userId)
            {
                string name = principal.FindFirst(ClaimTypes.Name)?.Value;
                return await this.BindAsync(LinUserIdentity.GitHub, name, openId, userId);
            }
    
            public async Task<UnifyResponseDto> BindQQAsync(ClaimsPrincipal principal, string openId, long userId)
            {
                string nickname = principal.FindFirst(ClaimTypes.Name)?.Value;
                return await this.BindAsync(LinUserIdentity.QQ, nickname, openId, userId);
            }
    
            public async Task<UnifyResponseDto> BindGiteeAsync(ClaimsPrincipal principal, string openId, long userId)
            {
                string name = principal.FindFirst(ClaimTypes.Name)?.Value;
                return await this.BindAsync(LinUserIdentity.Gitee, name, openId, userId);
            }
    
            private async Task<UnifyResponseDto> BindAsync(string identityType, string name, string openId, long userId)
            {
                LinUserIdentity linUserIdentity = await _userIdentityRepository.Where(r => r.IdentityType == identityType && r.Credential == openId).FirstAsync();
                if (linUserIdentity == null)
                {
                    var userIdentity = new LinUserIdentity(identityType, name, openId, DateTime.Now);
                    userIdentity.CreateUserId = userId;
                    await _userIdentityRepository.InsertAsync(userIdentity);
                    return UnifyResponseDto.Success("绑定成功");
                }
                else
                {
                    return UnifyResponseDto.Error("绑定失败,该用户已绑定其他账号");
                }
            }
    

    第三方账号绑定回调,调用方法如下,非全部代码,

    [HttpGet("signin-bind-callback")]
    public async Task<IActionResult> SignInBindCallBack(string provider, string redirectUrl = "", string token = "")
    {
        //更多xxx代码
        long userId = 11;
        UnifyResponseDto unifyResponseDto;
        switch (provider)
        {
            case LinUserIdentity.GitHub:
                unifyResponseDto = await _userCommunityService.BindGitHubAsync(authenticateResult.Principal, openIdClaim.Value, userId);
                break;
            case LinUserIdentity.QQ:
                unifyResponseDto = await _userCommunityService.BindQQAsync(authenticateResult.Principal, openIdClaim.Value, userId);
                break;
            case LinUserIdentity.Gitee:
                unifyResponseDto = await _userCommunityService.BindGiteeAsync(authenticateResult.Principal, openIdClaim.Value, userId);
                break;
            default:
                _logger.LogError($"未知的privoder:{provider},redirectUrl:{redirectUrl}");
                unifyResponseDto = UnifyResponseDto.Error($"未知的privoder:{provider}!");
                break;
        }
    
        return Redirect($"{redirectUrl}#bind-result?code={unifyResponseDto.Code.ToString()}&message={HttpUtility.UrlEncode(unifyResponseDto.Message.ToString())}");
    }
    

    那么,我们如何优化呢。我们也看下表结构。

    表结构

    1. 用户表 lin_user

    字段 备注 类型
    id 主键Id bigint
    username 用户名 varchar

    2. 用户身份认证登录表 lin_user_identity

    字段 备注 类型
    id char 主键Id
    identity_type varchar 认证类型Password,GitHub、QQ、WeiXin等
    identifier varchar 认证者,例如 用户名,手机号,邮件等,
    credential varchar 凭证,例如 密码,存OpenId、Id,同一IdentityType的OpenId的值是唯一的
    create_user_id bigint 绑定的用户Id
    create_time datetime

    实体类

    • 用户信息 LinUser
        [Table(Name = "lin_user")]
        public class LinUser : FullAduitEntity
        {
            public LinUser() { }
    
            /// <summary>
            /// 用户名
            /// </summary>
            [Column(StringLength = 24)]
            public string Username { get; set; }
    
            [Navigate("CreateUserId")]
            public virtual ICollection<LinUserIdentity> LinUserIdentitys { get; set; }
    
         
        }
    
    • 用户身份认证登录表 LinUserIdentity
        [Table(Name = "lin_user_identity")]
        public class LinUserIdentity : FullAduitEntity<Guid>
        {
            public const string GitHub = "GitHub";
            public const string Password = "Password";
            public const string QQ = "QQ";
            public const string Gitee = "Gitee";
            public const string WeiXin = "WeiXin";
    
            /// <summary>
            ///认证类型, Password,GitHub、QQ、WeiXin等
            /// </summary>
            [Column(StringLength = 20)]
            public string IdentityType { get; set; }
    
            /// <summary>
            /// 认证者,例如 用户名,手机号,邮件等,
            /// </summary>
            [Column(StringLength = 24)]
            public string Identifier { get; set; }
    
            /// <summary>
            ///  凭证,例如 密码,存OpenId、Id,同一IdentityType的OpenId的值是唯一的
            /// </summary>
            [Column(StringLength = 50)]
            public string Credential { get; set; }
    
        }
    

    如何将六个方法,拆到不同的类中呢。

    1. 创建一个IOAuth2Service的接口,里面有二个方法,一个将授权登录后的信息保存,另一个是绑定和当前用户绑定。
       public interface IOAuth2Service
        {
            Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId);
    
            Task<UnifyResponseDto> BindAsync(ClaimsPrincipal principal, string identityType, string openId, long userId);
        }
    

    然后,分别创建,GiteeOAuth2Service,GithubOAuth2Serivice,QQOAuth2Service

    在这之前,因为整体逻辑相似,我们可以提取一个抽象类,在抽象类中写通用 的逻辑,子类只需要 实现SaveUserAsync,具体不同的逻辑了。

       public abstract class OAuthService : IOAuth2Service
        {
            private readonly IAuditBaseRepository<LinUserIdentity> _userIdentityRepository;
    
            public OAuthService(IAuditBaseRepository<LinUserIdentity> userIdentityRepository)
            {
                _userIdentityRepository = userIdentityRepository;
            }
            private async Task<UnifyResponseDto> BindAsync(string identityType, string name, string openId, long userId)
            {
                LinUserIdentity linUserIdentity = await _userIdentityRepository.Where(r => r.IdentityType == identityType && r.Credential == openId).FirstAsync();
                if (linUserIdentity == null)
                {
                    var userIdentity = new LinUserIdentity(identityType, name, openId, DateTime.Now);
                    userIdentity.CreateUserId = userId;
                    await _userIdentityRepository.InsertAsync(userIdentity);
                    return UnifyResponseDto.Success("绑定成功");
                }
                else
                {
                    return UnifyResponseDto.Error("绑定失败,该用户已绑定其他账号");
                }
            }
    
            public abstract Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId);
    
            public virtual async Task<UnifyResponseDto> BindAsync(ClaimsPrincipal principal, string identityType, string openId, long userId)
            {
                string nickname = principal.FindFirst(ClaimTypes.Name)?.Value;
                return await this.BindAsync(identityType, nickname, openId, userId);
            }
    
        }
    

    我们拿Gitee登录为例,

    public class GiteeOAuth2Service : OAuthService, IOAuth2Service
        {
            private readonly IUserRepository _userRepository;
            private readonly IAuditBaseRepository<LinUserIdentity> _userIdentityRepository;
    
            public GiteeOAuth2Service(IAuditBaseRepository<LinUserIdentity> userIdentityRepository, IUserRepository userRepository) : base(userIdentityRepository)
            {
                _userIdentityRepository = userIdentityRepository;
                _userRepository = userRepository;
            }
            public override async Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId)
            {
    
                LinUserIdentity linUserIdentity = await _userIdentityRepository.Where(r => r.IdentityType == LinUserIdentity.Gitee && r.Credential == openId).FirstAsync();
    
                long userId = 0;
                if (linUserIdentity == null)
                {
                    string email = principal.FindFirst(ClaimTypes.Email)?.Value;
                    string name = principal.FindFirst(ClaimTypes.Name)?.Value;
                    string nickname = principal.FindFirst(GiteeAuthenticationConstants.Claims.Name)?.Value;
                    string avatarUrl = principal.FindFirst("urn:gitee:avatar_url")?.Value;
                    string blogAddress = principal.FindFirst("urn:gitee:blog")?.Value;
                    string bio = principal.FindFirst("urn:gitee:bio")?.Value;
                    string htmlUrl = principal.FindFirst("urn:gitee:html_url")?.Value;
    
                    LinUser user = new LinUser
                    {
                        Active = UserActive.Active,
                        Avatar = avatarUrl,
                        LastLoginTime = DateTime.Now,
                        Email = email,
                        Introduction = bio + htmlUrl,
                        LinUserGroups = new List<LinUserGroup>()
                        {
                            new LinUserGroup()
                            {
                                GroupId = LinConsts.Group.User
                            }
                        },
                        Nickname = nickname,
                        Username = "",
                        BlogAddress = blogAddress,
                        LinUserIdentitys = new List<LinUserIdentity>()
                        {
                            new LinUserIdentity(LinUserIdentity.Gitee,name,openId,DateTime.Now)
                        }
                    };
                    await _userRepository.InsertAsync(user);
                    userId = user.Id;
                }
                else
                {
                    userId = linUserIdentity.CreateUserId;
                }
    
                return userId;
            }
    
        }
    

    GitHub 登录,保存用户信息,伪代码。他们在获取用户信息中有些差别。

       public class GithubOAuth2Serivice : OAuthService, IOAuth2Service
        {
            private readonly IUserRepository _userRepository;
            private readonly IAuditBaseRepository<LinUserIdentity> _userIdentityRepository;
    
            public GithubOAuth2Serivice(IAuditBaseRepository<LinUserIdentity> userIdentityRepository, IUserRepository userRepository) : base(userIdentityRepository)
            {
                _userIdentityRepository = userIdentityRepository;
                _userRepository = userRepository;
            }
    
            public override async Task<long> SaveUserAsync(ClaimsPrincipal principal, string openId)
            {
                return userId;
            }
        }
    

    依赖注入我们使用Autofac。同一个接口,可以 注入多个实现,通过Named区分。

    builder.RegisterType<GithubOAuth2Serivice>().Named<IOAuth2Service>(LinUserIdentity.GitHub).InstancePerLifetimeScope();
    builder.RegisterType<GiteeOAuth2Service>().Named<IOAuth2Service>(LinUserIdentity.Gitee).InstancePerLifetimeScope();
    builder.RegisterType<QQOAuth2Service>().Named<IOAuth2Service>(LinUserIdentity.QQ).InstancePerLifetimeScope();
    

    注入成功后,如何使用呢。我们通过 IComponentContext得到我们想要的对象。

    回调登录保存用户信息,相当于生成一个账号。伪代码。

        public Oauth2Controller(IComponentContext componentContext)
        {
            _componentContext = componentContext;
        }
            
        [HttpGet("signin-callback")]
        public async Task<IActionResult> Home(string provider, string redirectUrl = "")
        {          
            AuthenticateResult authenticateResult = await HttpContext.AuthenticateAsync(provider);
                
            IOAuth2Service oAuth2Service = _componentContext.ResolveNamed<IOAuth2Service>(provider);
            long id = await oAuth2Service.SaveUserAsync(authenticateResult.Principal, openIdClaim.Value);
            
            //...省略生成token的过程
            string token = _jsonWebTokenService.Encode(claims);
                  
            return Redirect($"{redirectUrl}#login-result?token={token}");
        }
            
    

    这里的Provider的值就是 LinUserIdentity.GitHub,一个字符串值。

        public class LinUserIdentity : FullAduitEntity<Guid>
        {
            public const string GitHub = "GitHub";
            public const string QQ = "QQ";
            public const string Gitee = "Gitee";
       }
    

    源码

    接口

    抽象类

    实现

    调用

    接口注入

    总结

    总结来说,我们干掉了switch case,好处是

    • 实现了对扩展开放,对修改关闭,我们不需要修改现有的类,就能新增新的逻辑。
    • 在整体上逻辑更清晰,而不是有一个需求,加一个接口,加一个实现,这样无脑操作。

    作者: 、天上有木月OvO

    出处:https://cnblogs.com/igeekfan

    联系:luoyunchong@foxmail.com

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如有问题或建议,请多多赐教,非常感谢。
  • 相关阅读:
    params可变参数
    using释放资源
    第二章:深入C#数据类型
    体检套餐项目解析
    堆栈
    C#必看:《第17章节QQ管理系统》
    C#必看:《第15章节学生管理系统》
    ACM hdu 1008 Elavator
    jq尺寸和位置总结篇(width scrollTop position offset)
    find children slice
  • 原文地址:https://www.cnblogs.com/igeekfan/p/StragetyPattern-Remove-Switch-Case.html
Copyright © 2011-2022 走看看