可移植数据层 - 持久层





0/5 (0投票)
创建一个持久且可移动的层。
引言
多年来,我见过许多组织和个人开发了构建应用程序的框架。甚至还有书籍和杂志文章讨论了框架带来的好处和弊端。在见过和使用过其中一些框架后,我幸运地学到了一些东西。许多框架提供了分层和模式。在本文中,我想分享可移植层。
背景
随着我们的环境和工具的变化,编程也将不断变化。不幸的是,改变总是伴随着成本。我们已经将大型机应用程序迁移到客户端服务器应用程序。我们将应用程序迁移到基于 Web 的应用程序。现在,我们甚至将应用程序迁移到我们的手机上。在这些变化发生的同时,我们的编码风格也在改变。有时,风格已经开始成熟为模式。随着模式的改变,为了让我们的生活“更轻松”,我们会进行调整,让我们的生活“更轻松”。我在这里停下来,因为这是一个漫长的故事。
回顾过去
回顾历史,有助于学习和观察。有助于提出问题。这里的大问题是,“面对所有这些变化,我能做些什么来降低这些变化的成本?”
答案
一次又一次,我找到了一个问题的答案。曾经有一段时间,我想称之为“独立层”。“独立层”这个名字并不完全贴切。我想要一个可以移动的层,一个“可移植层”。为了简洁,我们称之为 T 层。现在,这一层并不新。我只是看到很多框架、书籍和文章甚至没有提到它。这很可惜,因为随着时间的推移,这会浪费我们的时间和增加风险。
它是什么?
T 层在很大程度上保存了我们的业务逻辑。然而,它的编写方式使其在很大程度上独立于其运行的环境。它与上层和下层有约定。目标仍然是可移植,因此它可以从 MVC 迁移到 MVVM,再到 ASP.NET Web 服务,再到 WCF 服务,再到 ServiceStack,再到 XYZ。
这可能吗?
大多数情况下可以。这取决于您的组织或您决定将什么放入 T 层。有时,拥有这一层可能不合适。例如,需要高性能的批处理过程肯定应该尽一切努力满足其要求。对于 UI 开发,绝对可以使用此层。它可以在 Web 服务中使用吗?当然。它可以不经更改地从 Web 服务移动到 UI 吗?可能。
这一层应该放在哪里?
对于那些处理大型 UI 模式或 SOA 模式的开发者来说,以下几张图有助于可视化这一层应该存在的位置。
正如您所见,T 层位于 UI 和持久化层之间。UI 应该使用 T 层的输入和输出来支持 UI 的需求。T 层对持久化层有一些要求,但它不应该担心如何执行持久化。对于 SOA,T 层位于通信层和持久化层之间。通信层应该使用 T 层的输入和输出来支持 SOA 的需求。目标是,T 层应该是可移植的。
T 层应该做什么?
这是一个棘手的问题,有很多答案。幸运的是,我找到了一些 T 层可以处理的常见任务。
总的来说,这些方法应该能很好地处理许多应用程序的功能。然而,了解所有可能需要使用特定功能的应用程序可能会导致无法知道要创建的正确方法。因此,可能需要一个迭代过程来维护 T 层。
T 层应该执行验证。这些验证应该在任何上层验证和下层验证之后应用。因此,如果 UI 执行检查以确保电子邮件地址有效,T 层应该拥有相同或更好的检查。如果持久化工具说电子邮件地址最多只能有 255 个字符,T 层应该检查电子邮件不超过 255 个字符。UI 层可以强制执行更严格的验证吗?当然,T 层应该处理许多用户应该遵循的验证。通常,验证是用于保存数据的。T 层应该执行查询或请求功能的验证吗?肯定可以。这取决于您或您的架构师决定是否应该这样做。我认为应该。假设有一个搜索潜在客户的方法,而潜在客户有数百万。该方法通过姓氏查找客户。如果未输入姓氏,则无内容可搜索,并且应返回一个验证消息,表明需要输入。我认为 T 层可以返回一条消息,说明需要姓氏。将验证业务规则保留在既定位置,最终确实会使生活更轻松。能够移动代码和逻辑来支持这些规则,最终会使生活更加轻松。
T 层可以支持处理。有简单的过程和复杂的处理。T 层是处理协调和可能执行处理的地方。许多时候,处理涉及逻辑,而 T 层应该能够处理很多逻辑。有时,还有其他组件和代码用于处理复杂的过程。在复杂情况下,T 层可以与用于处理这些复杂过程的组件和代码进行协调。为什么 T 层应该与应用程序本身而不是其他复杂组件进行交互?这是一个架构决策。我发现,在处理组件时,通常会进行预处理验证和后处理清理。T 层可以支持预处理和后处理。此外,让 T 层与组件协同工作提供了一个抽象级别,如果组件被更改或替换,可能会有所帮助。拥有一个 T 层,可以将其协调从 UI 或 SOA 代码中的一个位置移动到另一个位置,从长远来看会降低成本。
T 层应该执行授权检查吗?
有些人会说“是”,有些人会说“否”。我说“是”,因为授权是业务逻辑。依赖前端应用程序来正确执行授权是有风险的。应用程序仍应尝试将授权强制执行到应用程序的业务和处理逻辑中,而不是 T 层。有些人会说,应用程序应该决定用户是否有权执行 T 层管理的任务。使用 T 层的代码或使用 T 层的应用程序(处理授权方法)实际上不需要 T 层执行任何授权。其他人会说,无论该层在哪里,T 层都应该处理授权。在某些情况下,使用 T 层的代码和 T 层可能需要检查身份验证。授权检查的一个主要顾虑是性能。在某些环境中,授权代码执行起来可能很慢。因此,让应用程序和 T 层执行授权检查可能不是一个好主意。无论做出什么选择,都要努力保持一致,并确保构建支持该选择的环境。
深入细节
我们已经讨论了一些 T 层可以做的事情。为了使其正常工作,可能需要记住一些想法以保持一致性。T 层通常在上下文中运行。上下文可能是应用程序、客户、用户、语言、货币等。找出 T 层运行的上下文并不需要太长时间。为了使 T 层一致,上下文需要存在于可以传输的位置。曾经,我见过上下文放在 Principal 上。Principal 很好,因为它存在于线程上。但最近,我更倾向于将上下文作为对象传递给每个方法。这样传递上下文对大多数开发人员来说都很简单。如果上下文需要略有不同,它可能包含一个 Dictionary 来捕获额外的上下文值或使用接口继承。要注意使上下文对象可以跨层和跨 SOA 实现传递。上下文可能会变得“沉重”,所以要注意它的实现方式。
由于 T 层执行验证,如何将“无效”的验证返回到调用层甚至最高层,如 UI?我见过使用自定义异常来支持这一点,通常称为 `BusinessLogicException`。幸运的是,`System.Exception` 有 `Data` 集合可以返回值,以帮助将参数传递给上层。使用异常工作得很好,但可能会很慢,因为 .Net 需要构建异常。所以,如果您追求性能,使用异常来返回“无效”验证可能效果不佳。另一种方法是将“无效”验证放在所有 T 层方法的返回值上。将“无效”验证作为返回值,可以轻松检查验证是否无效。
if (tComponent.CallSomeMethod(context,
somevalue).Count == 0)
{ //we're good!!!}
现在,如果方法需要返回结果怎么办?如果我们的示例调用 `tComponent.CallSomeMethod` 返回一个 `List`,我们如何获取它?我们现在面临一个大问题,“如何处理结果和无效验证?”此外,我们希望使解决方案可移植且简单。我们的方法签名的好模板是什么?我见过不同的处理方法。一种方法是将“无效”验证放在返回对象中,并将结果作为输出参数。第二种方法是将结果作为返回对象,并将“无效”验证作为输出参数。第三种方法是使用一个复合结果对象,该对象同时包含“无效”验证和结果。第四种方法是让方法返回一个对象,并使用类型转换将对象转换为可以处理的内容。也可能有 AOP 和事件方式来处理验证的传回,但那些听起来并不简单和可移植。底线是,没有一种方法听起来真正很棒。在处理“无效”验证时,请确保您或您的架构师确定了方法并坚持使用。我会牢记方法返回数据的环境,例如 WCF 和 Rest 客户端。请记住,要使其可移植且简单。
验证似乎很难,异常怎么样?有各种各样的异常。我们的 T 层应该有大量的不同种类的异常吗?可以有,但会有点混乱。我使用过一种自定义异常,它有一个枚举属性来指示它代表哪种异常。如果自定义异常需要传递任何值,它们可以放在 Data 中或另一个自定义异常中,以处理可能偶尔出现的情况。同样,这是一个选择。当您遇到大约 50 种看起来非常相似的自定义异常时,您可能会开始对自定义异常有不同的看法。
记录异常也是一个讨论点。如果 T 层跨平台运行,日志记录就需要一种持久化异常的方法。有一些库,如 log4net、nLog、CommonLogger,可以帮助持久化异常。如果您或您的架构师决定了异常的日志记录策略,T 层可以提供持久化异常的钩子。另一个想法是,调用 T 层的代码应该管理 T 层异常。这种方法也有效,只需保持协调。如何处理传递给 T 层的异常?T 层可以将其包装在另一个 T 层自定义异常中并传递上去,或者像其他自定义异常一样持久化它。同样,这是需要讨论的事情。
异常帮助我们管理何时发生错误或意外情况,但当我们想知道事情进展如何时呢?跟踪应用程序的性能对于识别瓶颈、功能使用情况、用户使用情况等至关重要。T 层可以帮助监控。T 层可以包含所需的监控点,可以打开和关闭,并与库一起持久化收集到的数据。在某些方面,监控在可用库和持久化方面与异常类似。根据环境,可能不需要监控。至少这是值得思考的事情。
大多数应用程序都需要考虑授权。T 层肯定可以在这方面提供帮助。如果设置了上下文,T 层可以执行检查以确定用户是否可以执行该方法,并使用它来确定要采取的适当逻辑。等等,这里有几个问题:“这怎么会很快?每个 T 层方法都应该执行检查吗?”现在,我们真的开始深入细节了。T 层实际上由几个层组成。初始层应该执行授权检查。现在,如果您是一个基于角色的组织,那么在处理大型应用程序时您将遇到麻烦。如果您是一个基于功能、任务、权限、权利或任何您使用的粒度命名约定进行授权的组织,那么您就走在正确的道路上了。我喜欢用“权限”。T 层方法可以设置为检查一项或多项权限。现在,由于我们将上下文作为方法签名的一部分,我们的上下文通常有足够的数据点供 T 层方法确定是否应该执行该方法。上下文可以包含权限吗?当然。将用户的权限作为上下文的一部分可以为 T 层方法节省大量时间,因为 T 层方法不必检索权限。有时,例如在 SOA 环境中,将权限与上下文一起传递给 Web 服务可能不是一个好主意。所以,在某些情况下,T 层需要调用其他地方来检索权限。权限环境应该经过精心调整。我绝对建议研究基于分布式内存的解决方案(有人能构建一个像样的分布式 MMRDBMS 吗?拜托!我说“拜托”)。如果从头开始构建,请确保建立会话策略。会话在用户开始使用应用程序或进入应用程序环境时开始。一个示例策略是,用户的授权在应用程序的会话期间不会改变。另一个示例策略是,用户的授权是动态的,可能随时更改。这些决定可能会影响您在上下文中放置的内容以及在哪里收集和检查授权。
T 层确实有很多工作要做。为了将事情分开,T 层应该有许多层。我称第一层为“过程层”。它非常类似于控制器。我可以称它为“功能层”或“编排层”。无论如何,这一层向调用代码公开的方法应该与应用程序的调用需求一致。该层需要编排处理授权检查。它可以调用另一个方法来执行检查。然后“过程层”需要处理验证。同样,“过程层”可以调用验证函数。然后,“过程层”可以通过调用一组针对该功能的特定方法来执行该功能或操作。“过程层”将处理任何预处理和后处理。预处理是指调整数据或设置数据,是的,这可能是另一个函数。后处理与预处理类似。后处理可能是映射数据或处理异常。这取决于您或架构师决定这一层应该做什么。
我看到在敏捷编码模式中发生的事情之一是“意大利面条式代码”。关注点分离经常导致“意大利面条式代码”,这是一个真正的麻烦。没有什么比调用 15 多个函数来完成一项工作更糟糕的了。这里有一个想法,根据最适合创建自动化测试的内容进行分离。例如,过程层使用的每个方法都可以分解成单元测试。现在,如果过程层需要返回一个简单的字符串,或者计算 1 + 1 = 2,那么让过程层调用另一个函数来计算 1 + 1 = 2 可能没有意义。当变得复杂或多行时,那么将其分解成另一个函数调用可能是一个好主意。我不介意吃好的意大利面条。我介意读意大利面条。有些人会说,你需要将事物分开,以便在单元级别进行测试。所有这些单元测试都有成本,尤其是当良好的集成测试能够做到同样的事情并且如果设置正确的话执行速度足够快时。单元测试在解决特定问题时非常有用。请记住,单元测试也需要维护成本。在正确的时间编写好的测试。构建代码以支持这些测试。代码越少,意大利面条越少。
T 层应该有一个层来抽象出持久化层吗?很难说。有时是的,有时不是。例如,如果调用是针对外部 Web 服务的,您可能希望用自己的接口和映射代码来封装 Web 服务。如果外部 Web 服务发生变化,那么希望只有一个层需要进行调整。现在,假设您有一个成熟的应用程序,并且事物从不改变。创建一层来抽象出持久化层可能会被一些人认为是多余的。各位,这又是一个判断问题。现在,有了合同,肯定有助于创建这一层。有了合同,将有助于测试。所以至少要达到 T 层知道它需要与持久化数据交互的调用类型的程度。
优点和缺点
创建 T 层将提供几个关键好处
降低变化带来的风险,嘿,我们正试图让它可移植。
集中或指定位置来捕获可移植的业务规则。
开发一致性,一旦做出这些讨论过的决定,就可以反复重用。
创建 T 层确实有其弊端
初始成本,构建这些有时是冗余的项目需要时间。
性能,代码可能需要做一些冗余的工作,比如对象映射或授权。
不可移动代码的风险,技术、环境等仍可能发生变化,而可移植代码无法迁移。虽然核心业务规则很好地位于一个地方,但代码可能无法迁移。如果发生这种情况,大多数其他编码风格也可能无法迁移。
所以有几个优点和缺点。
示例代码中有什么?
示例代码提供了一个在 T 层处理地址的简单场景。相同的 T 层在 WCF 服务中使用,并在 ASP.NET MVC 3 with Razor 中使用。它更多的是概念验证代码,用于演示一些陷阱、处理模型到 T 层映射的方法、处理异常、处理验证、上下文以及可能的一些其他要点。
这是 WCF 保存实现的示例
/// <summary>
/// Saves and returns Saved Address.
/// Missed Validations on Returned Address.
/// </summary>
/// <param name="context"></param>
/// <param name="overrideWarning"></param>
/// <param name="original"></param>
/// <returns></returns>
public DataContract.Address Save(DataContract.Context context, bool overrideWarning
, DataContract.Address original)
{
DataContract.Address result = null;
try
{
//Convert Input to TLayer
//address
Dto.Address originalDto = ConversionHelper.ToDto(original);
//context
TLCommon.Method.Context contextCommon = ConversionHelper.ToDto(context);
//set ioc
//save ioc for later
PersistContract.IAddress addressDal = new AddressDal();
PersistContract.IMonitoring monitoringDal = new Persist.Common.MonitoringDal();
TLProcessContract.IAddressTL addressTL = new TLProcess.AddressTL();
addressTL.AddressPersist = addressDal;
addressTL.MonitoringPersist = monitoringDal;
List<TLCommon.Method.Validation> commonValidations = new List<TLCommon.Method.Validation>();
Dto.Address processResult = addressTL.Save(contextCommon, commonValidations, originalDto);
//convert process results to output
result = ConversionHelper.ToContract(processResult, commonValidations);
return result;
}
catch (Exception ex)
{
//handle error
//write error to log
//to shield or not to shield
throw ex;
}
}
示例代码显示了传递 `Context` 和 `Address` 的用法。`Context` 和 `Address` 都是合同。该方法还有一个 `overrideWarning` 的输入。`overrideWarning` 值是一个概念,其中验证也可以返回警告而不执行操作。有时操作就是必须发生,这提供了一种强制执行操作的方法。目前,`overrideWarning` 未使用。快速浏览代码,`Context` 和 `Address` 被转换为 T 层可以使用类。转换之后,有一些代码来设置接口。通常 IOC 库会处理这些,但这也不是为了演示 IOC。此时,我们终于准备好调用 T 层过程了。对于传递验证,这里显示的方法是将验证和地址作为单独的对象传递。值得注意的是,原始对象被保留,并且创建了一个过程结果对象。有时我们需要在另一个步骤中使用原始对象。有时原始对象可能包含我们不想传回的值。最后一步将过程结果对象和任何验证转换为返回合同。在这种情况下,返回合同同时包含结果和验证。
享受这些想法和决定。现在,行动起来!!!
历史
- 1.0.0 初始条目。