应用程序架构——驱动力、方法和实现考虑因素






4.61/5 (11投票s)
本文讨论了在决定应用程序架构时涉及的各种驱动力、方法和实现考虑因素。这并非什么高深莫测的科学——其全部目标是帮助您决定一个可能适合您场景的架构。
引言
首先,没有一种“普遍正确”的架构适合所有场景。架构可能会也应该根据项目驱动力和要求而变化。
本文几乎完全是关于应用程序架构的。我见过人们提出这样的问题:“你认为我们可以使用依赖注入吗?”、“如何解耦各个层?”、“我到底应该把业务逻辑放在哪里?”、“我们需要单独的业务对象和DTO对象吗?”、“我们应该采用面向服务架构吗?”等等。答案是“是或否,取决于你的场景”——一旦你有了项目结构,并且开发人员知道在哪里编写特定模块或功能的代码,那么项目就可以很容易地进行下去。
本文旨在讨论在决定解决方案架构时可能采用的一些实用概念。这并非什么高深莫测的科学——其全部目标是帮助您决定一个可能适合您场景的架构。此外,您可能会在本文中发现许多您可能不同意的观点。您可以自由地这样做,也欢迎您发表评论以提出替代方法或引发一场有益的讨论:)。我承认一些技术相关的场景主要来自微软技术方面,因为我主要在.NET领域工作。
顺便说一下,如果您对软件设计和架构感兴趣,这里有一些您可能感兴趣的文章:
您还可以访问我的网站 http://amazedsaint.blogspot.com,获取更多有趣的“食谱”。
决策参数
以下是选择平台的一组最基本的决策参数
客户选择/偏好
- 步骤 1:客户对技术平台的选择/一致性。
- 步骤 2:客户现有支持任何建议技术平台的基础设施。
成本/预算
- 步骤 1:对硬件和软件要求的影响(如托管平台成本、基础设施成本等)。
- 步骤 2:对资源要求的影响(资源可用性、培训成本等)。
- 步骤 3:针对每种技术的内部开发与外包的成本比较。
现有依赖项和代码库
- 步骤 1:这是一个端到端项目、重写、接管还是迁移?
- 步骤 2:如果不是端到端项目,您是否有任何现有的依赖项和代码库?
平台成熟度
- 步骤 1:查明建议平台是否能满足所有要求和未来要求。
- 步骤 2:它能满足接口要求吗?(例如,RoR 在处理 Web 服务方面不够成熟)。
可用框架
- 步骤 1:是否有任何现有的框架/开源工具可用?(例如:OSCommerce/PHP 用于电子商务)。
- 步骤 2:如果是,为在该现有框架中建立专业知识以降低TCO需要投入多少?
- 步骤 3:您是否有任何现有的框架/可重用组件来降低TCO?
复杂度和规模(使用FP或其他方法估算)
- 步骤 1:项目估算规模。
- 步骤 2:不同平台之间的成本比较(基于根据总FP计算每个平台的执行时间)?
- 步骤 3:您是否有任何现有实践曾成功应用于在某个技术平台中执行项目?
项目模式(时间和费用/固定报价等)
- 步骤 1:如果是固定报价项目,哪个平台最适合快速开发和部署?
- 步骤 2:您是否有任何现有实践曾成功应用于在某个技术平台中执行项目?
项目类型(桌面/Web应用/服务等)
- 步骤 1:针对该项目对候选技术平台进行可行性分析(例如,RoR 不适合 Windows 服务应用程序)。
- 步骤 2:淘汰不可行的技术。
需要处理的接口
- 步骤 1:对候选技术平台进行接口支持的可行性分析(例如,如果您正在使用 REST 服务,RoR 具有一定的权重)。
- 步骤 2:识别平台特定的接口要求。
预期的可伸缩性和性能
- 步骤 1:识别项目中对 OLAP/OLTP 场景的支持。
- 步骤 2:识别最低响应时间要求。
- 步骤 3:根据在各种技术下构建的 POC 或现有数据或以往经验进行权重分配。
内部专业知识可用性
- 步骤 1:基于我们内部专业知识和资源可用性进行权重分配。
- 步骤 2:产生的培训成本。
驱动力
首先,我需要打破“应用程序架构和设计由技术驱动”这一误区。技术应该只用于以最佳方式实现架构,而不是反之。此外,在我看来,架构是一个持续的过程。架构的目标是使解决方案越来越接近用户期望,并统一不同的系统以提供标准化。架构是一个持续的过程,因为用户期望可能会随着时间的推移而改变,因为用户渴望更好的系统、模型和更好的体验。
可能影响您架构的一些因素包括
- 组织内部/组织之间以及利益相关者之间的政治(相信我,这是一个关键因素)
- 可用资源和内部专业知识
- 系统预计如何被使用,以及部署考虑因素
- 非功能性需求,如可伸缩性、可用性、性能、安全性等。
- 项目相关的驱动因素,如成本、范围、质量和时间
- 可重用组件/实体可用
- 各种风险因素(例如未预料到的客户期望和变更请求)
如果您是架构师,您就会知道自己已经被上述一个或多个因素所束缚。我们开始吧 :)
政治
你是否曾参与过迁移项目?你是接收方!在大多数情况下,另一方(遗留系统)的人,即那些本应通过知识转移来大力支持您现有系统的人,却不愿这样做。原因很简单,一旦迁移或接管完成,他们可能会失业,或者他们在组织中的重要性会降低。
因此,最终,您被迫在没有真正理解要迁移的现有系统的情况下进入设计阶段。一个替代方案可能是采用迭代模型,但这仍然只是政治如何成为影响您架构的主要因素的一个例子。
可用资源和内部专业知识
我相信解决方案的架构设计应该是一项团队活动。至少,应该有一个架构审批工作流,以确保架构能够处理基本的非功能性需求,如可伸缩性、可扩展性、安全性等。
建立内部专业知识并记录成功和失败因素非常重要,这样专业知识才能被重复利用。为此,一个建议的解决方案是成立专注于安全、性能等领域的焦点小组,以确保在架构和设计中遵循最佳实践。这些团队还应该评估成功实施项目的架构,从中提炼出最佳实践。跨团队重用专业知识和建立内部专业知识的成功是影响最终架构的一个主要因素。
系统预计如何被使用,以及部署考虑因素
系统预计如何被用户使用是影响最终方案的另一个主要因素。随着时间的推移,我们有各种考虑,例如客户端服务器、分布式、面向服务等等,不一而足。
- 示例 - 1:您需要架构一个基于 Web 的系统,向用户显示当前温度(很简单,是吧?)。用户可以通过访问您的网站,使用浏览器查看温度。一个月后,您服务的客户需要将此作为一个服务出售给其他网站,以便其他网站也可以使用他们的服务并在其网页中显示温度。
- 示例 - 2:您需要架构一个基于 Web 的购物车。目前,您可以将表示层(UI)和业务层部署在同一台服务器上(即,Web 服务器和应用服务器合为一体),但您必须提供一个选项,以便将来当用户负载增加时,业务逻辑可以部署到另一台机器(一个单独的应用服务器)上。
- 示例 - 3:您需要架构一个基于 Web 的购物车。目前,您可能正在使用应用程序级别缓存。但将来,当您的负载增加,并且您转向 Web 场场景时,如何确保缓存能够在所有服务器之间共享?
非功能性需求,如可伸缩性、可用性、性能、安全性等
非功能性需求是“孤儿”——被客户和交付团队都忽视。通常,性能调优或安全推进计划只在项目的最后阶段作为“救命稻草”启动。
尽管 NFRs 大多被忽视,但它们是直接影响选择正确架构的关键因素之一。架构中错误的决策大多是因为在早期阶段忽视了 NFRs。
正如我之前提到的,焦点小组可以在很大程度上帮助确保在开发生命周期的每个阶段都适当地考虑 NFRs。
项目相关的驱动因素,如成本、范围、质量和时间
前段时间,当我与一位高级经理交谈时,他指出不可能将所有四个决策因素(成本、范围、质量和时间)都交给客户来决定。客户需要在一个因素上做出妥协。这不是因为客户“必须”妥协。在考虑一个项目时,将这四个决策因素都作为不变因素,简直是不可能的。如果有人选择了范围、质量和时间,那么成本将是可变因素。如果有人选择了范围、质量和成本,时间将是可变因素。
不幸的是,这一点没有 properly 地传达给客户,客户最终总是选择成本、范围和时间。结果,这将影响项目的质量——因为质量是唯一剩下的因素。
为了赶上截止日期,架构师最终会在可扩展性和其他非功能性需求等因素上妥协。
可重用组件/实体可用
可重用性是关键。在设计和架构阶段应强制执行高质量组件和最佳实践的无耻重用。可重用组件、工厂和最佳实践的可用性是影响架构的一个主要因素。
组织有责任确保经验和最佳实践得到记录、转移和适当重用。
未预料到的客户期望和变更请求
架构成功的一个主要标准是,当适应新的变更时,它不应该崩溃。话虽如此,但在大多数情况下并非如此。如今,大多数公司在策略上都非常敏捷。因此,客户公司可能会经常提出变更请求——这些请求是由于其业务模式的变化而触发的。
需求分析可能会遗漏客户的期望,这可能会在后期导致严重的架构级问题。更糟糕的是,客户可能默认期望产品能满足他们的所有需求。这些期望也可能在后期引发变更请求。
方法——思维过程
假设一旦您对需要完成的工作有了全局视图,一个典型的常见起点是识别您需要的层。例如,对于一个典型的应用程序,您可能会得到类似下面的结构
Data Tier <--> Data Access Tier <--> Business Tier <--> Presentation Logic Tier <--> GUI
现在,许多问题将开始浮现。您将开始思考这些层如何相互通信,数据如何上下传递,数据如何持久化,这些层如何连接等等。当然,答案很大程度上取决于需求和我们上面讨论的其他驱动力。
您可能还会考虑您可能在各层之间使用的各种服务——例如日志记录、缓存等。
与此同时,您可能还会思考性能、可用性、安全性等因素在每一层中如何相关和适用。
系统中的层
让我们先简单了解一下每一层。我们将从下到上进行
数据层
数据层是您持久化数据的地方,主要是数据库。数据层应提供数据的存储、检索、索引和查询等功能。当您考虑数据层时——正如我之前提到的——考虑事务支持、可伸缩性、可用性、安全性等因素至关重要。
数据访问层
在这里,您通常会有与数据层交互的代码。您可能需要注意的两个主要因素是可伸缩性和响应时间。您必须制定适当的策略来确保响应时间足够好。
例如,您可能需要缓存数据以避免每次都访问数据库。另一个流行的策略是连接数据库池。通常,数据访问层是无状态的。
业务层
当然,您的业务层负责对数据应用业务规则和转换,并执行计算。您可以在业务层中包含基于业务规则的验证。在大多数场景中,您可能会有一组业务对象供业务层操作。理想情况下,您的业务对象代表数据的特定领域模型。
业务层以业务对象的形式从表示层接收数据,执行所需的业务规则验证和转换,并调用数据访问层中所需的方法来执行存储和获取等操作。在大多数情况下,您可能需要在业务层进行状态管理。例如,在书店应用程序中,业务层处理将书籍添加到购物车、计算总成本等逻辑。
根据场景,当然也可以在业务层实现缓存和对象池等服务。
一些其他实际思考——从架构师的角度来看,将所有业务逻辑放在业务层是理想的。但有时,您必须为了性能而做出一些权衡——并将一些业务逻辑移到您的存储过程:)。这个决定取决于几个因素——主要因素是您的应用程序预计处理的数据量。但是,关键是确保您的存储过程将来可以以最小的开销移植到其他数据库平台。大多数数据库平台都发展得很完善——并且可以比您可能决定用于处理数据的最佳业务层设计和算法更好地处理负载。
理想情况下,您应该在所有相关层之间明智地分配负载。
演示逻辑层
演示逻辑层处理演示逻辑。例如,对于 Web 应用程序,演示层包含 ASP 或 JSP 页面。对于 Windows 应用程序,演示层由 Windows 窗体或 GUI 层能够显示的任何其他内容组成。
同样,演示逻辑层也可以实现缓存等服务。ASPX 页面的页面输出缓存就是一个很好的例子。
GUI
GUI 是用户与之交互的东西。在 Web 应用程序中,GUI 是用户在浏览器中看到的东西。理解为什么演示层实际上被分解为演示逻辑和 GUI 是很有趣的。这是因为,在某些情况下,演示逻辑位于服务器上,而 GUI 层将位于客户端。这在 Web 应用程序中尤其如此,其中 ASP 或 JSP 页面被渲染,然后将生成的标记(如 HTML 或 WML)发送到客户端
另一种方法——模型-视图-控制器
一种常见的方法是将应用程序分成不同的层,正如我们上面讨论的那样。然而,另一种不同的方法是将整个应用程序视为模型、视图和控制器(MVC 架构模式)。这尤其适用于 Web 应用程序。在本节中,我不会详细解释 MVC——目的只是为了传达 MVC 是对应用程序进行建模的另一种方式。
模型
在 MVC 中,“模型”代表数据的领域特定表示。MVC 对模型内部如何执行数据访问没有太多说明——模型应封装数据访问和对象持久化。
View
MVC 中的表示层被分解为“视图”和“控制器”。视图应以对用户有意义的方式呈现模型。例如,在 Web 应用程序中,如果您有一个包含一组员工的模型,则视图中的逻辑可以遍历员工并发出 HTML 代码以在浏览器中显示员工列表。一个模型可以存在多个视图是完全可能的。也就是说,同一个员工集合可以通过另一个视图呈现为项目符号列表。
控制器
控制器处理用户动作和手势,并响应用户事件。例如,当用户单击“新建”按钮添加新员工时,将调用该动作的控制器。然后控制器将对员工模型进行更改。视图将渲染修改后的员工模型到显示器,以便用户可以在员工列表中查看他添加的新员工。
在某些 MVC 实现中,业务逻辑封装在模型中,这是完全可行的。另一方面,一些开发人员可能选择在控制器内的事件处理程序中实现业务逻辑。由开发人员决定他应该将业务逻辑放在何处。
一些额外说明:在微软 P&P 团队发布的 Web Client Software factory 中,他们引入了一个变体——Model View Presenter。但似乎微软正朝着 ASP.NET 的纯 Model View Controller 框架发展。有关这方面的有趣阅读,请参阅 Scott 的博客文章。
实施考量
下一步是将您的所有“架构思想”整合起来,形成一个高层次的线框图(在您脑海中)。不要将这与高层次设计混淆。甚至在您开始高层次设计之前,您就应该对一些基本问题有答案。例如,这里是您最终可能会问自己的一些问题子集
- 所涉及的层次有哪些依赖关系?
- 各层之间是如何连接的?
- 扩展点在哪里?
- 我应该使用哪些服务,以及在哪里使用?
- 我的持久化机制是什么?
所涉及层次的依赖关系
我们讨论了系统中涉及的层——但仍然不够接近真实世界的场景。例如,在上述业务层的定义中,我们提到业务层位于数据访问层和表示层之间。
然而,在实际场景中,您的业务层可能会连接到其他子系统——也就是说,您的业务层可能会订阅其他服务进行规则处理。例如,如果您正在开发一个机票预订系统,您的业务层将与各种 Web 服务通信,以执行查询航班时间、预订机票等操作。在这种情况下,您可能需要使用 Facades 来访问您的子系统并隐藏子系统的复杂性
另一个常见场景是,您可能需要将查找两个层之间依赖关系的逻辑与您的实际实现分离。例如,假设您需要调用一个 Web 服务来从您的应用程序预订机票。使用依赖注入等技术,可以将识别“哪个”服务以及“在哪里”定位服务的工作与您的应用程序分离。您的应用程序可能只知道“如何”与服务通信。为了提供一个简单的场景——您可以考虑将机票预订 Web 服务的依赖项注入到您的业务层类中。
注入依赖项有多种方式,其中一种是基于属性的依赖注入。您的业务层类可能有一个类型为 ITicketBookingService
的属性。当实例化这个业务层类时,将创建您需要使用的机票预订 Web 服务的代理类(当然,这个代理类应该实现您的 ITicketBookingService
接口)的实例并将其分配(注入)给这个属性。DI 的详细讨论超出了本文的范围。Spring.NET 框架提供了出色的依赖注入能力。此外,Microsoft ObjectBuilder(用于 Web 客户端软件工厂等工厂)也可以用于此目的。(我仍然想知道为什么企业库中没有单独的“依赖注入应用程序块”。)
各层之间如何连接
让我们从一个例子开始。目前,您正在开发一个 Web 应用程序,并且您的演示层直接消费您的业务层类。您只需在 ASPX 页面的代码隐藏中创建业务层类的对象,并调用业务层对象中的方法来传递数据。开发和质量保证几乎完成,您正在等待批准将项目投入生产。
一天早上,您在收件箱中发现了一封来自您的技术经理的邮件,内容大致是:“伙计,让我们把应用程序投入生产。但为了维持负载,我们将演示层部署在 Web 服务器上,业务逻辑部署在单独的应用程序服务器上。”您陷入困境,因为您的架构不支持分布式逻辑——您根本无法单独部署这两个层。
因此,正如我之前提到的,在初始阶段考虑此类因素至关重要。一种将逻辑分布纳入考量的常见方法是在层之间引入代理层。例如,您可以在业务层和表示逻辑层之间放置一个代理层。代理层的目标是暴露一个层的功能,以便下一层可以访问它——以促进分布式计算的需求
Business Tier <--> Proxy Tier <-->Presentation Logic Tier
根据场景,您可以在代理层中使用 SOAP、RMI、DCOM、CORBA 等协议。例如,如果表示逻辑层和业务层都用 .NET 开发,并且都期望部署在同一个局域网中,您可以选择 .NET Remoting。如果业务逻辑需要作为服务公开并由异构客户端在企业域之外访问,您可以选择 SOAP,等等。
当您使用经典的远程处理技术(如 DCOM 和 RMI)时,两层之间的契约(方法和这些方法使用的数据类型)是预定义的。然而,在面向服务的系统中,客户端可以动态发现契约以使用相同的服务。例如,Web 服务将使用 Web 服务描述语言(WSDL)公开其契约,这提供了有效的解耦。
有时,您可能需要定义单独的数据契约或数据传输对象 (DTO),这些对象可以由代理层发送和接收。然后,在内部,代理层需要将 DTO 对象转换为业务对象,反之亦然。尽管这种转换会带来开销,但它确保即使您的领域对象模型发生更改,契约也不会被破坏。
扩展点在哪里?
理想情况下,您的应用程序应该在可能的情况下提供足够的扩展点。例如,如果您为了一个目的与三个服务进行通信,那么明天您应该能够在不更改核心框架代码的情况下向您的应用程序添加第四个服务。
一个经典的实现方式是使用 Provider 模式来定义扩展点。使用基于 Provider 的方法还将帮助您在将来解决各种伸缩问题。例如,假设您正在 Web 应用程序中缓存数据。通常,您使用默认缓存,它只能在当前应用程序域中缓存数据。明天,如果您要迁移到 Web 场场景,您必须在 Web 服务器之间使用共享/复制缓存,那么您就麻烦了。因此,当您考虑使用缓存时,您可以考虑使用 Provider 模型,以便您可以在以后需要时更改缓存 Provider。另一个例子是,使系统的某些部分基于插件。
您甚至可以采用基于配置文件的提供程序模型。从 .NET 2.0 开始,ASP.NET 为成员资格管理、角色管理等各种功能提供基于提供程序的扩展性。我很久以前写过一篇关于创建简单自定义提供程序框架[^]的文章。您也可以在此处[^]查看 Microsoft 提供程序工具包。
您可能也有兴趣了解 Microsoft 企业库如何使用提供者概念来提供配置文件驱动的功能。
在初始阶段就决定扩展点是确保可扩展性的关键。
我应该使用哪些服务,以及在哪里使用?
各种服务,如缓存、日志记录、安全性(身份验证和授权)等,需要在多个层中使用。这个决定非常关键,主要是为了确保正确满足各种非功能性需求。由于利益相关者可能甚至不知道实现这些服务所需的开销,因此在初始阶段就将这些情况传达给利益相关者非常重要。
关键在于可重用性。这些服务中的大多数在项目中都很常见。因此,如果您有一个包含这些服务的组织级框架,那将大大减少开销。
持久化机制是什么?
数据的持久化和查询是一个重要的考虑因素。有些人喜欢使用 ORM(对象关系映射)框架,如 Hibernate 或 NHibernate(Hibernate 的 .NET 端口)——www.nhibernate.org[^]——另一些人可能遵循编写存储过程,然后使用数据层类来消费它们的经典方式。
Subsonic[^] 是一个 .NET 框架,它提供了一种 ActiveRecord 机制,与 Ruby On Rails 框架非常相似。
另一种方法是使用代码生成技术,根据数据库表生成强类型类。Microsoft Web 服务软件工厂提供了一些类似的功能。此外,还有其他代码生成器或元编程框架——我之前在几个项目中使用了 My Generation[^]。
结论
我们刚刚讨论了架构新系统所涉及的一些驱动力、方法和考虑因素。这绝不是一个完整的列表——其全部目标是帮助您分析架构解决方案所涉及的思维过程。
作为下一步,您可以阅读我关于识别系统中实体和设计问题,并通过应用各种设计模式解决这些问题的文章。点击此处阅读。
访问我的网站 http://amazedsaint.blogspot.com——获取更多文章,以及 .NET 和设计模式的“食谱”。
此外,此处是我在 CodeProject 上发表的其他文章列表。