通过Python开发人员的眼光看清架构

你好!我叫Eugene,我是Python开发人员。在过去的一年半中,我们的团队开始积极应用“清洁架构”的原理,摆脱了传统的MVC模型。今天,我将讨论我们如何实现这一目标,它给我们带来了什么以及为什么直接从其他PL转移方法并不总是一个好的解决方案。



七年来,Python一直是我的主要开发工具。当他们问我最喜欢他的时候,我回答这是他出色的可读性。第一次相识始于对“编程集体思想”一书的阅读。我对其中描述的算法很感兴趣,但是所有示例都使用当时我还不熟悉的语言。这并不常见(Python尚未在机器学习中成为主流),列表通常以伪代码或使用图表编写。但是在快速介绍了该语言之后,我欣赏了它的简洁性:一切都很简单明了,没有多余和令人分心的东西,只是描述过程的本质它的主要优点是语言的惊人设计,非常直观的语法糖。这种表现力在社区中一直受到赞赏。什么是“ 导入此内容 ”,必须出现在任何教科书的第一页上:它看起来像是一个看不见的监督者,会不断评估您的行为。在论坛上,初学者值得在清单中的变量名称中使用CamelCase,因此,讨论的角度立即转向参考PEP8的拟议代码的成语。
对优雅的追求以及强大的语言活力创造了许多具有真正令人愉悦的API的库。 

尽管如此,Python虽然功能强大,但它只是一个工具,允许您编写表达性的,具有自我说明性的代码,但它不能保证此功能,也不保证 PEP8的兼容性。当我们在Django上看似简单的在线商店开始盈利并因此增加功能时,有一点我们了解到它并不是那么简单,即使进行基本更改也需要越来越多的努力,最重要的是,这种趋势正在增长。发生了什么事,什么都出错了?

错误的代码


错误代码不是不遵循PEP8或不满足圈复杂度要求的代码。首先,错误代码是不受控制的依赖关系,它导致以下事实:程序一个位置的更改导致其他部分的更改无法预测。我们正在失去对代码的控制;扩​​展功能需要对项目进行详细研究。这样的代码失去了灵活性,并且无法进行更改,而程序本身变得“脆弱”。 

干净的建筑


选择的应用程序体系结构应该避免这个问题,而且我们不是第一个遇到此问题的人:Java社区已经有很长时间讨论创建最佳的应用程序设计了。

早在2000年,Robert Martin(也称为Bob叔叔)在他的文章“ Design and Design Principles ”(设计和设计原则)中就以令人难忘的缩写SOLID汇集了设计OOP应用程序的五项原则。。这些原则已为社区所接受,并且远远超出了Java生态系统。然而,它们本质上是非常抽象的。后来,人们进行了几次尝试来开发基于SOLID原理的通用应用程序设计。其中包括:“六角形体系结构”,“端口和适配器”,“球状体系结构”,尽管实现细节不同,但它们都有很多共同点。 2012年,同一位罗伯特·马丁(Robert Martin)发表了一篇文章,在那里他提出了自己的版本《清洁建筑》。



据Bob叔叔说,架构主要是“ 边界和障碍 ”,有必要清楚地了解需求并限制软件接口,以免失去对应用程序的控制。为此,该程序分为几层从一层转到另一层,只能传输数据(简单的结构和DTO对象可以充当data )-这是边界的规则。另一个最常被引用的短语是“ 应用程序应该尖叫 ”,这意味着应用程序中的主要内容不是所使用的框架或数据存储技术,而是该应用程序的实际功能,执行的功能- 应用程序业务逻辑。因此,图层不具有线性结构,而是具有层次结构因此,还有另外两个规则:

  • 内层的优先级规则 -由内层决定与外界交互的接口;
  • 依赖性规则 -依赖性应从内层指向外层。

最后一条规则在Python世界中是非常不典型的。要应用任何复杂的业务逻辑方案,您始终需要访问外部服务(例如,数据库),但是要避免这种依赖性,业务逻辑层必须自己声明将与外界交互的接口该技术称为`` 依赖倒置 ''(SOLID中的字母D),并广泛用于具有静态类型的语言中。根据Robert Martin所说,这是OOP附带主要优点

这三个规则是Clean Architecture的本质:

  • 过境规则;
  • 依赖规则;
  • 内层的优先级规则。

这种方法的优点包括:

  • 易于测试 -分别隔离各层,可以在不进行猴子修补的情况下对其进行测试,可以根据其重要性程度为不同的层设置涂层的粒度;
  • 易于更改的业务规则,因为所有规则都收集在一个地方,不会散布在项目中,也不会与低级代码混合;
  • 不受外部代理的影响:在某些情况下,业务逻辑与外界之间存在抽象,因此您可以更改外部源而不会影响内部层。如果您没有将业务逻辑绑定到外部代理程序的特定功能(例如数据库事务),则此方法有效。
  • , , , .

, . . , . « Clean Architecture».

Python


这是一个理论,可以在Robert Martin的原始文章,报告和书中找到实际应用的示例。它们依赖于Java世界中的几种常见设计模式:适配器,网关,交互器,Fasade,存储库,DTO等。

那么,Python呢?就像我说的那样,简洁主义在Python社区中很有价值,而扎根于其他人远非它会扎根于我们这一事实。三年前,我第一次谈到这个话题,但是关于在Python中使用Clean Architecture的话题没有很多资料,但是Google的第一个链接是Leonardo Giordani 项目:作者详细介绍了为使用TDD方法查找房地产的网站创建API的过程,应用清洁架构。
不幸的是,尽管作出了周密的解释并遵循了鲍勃叔叔的所有教,,但这个例子还是很可怕的。 

项目API包含一种方法-获取具有可用过滤器的列表。我认为,即使对于新手开发人员来说,此类项目的代码也不会超过15行。但是在这种情况下,他拿了六个小包。您可以引用一个并非完全成功的布局,这是事实,但是在任何情况下,都很难有人参考该项目来解释这种方法的有效性

还有一个更严重的问题,如果您不阅读本文并立即开始研究该项目,那么就很难理解。考虑业务逻辑的实现:

from rentomatic.response_objects import response_objects as res

class RoomListUseCase(object):
   def __init__(self, repo):
       self.repo = repo
   def execute(self, request_object):
       if not request_object:
           return res.ResponseFailure.build_from_invalid_request_object(
               request_object)
       try:
           rooms = self.repo.list(filters=request_object.filters)
           return res.ResponseSuccess(rooms)
       except Exception as exc:
           return res.ResponseFailure.build_system_error(
               "{}: {}".format(exc.__class__.__name__, "{}".format(exc)))

实现业务逻辑的RoomListUseCase类(不是很类似于业务逻辑,对吗?)由repo对象初始化项目的。但是什么是回购协议?当然,从上下文中,我们可以了解到repo实现了用于访问数据的Repository模板,如果我们查看RoomListUseCase的主体,我们知道它必须具有一个list方法,其输入是一个过滤器列表,在输出中不清楚,您需要查看在ResponseSuccess中。而且,如果场景更复杂,可以访问数据源?原来了解回购是什么,您只能参考实现。但是她在哪里?它位于一个单独的模块中,该模块与RoomListUseCase无关。因此,要了解正在发生的事情,您需要进入上层(框架的层次),并在创建对象时查看馈入类的输入的内容。

您可能会认为我列出了动态类型的缺点,但这并非完全正确。它是动态类型,可让您编写富有表现力的紧凑型代码。我想到了与微服务的类比,当我们将一个整体切成微服务时,由于具有可以在微服务内部完成的任何事情(PL,框架,体系结构)的能力,因此设计具有更高的刚性,但是它必须符合声明的接口。所以在这里:当我们将项目划分为多个层时,层之间的关系必须与合同一致,而在层内部,合同是可选的。否则,您需要保持很大的头脑。记住,我曾说过,代码错误的问题是依赖关系,因此,在没有显式接口的情况下,我们会再次滑回我们想要摆脱的位置-由于缺少明显的因果关系

repo RoomListUseCase, execute — . - , , . - . , , , repo .

总的来说,那时我在一个新项目中放弃了Clean Architecture,再次应用了经典的MVC。但是,在填补了下一批锥体之后,一年后,他又回到了这个想法,最后,我们开始在Python 3.5+中启动服务。如您所知,他带来了类型注释数据类:两个强大的界面描述工具。基于它们,我草绘了服务的原型,结果已经好多了:尽管仍然有很多代码,尤其是与框架集成时,各层却停止了分散。但这足以在小型项目中开始采用这种方法。逐渐地,出现了专注于最大程度地使用类型注释的框架:apistar(现在为starlette),meltframework。 pydantic / FastAPI捆绑包现在很常见,并且与此类框架的集成变得更加容易。这是上述restomatic / services.py示例的样子:

from typing import Optional, List
from pydantic import BaseModel

class Room(BaseModel):
   code: str
   size: int
   price: int
   latitude: float
   longitude: float

class RoomFilter(BaseModel):
   code: Optional[str] = None
   price_min: Optional[int] = None
   price_max: Optional[int] = None

class RoomStorage:
   def get_rooms(self, filters: RoomFilter) -> List[Room]: ...

class RoomListUseCase:
   def __init__(self, repo: RoomStorage):
       self.repo = repo
   def show_rooms(self, filters: RoomFilter) -> List[Room]:
       rooms = self.repo.get_rooms(filters=filters)
       return rooms

RoomListUseCase-实现项目业务逻辑的类。您不应该注意show_rooms方法所做的只是对RoomStorage的调用这一事实(我没有给出此示例)。在现实生活中,还可以进行折扣计算,根据广告对列表进行排名等。但是,该模块是自给自足的。如果要在另一个项目中使用此方案,则必须实现RoomStorage。从模块中可以清楚地看到所需的内容。与前面的示例不同,这样的层是自给自足的,并且在更改时不必牢记整个上下文。从非系统性依赖只能是pytantic,为什么,它将在框架的插件中变得很清楚。没有依赖,这是提高代码可读性的另一种方式,而不是其他上下文,即使是新手开发人员也将能够理解该模块的功能。

业务逻辑场景不必一定是一个类;以下是以函数形式的类似场景的示例:

def rool_list_use_case(filters: RoomFilter, repo: RoomStorage) -> List[Room]:
   rooms = repo.get_rooms(filters=filters)
   return rooms


这是与框架的连接:

from typing import List
from fastapi import FastAPI, Depends
from rentomatic import services, adapters
app = FastAPI()

def get_use_case() -> services.RoomListUseCase:
   return services.RoomListUseCase(adapters.MemoryStorage())

@app.post("/rooms", response_model=List[services.Room])
def rooms(filters: services.RoomFilter, use_case=Depends(get_use_case)):
   return use_case.show_rooms(filters)

使用get_use_case函数,FastAPI实现了依赖注入模式。我们无需担心数据序列化,所有工作都由FastAPI与pydantic一起完成。不幸的是,数据并不总是适合于在餐厅现场直播的业务逻辑格式,相反,业务逻辑不知道数据来自何处-带有斜杠,请求正文,Cookie等。在这种情况下,房间功能的主体将对输入和输出数据进行一定的转换,但是在大多数情况下,如果我们使用API​​,那么这样一个简单的代理功能就足够了。 

, , , RoomStorage. , 15 , , , .

正如规范的“干净架构”模型所建议的那样,我故意没有分开业务逻辑层。Room类应该位于表示域区域的Entity域区域层中,但是对于此示例,则不需要这样做。通过合并Entity和UseCase层,该项目不会停止成为Clean Architecture实施。罗伯特·马丁本人曾多次说过,层数可以上下变化同时,该项目符合清洁建筑的主要标准:

  • 越境规则:大体模型,本质上是DTO,跨越边界
  • 依赖规则:业务逻辑层独立于其他层;
  • 内部层的优先级规则:业务逻辑层定义接口(RoomStorage),业务逻辑通过该接口与外界交互。

今天,我们团队中使用上述方法实施的几个项目正在开发中。我尝试通过这种方式来组织最小的服务。它训练得很好-提出之前我从未想过的问题。例如,这里的业务逻辑是什么?例如,如果您正在编写某种代理,这远非总是显而易见的。另一个重要的一点是要学会不同的想法。。当我们完成一项任务时,我们通常会开始考虑框架,所使用的服务,是否需要在此处进行存储,最好在哪里存储可以缓存的数据的行。在指示“干净架构”的方法中,我们必须首先实现业务逻辑,然后再继续实现与基础架构的交互,因为根据罗伯特·马丁说,架构主要任务是延迟与任何基础架构之间的连接时间。基础结构层将成为您应用程序的组成部分。

总的来说,我看到在Python中使用Clean Architecture的良好前景。但是,最有可能的形式将与其他PL接受的形式显着不同。在过去的几年中,我看到社区中对建筑这一主题的兴趣大大增加。因此,在上一届PyCon会议上,有几篇关于DDD使用的报告,应该单独记录来自干实验室的人员在我们公司中,许多团队已经在某种程度上实施了所描述的方法。我们都在做同样的事情,我们已经成长,我们的项目已经成长,Python社区必须与之合作,定义通用的样式和语言,例如,曾经成为所有Django的样式和语言。

All Articles