Khái niệm không khớp giữa Dịch vụ ứng dụng DDD và API REST


20

Tôi đang cố gắng thiết kế một ứng dụng có miền kinh doanh phức tạp và yêu cầu hỗ trợ API REST (không hoàn toàn là REST, nhưng hướng đến tài nguyên). Tôi gặp một số rắc rối khi nghĩ ra cách phơi bày mô hình miền theo cách định hướng tài nguyên.

Trong DDD, khách hàng của mô hình miền cần trải qua lớp 'Dịch vụ ứng dụng' theo thủ tục để truy cập bất kỳ chức năng kinh doanh nào, được thực hiện bởi Thực thể và Dịch vụ miền. Ví dụ: có một dịch vụ ứng dụng với hai phương thức để cập nhật thực thể Người dùng:

userService.ChangeName(name);
userService.ChangeEmail(email);

API của Dịch vụ ứng dụng này hiển thị các lệnh (động từ, thủ tục), không phải trạng thái.

Nhưng nếu chúng ta cũng cần cung cấp API RESTful cho cùng một ứng dụng, thì có một mô hình tài nguyên Người dùng, trông giống như sau:

{
name:"name",
email:"email@mail.com"
}

API hướng tài nguyên hiển thị trạng thái , không phải lệnh . Điều này đặt ra những mối quan tâm sau:

  • mỗi hoạt động cập nhật đối với API REST có thể ánh xạ tới một hoặc nhiều lệnh gọi thủ tục Dịch vụ ứng dụng, tùy thuộc vào thuộc tính nào đang được cập nhật trên mô hình tài nguyên

  • mỗi hoạt động cập nhật trông giống như ứng dụng khách API REST, nhưng nó không được thực hiện như thế. Mỗi cuộc gọi Dịch vụ Ứng dụng được thiết kế như một giao dịch riêng. Cập nhật một trường trên mô hình tài nguyên có thể thay đổi quy tắc xác thực cho các trường khác. Vì vậy, chúng ta cần xác thực tất cả các trường mô hình tài nguyên cùng nhau để đảm bảo rằng tất cả các lệnh gọi Dịch vụ ứng dụng tiềm năng là hợp lệ trước khi chúng ta bắt đầu thực hiện chúng. Xác thực một tập hợp các lệnh cùng một lúc sẽ ít tầm thường hơn khi thực hiện từng lệnh một. Làm thế nào để chúng ta làm điều đó trên một máy khách thậm chí không biết các lệnh riêng lẻ tồn tại?

  • gọi các phương thức Dịch vụ Ứng dụng theo các thứ tự khác nhau có thể có hiệu ứng khác nhau, trong khi API REST làm cho nó trông giống như không có sự khác biệt (trong một tài nguyên)

Tôi có thể đưa ra nhiều vấn đề tương tự, nhưng về cơ bản tất cả chúng đều do cùng một nguyên nhân. Sau mỗi cuộc gọi đến Dịch vụ Ứng dụng, trạng thái của hệ thống sẽ thay đổi. Quy tắc thay đổi hợp lệ là gì, tập hợp các hành động mà một thực thể có thể thực hiện thay đổi tiếp theo. API hướng tài nguyên cố gắng làm cho tất cả trông giống như một hoạt động nguyên tử. Nhưng sự phức tạp của việc vượt qua khoảng cách này phải đi đâu đó, và nó có vẻ rất lớn.

Ngoài ra, nếu UI có định hướng lệnh nhiều hơn, thường là như vậy, thì chúng ta sẽ phải ánh xạ giữa các lệnh và tài nguyên ở phía máy khách và sau đó quay lại phía API.

Câu hỏi:

  1. Tất cả sự phức tạp này có nên được xử lý bởi lớp ánh xạ REST-to-AppService (dày) không?
  2. Hay tôi đang thiếu một cái gì đó trong sự hiểu biết của tôi về DDD / REST?
  3. Có thể REST đơn giản là không thực tế để hiển thị chức năng của các mô hình miền trên một mức độ phức tạp (khá thấp) nhất định?

3
Cá nhân tôi không coi REST là cần thiết. Tuy nhiên, có thể shoehorn DDD vào nó: infoq.com/articles/rest-api-on-cqrs lập trình viên.stackexchange.com/questions/242884
Den

Hãy nghĩ về máy khách REST như một người dùng của hệ thống. Họ hoàn toàn không quan tâm đến việc hệ thống thực hiện các hành động mà nó thực hiện như thế nào. Bạn sẽ không còn mong muốn máy khách REST biết tất cả các hành động khác nhau trên miền mà bạn mong muốn người dùng sẽ làm. Như bạn nói logic này phải đi một số nơi, nhưng nó sẽ phải đi đến một nơi nào đó trong bất kỳ hệ thống nào, nếu bạn không sử dụng REST, bạn sẽ chuyển nó lên máy khách. Không làm điều đó chính xác là điểm của REST, khách hàng chỉ nên biết rằng nó muốn cập nhật trạng thái và không biết bạn sẽ làm thế nào về điều đó.
Cormac Mulhall

2
@astr Câu trả lời đơn giản là tài nguyên không phải là mô hình của bạn, vì vậy việc thiết kế mã xử lý tài nguyên sẽ không ảnh hưởng đến thiết kế mô hình của bạn. Tài nguyên là một khía cạnh hướng ra bên ngoài của hệ thống, trong đó mô hình là nội bộ. Nghĩ về tài nguyên giống như cách bạn nghĩ về UI. Một người dùng có thể nhấp vào một nút duy nhất trên giao diện người dùng và hàng trăm điều khác nhau xảy ra trong mô hình. Tương tự như một tài nguyên. Một khách hàng cập nhật một tài nguyên (một câu lệnh PUT duy nhất) và một triệu điều khác nhau có thể xảy ra trong mô hình. Đó là một mô hình chống kết hợp mô hình của bạn chặt chẽ với tài nguyên của bạn.
Cormac Mulhall

1
Đây là một bài nói hay về việc xử lý các hành động trong miền của bạn khi các tác dụng phụ của thay đổi trạng thái REST, giữ cho miền của bạn và web tách biệt (chuyển nhanh đến 25 phút cho bit juicy) yow.eventer.com/events/1004/talks/1047
Cormac Mulhall

1
Tôi cũng không chắc chắn về toàn bộ điều "người dùng như một robot / máy trạng thái". Tôi nghĩ rằng chúng ta nên cố gắng làm cho giao diện người dùng của chúng ta tự nhiên hơn thế nhiều ...
guillaume31

Câu trả lời:


10

Tôi đã có cùng một vấn đề và "giải quyết" nó bằng cách mô hình hóa các tài nguyên REST khác nhau, ví dụ:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

Vì vậy, về cơ bản tôi đã chia tài nguyên lớn hơn, phức tạp thành nhiều tài nguyên nhỏ hơn. Mỗi trong số này chứa một nhóm các thuộc tính gắn kết của tài nguyên ban đầu dự kiến ​​sẽ được xử lý cùng nhau.

Mỗi hoạt động trên các tài nguyên này là nguyên tử, mặc dù có thể được triển khai bằng một số phương thức dịch vụ - ít nhất là trong Spring / Java EE, không phải là vấn đề để tạo giao dịch lớn hơn từ một số phương thức ban đầu được dự định có giao dịch riêng (sử dụng giao dịch BẮT BUỘC Lan truyền). Bạn thường vẫn cần phải xác thực thêm cho tài nguyên đặc biệt này, nhưng nó vẫn hoàn toàn có thể quản lý được vì các thuộc tính (được cho là) ​​gắn kết.

Điều này cũng tốt cho cách tiếp cận của HATEOAS, bởi vì các tài nguyên chi tiết hơn của bạn truyền tải nhiều thông tin hơn về những gì bạn có thể làm với họ (thay vì có logic này trên cả máy khách và máy chủ vì nó không thể được trình bày dễ dàng trong tài nguyên).

Tất nhiên nó không hoàn hảo - nếu các UI không được mô hình hóa với các tài nguyên này (đặc biệt là các UI hướng dữ liệu), thì nó có thể tạo ra một số vấn đề - ví dụ: UI thể hiện dạng lớn của tất cả các thuộc tính của tài nguyên đã cho (và nguồn phụ của nó) và cho phép bạn chỉnh sửa tất cả và lưu chúng cùng một lúc - điều này tạo ra ảo tưởng về nguyên tử mặc dù khách hàng phải gọi một số hoạt động tài nguyên (bản thân chúng là nguyên tử nhưng toàn bộ chuỗi không phải là nguyên tử).

Ngoài ra, sự phân chia tài nguyên này đôi khi không dễ dàng hoặc rõ ràng. Tôi làm điều này chủ yếu trên các tài nguyên với các hành vi / vòng đời phức tạp để quản lý sự phức tạp của nó.


Đó cũng là những gì tôi đã nghĩ - tạo ra các biểu diễn tài nguyên chi tiết hơn vì chúng thuận tiện hơn cho các hoạt động ghi. Làm thế nào để bạn xử lý truy vấn tài nguyên khi chúng trở nên chi tiết? Tạo đại diện chỉ đọc không chuẩn hóa là tốt?
Astreltsov

1
Không, tôi không có các biểu diễn không chuẩn hóa chỉ đọc. Tôi sử dụng tiêu chuẩn jsonapi.org và nó có một cơ chế bao gồm các tài nguyên liên quan trong phản hồi cho tài nguyên đã cho. Về cơ bản tôi nói "cung cấp cho tôi Người dùng có ID 1 và cũng bao gồm email phụ của nó và kích hoạt". Điều này giúp loại bỏ các lệnh gọi REST bổ sung cho các nguồn con và nó không ảnh hưởng đến sự phức tạp của ứng dụng khách đối với các nguồn con nếu bạn sử dụng một số thư viện máy khách API JSON tốt.
qbd

Vì vậy, một yêu cầu GET trên máy chủ sẽ chuyển thành một hoặc nhiều truy vấn thực tế (tùy thuộc vào số lượng tài nguyên phụ được bao gồm) sau đó được kết hợp thành một đối tượng tài nguyên?
Astreltsov

Điều gì nếu nhiều hơn một cấp độ lồng là cần thiết?
Astreltsov

Có, trong dbs quan hệ, điều này có thể sẽ dịch sang nhiều truy vấn. Việc lồng ghép tùy ý được hỗ trợ bởi API JSON, nó được mô tả ở đây: jsonapi.org/format/#fetching-includes
qbd

0

Vấn đề chính ở đây là, logic kinh doanh được gọi trong suốt như thế nào khi một lệnh gọi REST được thực hiện? Đây là một vấn đề không được REST giải quyết trực tiếp.

Tôi đã giải quyết điều này bằng cách tạo lớp quản lý dữ liệu của riêng tôi qua một nhà cung cấp kiên trì như JPA. Sử dụng một mô hình meta với các chú thích tùy chỉnh, chúng ta có thể gọi logic nghiệp vụ phù hợp khi trạng thái thực thể thay đổi. Điều này đảm bảo rằng bất kể trạng thái thực thể thay đổi logic nghiệp vụ được gọi như thế nào. Nó giữ kiến ​​trúc của bạn DRY và logic kinh doanh của bạn ở một nơi.

Sử dụng ví dụ trên, chúng ta có thể gọi một phương thức logic nghiệp vụ được gọi là validateName khi trường tên được thay đổi bằng REST:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

Với một công cụ như vậy theo ý của bạn, tất cả những gì bạn cần làm là chú thích các phương pháp logic kinh doanh của bạn một cách thích hợp.


0

Tôi gặp một số rắc rối khi nghĩ ra cách phơi bày mô hình miền theo cách định hướng tài nguyên.

Bạn không nên phơi bày mô hình miền theo cách định hướng tài nguyên. Bạn nên phơi bày ứng dụng theo cách định hướng tài nguyên.

nếu UI được định hướng theo lệnh nhiều hơn, thường là như vậy, thì chúng ta sẽ phải ánh xạ giữa các lệnh và tài nguyên ở phía máy khách và sau đó quay lại phía API.

Hoàn toàn không - gửi các lệnh đến tài nguyên ứng dụng có giao diện với mô hình miền.

mỗi hoạt động cập nhật đối với API REST có thể ánh xạ tới một hoặc nhiều lệnh gọi thủ tục Dịch vụ ứng dụng, tùy thuộc vào thuộc tính nào đang được cập nhật trên mô hình tài nguyên

Vâng, mặc dù có một cách hơi khác để đánh vần điều này có thể làm cho mọi thứ đơn giản hơn; mỗi hoạt động cập nhật đối với api REST ánh xạ tới một quá trình gửi lệnh đến một hoặc nhiều tập hợp.

mỗi hoạt động cập nhật trông giống như ứng dụng khách API REST, nhưng nó không được thực hiện như thế. Mỗi cuộc gọi Dịch vụ Ứng dụng được thiết kế như một giao dịch riêng. Cập nhật một trường trên mô hình tài nguyên có thể thay đổi quy tắc xác thực cho các trường khác. Vì vậy, chúng ta cần xác thực tất cả các trường mô hình tài nguyên cùng nhau để đảm bảo rằng tất cả các lệnh gọi Dịch vụ ứng dụng tiềm năng là hợp lệ trước khi chúng ta bắt đầu thực hiện chúng. Xác thực một tập hợp các lệnh cùng một lúc sẽ ít tầm thường hơn khi thực hiện từng lệnh một. Làm thế nào để chúng ta làm điều đó trên một máy khách thậm chí không biết các lệnh riêng lẻ tồn tại?

Bạn đang đuổi theo đuôi sai ở đây.

Hãy tưởng tượng: đưa REST ra khỏi hình ảnh hoàn toàn. Thay vào đó hãy tưởng tượng rằng bạn đang viết một giao diện máy tính để bàn cho ứng dụng này. Hãy tưởng tượng thêm rằng bạn có yêu cầu thiết kế thực sự tốt và đang triển khai UI dựa trên nhiệm vụ. Vì vậy, người dùng có được một giao diện tối giản được điều chỉnh hoàn hảo cho nhiệm vụ họ đang làm việc; người dùng chỉ định một số đầu vào sau đó nhấn "ĐỘNG TỪ!" nút.

Chuyện gì xảy ra bây giờ? Từ quan điểm của người dùng, đây là một nhiệm vụ nguyên tử duy nhất phải được thực hiện. Từ phối cảnh của domainModel, đó là một số lệnh được chạy bởi các tập hợp, trong đó mỗi lệnh được chạy trong một giao dịch riêng biệt. Những cái đó hoàn toàn không tương thích! Chúng tôi cần một cái gì đó ở giữa để thu hẹp khoảng cách!

Cái gì đó là "ứng dụng".

Trên đường dẫn hạnh phúc, ứng dụng nhận được một số DTO và phân tích đối tượng đó để nhận một thông điệp mà nó hiểu và sử dụng dữ liệu trong thông báo để tạo các lệnh được tạo tốt cho một hoặc nhiều tập hợp. Ứng dụng sẽ đảm bảo rằng mỗi lệnh mà nó gửi đến các tập hợp được hình thành tốt (đó là lớp chống tham nhũng tại nơi làm việc) và nó sẽ tải các tập hợp và lưu các tập hợp nếu giao dịch hoàn tất thành công. Tổng hợp sẽ tự quyết định nếu lệnh hợp lệ, với trạng thái hiện tại của nó.

Các kết quả có thể xảy ra - tất cả các lệnh đều chạy thành công - lớp chống tham nhũng từ chối thông báo - một số lệnh chạy thành công, nhưng sau đó một trong các tổng hợp phàn nàn và bạn đã có một dự phòng để giảm thiểu.

Bây giờ, hãy tưởng tượng rằng bạn đã xây dựng ứng dụng đó; Làm thế nào để bạn tương tác với nó một cách RESTful?

  1. Máy khách bắt đầu với một mô tả hypermedia về trạng thái hiện tại của nó (tức là: UI dựa trên nhiệm vụ), bao gồm các điều khiển hypermedia.
  2. Máy khách gửi một đại diện của nhiệm vụ (tức là: DTO) đến tài nguyên.
  3. Tài nguyên phân tích yêu cầu HTTP đến, lấy đại diện và đưa nó cho ứng dụng.
  4. Ứng dụng chạy nhiệm vụ; từ quan điểm của tài nguyên, đây là một hộp đen có một trong những kết quả sau
    • ứng dụng đã cập nhật thành công tất cả các tập hợp: tài nguyên báo cáo thành công cho khách hàng, hướng nó đến trạng thái ứng dụng mới
    • lớp chống tham nhũng từ chối thông báo: tài nguyên báo cáo lỗi 4xx cho khách hàng (có thể là Yêu cầu xấu), có thể chuyển qua mô tả về sự cố gặp phải.
    • ứng dụng cập nhật một số tổng hợp: tài nguyên báo cáo cho khách hàng rằng lệnh đã được chấp nhận và hướng máy khách đến một tài nguyên sẽ cung cấp một biểu diễn về tiến trình của lệnh.

Được chấp nhận là khoản đồng thanh toán thông thường khi ứng dụng sẽ trì hoãn xử lý tin nhắn cho đến khi trả lời máy khách - thường được sử dụng khi chấp nhận lệnh không đồng bộ. Nhưng nó cũng hoạt động tốt trong trường hợp này, trong đó một hoạt động được cho là giảm thiểu nhu cầu nguyên tử.

Trong thành ngữ này, tài nguyên đại diện cho chính nhiệm vụ - bạn bắt đầu một phiên bản mới của nhiệm vụ bằng cách đăng đại diện thích hợp cho tài nguyên tác vụ và giao diện tài nguyên đó với ứng dụng và hướng bạn đến trạng thái ứng dụng tiếp theo.

Trong , gần như bất cứ khi nào bạn phối hợp nhiều lệnh, bạn muốn suy nghĩ về một quy trình (còn gọi là quy trình kinh doanh, còn gọi là saga).

Có một sự không phù hợp về khái niệm tương tự trong mô hình đọc. Một lần nữa, hãy xem xét giao diện dựa trên nhiệm vụ; nếu tác vụ yêu cầu sửa đổi nhiều tập hợp, thì UI để chuẩn bị tác vụ có thể bao gồm dữ liệu từ một số tập hợp. Nếu sơ đồ tài nguyên của bạn là 1: 1 với tổng hợp, điều đó sẽ khó sắp xếp; thay vào đó, cung cấp một tài nguyên trả về một đại diện của dữ liệu từ một số tập hợp, cùng với điều khiển hypermedia ánh xạ mối quan hệ "nhiệm vụ bắt đầu" đến điểm cuối nhiệm vụ như đã thảo luận ở trên.

Xem thêm: REST in Practice của Jim Webber.


Nếu chúng tôi đang thiết kế API để tương tác với miền của chúng tôi theo các trường hợp sử dụng của chúng tôi .. Tại sao không thiết kế mọi thứ theo cách mà Sagas hoàn toàn không bắt buộc? Có thể tôi đang thiếu một cái gì đó nhưng bằng cách đọc phản hồi của bạn, tôi thực sự tin rằng REST không phù hợp với DDD và tốt hơn là sử dụng các quy trình từ xa (RPC). DDD là trung tâm hành vi trong khi REST là trung tâm http-verb. Tại sao không xóa REST khỏi ảnh và phơi bày hành vi (lệnh) trong API? Rốt cuộc, có lẽ chúng được thiết kế để đáp ứng các tình huống sử dụng và thăm dò là giao dịch. Lợi thế của REST là gì nếu chúng ta sở hữu UI?
iberodev
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.