此篇文章主要讲述领域驱动设计在WebApi中的应用,以及设计方式,这种设计的原理以及有点。
随着技术的不断迭代升级,设计方式也在不断迭代更新,目前比较流行的就是领域驱动设计的方式来开发程序,领域驱动设计相对于传统设计模式的有点在于:1、更好地理解业务需求。2、更好的设计质量。3、更好的团队协作。4、更好的的业务创新。
领减驱动设计(Domain-DrixenDesig,简称DDD)是一种软件开发方法论,旨在解决复杂业务场景下的软件设计问
题。DDD的核心理念是将业务领域作为重点,将软件系统分解为多个子域,并通过领域模型、聚合、实体等概念来描述和
实现领域逻辑。
领域驱动设计的好处主要包括以下几点:
1.更好地理解业务需求:DDD强调将业务领域作为设计的重点,通过深入了解业务需求和领域知识,能够更好地把握业务流程和逻辑,从而更好地满足用户需求。
2.更好的设计质量:DDD通过建立领域模型、聚合和实体等概念,能够更好地划分系统结构和职责,提高系统的可维护性、可扩展性和可测试性。同时,领域模型也能够更好地反映业务实体之间的关系和行为,提高系统的质量和稳定性。
3.更好的团队协作:DDD强调跨部门、跨角色的协作,通过建立共同的领域模型和语言,能夠够更好地促进团队之间的
沟通和协作,提高团队的效率和协作能力。
4.更好的业务创新:DDD强调将业务领域作为重点,通过深入理解业务需求和领域知识,能够更好地发掘业务创新
点,提高业务竞争力。
总之,领域驱动设计可以帮助开发团队更好地理解业务需求、提高设计质量、促进团队协作和推动业务创新。
众所周知,对接结果的操作可分为请求(HttpRequest)和返回(HttpResponse)两个大类的操作
那么我们就根据这两个操作来进行业务拆分。
项目结构展示:
HttpResponse的父类 也就是项目结构中的IBaseRes
的设计方式
代码如下:
public interface IBaseRes
{
/// <summary>
/// 请求码
/// </summary>
int Code { get; set; }
/// <summary>
/// 文本信息
/// </summary>
string Msg { get; set; }
/// <summary>
/// 返回时间
/// </summary>
string Time { get; set; }
}
public abstract class BaseRes : IBaseRes
{
public BaseRes()
{
this.Code = 1;
this.Msg = "接收返回内容失败";
this.Time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
/// <summary>
/// 请求码
/// </summary>
[JsonProperty("code")]
public int Code { get; set; }
/// <summary>
/// 文本信息
/// </summary>
[JsonProperty("msg")]
public string Msg { get; set; }
/// <summary>
/// 返回时间
/// </summary>
[JsonProperty("time")]
public string Time { get; set; }
}
public static class Serialize
{
public static string ToJson(this GoodsSyncStockReq self) => JsonConvert.SerializeObject(self, Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
这里主要是提取所有接口返回的共用字段
,减少代码的冗余
以上代码可以看出,我们先设计了个接口,在接口中规定了有那些共用字段,在下方又实现了接口并使用Newtonsoft
序列化工具特性规定其在返回时的字段名称。
HttpRequest的父类 也就是项目结构中的BaseReq
的设计方式
代码如下(示例):
public interface IBaseReq<T> where T : IBaseRes
{
/// <summary>
/// 获取接口路径
/// </summary>
/// <returns></returns>
string GetApi();
}
这里主要是限制类型
必须是IBaseRes(返回基类)的派生类,并给出获取接口名称的方法
。
public interface IPingAnClient
{
/// <summary>
/// 执行请求
/// </summary>
/// <typeparam name="T">领域对象</typeparam>
/// <param name="request">请求数据</param>
/// <returns></returns>
Task<T> Send<T>(IBaseReq<T> request) where T : BaseRes;
}
这里也是以接口的方式实现,给出了一个Send方法,该方法是用于发起Http请求的方法。
以下给出的是业务领域模型设计的代码以及设计方式和原理
代码如下:
public class GoodsListsRes :BaseRes
{
[JsonProperty("data")]
public GoodsListInf Data { get; set; }
}
以上代码可以看出这里的业务领域模型继承了(BaseRes)返回基类
有的爱思考的朋友就会提问这里为什么data会在这里呢?
这里是因为每个领域返回的data内容的结构是不样的所有,就将他下放至每个领域中各自实现。
代码如下(示例):
public class GoodsListsReq : IBaseReq<GoodsListsRes>
{
/// <summary>
/// 商品类型 1销售中 2仓库中[可选]
/// </summary>
public int type { get; set; }
/// <summary>
/// 返回接口请求路径
/// </summary>
public string GetApi()
{
return "/goods/lists";
}
}
以上代码可以看出这里的业务领域模型继承了(IBaseReq)请求基类
眼尖的同学相必已经发现了这里我们传了一个GoodsListsRes那这又是为什么?
这里是为了方便我们后面返回时直接反射创建对象接收返回值。
public class DefaultPingAnClient : IPingAnClient
{
private string serverUrl;
private string appKey;
private string appSecret;
public DefaultPingAnClient(string serverUrl, string appKey, string appSecret)
{
this.appKey = appKey;
this.appSecret = appSecret;
this.serverUrl = serverUrl;
}
/// <summary>
/// 发起请求
/// </summary>
/// <typeparam name="T">领域对象</typeparam>
/// <param name="request">请求值</param>
/// <returns></returns>
public async Task<T> Send<T>(IBaseReq<T> request) where T : BaseRes
{
return await DoSend(request);
}
private async Task<T> DoSend<T>(IBaseReq<T> request) where T : BaseRes
{
return await Response(request, request.GetApi());
}
#region 请求相关
/// <summary>
/// 签名
/// </summary>
/// <param name="dir">请求参数</param>
/// <returns></returns>
private string Sign(Dictionary<string, object> dir)
{
string sign = string.Empty;
if (dir.Count() > 0)
{
//拼接请求参数,如果不存在不用拼接,拼接前要进行排序(a=1&b=2&c=3)
dir = dir.OrderBy(x => x.Key).ToDictionary(f => f.Key, f => f.Value);
sign = JsonConvert.SerializeObject(dir);
}
DateTimeOffset now = DateTimeOffset.Now;
//拼接unix时间戳
long unixTimestamp = (long)(now - new DateTime(1970, 1, 1)).TotalSeconds;
sign += unixTimestamp;
sign += appSecret;
sign = Md5(sign).ToString();
return sign.ToLower();
}
/// <summary>
/// 发起请求
/// </summary>
/// <param name="Url">请求路径</param>
/// <returns></returns>
private async Task<T> Response<T>(IBaseReq<T> request, string Url, ResponseType RType = ResponseType.Post) where T : BaseRes
{
var rspModel = Activator.CreateInstance<T>();
try
{
var url = serverUrl + Url;
Dictionary<string, object> parames = new Dictionary<string, object>();
if (request != null)
{
parames = JsonConvert.DeserializeObject<Dictionary<string, object>>(JsonConvert.SerializeObject(request)) ?? new Dictionary<string, object>();
}
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("sign", Sign(parames));
client.DefaultRequestHeaders.Add("timestamp", timeStamp().ToString());
client.DefaultRequestHeaders.Add("key", appKey);
client.Timeout = TimeSpan.FromSeconds(30);
HttpResponseMessage response = new HttpResponseMessage();
switch (RType)
{
case ResponseType.Post:
response = client.PostAsync(url, new StringContent(JsonConvert.SerializeObject(parames), Encoding.UTF8, "application/json")).Result;
break;
case ResponseType.Put:
response = client.PutAsync(url, new StringContent(JsonConvert.SerializeObject(parames), Encoding.UTF8, "application/json")).Result;
break;
case ResponseType.Delete:
response = client.DeleteAsync(url + "?" + buildParamStr(parames)).Result;
break;
case ResponseType.Get:
response = client.GetAsync(url + "?" + buildParamStr(parames)).Result;
break;
default:
rspModel.Code = 1;
rspModel.Msg = "请求类型错误!";
break;
}
if (response != null && response.IsSuccessStatusCode)
{
string responseText = response.Content.ReadAsStringAsync().Result;
if (!string.IsNullOrWhiteSpace(responseText))
{
if (responseText.Contains("签名"))
{
SginErroRes erroRes = JsonConvert.DeserializeObject<SginErroRes>(responseText);
rspModel.Msg = erroRes.Msg;
rspModel.Time=erroRes.Time;
}
else
{
rspModel = JsonConvert.DeserializeObject<T>(responseText);
}
}
else
{
rspModel.Msg = "返回内容为空!";
}
}
};
}
catch (Exception ex)
{
rspModel.Code = 1;
rspModel.Msg = ex.Message;
}
return rspModel;
}
/// <summary>
/// 请求类型
/// </summary>
private enum ResponseType
{
Post,
Put,
Delete,
Get,
}
/// <summary>
/// 拼接请求参数
/// </summary>
/// <param name="param">参数字典</param>
/// <returns></returns>
private static String buildParamStr(Dictionary<string, object> param)
{
String paramStr = String.Empty;
if (param != null)
{
foreach (var key in param.Keys.ToList())
{
string keyparam = param[key].ToString().Replace("+", "%2B");
//keyparam = HttpUtility.UrlEncode(keyparam);
if (param.Keys.ToList().IndexOf(key) == 0)
{
paramStr += (key + "=" + keyparam);
}
else
{
paramStr += ("&" + key + "=" + keyparam);
}
}
}
return paramStr;
}
/// <summary>
/// Md5加密
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private string Md5(string str)
{
if (string.IsNullOrEmpty(str)) str = "";
MD5 md5Hash = MD5.Create();
byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(str));
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++)
{
sBuilder.Append(data[i].ToString("x2"));
}
return sBuilder.ToString();
}
/// <summary>
/// 秒级时间戳
/// </summary>
/// <returns></returns>
private long timeStamp()
{
return (DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000000;
}
#endregion
}
这里需要着重讲解一下 Response
方法:
方法里使用到了:Activator.CreateInstance,微软文档是这样说的 传送门
其实我的理解就是利用反射创建对应对象,这里可能会返回null
还用到了:HttpClient 微软文档是这样说的 传送门
从这里就可以体现出领域驱动设计的效果了,商品领域的请求和返回只会用到它自己领域的东西。这也很好的避免了传统设计方式会出现的代码冗余问题。也可以方便后期维护,需要修改那个领域就可以很快的找到相关的代码。