前端领域的 “干净架构”

Posted 奇舞精选

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端领域的 “干净架构”相关的知识,希望对你有一定的参考价值。

大家好,我是 ConardLi,前端有架构吗?这可能是很多人心里的疑惑,因为在实际业务开发里我们很少为前端去设计标准规范的代码架构,可能更多的去关注的是工程化、目录层级、以及业务代码的实现。

今天我们来看一种前端架构的模式,原作者称它为“干净架构(Clean Architecture)”,文章很长,讲的也很详细,我花了很长时间去读完了它,看完很有收获,翻译给大家,文中也融入了很多我自己的思考,推荐大家看完。

  • https://dev.to/bespoyasov/clean-architecture-on-frontend-4311
  • 本文中示例的源码:https://github.com/bespoyasov/frontend-clean-architecture/
  • 首先,我们会简单介绍一下什么是干净架构(Clean architecture),比如领域、用例和应用层这些概念。然后就是怎么把干净架构应用于前端,以及值不值得这么做。

    接下来,我们会用干净架构的原则来设计一个商店应用,并从头实现一下,看看它能不能运行起来。

    这个应用将使用 React 作为它的 UI 框架,这只是为了表明这种开发方式是可以和 React 一起使用的。你也可以选择其他任何一种 UI 库去实现它。

    代码中会用到一些 TypeScript,这只是为了展示怎么使用类型和接口来描述实体。其实所有的代码都可以不用 TypeScript 实现,只是代码不会看起来那么富有表现力。

    迁移到 Angular,或者改变某些用例的时候不会变的那一部分。在商店这个应用中,领域就是产品、订单、用户、购物车以及更新这些数据的方法。

    数据结构和他们之间的转化与外部世界是相互隔离的。外部的事件调用会触发领域的转换,但是并不会决定他们如何运行。

    比如:将商品添加到购物车的功能并不关心商品添加到购物车的方式:

  • 用户自己通过点击“购买”按钮添加
  • 用户使用了优惠券自动添加。
  • 在这两种情况下,都会返回一个更新之后的购物车对象。

    一起将事件转换为我们的应用程序可以理解的信号。

    驱动型会和我们的基础设施交互。在前端,大部分的基础设施就是后端服务器,但有时我们也可能会直接与其他的一些服务交互,例如搜索引擎。

    注意,离中心越远,代码的功能就越 “面向服务”,离应用的领域就越远,这在后面我们要决定一个模块是哪一层的时候是非常重要的。

    目录下,应用层定义在 application 目录下,适配器都定义在 service 目录下。最后我们还会讨论目录结构是否会有其他的替代方案。

    DateTimeString 。这些其实都是类型别名:

    代替 string 来更清晰的表明这个字符串是用来做什么的。这些类型越贴近实际,就更容易排查问题。

    这些类型都定义在 shared-kernel.d.ts 文件里。共享内核指的是一些代码和数据,对他们的依赖不会增加模块之间的耦合度。

    在实践中,共享内核可以这样解释:我们用到 TypeScript,使用它的标准类型库,但我们不会把它们看作是一个依赖项。这是因为使用它们的模块互相不会产生影响并且可以保持解耦。

    并不是所有代码都可以被看作是共享内核,最主要的原则是这样的代码必须和系统处处都是兼容的。如果程序的一部分是用 TypeScript 编写的,而另一部分是用另一种语言编写的,共享核心只可以包含两种语言都可以工作的部分。

    在我们的例子中,整个应用程序都是用 TypeScript 编写的,所以内置类型的别名完全可以当做共享内核的一部分。这种全局都可用的类型不会增加模块之间的耦合,并且在程序的任何部分都可以使用到。

    ,而不是它们的具体实现。在这个阶段,描述必要的行为对我们来说很重要,因为这是我们在描述场景时在应用层所依赖的行为。

    如何实现现在不是重点,我们可以在最后再考虑调用哪些外部服务,这样代码才能尽量保证低耦合。

    另外还要注意,我们按功能拆分接口。与支付相关的一切都在同一个模块中,与存储相关的都在另一个模块中。这样更容易确保不的同第三方服务的功能不会混在一起。

    方法,这个方法将接受需要支付的金额,然后返回一个布尔值来表明支付的结果。

    会提示我们没有给出接口的实现,先不要管他。

    的方法来创建一个订单:

    了,现在我们来检查一下现实是否符合我们的需求。

    通常情况下是不会的,所以我们要通过封装适配器来调用第三方服务。

    来封装用例,建议把所有的服务都封装到里面,最后返回用例的方法:

    来作为一个依赖注入。首先我们使用 useNotifier,usePayment,useOrdersStorage 这几个 hook 来获取服务的实例,然后我们用函数 useOrderProducts 创建一个闭包,让他们可以在 orderProducts 函数中被调用。

    另外需要注意的是,用例函数和其他的代码是分离的,这样对测试更加友好。

    接口,我们先来实现一下。

    对于付款操作,我们依然使用一个假的 API 。同样的,我们现在还是没必要编写全部的服务,我们可以之后再实现,现在最重要的是实现指定的行为:

    这个函数会在 450 毫秒后触发的超时,模拟来自服务器的延迟响应,它返回我们传入的参数。

    来实现通知,因为代码是解耦的,以后再来重写这个服务也不成问题。

    Hooks 来实现本地存储。

    我们创建一个新的 context,然后把它传给 provider,然后导出让其他的模块可以通过 Hooks 使用。

    。这样我们就不会破坏服务接口和存储,至少在接口的角度来说他们是分离的。

     return useStore();

    此外,这种方法还可以使我们能够为每个商店定制额外的优化:创建选择器、缓存等。

    模块引入的 totalPrice 方法。这样使用本身没有什么问题,但是如果我们要考虑把代码拆分到独立的功能的时候,我们不能直接访问其他模块的代码。

    没有监控并强制执行它们的机制。

    这看起来也不是个问题:你是用 string 类型去替代 DateTimeString 也不会怎么样,代码还是会编译成功。但是,这样会让代码变得脆弱、可读性也很差,因为这样你可以用任意的字符串,导致错误的可能性会增加。

    有一种方法可以让 TypeScript 理解我们想要一个特定的类型 — ts-brandhttps://github.com/kourge/ts-brand)。它可以准确的跟踪类型的使用方式,但会使代码更复杂一些。

    函数的领域中创建了一个日期:

    这样的函数可能会被重复调用很多次,我们可以把它封装到一个 hleper 里面:

    函数最好是所有数据都从外面传进来,日期可以作为最后一个参数:

    没有任何问题。这样的 Helper 甚至可以被视为共享内核,因为它们只会减少代码的重复度。

    会包含 Cart, 因为购物车只表示 Product 列表:

    会更合理:

    函数很难独立于 React 来测试,这不太好。理想情况下,测试不应该消耗太多的成本。

    问题的根本原因我们使用 Hooks 来实现了用例:

    的外面,服务通过参数或者使用依赖注入传入用例:

    的代码就可以当做一个适配器,只有用例会留在应用层。orderProdeucts 方法很容易就可以被测试了。

    hooks 的情况下,我们可以将它们用作“容器”,返回指定接口的实现。是的,虽然还是手动实现的,但它不会增加上手门槛,并且对于新手开发人员来说阅读速度更快。

    中的块和修饰符概念来帮助你思考,如果我在 BEM 的上下文中考虑它,它可以帮助我确定我是否有一个单独的实体或代码的“修饰符扩展”。

    BEM - Block Element Modfier(块元素编辑器)是一个很有用的方法,它可以帮助你创建出可以复用的前端组件和前端代码。


    架构方面学习笔记-前端架构设计

    2022.02.08 今天读了一篇关于前端整洁架构的设计,因此对其中的内容进行了一些整理以及我自己的思考,后续阅读《领域驱动设计》后可以加入更多的内容。

    References:

    前端领域的 “干净架构”

    架构方面学习笔记(3)–前端架构设计

    文章目录

    整洁架构

    以一个🌰来介绍整洁架构:

    商店会出售不同种类的饼干,用户可以自己选择要购买的饼干,并通过三方支付服务进行付款。

    用户可以在首页看到所有饼干,但是只有登录后才能购买,点击登录按钮可以跳转到登录页。

    把饼干加进购物车后,用户就可以付款了。付款后,购物车会清空,并产生一个新的订单。


    上图明确出了整洁架构的三个部分,但它还是有一些抽象,在实际开发和设计中我们如何遵循这个架构进行设计呢?

    1. 明确实体,比如例子中的:商品、用户、购物车、订单。明确数据转换函数(必须仅依赖本层的各种实体和规则)如计算总价的方法
    2. 应用层:
      - 列 use case:①找出参与者②找出动作③明确结果
      - 写数据转换或者说描述 use case:side effect(从适配层与服务端的交互中拿数据) --> pure function(纯函数处理数据)–> side effect(存储处理结果)
      - 列 Interfaces
    3. 适配层:
      • 用户界面
      • API 请求
      • 存储或状态管理

    最后:整洁架构让每个 use case 独立起来,同时适配层让第三方服务随时可替换,这会让整个架构扩展性极强,但不可避免的会带来一些如代码量的增大这种劣势。

    我个人看法是整洁架构最关键的一点是希望逻辑和 UI、第三方服务 能够分离,而如今的 react vue 都提倡使用 hooks,核心也正是如此。在现实开发中实现理想状态下的整洁架构当然是具有一定的难度和不可预测性,比如你真的可以做到逻辑和状态管理的真正分离吗,你的项目可以随意从 react 和 vue 中切换吗,诸如此类的问题。但梳理整洁架构的过程仍然给了我们不少启发:

    • 通过列实体、列 use case 对于我们设计 store 结构具有很大的帮助
    • 将逻辑从状态管理、第三方服务中剥离出来,尽量做到各司其职和不依赖框架的测试
    // 整洁架构下的商品购买代码树
    src/
    |_domain/
      |_user.ts
      |_product.ts
      |_order.ts
      |_cart.ts
    |_application/
      |_addToCart.ts
      |_authenticate.ts
      |_orderProducts.ts
      |_ports.ts
    |_services/
      |_authAdapter.ts
      |_notificationAdapter.ts
      |_paymentAdapter.ts
      |_storageAdapter.ts
      |_api.ts
      |_store.tsx
    |_lib/
    |_ui/
    
    

    DDD(Design Driven Design) 领域驱动设计

    Reference:

    领域驱动设计在互联网业务开发中的实践

    《领域驱动设计》

    特点: 从开发到测试整个团队使用同一的架构语言;业务与架构强关联,从而建立针对业务变化的高响应力架构

    几个名词解释:

    DDD 中有较多的术语,这里仅写了几个,更多的可以参考 领域驱动设计-什么是领域驱动设计和怎么使用它

    • 领域:一个系统要解决的实际问题的集合,或者说业务本身
    • 通用语言:所谓通用语言讲的并不是开发和测试都用一种开发语言,比如 golang,JavaScript 等,而是与领域模型相关的结构化语言保证整个团队对整个系统的理解一致,比如一个商城系统中的订单收货地址和个人信息收货地址应该明确区分,而不是混为一谈「收货地址」

    上文中提到的前端架构设计其实类似于 DDD 六边形架构。

    DDD 相对于三层架构有什么提升?

    三层架构的劣势:MVC 可以看做是三层架构的一种实现模式,我们知道任何一个操作都是从 controller 层传入,services 层操作数据库或者第三方服务。

    • 严格分层模式下,用户界面层不能跨过业务逻辑层调用数据访问层
    • 三层架构下往往 service 层会越来越臃肿,最终一堆逻辑混杂在一起,不易于扩展以及满足新的业务需求

    三层架构又被称为「分层贫血领域模型架构」,贫血即指业务实体中没有或者很少方法。而 DDD 则被称为充血领域模型,正式因为领域对象拥有更多的能力。

    待增加更详细的内容

    以上是关于前端领域的 “干净架构”的主要内容,如果未能解决你的问题,请参考以下文章

    架构方面学习笔记-前端架构设计

    asp.net core系列 64 结合eShopOnWeb全面认识领域模型架构

    前端的发展和未来趋势

    Android中具有干净架构的mvvm和没有干净架构的mvvm有啥区别?

    前端缓存

    前端缓存