使用策略者模式减少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; }
    }
如何将六个方法,拆到不同的类中呢。
- 创建一个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";
   }
源码
接口
抽象类
实现
- https://github.com/luoyunchong/lin-cms-dotnetcore/blob/master/src/LinCms.Application/Cms/Users/GiteeOAuth2Service.cs
- https://github.com/luoyunchong/lin-cms-dotnetcore/blob/master/src/LinCms.Application/Cms/Users/GithubOAuth2Serivice.cs
- https://github.com/luoyunchong/lin-cms-dotnetcore/blob/master/src/LinCms.Application/Cms/Users/QQOAuth2Service.cs
调用
接口注入
总结
总结来说,我们干掉了switch case,好处是
- 实现了对扩展开放,对修改关闭,我们不需要修改现有的类,就能新增新的逻辑。
- 在整体上逻辑更清晰,而不是有一个需求,加一个接口,加一个实现,这样无脑操作。
使用策略者模式减少switch case 语句的更多相关文章
- 使用策略模式重构switch case 代码
		目录 1.背景 2.案例 3.switch…case…方式实现 4.switch…case…带来的问题 5.使用策略模式重构switch…case…代码 6.总结 1.背景 之前在看<重构 ... 
- if语句,if...else if语句和switch...case语句的区别和分析
		前段时间在工作中遇到了一个关于条件判断语句的问题,在if语句,if else if语句和switch case语句这三者之间分析,使用其中最有效率的一种方法. 所以就将这个问题作为自己第一篇博客的主要 ... 
- 为什么说在使用多条件判断时switch case语句比if语句效率高?
		在学习JavaScript中的if控制语句和switch控制语句的时候,提到了使用多条件判断时switch case语句比if语句效率高,但是身为小白的我并没有在代码中看出有什么不同.去度娘找了半个小 ... 
- java中的Switch case语句
		java中的Switch case 语句 在Switch语句中有4个关键字:switch,case break,default. 在switch(变量),变量只能是整型或者字符型,程序先读出这个变量的 ... 
- switch… case 语句的用法
		switch… case 语句的用法 public class Test7 { public static void main(String[] args) { int i=5; switch(i ... 
- Python | 基础系列 · Python为什么没有switch/case语句?
		与我之前使用的所有语言都不同,Python没有switch/case语句.为了达到这种分支语句的效果,一般方法是使用字典映射: def numbers_to_strings(argument): sw ... 
- 为什么switch...case语句比if...else执行效率高
		在C语言中,教科书告诉我们switch...case...语句比if...else if...else执行效率要高,但这到底是为什么呢?本文尝试从汇编的角度予以分析并揭晓其中的奥秘. 第一步,写一个d ... 
- JavaScript基础知识(if、if else、else if、while、switch...case语句)
		13.语句 概念:就是分号(:) 代表一条语句的结束 习惯:一行只编写一条语句:一行编写多条语句(代码可读性较差) 语句块:可以包含多条语句 "{ }"将多条语句包裹 u ... 
- C语言中switch case语句可变参实现方法(case 参数 空格...空格 参数 :)
		正常情况下,switch case语句是这么写的: : : ... ;break ; default : ... ;break ; } 接下来说一种不常见的,但是对于多参数有很大的帮助的写法: 先给一 ... 
随机推荐
- Hive和Spark分区策略
			1.概述 离线数据处理生态系统包含许多关键任务,最大限度的提高数据管道基础设施的稳定性和效率是至关重要的.这边博客将分享Hive和Spark分区的各种策略,以最大限度的提高数据工程生态系统的稳定性和效 ... 
- Java中对象初始化过程
			Java为对象初始化提供了多种选项. 当new一个对象的时候,对象初始化开始: 1.首先,JVM加载类(只加载一次,所以,即使多次new对象,下面的代码也只会在第一次new的时候执行一次),此时, 静 ... 
- .NetCore使用Docker安装ElasticSearch、Kibana 记录日志
			前言 最近园子里看到一篇<.Net Core with 微服务 - Elastic APM> 的文章(主要用于对接口的调用链.性能进行监控),非常实用,这里讲解.NetCore将日志写入E ... 
- hdu1233 最小生成树Prim算法和Kruskal算法
			Prim算法 时间复杂度:O(\(N^2\),N为结点数) 说明:先任意找一个点标记,然后每次找一条最短的两端分别为标记和未标记的边加进来,再把未标记的点标记上.即每次加入一条合法的最短的边,每次扩展 ... 
- 关于Word转Markdown的工具Writage安装及使用
			简介 Writage是为希望开始编写结构良好的文档,没有时间或不想深入了解 Markdown 语法的详细信息,或者更愿意使用 Word 作为文本编辑器的每个人设计的 下载并安装 安装包地址:https ... 
- 深入浅出,遇见Windows Terminal(Windows终端器),体验及美化新一代终端神器
			Windows Terminal 简介 Windows Terminal is a new, modern, feature-rich, productive terminal application ... 
- Vue Element-ui表单校验规则,你掌握了哪些?
			1.前言 Element-ui表单校验规则,使得错误提示可以直接在form-item下面显示,无需弹出框,因此还是很好用的. 我在做了登录页面的表单校验后,一度以为我已经很了解表单的校验规则. ... 
- (数据科学学习手札124)pandas 1.3版本主要更新内容一览
			本文示例代码及文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 就在几天前,pandas发布了其1.3版本 ... 
- AcWing 1250. 格子游戏
			#include<bits/stdc++.h> using namespace std; int n,m; int fa[1000000]; int found(int x) { if(f ... 
- Linux:CentOS-7配置VMware-15.5与本机IP同网段
			确认本机IP ctrl+R:输入cmd 回车 输入命令:ipconfig 可以看出一下信息:本机ip: 192.168.1.162网关:192.168.1.1DNS服务器:192.168.1.1 设 ... 
