CRUD申请的问题
创建读取的升级或简短的CRUD方法非常易于实现,它是将图形用户界面(GUI)与状态Web应用程序中的后端集成在一起的主要方法。因此,后端实体仅对需要持续存在的域逻辑状态进行建模,而不是对适合生产有效状态变化的业务运营和业务规则进行建模。
在最好的情况下,服务操作将命名这些操作,并包含实施所需业务规则的逻辑。这些规则很容易从整个代码库中泄漏,并且在图形用户界面中也经常发现,现在通常以在浏览器中运行的单页Web应用程序实现。
结果,我们有了马丁·福勒(Martin Fowler)所谓的贫血领域模型的应用程序,以及一个难以理解和维护的纠结的泥球,因为责任没有明确分开。基于启发式方法,GUI对实体可以做什么并实现导航逻辑的假设与后端分开。
Hateoas和Richardson成熟模型
通过文献查找解决此问题的解决方案,您可能会遇到HTTP应用程序API的Richardson成熟度模型。它从普通的旧XML开始,这意味着XML内容已发布到Web服务端点。
资源的概念是在1级API中引入的,从而使后端实体的运行单独运行,从而将大型服务端点分解为多个资源。 2级API进一步使用特定的HTTP动词,例如put,删除或补丁来完善操作的含义。马丁·福勒(Martin Fowler)表示,它提供了一组标准的动词,因此我们可以以相同的方式处理类似的情况,从而消除了不必要的更改。在第3级,通过向每个响应添加上下文特定的超链接,将可发现性纳入了API,从而使客户可以了解下一个可以使用给定资源执行哪些操作,或者链接相关资源。在此级别上,与API交换的“超文本”充当应用状态的引擎,植根于后端并显示给客户端(在我们的情况下,Web Frontend)。休息的API和HAL标准如何帮助我们解决后端封装业务逻辑的最初问题?让我们看一个特定的例子。
示例区域:制造资源计划
假设我们正在建立一个制造资源计划系统(MRP),产品经理可以在其中准备和提交生产订单。然后,制造商可以接受订单,指定预期的交货日期,并在生产产品后完成订单。
此外,以下业务规则适用:
提交后,产品经理无法更改生产订单。当制造商接受生产订单时,他必须指定将来可以完成订单的日期。用春季和角度设置项目
正如乔什·朗(Josh Long)所告诉我们的那样,每个项目都应以https://START.SPRING.IO开始。实际上,该页面非常方便,使我们可以轻松地使用所需技术引导新应用程序。对于我们的情况,我们选择以下依赖性:
Spring Data JDBC: A direct OR mapper based on java database connection (JDBC) that saves us from JPA overhead, perfect for persistent DDD-style aggregation H2 database: a relational database written in Java that can run Rest Repositories out of the box: a Spring library that allows our aggregation to be published as REST resources Lombok: a bytecode generator that greatly reduces the amount样板代码和Java提供了一些现代语言,例如Kotlin或Typescriptspring Boot DevTools:开发依赖性,每当代码基础更改时,会自动重新启动应用程序聚合:在状态符合业务规则的情况下
域驱动应用程序的核心是域模型。它不受技术和集成的影响,并尽可能遵循业务模型和术语。因此,申请的状态被捕获在所谓的实体中,可以在其中分类相关实体以形成聚合。每个聚合都定义了应用程序内的一致性边界,这意味着只有明确定义的状态变化在聚合中进行交易。
为了保持专注,让我们从没有儿童实体的非常简单的生产订单模型开始,只有四个领域:
ID:区分不同生产订单的标识符。为简单起见,我们将其建模并将数据库初始化。该评论告诉Spring Data JDBC @ID是主要键。名称:生产订单的名称。提交后,该名称可能不会更改。 ExpecterCompletiondate:接受生产订单时制造商提供的日期,表明计划完成制造过程状态:根据现场模型的生产订单状态。它可以假定,将,提交,接受,完成的价值草稿建模为枚举。我们使用Lombok的注释@getter注释来生成字节码以使用Getter检测我们的(正常)聚合以从外部访问这些字段的值。
现在,我们如何确保域模型仅允许定义明确的状态过渡,而不是通过设定器显示所有字段?答案是:通过执行各自的业务运营。
当然,我们可以使用构造函数来创建我们的实体。但是,我更喜欢提供一种静态工厂方法,该方法使我们能够将适当的业务术语用作名称,而不是技术新的语句。创建方法将产品订单的名称作为单个参数,并初始化了草稿状态。当汇总持续到数据库时,ID字段将稍后由框架初始化。
初始创建方法:
bpackage/b com.example.demo.demo.productionorders; bimport/b java.time.localdate; bimport/b org.springframework.data.data.data.data.data.id; bimport/bimport/b lombok.getter; bimport; bimport; bimport; bimport/bimport/b lombok.val; bprivate/b字符串名称; bprivate/b localdate turectionCompletionDate; Bprivate/B Productorderstate State; bpublic/b bstatic/b productiondorder create(字符串名称){val result=bnew/b productordorder(); result.name=name; result.state=productionOderState.Draft; Breturn/B结果; } bpublic/b Enum Productorderstate {草稿,已提交,接受; }}存储库库和基本REST API
为了将我们的聚合持续到数据库并从那里检索,我们定义了一个接口,该接口扩展了Crudrepositoryspring数据的接口,以简单地进行。我们没有以其名称使用“存储库”,而是将其命名为一种持续无处不在的语言。
为了将我们的聚合持续到数据库并从那里检索,我们定义了一个接口,该接口扩展了Crudrepositoryspring数据的接口,以简单地进行。我们将其命名为“生产”的语言,而不是使用“存储库”,而是将其命名为无处不在的语言。
bpackage/b com.example.demo.productionorders;bimport/b org.springframework.data.repository.CrudRepository;bimport/b org.springframework.data.brest/b.core.annotation.RepositoryRestResource;@RepositoryRestResourcebpublic/b b interface/b ProductionOrders contexts/b crudrepositoryproductionder,长{}启动应用程序并通过curl命令行工具查询其API,我们会得到以下响应:
$ curl http:fonti //localhost:8080/api/i/i/fontfont {/fontfont\’_links\’/fontfont : {/fontfont\’productionorders\’/fontfont\’productionors\’/fontfont : {/fontfont : {/fontfont : /fontfont\’http://localhost:8080/api/productorders\’/fontfont},/fontfont\’profile\’/fontfont : {/fontfont\’href\’/fontfont\’href\’/fontfont\’/fontfont\’/fontfont :“ /i/fontfont}}}/font您是否注意到field_links?是的,Spring Data Rest默认情况下以HAL格式生成响应。它向我们展示了API提供了一个集合资源“生产商”,包括指向如何导航的链接。如果每个资源提供客户端需要导航到相关资源和呼叫操作所需的所有链接,那么我们可以得出以下结论。
客户只需要知道一个URL,即“/api”。在真正的休息API中,可以从API的响应中检索所有其他URL。
为了进一步说明这一概念,我们在演示申请类中创建并持续了一些生产订单,然后专注于生产货币资源的HREF-Property。
$ curl http:fonti //localHost:8080/api/productorders/i/fontfont {/fontfont\’_embedded\’/fontfont : {/fontfont\’productorders\’\’/fontfont\’\’ /fontfont\’Order 1\’/fontfont, /fontfont\’expectedCompletionDate\’/fontfont : bnull/b, /fontfont\’state\’/fontfont : /fontfont\’DRAFT\’/fontfont, /fontfont\’_links\’/fontfont : { /fontfont\’self\’/fontfont : {/fontfont\’href\’/fontfont :/fontfont\’http://localHost3:8080/api/producationorDers/1\’/fontfont},/fontfont\’productiondroder\’/fontfont\’productionder\’/fontfont\’/fontfont : /fontfont\’http://localhost:8080/api/productionOrders/1\’/fontfont } } }, { /fontfont\’name\’/fontfont : /fontfont\’Order 2\’/fontfont, /fontfont\’expectedCompletionDate\’/fontfont : bnull/b, /fontfont\’State\’ /fontfont : /fontfont\’draft\’ /fontfont, /fontfont\’_links\’ /fontfont : { /fontfont\’self\’ /fontfont\’ /fontfont : /fontfont\’http://localhost:8080/api/productorders/2\’/fontfont},/fontfont\’productiondorder\’/fontfont : {/fontfont\’href\’/fontfont\’href\’/fontfont\’/fontfont 333660 /fontfont\’http://localHost:8080/api/productorders/2\’/fontfont}}}}}}}}}}}}},//fontfont\’_links\’/fontfont : {//fontfont\’http://localhost:8080/api/productorders\’/fontfont},/fontfont\’profile\’/fontfont\’/fontfont : {/fontfont\’href\’/fontfont\’href\’/fontfont\’/fontfont 333660 /fontfont\’http://localhost:8080/api/profile/profienters\’/fontfont}}}}}}/font,我们看到返回了两个生产订单,其中包含在HAL规范定义的特殊字段的属性中。如果与资源的互动需要其他信息,例如下拉列表的值,以过滤特定状态的生产订单,则可以将此数据添加到响应的_embedded属性下的另一个字段中。
每个生产订单资源都提供一组链接,默认情况下这是非常微不足道的:一个自链接和生产顺序链接,都指向资源本身。在下一步中,我们现在将在生产订单类中添加业务运营,并将其称为Sitelinks的端点。
增加业务行为
如果我们回顾业务需求的轮廓,我们的汇总需要支持以下操作。
在选秀状态下,允许将订单提交订单的操作重命名,提供预期的交货日期。该实现遵循我们在创建方法中使用的方法。我们不提供Getters和Setter,而是实现具有与我们的现场语言相匹配的名称的方法:Renameto,提交,接受。
如上所述,聚集被视为一致性的边界。由于额外的方法不再是静态的,因此我们可以直接利用类的字段来执行所需的业务规则并仅允许定义明确的状态过渡。例如,我们可以完全确定我们将永远不会遇到未完成日期的公认生产订单,这是我们第二个业务规则所要求的。与基于Setter的方法相比,这有多大的区别!
总生产订单量中实施了三个业务运营,仅允许清晰的状态过渡。
bpackage/b com.example.demo.demo.productionorders; bimport/b java.time.localdate; bimport/b java.util.Objects; bimport/bimport/b org.springframework.data.data.data.data.annotation.annotation.ID; {@id bprivate/b长ID; bprivate/b字符串名称; bprivate/b localdate turectionCompletionDate; Bprivate/B Productorderstate State; bpublic/b bstatic/b productiondorder create(字符串名称){val result=bnew/b productordorder(); result.name=name; result.state=productionOderState.Draft; Break-B结果; } bpublic/b生产点renameto(字符串newname){bif/b(state!=productorderState.draft){bthrow/b bnew/b bnew/b IllegalstateException(font\’cannot in State\’/fontfont + state in State\’/font\’cannot Rennot Rename Production订单}名称=newname; Breturn/b this/b; } bpublic/b Productordorder submist(){bif/b(state!=productorDorderState.draft){bthrow/b bnew/b bnew/b illegalstateException(/fontfont\’cannot\’cannot\’cannot\’/fontfont + state in state\’/fontfont + state); } state=productionorderstate.submitter; Breturn/b this/b; } bpublic/b productorder convect(localdate endurectOmpletiondate){bif/b(state!=producationOderState.submittit){bthrow/b bnew/b bnew/b iLlegalstateException(/fontgtgt
;<font>\”Cannot accept production order in state \”</font><font> + state); } Objects.requireNonNull(expectedCompletionDate, </font><font>\”expectedCompletionDate is required to submit a production order\”</font><font>); <b>if</b> (expectedCompletionDate.isBefore(LocalDate.now())) { <b>throw</b> <b>new</b> IllegalArgumentException(</font><font>\”Expected completion date must be in the future, but was \”</font><font> + expectedCompletionDate); } state = ProductionOrderState.ACCEPTED; <b>this</b>.expectedCompletionDate = expectedCompletionDate; <b>return</b> <b>this</b>; } <b>public</b> enum ProductionOrderState { DRAFT, SUBMITTED, ACCEPTED; }} </font>在REST API中公开业务能力
作为下一步,我们现在要在REST API中公开业务操作。我们需要这样做的成分是。
为每个行动提供新的端点,其形式为/api/productionOrders/{id}/{action}。ProductionOrder资源的HAL表示中的链接仔细想想,如果我们只暴露一个指向端点的链接,如果相应的动作实际上是允许的,这不是很好吗,这取决于特定生产订单的状态?这可以通过以下方式轻松实现。
我们首先实现一个ProductionOrderController类,并在类的层面上将其映射到/api/productionOrders端点(如果你在映射中遇到麻烦,请参见bug https://github.com/spring-projects/spring-data-rest/issues/1342)。
这使得我们可以通过额外的方法来扩展Spring Data REST提供的标准API:rename, submit, accept。这些方法从路径中获取生产订单的ID,并从请求体中获取任何额外的必要参数。由于与网络技术的集成是应用层的问题,我们把它放在子包web中,以便将其与领域逻辑明确分开。
在聚合上应用动作的模式总是相同的:从持久性存储中加载聚合,调用业务操作,并将其保存到存储中。这同样适用于我们案例中的关系型持久化,但也适用于事件源模型。为了简单起见,我们在控制器中做了所有的事情,而在一个更大的或更纯粹的应用中,控制器将委托给一个域服务。
关于这篇博文的主题,更有趣的部分是通过实现Spring HATEOAS的RepresentationModelProcessor接口。在这个方法过程中,它把生产订单包装成一个实体模型。这个实体模型允许向生产订单资源添加额外的链接。因为该模型也提供了生产订单本身,我们可以很容易地检查它的状态,然后决定是否生成一个特定的链接。
Spring提供了静态的辅助方法linkTo和methodOn来动态地导出引用的控制器方法的URL。
<b>package</b> com.example.demo.productionorders.web;<b>import</b> <b>static</b> org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;<b>import</b> <b>static</b> org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;<b>import</b> java.time.LocalDate;<b>import</b> org.springframework.hateoas.EntityModel;<b>import</b> org.springframework.hateoas.server.RepresentationModelProcessor;<b>import</b> org.springframework.http.HttpStatus;<b>import</b> org.springframework.http.ResponseEntity;<b>import</b> org.springframework.web.bind.annotation.ExceptionHandler;<b>import</b> org.springframework.web.bind.annotation.PathVariable;<b>import</b> org.springframework.web.bind.annotation.PostMapping;<b>import</b> org.springframework.web.bind.annotation.RequestBody;<b>import</b> org.springframework.web.bind.annotation.RequestMapping;<b>import</b> org.springframework.web.bind.annotation.RestController;<b>import</b> org.springframework.web.server.ResponseStatusException;<b>import</b> com.example.demo.productionorders.ProductionOrder;<b>import</b> com.example.demo.productionorders.ProductionOrder.ProductionOrderState;<b>import</b> com.example.demo.productionorders.ProductionOrders;<b>import</b> lombok.NonNull;<b>import</b> lombok.RequiredArgsConstructor;<b>import</b> lombok.Value;<b>import</b> lombok.val;@RestController@RequestMapping(<font>\”/api/productionOrders\”</font><font>)@RequiredArgsConstructor<b>public</b> <b>class</b> ProductionOrderController implements RepresentationModelProcessor<EntityModel<ProductionOrder>> { <b>public</b> <b>static</b> <b>final</b> String REL_RENAME = </font><font>\”rename\”</font><font>; <b>public</b> <b>static</b> <b>final</b> String REL_SUBMIT = </font><font>\”submit\”</font><font>; <b>public</b> <b>static</b> <b>final</b> String REL_ACCEPT = </font><font>\”accept\”</font><font>; <b>private</b> <b>final</b> ProductionOrders productionOrders; @PostMapping(</font><font>\”/{id}/rename\”</font><font>) <b>public</b> ResponseEntity<?> rename(@PathVariable Long id, @RequestBody RenameRequest request) { <b>return</b> productionOrders.findById(id) .map(po -> productionOrders.save(po.renameTo(request.newName))) .map(po -> ResponseEntity.ok().body(EntityModel.of(po))) .orElse(ResponseEntity.notFound().build()); } @PostMapping(</font><font>\”/{id}/submit\”</font><font>) <b>public</b> ResponseEntity<?> submit(@PathVariable Long id) { <b>return</b> productionOrders.findById(id) .map(po -> productionOrders.save(po.submit())) .map(po -> ResponseEntity.ok().body(EntityModel.of(po))) .orElse(ResponseEntity.notFound().build()); } @PostMapping(</font><font>\”/{id}/accept\”</font><font>) <b>public</b> ResponseEntity<?> accept(@PathVariable Long id, @RequestBody CompleteRequest request) { <b>return</b> productionOrders.findById(id) .map(po -> productionOrders.save(po.accept(request.expectedCompletionDate))) .map(po -> ResponseEntity.ok().body(EntityModel.of(po))) .orElse(ResponseEntity.notFound().build()); } @Override <b>public</b> EntityModel<ProductionOrder> process(EntityModel<ProductionOrder> model) { val order = model.getContent(); <b>if</b> (order.getState() == ProductionOrderState.DRAFT) { model.add(linkTo(methodOn(getClass()).rename(order.getId(), <b>null</b>)).withRel(REL_RENAME)); model.add(linkTo(methodOn(getClass()).submit(order.getId())).withRel(REL_SUBMIT)); } <b>if</b> (order.getState() == ProductionOrderState.SUBMITTED) { model.add(linkTo(methodOn(getClass()).accept(order.getId(), <b>null</b>)).withRel(REL_ACCEPT)); } <b>return</b> model; } @ExceptionHandler({IllegalArgumentException.<b>class</b>, IllegalStateException.<b>class</b>}) <b>void</b> handleValidationException(Exception exception) { <b>throw</b> <b>new</b> ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, exception.getMessage()); } @Value <b>static</b> <b>class</b> RenameRequest { @NonNull String newName; } @Value <b>static</b> <b>class</b> CompleteRequest { @NonNull LocalDate expectedCompletionDate; } }</font>再次查询productionOrders资源为我们提供了每个生产订单资源上的新链接。
$ curl http:<font><i>//localhost:8080/api/productionOrders</i></font><font>{ </font><font>\”_embedded\”</font><font> : { </font><font>\”productionOrders\”</font><font> : [ { </font><font>\”name\”</font><font> : </font><font>\”Order 1\”</font><font>, </font><font>\”expectedCompletionDate\”</font><font> : <b>null</b>, </font><font>\”state\”</font><font> : </font><font>\”DRAFT\”</font><font>, </font><font>\”_links\”</font><font> : { </font><font>\”self\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrders/1\”</font><font> }, </font><font>\”productionOrder\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrders/1\”</font><font> }, </font><font>\”rename\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrder/1/rename\”</font><font> }, </font><font>\”submit\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrder/1/submit\”</font><font> } } }, { </font><font>\”name\”</font><font> : </font><font>\”Order 2\”</font><font>, </font><font>\”expectedCompletionDate\”</font><font> : <b>null</b>, </font><font>\”state\”</font><font> : </font><font>\”DRAFT\”</font><font>, </font><font>\”_links\”</font><font> : { </font><font>\”self\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrders/2\”</font><font> }, </font><font>\”productionOrder\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrders/2\”</font><font> }, </font><font>\”rename\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrder/2/rename\”</font><font> }, </font><font>\”submit\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrder/2/submit\”</font><font> } } } ] }, </font><font>\”_links\”</font><font> : { </font><font>\”self\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/productionOrders\”</font><font> }, </font><font>\”profile\”</font><font> : { </font><font>\”href\”</font><font> : </font><font>\”http://localhost:8080/api/profile/productionOrders\”</font><font> } }}</font>你看到Spring并没有公开聚合的ID属性。我们稍后会看到,我们不需要在客户端知道它,因为它包含在链接中。
还请注意,每个链接都有一个关系属性,简称 \”rel\”。这个属性非常重要,因为它定义了与API客户端的契约,即一个特定资源存在哪些链接。我们很快就会看到;我们的后端现在已经完成了,我们可以继续在前端利用它了。
在前端消费HAL模型
正如我之前所说,一个真正的REST-ful API的客户端应该只知道一个URL。/api。客户端调用的任何其他URL都应该从响应中的链接中获得。
从我们Angular应用程序的顶级组件(即AppComponent)中发出对基本URL的请求,并将其作为输入传递给子组件,这将是一个自然的选择。为了保持简单,我们在ProductionOrderListComponent中做了所有事情,作为onInit方法的一部分获取API,并将响应存储在字段根中。见下面代码
为了显示生产订单,我们在类顶部的@Component-decorator中添加一个HTML模板,并通过跟踪根资源中与 \”productionOrders \”有关的链接,从后台加载productionOrders。正如我们在上面看到的,这个链接的url属性是http://localhost:8080/api/productionOrders,但前端对此是不知道的。事实上,后端可以在一个完全不同的URL下提供生产订单,而我们的前端仍然可以工作。只有 \”productionOrders \”这个关系,也就是后端和前端之间的契约,必须保持稳定。
最初的生产订单列表组件,首先加载API资源,然后通过各自关系下提供的URL加载生产订单。
<b>import</b> { HttpClient } from \’@angular/common/http\’;<b>import</b> { Component, OnInit } from \’@angular/core\’;<b>import</b> { ProductionOrderResource } from \’../model\’;<b>const</b> API = <font>\”/api\”</font><font>;<b>const</b> REL = </font><font>\”productionOrders\”</font><font>; @Component({ selector: \’app-production-order-list\’, template: ` <ul> <li *ngFor=</font><font>\”let order of productionOrders\”</font><font>>{{order.name}} <span *ngIf=</font><font>\”order.expectedCompletionDate\”</font><font>> Expected <b>for</b> {{order.expectedCompletionDate|date}} </span> ({{order.state}}) </li> </ul> `, styleUrls: [\’./production-order-list.component.css\’]})export <b>class</b> ProductionOrderListComponent implements OnInit { root: any; productionOrders?: ProductionOrderResource[]; constructor(<b>private</b> http: HttpClient) { } ngOnInit(): <b>void</b> { <b>this</b>.http.get(API).subscribe( response => { <b>this</b>.root = response; <b>this</b>.reload(); }, error => alert(error) ); } <b>private</b> reload(): <b>void</b> { <b>if</b> (<b>this</b>.root) { <b>this</b>.http.get<any>(<b>this</b>.root._links[REL].href).subscribe( response => <b>this</b>.productionOrders = response._embedded[REL], error => alert(error) ) } }}</font>接下来,我们需要添加一种方式,让用户可以在模型上执行相应的业务操作。最简单的方法是为当前允许的每个动作在生产订单旁边添加一个按钮。
为每个生产订单添加按钮,根据生产订单资源中相关链接关系的存在与否,显示或隐藏这些按钮。
<ul> <li *ngFor=<font>\”let order of productionOrders\”</font><font>>{{order.name}} <span *ngIf=</font><font>\”order.expectedCompletionDate\”</font><font>> Expected <b>for</b> {{order.expectedCompletionDate|date}} </span> ({{order.state}}) <button *ngIf=</font><font>\”can(\’rename\’, order)\”</font><font> (click)=</font><font>\”do(\’rename\’, order)\”</font><font>>rename</button> <button *ngIf=</font><font>\”can(\’submit\’, order)\”</font><font> (click)=</font><font>\”do(\’submit\’, order)\”</font><font>>submit</button> <button *ngIf=</font><font>\”can(\’accept\’, order)\”</font><font> (click)=</font><font>\”do(\’accept\’, order)\”</font><font>>accept</button> </li></ul></font>为了决定一个给定的动作是否被允许,以及相应的按钮是否应该被显示,我们查询底层资源,以获取与给定关系的链接。事实上,我们只是实现了一个功能切换:如果该关系被提供为一个链接,则该动作被启用,否则它在GUI中被隐藏。
请注意,示例代码(你可以在这篇文章的末尾找到链接)添加了一些接口,以允许对生产订单资源的_links-property进行类型安全的访问。
最后,我们只需要为那些需要提交额外数据的动作添加特殊处理。在这里,我们再次使用最简单的解决方案,使用本地的提示控制,在重命名动作的情况下接受新的名称,在接受动作的情况下接受预期完成日期。
一个非常简单的 \”能 \”和 \”做 \”方法的实现,利用关系和联系或生产秩序资源。
can(action: string, order: any): <b>boolean</b> { <b>return</b> !!order._links[action]; } <b>do</b>(action: string, order: ProductionOrderResource): <b>void</b> { <b>var</b> body = {}; <b>if</b> (action === \’rename\’) { <b>const</b> newName = prompt(<font>\”Please enter the new name:\”</font><font>); <b>if</b> (!newName) { <b>return</b>; } body = { newName: newName }; } <b>else</b> <b>if</b> (action === \’accept\’) { <b>const</b> expectedCompletionDate = prompt(</font><font>\”Expected completion date (yyyy-MM-dd):\”</font><font>); <b>if</b> (!expectedCompletionDate) { <b>return</b>; } body = { expectedCompletionDate: expectedCompletionDate } } <b>const</b> url = order._links[action].href; <b>this</b>.http.post(url, body).subscribe( _ => <b>this</b>.reload(), response => alert([response.error.error, response.error.message].join(</font><font>\”\\n\”</font><font>))); }</font>因为我们在can-method中检查了一个链接的存在,所以我们可以通过提取该链接的href-property轻松地确定要发布正文的URL。这样一来,我们的小演示程序的前端也就完成了。
总结
用户评论
若他只爱我。
这个文档太棒了!终于看到了用 DDD 和 Spring HATEOAS 构建 MRP API 的实际例子。代码注释清晰易懂,而且能看到如何实现领域模型、资源约束以及 HATEOAS 链接操作,学习这两种架构模式都非常有帮助。
有10位网友表示赞同!
oО清风挽发oО
我一直在犹豫是否要学习 DDD,这个案例的讲解很有说服力!感觉 DDD 可以有效解决复杂的业务逻辑问题,降低系统的耦合度。源码也做得很好,可以参考学习。
有18位网友表示赞同!
挽手余生ら
很不错!我一直关注着 DDD 的发展,这篇文章正好给我了解了如何将它与 Spring HATEOAS 结合起来用于 API 开发。希望后续还有更多类似案例分享,进一步深入讲解领域的核心设计原则。
有15位网友表示赞同!
有一种中毒叫上瘾成咆哮i
我觉得 Spring HATEOAS 的实现过于复杂了,这个示例代码看起来也比较难懂。也许可以提供一些更简化的替代方案吗?
有12位网友表示赞同!
仅有的余温
在实际开发中会遇到很多边界问题。文档只是解释了理论知识,没有涉及到如何在真实场景下使用 DDD 和 Spring HATEOAS 进行 API 的设计和实现。如果能添加更多实践经验分享,会更加实用。
有14位网友表示赞同!
愁杀
我比较关注 HATEOAS 的部分,作者讲解链接关系的定义和实现方式很清楚,对于新手也有帮助。但是对于如何自动化生成 HATEOAS 链接,文档并没有详细介绍,希望可以补充相关内容。
有12位网友表示赞同!
红尘烟雨
这篇文章对 MRP API 做了很好的概述,并结合 DDD 和 Spring HATEOAS 的实战案例来进行讲解。我目前正在着手学习 API 设计,这个资源非常适合入门阶段的学习者,能帮助我理解 API 架构和实现方式。
有18位网友表示赞同!
我一个人
对于熟悉 DDD 和 Spring Boot 的开发者来说,这个代码示例并不是很新颖,文档缺少深入性的分析和探讨。希望可以针对更复杂的场景进行设计案例分享,并提供一些最佳实践建议。
有16位网友表示赞同!
肆忌
在项目中遇到类似的 API 设计问题,参考了这篇博客,找到了很好的解决方案并借鉴了代码实现方式。感谢作者的分享!
有15位网友表示赞同!
她最好i
我比较好奇这个 MRP 的具体业务场景是什么?文档中并没有详细描述系统的功能和应用范围,希望可以补充相关的背景信息,以便更好地理解项目的目的和目标。
有6位网友表示赞同!
今非昔比'
很喜欢使用 DDD 和 Spring Boot 开发 web 应用,但是以前没有接触过 HATEOAS。这篇文章让我对 HATEOAS 的实现方式有了更清晰的了解,以后可以尝试将其应用到我的项目中去。
有11位网友表示赞同!
裸睡の鱼
文档的讲解比较浅显易懂,但是代码注释部分需要更加详细,比如一些常用的方法和变量的实现细节,能够更好地帮助我理解代码逻辑的运作流程。
有10位网友表示赞同!
关于道别
希望作者能提供更多关于 DDD 和 Spring HATEOAS 的实践经验分享,例如如何进行测试、部署以及维护这类系统。也能分享一些开源项目案例,让读者更直观的了解它们的应用场景和优势。
有9位网友表示赞同!
暮光薄凉
代码示例中有些部分使用了很多 Spring MVC 的框架知识,对于不熟悉 Spring MVC 的开发者来说可能会比较难理解。希望能提供一些基础知识的介绍,或者用其他的方式来实现 API 接口设计,使其更容易被大众接受。
有20位网友表示赞同!