我想与您分享一种我多年来一直在开发应用程序(包括Web应用程序)中使用的方法。许多桌面,服务器和移动应用程序开发人员都熟悉这种方法。虽然在构建此类应用程序时是很重要的,但是在Web上它的表现却很差,尽管肯定有人愿意使用这种方法。此外,VS Code编辑器就是以此方式编写的。
应用此方法的结果是,您将摆脱特定的框架。您可以轻松地在应用程序内部切换表示库,例如React,Preact,Vue,Mithril,而无需重写业务逻辑,在大多数情况下甚至无需查看。如果您在Angular 1上有一个应用程序,则可以轻松地将其转换为Angular 2 +,React,Svelte,WebComponents甚至是您的演示文稿库。如果您在Angular 2+上有一个应用程序,但是没有专家,那么您可以轻松地将该应用程序转移到更流行的库中,而无需重写业务逻辑。但是最后,完全忘记了从框架迁移到框架的问题。这是什么魔术?什么是清洁建筑
为了理解这一点,最好阅读Martin Robert“ Clean Architecture»(由Robert C.Martin” Clean Architecture»)一书。文章的简要摘录供参考。嵌入在体系结构中的主要思想:- 独立于框架。体系结构不依赖于任何库的存在。这使您可以将框架用作工具,而不是将系统挤入限制。
- 可测试性。可以在没有用户界面,数据库,Web服务器或任何其他外部组件的情况下测试业务规则。
- 独立于用户界面。用户界面可以轻松更改,而无需更改系统的其余部分。例如,可以在不更改业务规则的情况下用控制台替换Web界面。
- 独立于数据库。您可以将Oracle或SQL Server交换为MongoDB,BigTable,CouchDB或其他内容。您的业务规则与数据库无关。
- 独立于任何外部服务。实际上,您的业务规则根本不了解外界。
多年来描述的思想一直是在各个领域中构建复杂应用程序的基础。通过将应用程序划分为服务,存储库,模型层来实现这种灵活性。我将MVC方法添加到Clean Architecture中,并得到以下几层:- 视图 -向客户端显示数据,实际上向客户端可视化逻辑状态。
- 控制器 -负责通过IO(输入输出)与用户进行交互。
- 服务 -负责业务逻辑及其在组件之间的重用。
- 储存库 -负责从外部资源(例如数据库,api,本地存储等)接收数据。
- 模型 -负责在层和系统之间传输数据以及处理该数据的逻辑。
每层的目的在下面讨论。谁是纯建筑
从简单的jquery脚本编写到开发大型SPA应用程序,Web开发已经走了很长一段路。现在,Web应用程序已经变得如此之大,以至于业务逻辑的数量已经可以与服务器,台式机和移动应用程序相比甚至更高。对于编写复杂的大型应用程序以及将业务逻辑从服务器传输到Web应用程序以节省服务器成本的开发人员而言,Clean Architecture将帮助组织代码并无问题地大规模扩展。同时,如果您的任务只是登录页面的布局和动画,那么Clean Architecture根本无处可插。如果您的业务逻辑位于后端,并且您的任务是获取数据,将其显示给客户端并处理单击按钮,那么您将不会感到Clean Architecture的灵活性,但是它可以成为应用程序爆炸性增长的绝佳跳板。哪里已经申请?
纯粹的体系结构不与任何特定的框架,平台或编程语言绑定。几十年来,它一直被用来编写桌面应用程序。它的参考实现可以在服务器应用程序Asp.Net Core,Java Spring和NestJS的框架中找到。在编写iO和Android应用程序时,它也非常受欢迎。但是在Web开发中,他以极不成功的形式出现在Angular框架中。由于我自己不仅是Typescript,而且还是C#开发人员,因此,我将为Asp.Net Core采用此体系结构的参考实现。这是一个简化的示例应用程序:Asp.Net Core上的示例应用程序
@model WebApplication1.Controllers.Profile
<div class="text-center">
<h1> @Model.FirstName</h1>
</div>
/**
* Controller
*/
public class IndexController : Controller
{
private static int _counter = 0;
private readonly IUserProfileService _userProfileService;
public IndexController(IUserProfileService userProfileService)
{
_userProfileService = userProfileService;
}
public async Task<IActionResult> Index()
{
var profile = await this._userProfileService.GetProfile(_counter);
return View("Index", profile);
}
public async Task<IActionResult> AddCounter()
{
_counter += 1;
var profile = await this._userProfileService.GetProfile(_counter);
return View("Index", profile);
}
}
public interface IUserProfileService
{
Task<Profile> GetProfile(long id);
}
public class UserProfileService : IUserProfileService
{
private readonly IUserProfileRepository _userProfileRepository;
public UserProfileService(IUserProfileRepository userProfileRepository)
{
this._userProfileRepository = userProfileRepository;
}
public async Task<Profile> GetProfile(long id)
{
return await this._userProfileRepository.GetProfile(id);
}
}
public interface IUserProfileRepository
{
Task<Profile> GetProfile(long id);
}
public class UserProfileRepository : IUserProfileRepository
{
private readonly DBContext _dbContext;
public UserProfileRepository(DBContext dbContext)
{
this._dbContext = dbContext;
}
public async Task<Profile> GetProfile(long id)
{
return await this._dbContext
.Set<Profile>()
.FirstOrDefaultAsync((entity) => entity.Id.Equals(id));
}
}
public class Profile
{
public long Id { get; set; }
public string FirstName { get; set; }
public string Birthdate { get; set; }
}
如果您不理解它没什么不好的,那么我们将对每个片段进行部分分析。给出了一个Asp.Net Core应用程序的示例,但对于Java Spring,WinForms,Android,React,其体系结构和代码将相同,只是语言和与视图一起使用(如果有)会发生变化。Web应用程序
尝试使用Clean Architecture的唯一框架是Angular。但是事实证明,在1,在2+中是可怕的。这有很多原因:- 有角度的整体框架。这是他的主要问题。如果您不喜欢其中的某些内容,则必须每天对其进行cho塞,对此您无能为力。它不仅存在很多问题,而且还与纯建筑的意识形态相矛盾。
- DI. , Javascript.
- . JSX. , 6, . .
- . Rollup ES2015 2 4 , angular 9.
- 还有更多问题。通常,相对于React,最多的现代技术滚动会延迟5年。
但是其他框架呢?React,Vue,Preact,Mithril和其他仅是表示库,不提供任何体系结构……而且我们已经有了体系结构……将所有东西组装成一个整体是必要的!我们开始创建一个应用程序
我们将以虚拟应用程序为例来考虑Pure Architecture,该应用程序应尽可能接近真实的Web应用程序。这是保险公司的办公室,显示用户资料,被保险事件,建议的保险费率和用于处理此数据的工具。
在该示例中,仅会实现一小部分功能,但是从中您可以了解其余功能的位置和放置方式。让我们从Controller层开始创建应用程序,并在最后连接View层。在创建过程中,我们会更详细地考虑每个层。控制器模式
控制器 -负责用户与应用程序的交互。这可以是单击网页,桌面应用程序,移动应用程序上的按钮,或者在Linux控制台中输入命令,或者是网络请求或任何其他进入该应用程序的IO事件。干净架构中最简单的控制器如下:export class SimpleController {
public todos: string[] = [];
public addTodo(todo: string): void {
this.todos.push(todo);
}
public removeTodo(index: number): void {
this.todos.splice(index, 1);
}
}
它的任务是接收来自用户的事件并启动业务流程。在理想情况下,Controller不了解View,因此可以在Web,React-Native或Electron等平台之间重用。现在让我们为应用程序编写一个控制器。其任务是获取用户个人资料,可用的资费并向用户提供最佳资费:UserPageController。具有业务逻辑的控制器export class UserPageControlle {
public userProfile: any = {};
public insuranceCases: any[] = [];
public tariffs: any[] = [];
public bestTariff: any = {};
constructor() {
this.activate();
}
public activate(): void {
this.requestUserProfile();
this.requestTariffs();
}
public async requestUserProfile(): Promise<void> {
try {
const response = await fetch("./api/user-profile");
this.userProfile = await response.json();
this.findBestTariff();
} catch (e) {
console.error(e);
}
}
public async requestTariffs(): Promise<void> {
try {
const response = await fetch("./api/tariffs");
this.tariffs = await response.json();
this.findBestTariff();
} catch (e) {
console.error(e);
}
}
public findBestTariff(): void {
if (this.userProfile && this.tariffs) {
this.bestTariff = this.tariffs.find((tarif: any) => {
return tarif.ageFrom <= this.userProfile.age && this.userProfile.age < tarif.ageTo;
});
}
}
}
我们得到了一个没有干净架构的常规控制器,如果我们从React.Component继承它,我们将获得一个具有逻辑的工作组件。许多Web应用程序开发人员都在编写,但是这种方法有很多明显的缺点。主要的原因是无法在组件之间重用逻辑。毕竟,建议的费率不仅可以显示在您的个人帐户中,还可以显示在着陆页和许多其他地方,以吸引客户使用该服务。为了能够在组件之间重用逻辑,必须将其放置在称为服务的特殊层中。服务模式
服务 -负责应用程序的整个业务逻辑。如果控制器需要接收,处理和发送一些数据,则它通过服务来完成。如果多个控制器需要相同的逻辑,则它们可与服务一起使用。但是服务层本身不应该对控制器和视图层及其工作环境一无所知。让我们将逻辑从控制器移动到服务,并在控制器中实现服务:UserPageController。没有业务逻辑的控制器import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
export class UserPageController {
public userProfile: any = {};
public insuranceCases: any[] = [];
public tariffs: any[] = [];
public bestTariff: any = {};
private readonly userProfilService: UserProfilService = new UserProfilService();
private readonly tarifService: TariffService = new TariffService();
constructor() {
this.activate();
}
public activate(): void {
this.requestUserProfile();
this.requestTariffs();
}
public async requestUserProfile(): Promise<void> {
try {
this.userProfile = await this.userProfilService.getUserProfile();
this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
} catch (e) {
console.error(e);
}
}
public async requestTariffs(): Promise<void> {
try {
this.tariffs = await this.tarifService.getTariffs();
} catch (e) {
console.error(e);
}
}
}
UserProfilService。使用用户个人资料的服务export class UserProfilService {
public async getUserProfile(): Promise<any> {
const response = await fetch("./api/user-profile");
return await response.json();
}
}
TariffService。收费服务export class TariffService {
public async getTariffs(): Promise<any> {
const response = await fetch("./api/tariffs");
return await response.json();
}
public async findBestTariff(userProfile: any): Promise<any> {
const tariffs = await this.getTariffs();
return tariffs.find((tarif: any) => {
return tarif.ageFrom <= userProfile.age &&
userProfile.age < tarif.ageTo;
});
}
}
现在,如果多个控制器需要获取用户资料或资费,则它们可以重用服务中的相同逻辑。在服务中,最主要的是不要忘记SOLID原则,并且每个服务都应负责其责任范围。在这种情况下,一个服务负责使用用户个人资料,而另一服务负责使用费率。但是,如果数据源发生更改,例如,获取可能更改为websocket或grps或数据库,而实际数据需要替换为测试数据怎么办?通常,为什么业务逻辑需要了解有关数据源的某些知识?为了解决这些问题,有一个存储库层。储存库模式
存储库 -负责与数据仓库的通信。该存储可以是服务器,数据库,内存,本地存储,会话存储或任何其他存储。它的任务是从特定的存储实现中抽象服务层。让我们从存储库中的服务发出网络请求,而控制器不变:UserProfilService。使用用户个人资料的服务import { UserProfilRepository } from "./UserProfilRepository";
export class UserProfilService {
private readonly userProfilRepository: UserProfilRepository =
new UserProfilRepository();
public async getUserProfile(): Promise<any> {
return await this.userProfilRepository.getUserProfile();
}
}
UserProfilRepository。用于处理用户个人资料存储的服务export class UserProfilRepository {
public async getUserProfile(): Promise<any> {
const response = await fetch("./api/user-profile");
return await response.json();
}
}
TariffService。收费服务import { TariffRepository } from "./TariffRepository";
export class TariffService {
private readonly tarifRepository: TariffRepository = new TariffRepository();
public async getTariffs(): Promise<any> {
return await this.tarifRepository.getTariffs();
}
public async findBestTariff(userProfile: any): Promise<any> {
const tariffs = await this.tarifRepository.getTariffs();
return tariffs.find((tarif: any) => {
return tarif.ageFrom <= userProfile.age &&
userProfile.age < tarif.ageTo;
});
}
}
TariffRepository。用于资费存储的存储库export class TariffRepository {
public async getTariffs(): Promise<any> {
const response = await fetch("./api/tariffs");
return await response.json();
}
}
现在,只需编写一次数据请求就足够了,任何服务都可以重用该请求。稍后,我们将看一个示例,该示例说明如何在不触及服务代码的情况下重新定义存储库,并实现用于测试的mocha存储库。在UserProfilService服务中,似乎不需要它,并且控制器可以直接访问存储库中的数据,但事实并非如此。在任何时候,需求都可能出现在业务层中或在业务层中发生变化,可能需要其他请求,或者可能会丰富数据。因此,即使服务层中没有逻辑,也必须保留Controller-Service-Repository链。这是对您明天的贡献。现在该弄清楚要设置哪种存储库,它们是否正确。模型层对此负责。型号:DTO,实体,ViewModels
模型 -负责描述应用程序所使用的结构。这样的描述极大地帮助了新项目开发人员了解应用程序正在使用什么。此外,将其用于构建数据库或验证存储在模型中的数据非常方便。根据使用类型,模型分为不同的模式:- 实体 -负责使用数据库,并且是在数据库中重复表或文档的结构。
- DTO(数据传输对象) -用于在应用程序的不同层之间传输数据。
- ViewModel-包含在视图中显示所需的预先准备的信息。
将用户配置文件模型和其他模型添加到应用程序中,并让其他层知道现在我们不在使用抽象对象,而是在使用非常特定的配置文件:UserPageController。代替任何模型,使用所描述的模型。import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
export class UserPageController {
public userProfile: UserProfileDto = new UserProfileDto();
public insuranceCases: InsuranceCaseDto[] = [];
public tariffs: TariffDto[] = [];
public bestTariff: TariffDto | void = void 0;
private readonly userProfilService: UserProfilService = new UserProfilService();
private readonly tarifService: TariffService = new TariffService();
constructor() {
this.activate();
}
public activate(): void {
this.requestUserProfile();
this.requestTariffs();
}
public async requestUserProfile(): Promise<void> {
try {
this.userProfile = await this.userProfilService.getUserProfile();
this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
} catch (e) {
console.error(e);
}
}
public async requestTariffs(): Promise<void> {
try {
this.tariffs = await this.tarifService.getTariffs();
} catch (e) {
console.error(e);
}
}
}
UserProfilService。而不是任何,指定返回的模型import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";
export class UserProfilService {
private readonly userProfilRepository: UserProfilRepository =
new UserProfilRepository();
public async getUserProfile(): Promise<UserProfileDto> {
return await this.userProfilRepository.getUserProfile();
}
}
TariffService。而不是任何,指定返回的模型import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";
export class TariffService {
private readonly tarifRepository: TariffRepository = new TariffRepository();
public async getTariffs(): Promise<TariffDto[]> {
return await this.tarifRepository.requestTariffs();
}
public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
const tariffs = await this.tarifRepository.requestTariffs();
return tariffs.find((tarif: TariffDto) => {
const age = userProfile.getAge();
return age &&
tarif.ageFrom <= age &&
age < tarif.ageTo;
});
}
}
UserProfilRepository。而不是任何,指定返回的模型import { UserProfileDto } from "./UserProfileDto";
export class UserProfilRepository {
public async getUserProfile(): Promise<UserProfileDto> {
const response = await fetch("./api/user-profile");
return await response.json();
}
}
TariffRepository。而不是任何,指定返回的模型import { TariffDto } from "./TariffDto";
export class TariffRepository {
public async requestTariffs(): Promise<TariffDto[]> {
const response = await fetch("./api/tariffs");
return await response.json();
}
}
UserProfileDto。描述我们处理的数据的模型export class UserProfileDto {
public firstName: string | null = null;
public lastName: string | null = null;
public birthdate: Date | null = null;
public getAge(): number | null {
if (this.birthdate) {
const ageDifMs = Date.now() - this.birthdate.getTime();
const ageDate = new Date(ageDifMs);
return Math.abs(ageDate.getUTCFullYear() - 1970);
}
return null;
}
public getFullname(): string | null {
return [
this.firstName ?? "",
this.lastName ?? ""
]
.join(" ")
.trim() || null;
}
}
TariffDto。描述我们处理的数据的模型export class TariffDto {
public ageFrom: number = 0;
public ageTo: number = 0;
public price: number = 0;
}
现在,无论我们位于应用程序的哪一层,我们都知道确切的数据。同样,由于模型的描述,我们在我们的服务中发现了一个错误。在服务逻辑中,使用了userProfile.age属性,该属性实际上不存在,但是具有出生日期。并且要计算年龄,必须调用userProfile.getAge()模型方法。但是有一个问题。如果尝试使用当前存储库提供的模型中的方法,则会得到异常。问题是response.json()和JSON.parse()方法它返回的不是我们的模型,而是一个JSON对象,该对象与我们的模型没有任何关联。如果执行UserProfileDto的命令userProfile instanceof,则会得到一条错误的语句,您可以对此进行验证。为了将从外部源接收的数据转换为所描述的模型,存在数据反序列化的过程。数据反序列化
反序列化 -从字节序列恢复必要结构的过程。如果数据包含模型中未指定的信息,它将被忽略。如果数据中的信息与模型描述相矛盾,则会发生反序列化错误。最有趣的是,在设计ES2015并添加class关键字时,他们忘记了添加反序列化...在所有语言中都是开箱即用的事实,在ES2015中他们根本就忘记了...为了解决这个问题,我为TS-Serializable反序列化编写了一个库,有关该问题的文章可以在此链接上阅读。目的是返回丢失的功能。在模型中添加反序列化支持,并将反序列化本身添加到存储库:TariffRepository。添加反序列化过程import { UserProfileDto } from "./UserProfileDto";
export class UserProfilRepository {
public async getUserProfile(): Promise<UserProfileDto> {
const response = await fetch("./api/user-profile");
const object = await response.json();
return new UserProfileDto().fromJSON(object);
}
}
TariffRepository。添加反序列化过程import { TariffDto } from "./TariffDto";
export class TariffRepository {
public async requestTariffs(): Promise<TariffDto[]> {
const response = await fetch("./api/tariffs");
const objects: object[] = await response.json();
return objects.map((object: object) => {
return new TariffDto().fromJSON(object);
});
}
}
ProfileDto。添加反序列化支持import { Serializable, jsonProperty } from "ts-serializable";
export class UserProfileDto extends Serializable {
@jsonProperty(String, null)
public firstName: string | null = null;
@jsonProperty(String, null)
public lastName: string | null = null;
@jsonProperty(Date, null)
public birthdate: Date | null = null;
public getAge(): number | null {
if (this.birthdate) {
const ageDifMs = Date.now() - this.birthdate.getTime();
const ageDate = new Date(ageDifMs);
return Math.abs(ageDate.getUTCFullYear() - 1970);
}
return null;
}
public getFullname(): string | null {
return [
this.firstName ?? "",
this.lastName ?? ""
]
.join(" ")
.trim() || null;
}
}
TariffDto。添加反序列化支持import { Serializable, jsonProperty } from "ts-serializable";
export class TariffDto extends Serializable {
@jsonProperty(Number, null)
public ageFrom: number = 0;
@jsonProperty(Number, null)
public ageTo: number = 0;
@jsonProperty(Number, null)
public price: number = 0;
}
现在,在应用程序的所有层中,您都可以绝对确定我们正在使用期望的模型。在视图,控制器和其他层中,可以调用描述的模型的方法。什么是Serializable和jsonProperty?Serializable — Ecmascript Typescript. jsonProperty , Typescript uniontypes. .
现在我们已经完成了申请。现在该测试在Controller,Service和Models层中编写的逻辑了。为此,我们需要在存储库层中返回专门准备的测试数据,而不是向服务器发送真实请求。但是如何在不触及生产代码的情况下替换存储库。有一个依赖注入模式。依赖注入-依赖注入
依赖注入 -将依赖注入到Contoller,Service,Repository层,并允许您在这些层之外覆盖这些依赖。在程序中,Controller层取决于Service层,并且取决于Repository层。在当前形式中,层本身通过实例化导致其依赖性。为了重新定义依赖性,该层需要从外部设置这种依赖性。有很多方法可以做到这一点,但是最流行的是在构造函数中将依赖项作为参数传递。然后创建一个具有所有依赖项的程序,如下所示:var programm = new IndexPageController(new ProfileService(new ProfileRepository()));
同意-看起来糟透了。即使考虑到程序中只有两个依赖关系,它看起来也很糟糕。关于具有成千上万个依赖项的程序该说些什么。要解决该问题,您需要一个特殊的工具,但是为此您需要找到它。如果我们转向其他平台(例如Asp.Net Core)的经验,那么依赖项的注册就发生在程序的初始化阶段,看起来像这样:DI.register(IProfileService,ProfileService);
然后,在创建控制器时,框架本身将创建并实现此依赖关系。但是存在三个重大问题:- 在Javascript中编译Typescript时,接口中没有任何痕迹。
- 属于经典DI的所有内容都将永远保留在其中。在重构期间很难清理它。在Web应用程序中,您需要保存每个字节。
- 几乎所有视图库都不使用DI,并且控制器设计人员正忙于处理参数。
在Web应用程序中,DI仅在Angular 2+中使用。在Angular 1中,当注册依赖项时,使用一个字符串代替接口;在InversifyJS中,使用Symbol代替接口。而且所有这些操作的实现都非常糟糕,因此,与本解决方案的第一个示例一样,最好有很多新功能。为了解决这三个问题,我发明了自己的DI,该解决方案帮助我找到了Java Spring框架及其自动装配器。在链接的文章和GitHub存储库中可以找到有关此DI如何工作的描述。现在是时候在我们的应用程序中应用生成的DI了。放在一起
为了在所有层上实现DI,我们将添加一个反射装饰器,这将使Typescript生成有关依赖项类型的其他元信息。在需要调用依赖项的控制器中,我们将挂起自动装配的装饰器。在程序初始化的地方,我们确定将在哪个环境中实现哪个依赖项。对于UserProfilRepository存储库,创建相同的存储库,但使用测试数据而不是实际请求。结果,我们得到以下代码:主站 程序初始化位置import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
if (process.env.NODE_ENV === "test") {
override(UserProfilRepository, MockUserProfilRepository);
}
UserPageController。通过自动连线装饰器的依赖import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
export class UserPageController {
public userProfile: UserProfileDto = new UserProfileDto();
public insuranceCases: InsuranceCaseDto[] = [];
public tariffs: TariffDto[] = [];
public bestTariff: TariffDto | void = void 0;
@autowired()
private readonly userProfilService!: UserProfilService;
@autowired()
private readonly tarifService!: TariffService;
constructor() {
this.activate();
}
public activate(): void {
this.requestUserProfile();
this.requestTariffs();
}
public async requestUserProfile(): Promise<void> {
try {
this.userProfile = await this.userProfilService.getUserProfile();
this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
} catch (e) {
console.error(e);
}
}
public async requestTariffs(): Promise<void> {
try {
this.tariffs = await this.tarifService.getTariffs();
} catch (e) {
console.error(e);
}
}
}
UserProfilService。介绍反射和依赖项生成import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class UserProfilService {
private readonly userProfilRepository: UserProfilRepository;
constructor(userProfilRepository: UserProfilRepository) {
this.userProfilRepository = userProfilRepository;
}
public async getUserProfile(): Promise<UserProfileDto> {
return await this.userProfilRepository.getUserProfile();
}
}
TariffService。介绍反射和依赖项生成import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class TariffService {
private readonly tarifRepository: TariffRepository;
constructor(tarifRepository: TariffRepository) {
this.tarifRepository = tarifRepository;
}
public async getTariffs(): Promise<TariffDto[]> {
return await this.tarifRepository.requestTariffs();
}
public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
const tariffs = await this.tarifRepository.requestTariffs();
return tariffs.find((tarif: TariffDto) => {
const age = userProfile.getAge();
return age &&
tarif.ageFrom <= age &&
age < tarif.ageTo;
});
}
}
UserProfilRepository。介绍反射生成import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class UserProfilRepository {
public async getUserProfile(): Promise<UserProfileDto> {
const response = await fetch("./api/user-profile");
const object = await response.json();
return new UserProfileDto().fromJSON(object);
}
}
MockUserProfilRepository。新的测试库import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class MockUserProfilRepository {
public async getUserProfile(): Promise<UserProfileDto> {
const profile = new UserProfileDto();
profile.firstName = "";
profile.lastName = "";
profile.birthdate = new Date(Date.now() - 1.5e12);
return Promise.resolve(profile);
}
}
TariffRepository。介绍反射生成import { TariffDto } from "./TariffDto";
import { reflection } from "first-di";
@reflection
export class TariffRepository {
public async requestTariffs(): Promise<TariffDto[]> {
const response = await fetch("./api/tariffs");
const objects: object[] = await response.json();
return objects.map((object: object) => {
return new TariffDto().fromJSON(object);
});
}
}
现在,程序中的任何地方都有机会更改任何逻辑的实现。在我们的示例中,将使用测试数据,而不是在测试环境中向服务器发送真实的用户配置文件请求。在现实生活中,可以在任何地方找到替换项,例如,您可以更改服务中的逻辑,在生产中实现旧服务,并且在重构中它已经是新的。使用业务逻辑进行A / B测试,将基于文档的数据库更改为关系数据库,并且通常将网络请求更改为Web套接字。所有这一切都无需停止开发来重写解决方案。现在是时候查看程序的结果了。为此有一个View层。实施视图
视图层负责向用户显示控制器层中包含的数据。在示例中,我将为此使用React,但是可以使用其他任何位置,例如Preact,Svelte,Vue,Mithril,WebComponent或任何其他位置。为此,只需从React.Component继承我们的控制器,然后向其添加一个带有视图表示的render方法:主站 开始绘制一个React组件import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
import { UserPageController } from "./UserPageController";
import React from "react";
import { render } from "react-dom";
if (process.env.NODE_ENV === "test") {
override(UserProfilRepository, MockUserProfilRepository);
}
render(React.createElement(UserPageController), document.body);
UserPageController。从React.Component继承并添加一个render方法import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import React from "react";
export class UserPageController extends React.Component<object, object> {
public userProfile: UserProfileDto = new UserProfileDto();
public insuranceCases: InsuranceCaseDto[] = [];
public tariffs: TariffDto[] = [];
public bestTariff: TariffDto | void = void 0;
@autowired()
private readonly userProfilService!: UserProfilService;
@autowired()
private readonly tarifService!: TariffService;
constructor(props: object, context: object) {
super(props, context);
}
public componentDidMount(): void {
this.activate();
}
public activate(): void {
this.requestUserProfile();
this.requestTariffs();
}
public async requestUserProfile(): Promise<void> {
try {
this.userProfile = await this.userProfilService.getUserProfile();
this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
this.forceUpdate();
} catch (e) {
console.error(e);
}
}
public async requestTariffs(): Promise<void> {
try {
this.tariffs = await this.tarifService.getTariffs();
this.forceUpdate();
} catch (e) {
console.error(e);
}
}
public render(): JSX.Element {
return (
<>
<div className="user">
<div className="user-name">
: {this.userProfile.getFullname()}
</div>
<div className="user-age">
: {this.userProfile.getAge()}
</div>
</div>
<div className="tarifs">
{}
</div>
</>
);
}
}
通过仅添加两行代码和一个演示模板,我们的控制器变成了具有工作逻辑的React组件。为什么要调用forceUpdate而不是setState?forceUpdate , setState. setState, Redux, mobX ., . React Preact forceUpdate, ChangeDetectorRef.detectChanges(), Mithril redraw . Observable , MobX Angular, . .
但是即使在这样的实现中,事实证明我们还是将React库与它的生命周期,视图实现以及使视图无效的原理联系在一起,这与Clean Architecture的概念相矛盾。而且逻辑和布局在同一个文件中,这使排版机和开发人员的并行工作变得复杂。控制器与视图分离
为了解决这两个问题,我们将视图层放入一个单独的文件中,而不是使用React组件,而是制作了一个基本组件,该组件将从特定的表示库中抽象出我们的控制器。同时,我们描述了组件可以采用的属性。我们得到以下变化:UserPageView。他们将视图放到一个单独的文件中import { UserPageOptions, UserPageController } from "./UserPageController";
import React from "react";
export const userPageView = <P extends UserPageOptions, S>(
ctrl: UserPageController<P, S>,
props: P
): JSX.Element => (
<>
<div className="user">
<div className="user-name">
: {ctrl.userProfile.getFullname()}
</div>
<div className="user-age">
: {ctrl.userProfile.getAge()}
</div>
</div>
<div className="tarifs">
{}
</div>
</>
);
UserPageOptions。查看并反应到一个单独的文件import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import { BaseComponent } from "./BaseComponent";
import { userPageView } from "./UserPageview";
export interface UserPageOptions {
param1?: number;
param2?: string;
}
export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {
public userProfile: UserProfileDto = new UserProfileDto();
public insuranceCases: InsuranceCaseDto[] = [];
public tariffs: TariffDto[] = [];
public bestTariff: TariffDto | void = void 0;
public readonly view = userPageView;
@autowired()
private readonly userProfilService!: UserProfilService;
@autowired()
private readonly tarifService!: TariffService;
constructor(props: P, context: S) {
super(props, context);
}
public activate(): void {
this.requestUserProfile();
this.requestTariffs();
}
public async requestUserProfile(): Promise<void> {
try {
this.userProfile = await this.userProfilService.getUserProfile();
this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
this.forceUpdate();
} catch (e) {
console.error(e);
}
}
public async requestTariffs(): Promise<void> {
try {
this.tariffs = await this.tarifService.getTariffs();
this.forceUpdate();
} catch (e) {
console.error(e);
}
}
}
基本组件 使我们从特定框架中抽象出来的组件import React from "react";
export class BaseComponent<P, S> extends React.Component<P, S> {
public view?: (ctrl: this, props: P) => JSX.Element;
constructor(props: P, context: S) {
super(props, context);
}
public componentDidMount(): void {
this.activate && this.activate();
}
public shouldComponentUpdate(
nextProps: Readonly<P>,
nextState: Readonly<S>,
nextContext: any
): boolean {
return this.update(nextProps, nextState, nextContext);
}
public componentWillUnmount(): void {
this.dispose();
}
public activate(): void {
}
public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
return false;
}
public dispose(): void {
}
public render(): React.ReactElement<object> {
if (this.view) {
return this.view(this, this.props);
} else {
return React.createElement("div", {}, " ");
}
}
}
现在我们的视图在一个单独的文件中,并且控制器对此一无所知,除了它是。也许您不应该通过controller属性注入视图,而是像Angular一样通过装饰器注入视图,但这是一些思考的话题。基本组件还包含对框架生命周期的抽象。它们在所有框架中都不同,但是在所有框架中都相同。 Angular是ngOnInit,ngOnChanges,ngOnDestroy。在React和Preact中,这是componentDidMount,shouldComponentUpdate,componentWillUnmount。在Vue中,它是创建,更新,销毁的。在秘银中,它是oncreate,onupdate,onremove。在WebComponents中,这是connectedCallback,attributeChangedCallback,disconnectedCallback。在每个图书馆中都是如此。大多数甚至具有相同或相似的界面。另外,现在可以使用它们自己的逻辑扩展库组件,以在所有组件之间进行后续重用。例如,引入用于分析,监视,日志记录等的工具。看结果
剩下的只是评估发生了什么。整个程序具有以下最终形式:主站 从中启动程序的文件import { override } from "first-di";
import { UserProfilRepository } from "./UserProfilRepository";
import { MockUserProfilRepository } from "./MockUserProfilRepository";
import { UserPageController } from "./UserPageController";
import React from "react";
import { render } from "react-dom";
if (process.env.NODE_ENV === "test") {
override(UserProfilRepository, MockUserProfilRepository);
}
render(React.createElement(UserPageController), document.body);
UserPageView。表示程序组件之一。import { UserPageOptions, UserPageController } from "./UserPageController";
import React from "react";
export const userPageView = <P extends UserPageOptions, S>(
ctrl: UserPageController<P, S>,
props: P
): JSX.Element => (
<>
<div className="user">
<div className="user-name">
: {ctrl.userProfile.getFullname()}
</div>
<div className="user-age">
: {ctrl.userProfile.getAge()}
</div>
</div>
<div className="tarifs">
{}
</div>
</>
);
UserPageController。用户交互的组成部分之一的逻辑import { UserProfilService } from "./UserProfilService";
import { TariffService } from "./TariffService";
import { UserProfileDto } from "./UserProfileDto";
import { TariffDto } from "./TariffDto";
import { InsuranceCaseDto } from "./InsuranceCasesDto";
import { autowired } from "first-di";
import { BaseComponent } from "./BaseComponent";
import { userPageView } from "./UserPageview";
export interface UserPageOptions {
param1?: number;
param2?: string;
}
export class UserPageController<P extends UserPageOptions, S> extends BaseComponent<P, S> {
public userProfile: UserProfileDto = new UserProfileDto();
public insuranceCases: InsuranceCaseDto[] = [];
public tariffs: TariffDto[] = [];
public bestTariff: TariffDto | void = void 0;
public readonly view = userPageView;
@autowired()
private readonly userProfilService!: UserProfilService;
@autowired()
private readonly tarifService!: TariffService;
public activate(): void {
this.requestUserProfile();
this.requestTariffs();
}
public async requestUserProfile(): Promise<void> {
try {
this.userProfile = await this.userProfilService.getUserProfile();
this.bestTariff = await this.tarifService.findBestTariff(this.userProfile);
this.forceUpdate();
} catch (e) {
console.error(e);
}
}
public async requestTariffs(): Promise<void> {
try {
this.tariffs = await this.tarifService.getTariffs();
this.forceUpdate();
} catch (e) {
console.error(e);
}
}
}
基本组件 所有程序组件的基类import React from "react";
export class BaseComponent<P, S> extends React.Component<P, S> {
public view?: (ctrl: this, props: P) => JSX.Element;
constructor(props: P, context: S) {
super(props, context);
}
public componentDidMount(): void {
this.activate && this.activate();
}
public shouldComponentUpdate(
nextProps: Readonly<P>,
nextState: Readonly<S>,
nextContext: any
): boolean {
return this.update(nextProps, nextState, nextContext);
}
public componentWillUnmount(): void {
this.dispose();
}
public activate(): void {
}
public update(nextProps: Readonly<P>, nextState: Readonly<S>, nextContext: any): boolean {
return false;
}
public dispose(): void {
}
public render(): React.ReactElement<object> {
if (this.view) {
return this.view(this, this.props);
} else {
return React.createElement("div", {}, " ");
}
}
}
UserProfilService。用于重用组件之间的逻辑以使用用户概要文件的服务import { UserProfilRepository } from "./UserProfilRepository";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class UserProfilService {
private readonly userProfilRepository: UserProfilRepository;
constructor(userProfilRepository: UserProfilRepository) {
this.userProfilRepository = userProfilRepository;
}
public async getUserProfile(): Promise<UserProfileDto> {
return await this.userProfilRepository.getUserProfile();
}
}
TariffService。重用组件之间的逻辑以使用费率的服务import { TariffRepository } from "./TariffRepository";
import { TariffDto } from "./TariffDto";
import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class TariffService {
private readonly tarifRepository: TariffRepository;
constructor(tarifRepository: TariffRepository) {
this.tarifRepository = tarifRepository;
}
public async getTariffs(): Promise<TariffDto[]> {
return await this.tarifRepository.requestTariffs();
}
public async findBestTariff(userProfile: UserProfileDto): Promise<TariffDto | void> {
const tariffs = await this.tarifRepository.requestTariffs();
return tariffs.find((tarif: TariffDto) => {
const age = userProfile.getAge();
return age &&
tarif.ageFrom <= age &&
age < tarif.ageTo;
});
}
}
UserProfilRepository。用于从服务器接收配置文件,对其进行检查和验证的存储库import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class UserProfilRepository {
public async getUserProfile(): Promise<UserProfileDto> {
const response = await fetch("./api/user-profile");
const object = await response.json();
return new UserProfileDto().fromJSON(object);
}
}
MockUserProfilRepository.import { UserProfileDto } from "./UserProfileDto";
import { reflection } from "first-di";
@reflection
export class MockUserProfilRepository {
public async getUserProfile(): Promise<UserProfileDto> {
const profile = new UserProfileDto();
profile.firstName = "";
profile.lastName = "";
profile.birthdate = new Date(Date.now() - 1.5e12);
return Promise.resolve(profile);
}
}
TariffRepository. ,import { TariffDto } from "./TariffDto";
import { reflection } from "first-di";
@reflection
export class TariffRepository {
public async requestTariffs(): Promise<TariffDto[]> {
const response = await fetch("./api/tariffs");
const objects: object[] = await response.json();
return objects.map((object: object) => {
return new TariffDto().fromJSON(object);
});
}
}
UserProfileDto.import { Serializable, jsonProperty } from "ts-serializable";
export class UserProfileDto extends Serializable {
@jsonProperty(String, null)
public firstName: string | null = null;
@jsonProperty(String, null)
public lastName: string | null = null;
@jsonProperty(Date, null)
public birthdate: Date | null = null;
public getAge(): number | null {
if (this.birthdate) {
const ageDifMs = Date.now() - this.birthdate.getTime();
const ageDate = new Date(ageDifMs);
return Math.abs(ageDate.getUTCFullYear() - 1970);
}
return null;
}
public getFullname(): string | null {
return [
this.firstName ?? "",
this.lastName ?? ""
]
.join(" ")
.trim() || null;
}
}
TariffDto.import { Serializable, jsonProperty } from "ts-serializable";
export class TariffDto extends Serializable {
@jsonProperty(Number, null)
public ageFrom: number = 0;
@jsonProperty(Number, null)
public ageTo: number = 0;
@jsonProperty(Number, null)
public price: number = 0;
}
结果,我们得到了一个模块化的可扩展应用程序,该应用程序具有很少的样板(一个基本组件,每个类用于实现依赖关系的3行)和非常低的开销(实际上仅用于实现依赖关系,其他所有东西都是逻辑)。此外,我们不受任何演示库的约束。Angular 1死后,许多人开始在React中重写应用程序。当Angular 2的开发人员用尽时,许多公司开始因开发速度而受苦。当React再次死亡时,您将不得不重写与其框架和生态系统相关的解决方案。但是使用Chita Architecture,您可以忘记绑定框架。与Redux相比有什么优势?
为了理解差异,让我们看看应用程序增长时Redux的行为。
从图中可以看出,随着Redux应用程序的增长垂直扩展,Restores和Reducers的数量也会增加,并成为瓶颈。重建商店和找到合适的Reducer的开销开始超过有效负载。您可以通过简单的测试来检查中型应用程序的开销与有效负载的比率。let create = new Function([
"return {",
...new Array(100).fill(1).map((val, ind) => `param${ind}:${ind},`),
"}"
].join(""));
let obj1 = create();
console.time("store recreation time");
let obj2 = {
...obj1,
param100: 100 ** 2
}
console.timeEnd("store recreation time");
console.time("clear logic");
let val = 100 ** 2;
console.timeEnd("clear logic");
console.log(obj2, val);
重新创建100个属性中的Store所花费的时间是逻辑本身的8倍以上。如果有1000个元素,则已经增加了50倍。此外,一个用户动作可以生成整个动作链,很难捕获和调试这些动作的调用。您当然可以辩称,重新创建商店的0.04毫秒很小,并且不会减慢速度。但是0.04毫秒是在Core i7处理器上执行的一个操作。考虑到移动处理器的性能较弱以及单个用户动作可以产生数十个动作的事实,所有这些都导致以下事实:计算不适合16毫秒,并且会产生一种感觉,应用程序的运行速度变慢。让我们比较一下Clean Architecture应用程序的增长方式:
可以看出,由于各层之间逻辑和职责的分离,因此应用程序是水平扩展的。如果有任何组件需要处理数据,它将转向相应的服务,而不会涉及与其任务无关的服务。收到数据后,将仅重绘一个组件。数据处理链非常短且显而易见,最多可以跳转4个代码,您可以找到必要的逻辑并对其进行调试。另外,如果我们用砖墙作一个类比,那么我们可以从该墙壁上取下任何砖块,并用另一块砖块替换,而不会影响该墙的稳定性。奖励1:框架组件的增强功能
一个额外的好处是能够补充或更改表示库组件的行为。例如,如果进行了一些修改,则视图中发生错误时的反应不会呈现整个应用程序:export class BaseComponent<P, S> extends React.Component<P, S> {
...
public render(): React.ReactElement<object> {
try {
if (this.view) {
return this.view(this, this.props);
} else {
return React.createElement("div", {}, " ");
}
} catch (e) {
return React.createElement(
"div",
{ style: { color: "red" } },
` : ${e}`
);
}
}
}
现在,反应将不会仅绘制发生错误的组件。以相同的简单方式,您可以添加监视,分析和其他nishtyaki。奖励2:数据验证
除了描述数据和在各层之间传输数据之外,模型还有很大的机会。例如,如果连接class-validator库,则只需悬挂装饰器,就可以验证这些模型中的数据,包括 稍加改进,即可验证Web表单。奖励3:创建实体
另外,如果需要使用本地数据库,则可以连接typeorm库,并且您的模型将变成实体,通过它们可以生成和运行数据库。谢谢您的关注
如果您喜欢这篇文章或方法,请喜欢并且不要害怕尝试。如果您是Redux的拥护者,并且不喜欢异议,请在评论中说明如何扩展,测试和验证应用程序中的数据。