問題引出
通常在很多的公司里面,對於接口的返回值沒做太大規范,所以會比較常看到各個項目各自定義隨意的返回值,比如以下情況:
1. 直接返回bool值(True或者False)
2. 返回void,只要不是異常信息,默認成功
3. 直接返回異常詳情(這個非常不好,通過一些低級的異常,客戶可以看到公司的一個技術水平)
4. 返回多個值,還要使用 out 來添加返回參數
5. 。。。
對於項目數量稍微多點的公司來說,接手多個項目的同事估計要吐血,所以項目間的業務通信規范是很有必要的。
解決方案
結合個人項目經驗,定義一個專門用來封裝返回值信息的通用類,如下:
/// <summary> /// 返回結果 /// </summary> public interface IResult { /// <summary> /// 結果狀態碼 /// </summary> ResultCode Code { get; set; } /// <summary> /// 提示信息 /// </summary> /// <example>操作成功</example> string Message { get; set; } /// <summary> /// 是否成功 /// </summary> bool Success { get; } } /// <summary> /// 返回的附帶泛型數據 /// </summary> public interface IResult<TType> : IResult { /// <summary> /// 返回的附帶數據 /// </summary> TType Data { get; set; } }
這個ResultCode是針對業務操作結果的自定義枚舉,用來標志當前返回的一個業務結果
public enum ResultCode { /// <summary> /// 操作成功 ///</summary> [Display(Name = "操作成功")] Ok = 1, /// <summary> /// 操作失敗 ///</summary> [Display(Name = "操作失敗")] Fail = 11, /// <summary> /// 登陸失敗 ///</summary> [Display(Name = "登陸失敗")] LoginFail = 12, /// <summary> /// 沒有該數據 ///</summary> [Display(Name = "沒有數據")] NoRecord = 13, /// <summary> /// 用戶不存在 ///</summary> [Display(Name = "用戶不存在")] NoSuchUser = 14, /// <summary> /// 未登錄 ///</summary> [Display(Name = "未登錄")] Unauthorized = 20, /// <summary> /// 未授權 /// </summary> [Display(Name = "未授權")] Forbidden = 21, /// <summary> /// 無效Token /// </summary> [Display(Name = "無效Token")] InvalidToken = 22, /// <summary> /// 參數驗證失敗 /// </summary> [Display(Name = "參數驗證失敗")] InvalidData = 23, /// <summary> /// 無效用戶 /// </summary> [Display(Name = "無效用戶")] InvalidUser = 24 }
有了以上的接口,我們可以看一下具體實現
public class Result : IResult { private string _message; /// <summary> /// 是否成功 /// </summary> public bool Success => Code == ResultCode.Ok; /// <summary> /// 結果碼 /// </summary> public ResultCode Code {get; set;} /// <summary> /// 提示信息 /// </summary> public string Message { get { return _message ?? Code.DisplayName(); } set { _message = value; } } /// <summary> /// 返回結果,默認成功 /// </summary> public Result() { Code = ResultCode.Ok; } /// <summary> /// 返回結果 /// </summary> /// <param name="code">狀態碼</param> /// <param name="message">提示信息</param> public Result(ResultCode code, string message = null) { Code = code; Message = message; } }
這里我們定義了實現類,注意默認的構造函數是返回成功的,這方便我們后面針對業務對這個返回結果再次進行擴展。細心的大家應該注意到了返回的提示信息,我們針對上面的自定義枚舉的提示信息會進行顯示,后面具體實現再看。先看一下我們的泛型返回結果的實現
/// <summary> /// 返回結果 /// </summary> public class Result<TType> : Result, IResult<TType> { /// <summary> /// cotr /// </summary> public Result() { } /// <summary> /// 返回結果 /// </summary> public Result(TType data) : base(ResultCode.Ok) { Data = data; } /// <summary> /// 返回結果 /// </summary> /// <param name="code">狀態碼</param> /// <param name="message">提示信息</param> public Result(ResultCode code, string message = null) : base(code, message) { } /// <summary> /// 返回結果 /// </summary> public Result(ResultCode code, string message = null, TType data = default(TType)) : base(code, message) { Data = data; } /// <summary> /// 返回業務數據 /// </summary> public TType Data { get; set; } }
好有了這些,我們在Result類中定義一些靜態方法對結果進行封裝,這樣可以讓我們在業務層進行快速的調用
/// <summary> /// 返回指定 Code /// </summary> public static Result FromCode(ResultCode code, string message = null) { return new Result(code, message); } /// <summary> /// 返回錯誤信息 /// </summary> public static Result FromError(string message, ResultCode code = ResultCode.Fail) { return new Result(code, message); } /// <summary> /// 返回成功 /// </summary> public static Result Ok(string message = null) { return FromCode(ResultCode.Ok, message); } /// <summary> /// 返回指定 Code /// </summary> public static Result<T> FromCode<T>(ResultCode code, string message = null) { return new Result<T>(code, message); } /// <summary> /// 返回指定 Code和提示信息 /// </summary> public static Result<T> FromCode<T>(ResultCode code, T data, string message = null) { return new Result<T>(code, message, data); } /// <summary> /// 返回錯誤信息 /// </summary> public static Result<T> FromError<T>(string message, ResultCode code = ResultCode.Fail) { return new Result<T>(code, message); } /// <summary> /// 返回數據 /// </summary> public static Result<T> FromData<T>(T data) { return new Result<T>(data); } /// <summary> /// 返回數據和提示信息 /// </summary> public static Result<T> FromData<T>(T data, string message) { return new Result<T>(ResultCode.Ok, message, data); } /// <summary> /// 返回成功 /// </summary> public static Result<T> Ok<T>(T data) { return FromData(data); }
好了有了上面這些,我們該如何調用呢?當我們需要直接返回成功時,我們可以這樣
return Result.Ok();
前端接收到的結果如下:
當我們需要返回帶有數據的結果時,我們可以這樣:
var list = new List<string> { "lex1", "lex2" }; return Result.FromData(list);
前端接收到的結果如下:
當我們需要返回指定Code的時候,如下:
return Result.FromCode(ResultCode.LoginFail);
前端接收到的結果如下:
我們可以看到上面的提示信息是我們在枚舉上定義的信息,這是我們在Result類中對Message進行了Code.DisplayName(),思想很簡單,就是對枚舉進行了擴展,利用DisplayAttribute的公用方法顯示信息,那我們怎么知道什么時候調用DisplayAttribute的合適方法呢?
我們先定義一個類DisplayProperty,用來對應DisplayAttribute的各個屬性
public enum DisplayProperty { /// <summary> /// 名稱 /// </summary> Name, /// <summary> /// 短名稱 /// </summary> ShortName, /// <summary> /// 分組名稱 /// </summary> GroupName, /// <summary> /// 說明 /// </summary> Description, /// <summary> /// 排序 /// </summary> Order, /// <summary> /// 水印信息 /// </summary> Prompt, }
有了這個之后,我們的枚舉擴展方法如下:
/// <summary> /// 獲取枚舉說明 /// </summary> public static string DisplayName(this Enum val) { return val.Display(DisplayProperty.Name) as string; } /// <summary> /// 獲取枚舉短名稱說明 /// </summary> public static string DisplayShortName(this Enum val) { return val.Display(DisplayProperty.ShortName) as string; } /// <summary> /// 獲取枚舉水印信息 /// </summary> public static string DisplayPrompt(this Enum val) { return val.Display(DisplayProperty.Prompt) as string; } /// <summary> /// 獲取枚舉備注 /// </summary> public static string DisplayDescription(this Enum val) { return val.Display(DisplayProperty.Description) as string; } /// <summary> /// 獲取枚舉指定的顯示內容 /// </summary> public static object Display(this Enum val, DisplayProperty property) { var enumType = val.GetType(); var str = val.ToString(); if (enumType.GetAttribute<FlagsAttribute>() != null && str.Contains(",")) { var array = str.Split(",", StringSplitOptions.RemoveEmptyEntries).Select(o => o.Trim()); var result = array.Aggregate("", (s, s1) => { var f = enumType.GetField(s1); if (f != null) {
//MethodInfo的擴展,方法在下面 var text = f.Display(property); return s.IsNullOrEmpty() ? text.ToString() : $"{s},{text}"; } return s; }); return result.IsNullOrEmpty() ? null : result; } var field = enumType.GetField(str); if (field != null) { return field.Display(property); } return null; }
再看針對MemberInfo的一個擴展,這里面就根據我們傳入的DisplayProperty屬性值調用了DisplayAttribute的對應方法
/// <summary> /// 獲取枚舉指定的顯示內容 /// </summary> public static object Display(this MemberInfo memberInfo, DisplayProperty property) { if (memberInfo == null) return null; var display = memberInfo.GetAttribute<DisplayAttribute>(); if (display != null) { switch (property) { case DisplayProperty.Name: return display.GetName(); case DisplayProperty.ShortName: return display.GetShortName(); case DisplayProperty.GroupName: return display.GetGroupName(); case DisplayProperty.Description: return display.GetDescription(); case DisplayProperty.Order: return display.GetOrder(); case DisplayProperty.Prompt: return display.GetPrompt(); } } return null; }
到此我們的這個業務通訊結果已經可以了,再細想,有幾個問題需要我們解決的:
1. ResultCode的意義?
2. 公司這么多項目都這樣的話,如果某個系統需要新增一個提示或者英文不規范修改了,那會不會造成不一致呢?
后續文章會針對這些問題和可能存在的問題進行探討!