typescript
by Warren Bell
沃伦·贝尔(Warren Bell)
There are many videos and articles explaining clean architecture. Most of these go over the concepts from a 20,000 foot view. I don’t know about you, but I don’t learn things very well at that elevation. Not a lot of oxygen up there. I learn by jumping in head first and coding. This article and the accompanying code is what I ended up with after such a leap.
有许多视频和文章介绍了干净的体系结构。 其中大多数从20,000英尺的角度审视了这些概念。 我不了解你,但是我在那个高度上学得不好。 那里没有很多氧气。 我通过跳入头脑和编码来学习。 经过如此飞跃,本文和附带的代码才是我最终的目标。
The term “Clean Architecture” was made popular by Robert Martin (Uncle Bob) and his book “Clean Architecture: A Craftsman’s Guide to Software Structure and Design.” Now I don’t proclaim to be an expert in this field and I haven’t read his book, though I intend to. But I can completely relate to the problems it is trying to solve.
Robert Martin(鲍勃叔叔)和他的书《 Clean Architecture:软件结构和设计的工匠指南》使“ Clean Architecture”一词成为流行。 现在,尽管我打算,但我并没有宣布自己是该领域的专家,也没有读过他的书。 但是我完全可以解决它要解决的问题。
How do you write a software system that is not dependent on anything other than a primary language ? We were promised this in the past with interfaces and other OO principles, but I had never before seen a “clean”, pun intended, explanation on how to do this regarding the whole system. And yes, I am a bit late to this party, being that Uncle Bob started to talk about these concepts in 2012, which is a century ago in software years.
您如何编写不依赖于主要语言之外的任何东西的软件系统? 过去,我们曾通过接口和其他OO原则向我们承诺过这一点,但是我从来没有见过关于如何在整个系统上做到这一点的“干净的”双关语。 是的,我参加这个聚会有点晚了,因为Bob叔叔在2012年(这是一个世纪的软件时代)开始谈论这些概念。
Here is the original diagram Uncle Bob and others used in their presentations when explaining Clean Architecture. This simple little diagram became an obsession of mine. I had long ago purged my memory of anything UML related and was struggling with the has-a, and uses-a relationships indicated by the open and close arrow heads. The only way I was going to figure this out was by writing some code.
这是Bob叔叔和其他人在解释Clean Architecture时在演示中使用的原始图。 这个简单的小图成了我的痴迷。 我很早以前就清除了与UML相关的任何内容,并且一直在用打开和关闭箭头指示的has-a和use-a关系进行挣扎。 我要弄清楚这一点的唯一方法是编写一些代码。
One way to look at Clean Architecture is as an onion with layers. All layers can only depend on a layer that is closer to the center. That is, all dependencies point inward and not outward.
观察“干净架构”的一种方法是将洋葱分层。 所有图层只能依赖于更靠近中心的图层。 也就是说,所有依赖关系都指向内部而不是向外。
In our example, there are 4 modules that correspond with each layer of this onion. Eventually these could be separate npm modules. For readability’s sake, I tried to name things according to Uncle Bob’s original Clean Architecture diagram at the top of this article. In the real world you would probably preface all names with what ever use case they represented.
在我们的示例中,有4个模块与该洋葱的每一层相对应。 最终,这些可以是单独的npm模块。 出于可读性考虑,我尝试根据本文顶部Bob叔叔的原始Clean Architecture图来命名。 在现实世界中,您可能会在所有名称的开头加上它们所代表的用例。
The infrastructure (blue) layer is where all of our outside pluggable systems live. These outside systems such as devices, web, and UIs, shown in the onion diagram, will use our IRequest and IViewModel interfaces to communicate with our Controller and Presenter while the db and external interfaces, shown in the onion diagram, will use the IEntityGateway interface to communicate with our Interactor.
基础架构(蓝色)层是我们所有外部可插拔系统所在的位置。 洋葱图中显示的这些外部系统,例如设备,Web和UI,将使用IRequest和IViewModel接口与Controller和Presenter进行通信,而洋葱图中显示的db和外部接口将使用IEntityGateway接口与我们的交互器进行通信。
Our example will have one entity called a Widget with three properties. It also uses one use case “create widget” which takes a widget from the UI, saves the widget to some sort of storage, and returns back to the UI a newly created widget with an id and a revision number.
我们的示例将具有一个名为Widget的实体,该实体具有三个属性。 它还使用了一个用例“创建窗口小部件”,它从UI中获取窗口小部件,将窗口小部件保存到某种存储中,并将具有ID和修订号的新创建的窗口小部件返回给UI。
Here is the directory structure. Everything gets wired together in each module’s index.ts file. The entry point is demonstrated in a test located in infrastructure/test/TestEntryPoint.spec.ts .
这是目录结构。 一切都在每个模块的index.ts文件中连接在一起。 入口点在位于Infrastructure / test / TestEntryPoint.spec.ts中的测试中进行了演示。
One of my original hangups was how the outer most layer communicates with the inner layers. I thought all you had to do is call some createWidget() function, for example, on a Controller and you would get a nice shiny new widget returned back to you. Wrong.
我最初的挂断之一是最外层如何与内层进行通信。 我以为您要做的就是在控制器上调用一些createWidget()函数,您会得到一个漂亮的闪亮新小部件,该小部件将返回给您。 错误。
What you want to do is send the widget to be created down to the use case (Interactor) on a certain path and have the use case (Interactor) send the new widget back up to you on a different path. This is similar to a callback function or a Promise. I found a good diagram illustrating this in an article titled “Implementing Clean Architecture — Of controllers and presenters” (link below). In our example I have not implemented a RequestModel or a ResponseModel.
您想要做的是将要创建的小部件发送到特定路径上的用例(Interactor),并让用例(Interactor)在另一条路径上将新的小部件发送回给您。 这类似于回调函数或Promise。 我在题为“实现控制器和演示者的清洁架构”的文章中找到了说明这一点的好图表(下面的链接)。 在我们的示例中,我尚未实现RequestModel或ResponseModel。
So let’s first create a widget with classes and interfaces.
因此,让我们首先创建一个带有类和接口的小部件。
This is the entry point. This code would be located in the infrastructure (blue) layer. This layer is where your mobile app, web app, API, CLI, etc. lives. Also all of your outside systems like external APIs, frameworks, libraries and databases live here too. Everything is pluggable in this layer and communicates with our system via interfaces we provide.
这是入口点。 此代码将位于基础结构(蓝色)层中。 该层是您的移动应用程序,Web应用程序,API,CLI等所在的位置。 同样,您的所有外部系统(例如外部API,框架,库和数据库)也都位于此处。 一切都可插入此层,并通过我们提供的接口与我们的系统通信。
First you create your ViewModel implementation of the IViewModel interface where a new widget will appear and you can update your UI within the implemented function presentWidget(widget).
首先,您将创建IViewModel接口的ViewModel实现,其中将出现一个新的小部件,并且可以在已实现的函数presentWidget(widget)中更新UI。
You then create a Controller that implements the IRequest interface by passing the EntityGateway and the ViewModel you created above to a constructor. Finally your UI calls createWidget(widget) on the Controller where your new widget begins its journey to the Interactor.
然后,通过将上面创建的EntityGateway和ViewModel传递给构造函数,来创建实现IRequest接口的Controller。 最后,您的UI调用Controller上的createWidget(widget),新的小部件开始进入Interactor的旅程。
An EntityGateway implements the IEntityGateway interface and is where you implement specific code that persists your widget. It lives in the infrastructure (blue) layer. This could be any type of existing or future external API or persistence system such as a database.
EntityGateway实现IEntityGateway接口,在这里您可以实现持久化窗口小部件的特定代码。 它位于基础结构(蓝色)层中。 它可以是任何类型的现有或将来的外部API或持久性系统,例如数据库。
To change out to a different system, you would just simply wire together the new EntityGateway implementation with the IEntityGateway interface. In this example, I use a Promise to simulate some sort of persistence operation.
要更改为其他系统,只需将新的EntityGateway实现与IEntityGateway接口连接在一起。 在此示例中,我使用Promise模拟某种持久性操作。
The file infrastructure/src/index.ts in the infrastructure module is where you can wire up your different implementations of your IEntityGateway interface. The “from” path of the import statement points to the correct implementation. In this case it is a persistence system named AnyDB.
基础结构模块中的文件Infrastructure / src / index.ts是连接IEntityGateway接口的不同实现的位置。 import语句的“ from”路径指向正确的实现。 在这种情况下,它是一个名为AnyDB的持久性系统。
Uncle Bob also talks about the use of a Main class where you can do this type of wiring up or do other initializing code. The Main class would also live in the infrastructure module and be pluggable. It would also communicate in the same manner as the other systems in the infrastructure module do. For example, you would initiate this class in your UI’s initializing code and pass it down into your more inner layers to be used via some sort of configuration interface.
Bob叔叔还谈到了Main类的使用,您可以在其中进行这种类型的连接或进行其他初始化代码。 Main类还将存在于基础结构模块中并且是可插入的。 它还将以与基础结构模块中其他系统相同的方式进行通信。 例如,您可以在UI的初始化代码中初始化此类,然后将其传递到更内部的层中,以通过某种配置界面使用。
Our example does not use a Main class and instead is passing the persistence system down into the interactor via the createWidget() function. This is probably not the “pure” way of doing this, but was done to make our example easier to read.
我们的示例不使用Main类,而是通过createWidget()函数将持久性系统向下传递给交互器。 这可能不是做到这一点的“纯粹”方法,但是这样做是为了使我们的示例更易于阅读。
The Controller is a very busy place. First, the EntityGateway passes through unchanged to our Interactor constructor. Then our ViewModel gets passed to the constructor of our Presenter which in turn also gets passed to our Interactor constructor. This all happens in the createWidget(widget) function of the Controller which was called by our UI in step 1 via the IRequest interface. We will talk about our Presenter in step 4 when the newly created widget travels back up to the UI.
控制器是一个非常繁忙的地方。 首先,EntityGateway不变地传递给我们的Interactor构造函数。 然后,我们的ViewModel被传递给Presenter的构造函数,后者又被传递给Interactor构造函数。 所有这些都发生在Controller的createWidget(widget)函数中,该函数在第1步中通过IRequest接口由我们的UI调用。 当新创建的小部件返回到UI时,我们将在步骤4中讨论Presenter。
Finally we are at the most inner layer of our journey, the usecase layer where our Interactor lives. Or better known as our home for all of our app’s use case logic. There is one more inner layer, the domain. This is where all of our business entities and business specific logic lives. In this example, we really don’t have any need to go there except to borrow the WidgetType and IEntityGateway interfaces.
最后,我们处于旅程的最内层,即Interactor所在的用例层。 或更广为人知的是我们所有应用程序用例逻辑的家。 还有一层内层,即领域。 这是我们所有业务实体和业务特定逻辑的所在地。 在此示例中,除了借用WidgetType和IEntityGateway接口之外,我们真的不需要去那里。
Here in our Interactor we take the EntityGateway that was passed through from the Controller and call its saveWidget(widget) function via the IEntityGateway interface. This function returns a Promise from the EntityGateway which resolves in .then() with a newly created widget. We then call the Presenter’s presentWidget(widget) function via the IOutputBoundary interface which starts the newly created widget back up to the UI. This all happens in the Interactor’s createWidget(widget) function which was called by our Controller via the IInputBoundary interface in step 2.
在我们的Interactor中,我们采用从Controller传递过来的EntityGateway,并通过IEntityGateway接口调用其saveWidget(widget)函数。 此函数从EntityGateway返回一个Promise,它使用新创建的小部件在.then()中解析。 然后,我们通过IOutputBoundary接口调用Presenter的presentWidget(widget)函数,该接口将新创建的小部件启动回到UI。 这一切都发生在Interactor的createWidget(widget)函数中,该函数在步骤2中由我们的控制器通过IInputBoundary接口调用。
Here in our Presenter, we simply pass the widget to our ViewModel’s presentWidget(widget) function we created in our UI. This all happens in the Presenter’s presentWidget(widget) function via the IOutputBoundary interface which was called in the Interactor’s createWidget(widget) function in step 3. More can happen here, but not in our example.
在Presenter中,我们只需将小部件传递给我们在UI中创建的ViewModel的presentWidget(widget)函数。 所有这些都通过IOutputBoundary接口在Presenter的presentWidget(widget)函数中发生,该接口在第3步中的Interactor的createWidget(widget)函数中被调用。
Finally our newly created widget is back home ready to be displayed in our UI. This is the exact spot (code) where we started in step 1. Updating the UI happens in the ViewModel’s presentWidget(widget) function via the IViewModel interface which was called in the Presenter’s presentWidget(widget) function in step 4.
最后,我们新创建的窗口小部件已准备就绪,可以在我们的UI中显示。 这是我们从步骤1开始的确切位置(代码)。UI的更新是通过IViewModel接口在ViewModel的presentWidget(widget)函数中进行的,该接口在步骤4中在Presenter的presentWidget(widget)函数中调用。
Here are all the remaining interfaces and type definitions clumped together in one file.
这是所有剩余的接口和类型定义,它们集中在一个文件中。
I wrote the class and interface version of this project first. I wanted to try and make it match Uncle Bob’s original diagram as close as I could. When I finished that project, I realized I could have done the same thing with functions and type definitions. So I created an identical project and replaced Classes with Functions and Interfaces with Type definitions.
我首先编写了该项目的类和接口版本。 我想尝试使其与Bob叔叔的原始图尽可能地接近。 当我完成该项目时,我意识到我可以使用函数和类型定义完成同样的事情。 因此,我创建了一个相同的项目,并使用函数和接口将类型替换为类型定义。
And here is the difference between a Controller class and a Controller function.
这是Controller类和Controller函数之间的区别。
Now lets give a stab at creating widgets with functions and type definitions.
现在,让我们扼杀使用函数和类型定义创建小部件的过程。
WidgetType is identical as the OO version above and IEntityGateway, IRequest, IViewModel, IInputBoundary, and IOutputBoundary are now type definitions instead of interfaces.
WidgetType与上面的OO版本相同,并且IEntityGateway,IRequest,IViewModel,IInputBoundary和IOutputBoundary现在是类型定义,而不是接口。
Every thing is the same as OO step 1 above, other than that we are now importing a function named “controllerConstructor” instead of a class named “Controller.” And importing a function named “entityGateway” instead of a class named EntityGateway. Last but not least, the ViewModel we created is now an object with a presentWidget() function in it instead of a class with a presentWidget() function.
每件事都与上面的OO步骤1相同,除了我们现在要导入一个名为“ controllerConstructor”的函数,而不是一个名为“ Controller”的类。 然后导入一个名为“ entityGateway”的函数,而不是一个名为EntityGateway的类。 最后但并非最不重要的一点是,我们创建的ViewModel现在是一个带有presentWidget()函数的对象,而不是带有presentWidget()函数的类。
The EntityGateway does the same task as the OO version above. It is now a function instead of a class. It returns a saveWidget() function wrapped in an object.
EntityGateway与上述OO版本执行相同的任务。 现在它是一个函数而不是一个类。 它返回包装在对象中的saveWidget()函数。
Same as OO version above except we are now exporting a function instead of a class.
与上面的OO版本相同,除了我们现在导出的是函数而不是类。
Our Controller is still a busy place and does the same tasks as the OO version. We are now importing a function named interactorConstructor instead of a class named Interactor. We are exporting a function named “controllerConstructor” instead of a class named “Controller.” It returns a function named “createWidget wrapped in an object.
我们的控制器仍然很忙,并且执行与OO版本相同的任务。 现在,我们将导入一个名为interactorConstructor的函数,而不是一个名为Interactor的类。 我们正在导出一个名为“ controllerConstructor”的函数,而不是一个名为“ Controller”的类。 它返回一个包装在对象中的名为“ createWidget”的函数。
Back in the Iteractor in our usecase module, we are executing the same tasks as the OO version above. We are now exporting a function named “interactorConstructor” instead of a class named “Interactor.” It returns a function named “createWidget wrapped in an object.
回到用例模块的Iteractor中,我们正在执行与上述OO版本相同的任务。 现在,我们正在导出一个名为“ interactorConstructor”的函数,而不是一个名为“ Interactor”的类。 它返回一个包装在对象中的名为“ createWidget”的函数。
We are now passing the newly created widget back up in our Presenter where we are executing the same tasks as the OO version above. We export a function named “presenterConstructor” instead of a class named “Presenter.” It returns a function named “presentWidget wrapped in an object.
现在,我们将新创建的小部件传递回Presenter中,在其中执行与上述OO版本相同的任务。 我们导出一个名为“ presenterConstructor”的函数,而不是一个名为“ Presenter”的类。 它返回一个包装在对象中的名为“ presentWidget”的函数。
Again we have come full circle and we are back in the exact spot (code) where we started in step 1. Our UI gets updated with our newly created widget in the ViewModel’s presentWidget() function.
再次,我们绕了一圈,回到了第一步中的确切位置(代码)。在ViewModel的presentWidget()函数中,使用新创建的小部件对UI进行了更新。
Here are all the remaining type definitions clumped together in one file. These are our interfaces.
以下是所有剩余的类型定义,它们集中在一个文件中。 这些是我们的界面。
Yes, but you also get the promise of a completely decoupled system where you can plug in different implementations of your outside (infrastructure blue layer) systems, including different types of UIs, external APIs, databases, libraries, frameworks and more.
是的,但是您也可以得到一个完全解耦的系统的希望,您可以在其中插入外部(基础架构蓝色层)系统的不同实现,包括不同类型的UI,外部API,数据库,库,框架等。
My original hunch was that the class and interface version would be slower than the function version. So I ran both projects through my advance profiling tools of typing “npm test” and pressing enter until my finger cramped up.
我最初的直觉是类和接口的版本会比函数的版本慢。 因此,我通过高级配置工具(输入“ npm test”)并按Enter直到手指收紧,来运行两个项目。
My first observation was that the function version was about twice as fast, WOW. Then I decided to refactor the function version to return all of the important functions wrapped in objects so I could enforce the function names. I then ran both versions through my advance profilers and they were about the same speed.
我的第一个观察结果是该功能版本的运行速度大约是WOW的两倍。 然后,我决定重构函数版本,以返回包装在对象中的所有重要函数,以便执行函数名。 然后,我通过高级分析器运行了两个版本,它们的速度大致相同。
I have no idea why wrapping a function in an object would slow it down that much. Maybe I didn’t actually get Adobe Flash completely uninstalled from my laptop and it decided to interfere. Anyways, it would be interesting to get a more accurate measure of speed using the correct tools against the compiled Javascript.
我不知道为什么将函数包装在对象中会降低它的速度。 也许我实际上并没有从笔记本电脑上完全卸载Adobe Flash,它决定进行干预。 无论如何,使用针对已编译Javascript的正确工具来获得更准确的速度度量将很有趣。
The OO version has more code but may be easier to read and follow. The function version has less code but may be harder to read and follow.
OO版本具有更多代码,但可能更易于阅读和遵循。 该功能版本的代码较少,但可能难以阅读和遵循。
Personally I like the function version, being that I have done a lot of programming in Java and I am tired of writing so many classes. One of the things I like the most about TypeScript/Javascript is the ability to use object literals. And with TypeScript type definitions, you can now apply some safety to using object literals.
就我个人而言,我喜欢函数版本,因为我已经用Java完成了许多编程工作,并且我厌倦了编写这么多类。 我最喜欢TypeScript / Javascript的一件事是使用对象文字的能力。 通过TypeScript类型定义,您现在可以在使用对象文字时应用一些安全措施。
Another take away is that you don’t need to rigidly conform to the clean architecture as diagrammed above to achieve a decoupled system. For example, you could just as easily have your UI communicate directly with your use case layer bypassing the delivery layer if it’s not needed. All of these layers may physically live in different places and have different ways of communicating with each other.
另一个要解决的问题是,您无需严格遵循上述架构,即可实现分离的系统。 例如,如果不需要,您可以轻松地让UI绕过交付层直接与用例层进行通信。 所有这些层在物理上可能都生活在不同的地方,并且具有彼此通信的不同方式。
Here are some of the things I intend to enforce in my next project.
这是我打算在下一个项目中执行的一些操作。
Please ask questions and give feedback, there is no better way to learn than to get constructive criticism from your peers. And it is highly probable that I missed something somewhere.
请提出问题并提供反馈,没有比从同行那里获得建设性批评更好的学习方法了。 而且我很可能在某处错过了一些东西。
https://github.com/warrenbell/cleanarch-tsoo
https://github.com/warrenbell/cleanarch-tsoo
https://github.com/warrenbell/cleanarch-tsfun
https://github.com/warrenbell/cleanarch-ts有趣
Handy little TypeScript tool.
方便的小型TypeScript工具。
https://github.com/TypeStrong/ts-node
https://github.com/TypeStrong/ts-node
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164
https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164
They are all basically the same except for the the first 5 minutes where Uncle Bob likes to muse about something loosely related and then makes a hard segue into clean architecture.
除了前5分钟外,它们基本上都是相同的,前5分钟Bob叔叔喜欢思考一些松散相关的内容,然后艰难地研究出干净的建筑。
https://www.youtube.com/watch?v=Nltqi7ODZTM
https://www.youtube.com/watch?v=Nltqi7ODZTM
https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/
https://plainionist.github.io/Implementing-Clean-Architecture-Controller-Presenter/
https://herbertograca.com/2017/09/28/clean-architecture-standing-on-the-shoulders-of-giants/
https://herbertograca.com/2017/09/28/clean-architecture-standing-on-the-shoulders-of-giants/
https://softwareengineering.stackexchange.com/questions/357052/clean-architecture-use-case-containing-the-presenter-or-returning-data
https://softwareengineering.stackexchange.com/questions/357052/clean-architecture-use-case- contains-the-presenter-or-returning-data
https://stackoverflow.com/questions/46510550/clean-architecture-what-are-the-jobs-of-presenter
https://stackoverflow.com/questions/46510550/clean-architecture-what-are-the-jobs-of-presenter
翻译自: https://www.freecodecamp.org/news/a-typescript-stab-at-clean-architecture-b51fbb16a304/
typescript