本文是khalilstemmler.com上关于如何系统学习软件设计和架构的文章。原文地址为:How to Learn Software Design and Architecture | The Full-stack Software Design & Architecture Map。如有侵权请联系我。
因第一次翻译这个作者的文章,并对作者也不甚了解。所以简单的将作者介绍一下。作者:khalil stemmler,在github上查到他人在加拿大,在Apollo GraphQL工作。
从这片文章翻译过程中,遇到一些问题。发现作者的思维比较跳跃,上下文的跳跃性比较强。所以翻译和阅读有一些麻烦。并文中所涉及到的内容皆为作者原意,翻译的意见可能与本文有出入。
在计算领域,软件设计和体系结构是计算机所特有的研究领域,比如DevOps或UX设计。这里有张图描述了从Clean Code到微内核中的软件设计和架构。
本主题摘自Solid Book的《w/TypeScript + Node.js的软件架构和设计手册》。如果有兴趣可以查看。
想到Facebook曾经是某人电脑上的一个空白代码文件,而现在正是这家庞大的公司几乎涉足了所有的行业,并影响了全球15.9亿人,这让我觉得很疯狂。
作为一个自学成才的初级开发人员,甚至是中级开发人员,要想继续成长,真正学会如何设计Clean和可伸缩的系统。这个成长路线图似乎有些令人望而生畏。
对我们很多人来说项目会坚持不过一两次迭代,其中一部分原因是因为代码变成了无法维护的混乱。
那么我们从哪里开始学习如何改进我们的设计呢?
事实是:
软件设计和体系结构是一个巨大的课题。
了解如何:
- 设计一个系统来满足用户的需求
- 编写易于更改的代码
- 编写易于维护的代码
- 编写易于测试的代码
... 很难。所需的学习的知识实在太大了。
尽管你知道如何编写代码使事情至少运行起来,但更大的挑战是如何编写代码,使其易于修改,以满足当前的需求。
但是,从哪里开始呢?
每当我面对一个复杂的问题时,我都会使用第一原则。
第一原则
第一原则是解决问题最有效的方法。
它的工作原理是将一个问题分解到原子级,原子级是我们无法分解,然后从我们确信是真实问题出发重新构造一个解决方案。
因此,让我们首先说明目标,将其应用于软件。
软件的主要目标是什么?
软件的目标是不断地生产满足其用户需求的东西,同时尽可能减少所需的努力。
这是在我很长一段时间以来一直在努力想出的最好的定义,我准备和你争论为什么我认为这是准确的。
不能满足用户需求的软件根本就不是好软件。
而且由于用户的需求经常变化,所以确保软件的设计是为了能够被改变而设计的,这一点很重要。
如果软件不能(容易地)被修改,那它就成了糟糕的软件,因为它阻止了我们满足用户当前的需求。
我们已经确定设计很重要,接下来学习如何制作设计良好的软件就变成很重要的,但这可能是一条漫长的道路。
在本文中,我想向您介绍我认为是软件设计和体系结构的具体支柱。
学习堆栈
在我给你看学习路径之前,让我先给你看一下学习堆栈。
与OSI模型类似,每一层都建立在前一层的基础之上。
在堆栈中,我已经在该层中包含了一些最重要概念的示例,但不是全部(因为太多了)。
现在开始看地图。虽然我认为堆栈可以看到更大的图片,但地图更详细一点,因此,我认为它更有用。
图
为了避免占用带宽,我降低了网站上显示的地图的质量。如果你想得到一个高质量的png,你可以在我的GitHub上找到。
下面是软件设计和架构图。
阶段1:Clean code
创建有韧性的软件,第一步就是找出如何编写Clean code。
如果你问任何人他们认为什么是写Clean code,你可能每次都会得到不同的答案。很多时候,您会听说干净的代码是易于理解和修改的代码。在底层,这体现在一些设计选择上,比如:
- 始终如一
- 推荐有意义的变量名、方法名和类名,而不是写注释
- 确保代码有适当的缩进和间隔
- 确保所有测试都可以运行
- 编写没有副作用的纯函数
- 不传递null
这些可能看起来像是小事,但把它想象成一场游戏。为了使我们的项目结构在一段时间内保持稳定,像缩进、小类和方法以及有意义的名称这样的东西从长远来看会有很大的回报。
如果你问我Clean code的这一方面是关于有好的编码惯例并遵循它们。
我相信这只是编写Clean code的一个方面。
我对Clean code的明确解释包括:
🧠 你的开发者心态(同理心、手艺、成长心态、设计思维)
⚙️ 您的编码约定(命名、重构、测试等)
🤹🏼 您的技能和知识(关于模式、原则以及如何避免代码气味和反模式)
如果你想写Clean code,那么进入正确的心态是非常重要的。一个要求是,您应该足够关心了解您正在编写代码的业务。如果我们不关心域,不足以理解它,那么我们如何确定我们使用好的名称来表示域概念?我们如何确保我们已经准确地捕获了功能需求?
如果我们不关心我们正在编写的代码,我们就不太可能实现基本的编码约定,进行有意义的讨论,并就我们的解决方案征求反馈意见。
我们经常认为编写代码仅仅是为了满足最终用户的需求,但是我们忘记了我们编写代码的其他人:我们、我们的团队成员和项目未来的维护者。了解设计的原理以及人类心理如何决定设计的好坏,将有助于我们编写更好的代码。
所以从本质上说,最好的词来描述你这一步的旅程?移情。
一旦我们完成了这一点,就要学习这个行业的诀窍,并通过提高您对基本软件开发模式和原则的了解来不断改进它们。
学习资源
代码整洁之道,作者:Robert C. Martin
重构(第二版),作者:Martin Fowler
程序员修炼之道,作者:Andrew Hunt / David Thomas
设计心理学,作者:Donald Norman
学习如何编写Clean code的最佳资源是鲍勃叔叔的书《代码整洁之道》。
阶段2:编程范例
既然我们正在编写易于维护的可读代码,那么真正理解3种主要编程范式以及它们对我们编写代码的影响方式将是一个好主意。
在鲍勃叔叔的《架构整洁之道》一书中,他让人们注意到:
- 面向对象编程是最适合定义我们如何使用多态性和插件跨越体系结构边界的工具。
- 函数式编程是我们用来将数据推送到应用程序边界的工具。
- 结构化编程是我们用来编写算法的工具
这意味着有效的软件在不同的时间使用混合的三种编程范式样式。
虽然您可以采用严格的函数式或严格面向对象的方法编写代码,但了解每种方法的优点将提高设计的质量。
如果你只有一把锤子,一切都像钉子。
学习资源
架构整洁之道,作者:Robert C. Martin
程序设计语言概念(第十版), Robert W. Sebesta (10th edition)
阶段3:面向对象编程
了解每个范例是如何工作的,以及它们如何促使您在其中构建代码是很重要的,但是对于体系结构,面向对象编程是这项工作的明确工具。
面向对象编程不仅使我们能够创建一个插件体系结构并在项目中构建灵活性;OOP还具有OOP的4个原则(封装、继承、多形性和抽象性),帮助我们创建丰富的领域模型。
大多数学习面向对象编程的开发人员从来没有接触过这一部分:学习如何创建问题域的软件实现,并将其定位在分层web应用程序的中心。
在这个场景中,函数式编程似乎是达到所有目的的方法,但我建议您熟悉模型驱动设计和领域驱动设计,以了解对象建模者如何能够将整个业务封装在一个零依赖域模型中。
为什么这是个大买卖?
这是巨大的,因为如果你可以创建一个商业的心理模型,你可以创建一个软件实现的业务。
学习资源
Object-Design Style Guide,作者:Matthias Noback
架构整洁之道,作者:Robert C. Martin
领域驱动设计,作者:Eric Evans
阶段4:设计原则
此时,您将了解到面向对象编程对于封装富域模型和解决第三类“硬软件问题”—复杂域非常有用。
但是OOP可能会带来一些设计挑战。
我什么时候应该用组合?
我什么时候应该使用继承?
我什么时候应该使用抽象类?
设计原则是非常成熟的、经过实战测试的面向对象的最佳实践,您可以将其用作护栏。
您应该熟悉的一些常见设计原则有:
组合优先于继承
封装变化
针对抽象而不是具体的编程
好莱坞原则:“别打电话给我们,我们会打给你的”
SOLID原则,特别是单一责任原则
DRY(不要重复)
YAGNI(你不需要它)
不过,一定要得出自己的结论。不要只听别人说你应该做什么。确保它对你有意义。
学习资源
Head First设计模式,作者:various authors
Design Patterns,作者:GoF
阶段5:设计模式
软件中几乎所有的问题都已经被分类和解决了。实际上,我们称这些模式为设计模式。
设计模式分为三类:创建型、结构型和行为型。
创建型
创建模式是控制对象创建方式的模式。
创建模式的示例包括:
单例模式,用于确保一个类只能存在一个实例
抽象工厂模式,用于创建多个类族的实例
原型模式,用于从现有实例克隆的实例开始
结构型
结构模式是简化定义组件之间关系的模式。
结构模式的示例包括:
适配器模式,用于创建一个接口,使通常不能一起工作的类能够协同工作。
桥接模式,用于将实际上应该是一个或多个的类拆分为属于层次结构的一组类,从而使实现能够相互独立地开发。
装饰器模式,用于动态地向对象添加职责。
行为型
行为模式是促进物体之间优雅交流的常见模式。
行为模式的示例包括:
模板模式,用于将算法的确切步骤推迟到子类。
中介模式,用于定义类之间允许的确切通信通道。
观察者模式,用于使类订阅感兴趣的内容,并在发生更改时得到通知。
设计模式批评
设计模式很好,但有时它们会增加我们设计的复杂性。重要的是要记住YAGNI,并尽量使我们的设计尽可能简单。只有当你确定你需要的时候才使用设计模式,到了那时你就会知道。
如果我们知道每个模式是什么,什么时候使用它们,什么时候甚至不用费心去使用它们,我们就可以开始理解如何构建更大的系统了。
这背后的原因是架构模式只是扩展到高层的设计模式,而设计模式是低层次的实现(更接近于类和函数)。
学习资源
Head First设计模式,作者:various authors
阶段6:架构原则
现在我们的思维水平已经超越了课堂层面。
我们现在了解到,我们在组织和建立高层和底层组件之间的关系方面所做的决定,将对项目的可维护性、灵活性和可测试性产生重大影响。
学习指导原则,帮助您构建代码库所需的灵活性,以便能够以尽可能少的努力对新特性和需求做出反应。
以下是我建议您立即学习:
组件设计原则:稳定抽象原则、稳定依赖原则和非循环依赖原则,用于如何组织组件、它们的依赖关系、何时耦合它们,以及意外创建依赖循环和依赖不稳定组件的影响。
策略与细节,了解如何将应用程序的规则与实现细节分开。
边界,以及如何标识应用程序的功能所属的子域。
Bob叔叔发现并最初记录了许多这些原则,因此了解这些原则的最佳资源再次是“架构整洁之道”。
学习资源
架构整洁之道,作者:Robert C. Martin
阶段7:架构风格
架构是重要的东西。
它是关于确定一个系统需要什么才能成功,然后通过选择最适合需求的体系结构来增加成功的几率。
例如,一个具有大量业务逻辑复杂性的系统可以使用分层体系结构来封装这种复杂性。
像Uber这样的系统需要能够同时处理很多实时事件并更新驱动程序的位置,所以发布订阅式的架构可能是最有效的。
我在这里重复一遍,因为需要注意的是,3类架构风格与3类设计模式相似,因为架构风格是高层的设计模式。
结构
具有不同级别组件和广泛功能的项目,采用架构要么受益,要么遭殃。
以下是几个例子:
- 基于组件模式强调系统中各个组件之间的关注点分离。想想谷歌吧。考虑他们的企业中有多少应用程序(Google Docs、Google Drive、Google Maps等)。对于具有大量功能的平台,基于组件的体系结构将关注点划分为松散耦合的独立组件。这是水平分隔。
- 单体模式意味着应用程序被组合到一个单独的平台或程序中,并一起部署。注意:如果正确地分离应用程序,并将其作为一个整体部署,那么就可以有一个基于组件的单片体系结构。
- 分层模式通过将软件划分为基础设施、应用程序和域层来垂直分离关注点。
使用分层体系结构垂直切割应用程序关注点的示例。请阅读此处了解有关如何执行此操作的更多信息。
消息传递
根据您的项目,消息传递可能是系统成功的一个非常重要的组件。对于这样的项目,基于消息的体系结构建立在函数式编程原则和行为设计模式(如观察者模式)之上。
以下是基于消息的体系结构样式的几个示例:
事件驱动模式将所有重要的状态更改都视为事件。例如,在乙烯基交易应用程序中,当双方同意交易时,报价的状态可能会从“待定”变为“已接受”。
发布-订阅模式建立在观察者设计模式之上,使其成为系统本身、最终用户/客户机以及其他系统和组件之间的主要通信方法。
分布式
分布式模式简单地说系统的组件是单独部署的,并通过网络协议进行通信来操作。分布式系统对于扩展吞吐量、扩展团队和将(潜在的昂贵任务或)责任委派给其他组件非常有效。
分布式模式的一些示例包括:
- 客户端服务器架构。最常见的架构之一,我们将要完成的工作划分为客户机(表示)和服务器(业务逻辑)。
- P2P模式在同等特权的参与者之间分配应用层任务,形成对等网络。
学习资源
架构整洁之道,作者:Robert C. Martin
Software Architect's Handbook,作者:Joseph Ingeno
阶段8:架构模式
架构模式更详细地解释了如何实际实现其中一种架构样式。
以下是一些架构模式及其继承样式的示例:
领域驱动设计是一种针对真正复杂问题领域的软件开发方法。为了使DDD成功,我们需要实现一个分层的体系结构,以便将域模型的关注点与使应用程序实际运行的基础结构细节(如数据库、web服务器、缓存等)分离开来。
模型-视图-控制器可能是开发基于用户界面的应用程序的最著名的体系结构模式。它将应用程序分成3个组件:模型、视图和控制器。当你刚开始的时候,MVC是非常有用的,它帮助你向其他架构靠拢,但是当我们意识到MVC对于很多业务逻辑的问题是不够的。
事件溯源是一种功能性方法,我们只存储事务,而不存储状态。如果我们需要状态,我们可以从一开始就应用所有事务。
学习资源
领域驱动设计,作者:Eric Evans
实现领域驱动设计,作者:Vaughn Vernon
阶段9:企业模式
您选择的任何架构模式都会引入大量的构造和技术术语,以使您熟悉并决定是否值得使用它。
举一个我们很多人都知道的例子,在MVC中,视图包含所有的表示层代码,控制器将视图中的命令和查询转换为由模型处理并由控制器返回的请求。
在模型(M)中,我们在哪里处理这些事情?:
验证逻辑
不变规则
领域事件
用例
复杂查询
和业务逻辑
如果我们简单地使用一个像Sequelize或TypeORM这样的ORM(对象关系映射器)作为模型,那么所有这些重要的东西都将留给解释它应该去哪里,它会发现自己位于模型和控制器之间的某个未指定的层中(应该是丰富的)。
如果说在我的旅程中我学到了一些东西,超越MVC,那就是所有东西都有一个结构。
对于MVC未能解决的每一个问题,特别是在领域驱动设计中,有几种企业模式可以解决它们。例如:
实体描述具有标识的模型。
值对象是没有标识的模型,可以用于封装验证逻辑。
领域事件是指发生的一些相关业务事件的事件,并且可以从其他组件订阅。
根据您选择的体系结构风格,为了最大限度地实现该模式,您还需要学习大量其他企业模式。
学习资源
这些只是一些不同的学习资源,主要集中在领域驱动设计和企业应用程序架构上。但这是最值得学习的地方,也是你可以深入学习的地方,因为它建立在我们迄今所学的一切的基础之上。
企业应用程序架构模式,作者:Martin Fowler
企业集成模式,作者:Gregor Hohpe
领域驱动设计,作者:Eric Evans
实现领域驱动设计,作者:Vaughn Vernon
资源与结论
在这个博客中,我们讨论了很多领域驱动的设计,但是在我们深入研究如何使用TypeScript构建丰富的领域模型之前,首先了解(比如分层体系结构、oop、模型驱动设计、设计原则和模式)会让很多读者受益匪浅。
如果你想找一个一站式的资源,我刚刚启动solidbook.io-软件设计与架构手册。我教读者我认为他们真正需要知道的东西,在这个地图的每个阶段,为了生产出我们在本文中讨论的好的软件。它目前在销售,直到它完全完成,但我也很高兴推荐一些其他优秀的资源,我个人在学习软件设计和架构时使用过。