纯粹的解决方案架构,无障碍的测试以及我如何做到这一点

亲爱的读者您好! 在本文中,我想谈谈我的项目的体系结构,该项目在启动时进行了4次重构,因为我对结果不满意。 我将讨论流行方法的弊端并展示自己的方法。


我想马上说这是我的第一篇文章,我不是在说要怎么做-对。 我只想展示自己的所作所为,告诉我如何获得最终结果,最重要的是-获得他人的意见。


我参加了几次竞选活动,看到了很多本来可以做的事情。


例如,我经常看到N层体系结构,有一个用于处理数据(DA)的层,有一个具有业务逻辑(BL)的层,该层使用DA和可能的其他服务工作,还有一个视图层\ API,接收请求,使用BL处理。 看起来很方便,但是看一下代码,我看到了这种情况:


  • [DA]即使复杂的查询也可以拉取\写入\更改数据-确定
  • [BL] 80%调用1方法并将结果滚动到上方-为什么选择此空层?
  • [查看] 80%调用1个BL方法抛出上面的结果-为什么要保留空白层?

此外,包装接口很时髦,以便以后可以锁定和测试-哇,哇!


  • 为什么要弄湿?
  • 好吧,在测试过程中减少副作用。
  • 就是说,我们将抗议而没有副作用,但是在生产时会产生副作用吗?
    ...

这是我在该架构中不喜欢的基本事物,因为要解决诸如“列出用户的喜欢”之类的问题是一个很大的过程,但是实际上数据库中只有一个查询,可能还有映射。


样品溶液

1)[DA]向DA添加请求
2)[BL]转发DA响应
3)[查看]转发BA结果,可以提升


不要忘记,所有这些方法仍然需要添加到界面中,我们正在编写一个项目是为了弄湿,而不是寻求解决方案。


在其他地方,我看到了具有CQRS方法的API实现。


解决方案看起来不错,有1个文件夹-1个功能。 制作功能的开发人员坐在他的文件夹中,几乎总是可以忘记他的代码对其他功能的影响,但是文件太多,简直就是一场噩梦。 请求/响应模型,验证器,帮助器,逻辑本身。 在工作室中进行搜索实际上是行不通的,对扩展进行了查找以查找代码中的必要内容。


还有很多要说的,但我强调了导致我拒绝的主要原因


最后到我的项目


正如我所说,我多次重构了项目,那时候我遇到了“程序员压抑”,我只是对自己的代码不满意,然后一次又一次地对其进行重构,最后,我开始观看有关应用程序体系结构的视频,以了解如何别人做。 我遇到了安东·摩尔多瓦(Anton Moldovan)关于DDD和函数式编程的报告,并想:“在这里,我需要F#!”。


在F#上花了几天之后,我意识到,原则上,我将在C#中执行相同的操作,并且不会更糟。 视频显示:


  • 这是C#代码,很糟糕
  • 这是F#很酷,少写了-超级。

但是诀窍在于,F#上的解决方案实施方式不同,与此相反,他们在C#上的实施效果很差。 主要原则是BL并不是调用DA服务并完成所有工作的东西,而是纯函数


当然,F#很好,我喜欢一些功能,但是,像C#一样,这只是一个可以以不同方式使用的工具。


然后我回到C#并开始创建。


我在解决方案中创建了这样的项目:


  1. API
  2. 核心
  3. 服务项目
  4. 测验

我还使用了C#8功能,尤其是可为空的引用类型,我将展示其应用程序。
简要介绍一下我给他们的任务。


API
1)接收请求,请求模型+验证,限制


更多细节

图片


2)从核心和服务调用功能


更多细节

图片


在这里我们看到一个简单易读的代码,我想每个人都会理解这里写的内容。
观察到清晰的图案
1)获取数据
2)处理,修改等-这部分需要测试。
3)保存。


3)映射(如果需要)
4)错误处理(记录+人为响应)


更多细节

此类包含异常处理程序响应的所有可能的应用程序错误。


图片


图片


事实证明,该应用程序可以运行,或者给出特定的错误,并且处理后的错误不是副作用或错误,这些错误的日志会立即与我聊天,并通过电报与我聊天。


我有AppError.Bug这个错误的情况不清楚。


我有一个来自其他服务的CallBack,它在我的系统中将有一个userId,如果我找不到具有此ID的用户,则该用户发生了某些事情或根本不清楚,这样的错误像CRITICAL一样飞向我,理论上不应出现,但如果确实如此,则需要我的干预。


图片


核心,最有趣


我一直牢记,BL只是使用相同输入给出相同结果的函数。 该层中代码的复杂性在于实验室的工作水平,而不是出色的功能,而这些功能却清晰无误地发挥了作用。 而且重要的是,函数内部必须没有副作用,而所需的全部功能就是其参数。


如果功能需要用户平衡,则我们将获得平衡并将其转移给功能,并且请勿将用户服务推入BL。


1)实体的基本行动


更多细节

图片
图片


我想出了一些方法作为扩展方法,以使类不会膨胀,并且可以按功能对功能进行分组。


图片
图片


我认为良好的实体模型构建同样重要。


例如,我有一个用户,该用户具有几种货币的余额。 我毫不犹豫地做出的典型决定之一就是“余额”的本质,并且只是在用户中设置了一系列余额。 但是什么样的便利带来了这样的决定?


1)添加/删除货币。 此任务对我们而言立即意味着不仅要编写新代码,而且还要迁移(填充/删除所有现有用户),这是最简单的选择。 上帝禁止,要添加一种新货币,您必须为用户创建一个按钮,他单击该按钮并启动用于某种业务流程的新钱包的创建。 结果,只需要为新货币扩展枚举,他们编写了另一个功能,即可以通过按钮创建钱包,他们将另一个任务放在了最前面。


2)在代码中,使用FirstOrDefault常量(s => s.Currency ==货币)并检查是否为空


我的决定

图片


通过模型本身,我保证自己不会有空余额,并且通过创建索引器运算符,我在与余额进行交互的所有位置简化了我的代码。


服务项目


这一层为我提供了使用各种服务的便捷工具。
在我的项目中,我使用MongoDB,为了方便使用,我将集合包装在这样的存储库中。


更多细节

仓库本身
图片


Monga分别在处理文档时将其阻止,这将帮助我们解决请求竞争中的问题。 在旺季中,有多种方法可以搜索实体并对其进行操作,例如:“找到具有ID的用户,并将其当前余额加10”


现在介绍一下C#8的功能。


图片


图片


方法签名告诉我,当我看到User时,User可以分别返回,并且可能返回Null。 我立即收到编译器警告,并执行空检查。


图片


当该方法返回User时,我会自信地使用它。


图片


我还想提请注意以下事实:没有尝试捕获,因为异常只能来自“奇怪的情况”,而错误的数据由于存在验证而不能到达此处。 API层也没有try catch,只有一个全局异常处理程序。


只有一种抛出Exception的方法是Update方法。
它在多线程模式下实现了防止数据丢失的保护。
图片


您为什么不使用上面提到的monga方法?
在某些地方,我仍然不确定是否可以与用户合作,也许他没有钱进行此操作,所以一开始我会让用户离开并进行检查,然后进行变异并保存。


图片


从理论上讲,我的应用程序每秒将改变用户的平衡超过1次,因为这将是快速的游戏。


但是用户模型本身很明显,用户的推荐是可选的,并且您可以处理其他所有内容而无需考虑null。


图片


最终测试


就像我说的,您只需要测试逻辑,我们功能的逻辑就没有副作用。
因此,我们可以使用不同的参数快速运行测试。


更多细节

我下载了nuget FSCheck,它会生成随机输入的数据,并允许许多不同的情况。


我只需要创建各种用户,提供他们的测试并检查更改即可。


有一个用于创建此类用户的小型构建器,但扩展很容易。


图片


这是测试本身


图片


图片


图片


进行一些更改后,我运行测试,在1-2秒后,我看到一切正常。
还计划编写E2E测试,以便从外部检查整个API,并确保从请求到响应,它都能正常工作。


薯条


您可能需要的很酷的东西

我的每个请求都是掺杂的,当发生错误时,我会找到requestId,并且可以通过重复请求轻松地重现该错误,因为我的API没有状态,并且每个请求仅取决于请求参数。


图片


总结一下。


我们确实写了一个解决方案,而不是一个包含大量额外抽象和mok的框架。 我们在一个地方进行了错误处理,它们很少发生。 我们将BL和副作用分开了,现在BL只是可以重用的局部逻辑。 我们没有编写额外的函数来简单地将调用转发给其他函数。 我会积极阅读评论并补充文章,谢谢!

Source: https://habr.com/ru/post/zh-CN459394/


All Articles