使用策略者模式減少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,好處是

  • 實現了對擴展開放,對修改關閉,我們不需要修改現有的類,就能新增新的邏輯。
  • 在整體上邏輯更清晰,而不是有一個需求,加一個接口,加一個實現,這樣無腦操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM