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

    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如有问题或建议,请多多赐教,非常感谢。
  • 相关阅读:
    Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
    DHCP "No subnet declaration for xxx (no IPv4 addresses)" 报错
    Centos安装前端开发常用软件
    kubernetes学习笔记之十:RBAC(二)
    k8s学习笔记之StorageClass+NFS
    k8s学习笔记之ConfigMap和Secret
    k8s笔记之chartmuseum搭建
    K8S集群集成harbor(1.9.3)服务并配置HTTPS
    Docker镜像仓库Harbor1.7.0搭建及配置
    Nginx自建SSL证书部署HTTPS网站
  • 原文地址:https://www.cnblogs.com/igeekfan/p/StragetyPattern-Remove-Switch-Case.html
Copyright © 2011-2022 走看看