序
从事Python开发也有段时间了,企业开发Python对解决复杂度问题可能是每个Pythoner的痛,有些人专研语法,有些人沿用其它语言的思考模式。但是却没有一个合适的指导思考方式去构建应用程序。况且,领域编程并不是某项特定技术,而是一种组织和思考模式来应对复杂的业务,由于Python的简洁易懂的特性,不会贴大量的代码给读者造成负担,从而更加去偏向思考业务本身而非语法
本系列文章奖分为以下两大章
1. 领域驱动建模
2. 事件驱动架构
我们的设计错在哪了?
当你听到“乱”这个词的时候,你会想到什么?也许你会想到嘈杂的菜市场,比如你的出租屋,看起来一切都混乱不堪,当你想到“整洁有序”这个词的时候,你又会想到什么?也许会想到一个空荡荡的房间,宁静而平静。然而对于科学家来说,混乱是拥有同质性的,而复杂性确实有序的。
例如,一个精心照料的花园是一个高度有序的系统。园丁们用小路和篱笆来划定界限,他们划出花坛或菜地。随着时间的推移,花园变得越来越丰富。但是只要一旦没有刻意努力,花园就会变得荒芜。杂草和青草会阻塞其他植物的生长,覆盖道路。直到最后每个地方都看起来一样--野生且无人管理
软件系统也趋向于混乱。当我们第一次开始构建一个新的系统时,我们总是认为我们的代码是干净且有序的,但随着时间的推移,我们发现它从一些粗糙的边缘case,慢慢编程一个令人困惑的管理器类,一堆utils模块的泥潭。我们发现我们层次分明的建筑变成了一滩软泥怪。但混乱的软件系统同样拥有API,发送电子邮件,执行日志记录,不执行计算但执行I/O的业务逻辑类,以及一切耦合的东西,因此改变系统的任何地方都充满了风险。这种情况非常普遍,以至于程序员们对这种混乱叫做:*大粪球*
大粪球是软件的自然状态,就像荒芜的土地其实就是你家的花园,它必须拥有原定或者能量才能防止塌陷成荒地
幸运的是,避免形成一个大粪球的技术并不复杂。
封装和抽象
封装和抽象是我们作为程序员经常挂在嘴边的工具,但是你真的知道什么是封装和抽象嘛?其实我们必须要详细讨论下这个问题,因为它将是后续文章中反复出现的东西。
封装包含两个概念:简化行为和隐藏数据。我们要通过第一直觉来识别需要在代码中完成的任务来封装行为,并将任务交给定义良好的对象或函数。我们称这个对象或函数为抽象。
怎么讲?来看代码
我们使用urllib进行搜索
import json from urllib.request import urlopen from urllib.parse import urlencode params = dict(q='Sausages', format='json') handle = urlopen('http://api.duckduckgo.com?' + urlencode(params)) raw_text = handle.read().decode('utf8') parsed = json.loads(raw_text) results = parsed['RelatedTopics'] for r in results: if 'Text' in r: print(r['FirstURL'] + '-' r['text'])
我们使用requests来搜索
import requests params = dict(q='Sausages', format='json') parsed = requests.get('http://api.duckduckgo.com/', params=params).json() results = parsed['RelatedTopics'] for r in results: if 'Text' in r: print(r['FirstURL'] + ' - ' + r['Text'])
这两个代码清单做同样的事情:为了使用搜索引擎API,它们向URL提交表单,但是第二种是不是更人性化,更容易阅读和理解,因为它在更高的抽象层次上操作
那我们更进一步,通过识别和命名我们希望的分吗为我们执行的任务,并使用更高层次的抽象
import duckduckgo for r in duckduckgo.query('Sausages').results: print(r.url + ' - ' + r.text)
通过使用抽象来封装行为是使代码更具表达性,更可测试和更容易维护。
在面向对象(OO)领域的文献中,这种方法的经典特征之一被称为responsibility-driven design 责任驱动式设计;它使用的关键字 角色 role 及 responsibilities 责任 高于 tasks 任务. 关键是要从行为的角度思考代码,而不是从数据或算法的角度
关于抽象概念
在其他语言中,比如Java和C#使用接口或继承来定义抽象,在python中你可以使用abc模块,但如果你用鸭子类型来定义抽象也未尝不可
使用抽象只是意味着,“你正在使用一个事物的公共API”,它可以是一个函数加一些参数,也可以是一个鸭子类
分层
封装和抽象通过隐藏细节和保护数据的一致性来保护我们,但是我们也需要注意对象和函数之间的互动。当一个函数、模块或对象使用另一个时,我们说这个函数、模块或对象依赖于另一个函数、模块或对象。这些依赖就形成了一个网状结构或图结构
在一个大粪球中,依赖关系失去了控制,更改任何一个节点,都变得困难,因为我它有可能影响系统的许多其他部分。分层架构时解决这个问题的一种方法。在分层体系结构中,我们将代码划分为不连续的类别或角色,并引入关于哪些类别的代码可以互相调用的规则。
分层架构可能时构建业务软件最常用的模式。在这个模型中,我们有用户界面组件,可以是网页,API或者命令行,这些用户于界面组件于包含业务规则和工作流的业务逻辑层进行通信,最后,我们还有个负责存储和检索数据的数据持久化层
后面的文章,我们将遵循一个简单的原则,对这种模型翻个底朝天。
依赖倒置原则(DIP)
你可能听说过依赖倒置原则,因为它时SOLID(软件设计中的6大基本原则)中的D
不幸的时,我们不能像封装那样用3个小例子来演示依赖倒置。然而,在接下来的文章中实现依赖倒置的一个例子。
那么,我们先谈谈依赖倒置的具体定义
1. 高级模块不应该依赖于低级模块,两者都应该依赖于抽象
2. 抽象不应该依赖于细节,细节应该依赖于抽象
但这个意味着什么呢?
高级模块,是你公司领导真正关心的代码。例如你在一家电子商务公司,你的高级模块就处理商品和客户,如果你在银行工作,你的高级模块就管理交易和金钱。软件系统的高级模块是处理现实世界的函数、类和包
相比之下,低级模块是你公司领导不关心的代码。你的产品经理不可能对文件系统和socket感到兴奋。财务部门也不会跟你讨论STMP,HTTP,AMQP。对于非技术涉众来讲,这些低层次的概念即无趣也无关紧要。他们关心的只是高层次的概念能否正确工作。如果工资单照常运行,你的业务不太可能关心究竟是个crontab还是在一个K8S上运行的临时函数。
依赖并不一定意味着导入或调用,而是一个模块知道或需要另一个模块。
我们已经提到了抽象:他们是封装行为的简化接口,就像我们的duckduckgo模块封装了搜索引擎的API一样。所谓计算机世界里,所有问题不是一个中间件解决不了的,如果有就两个。
因此依赖倒置的第一部分说我们的业务代码不应该依赖于技术细节;相反,两者都应该使用抽象。
为什么?从广义上说,因为我们希望能够独立的改变他们。高级模块应该易于根据业务需求进行更改。在实践中,底层模块通常很难改变,考虑通过重构更改函数名,而不是通过定义,测试和部署数据库迁移来更改列明。我们不希望业务逻辑更改拖慢速度,因为他们与底层基础设施细节紧密耦合。但是同样的,在需要时能够更改基础结构的细节也很重要,例如我们数据库不够要分库,而不需要对业务层进行更改。在他们之间添加一个抽象允许两者相互独立地更改。
那第二部分就更难以理解了,“抽象不应该依赖细节”看起来很清楚,但是细节应该依赖于抽象就很难想象。我们怎么能有一个不依赖于细节的抽象呢?这在我之后的文章也会有例子,以后再讲。
我们所有业务逻辑的位置:领域模型
但是,在我们将三层体系结构干翻之前,我们需要更多的讨论中间层:高级模块和业务逻辑。我们的写出混乱代码最常见的原因是业务逻辑分散在应用程序的各个层次,让其难以识别、理解和更改。
接下来我们就会详细讨论第一部分:领域模型