Kiến trúc sạch: Trường hợp sử dụng có chứa người trình bày hoặc trả lại dữ liệu?


42

Các kiến trúc sạch gợi ý để cho một trường hợp sử dụng tương tác bằng gọi việc thực hiện thực tế của người dẫn chương trình (được tiêm, sau khi DIP) để xử lý các phản ứng / hiển thị. Tuy nhiên, tôi thấy mọi người thực hiện kiến ​​trúc này, trả lại dữ liệu đầu ra từ trình tương tác và sau đó để bộ điều khiển (trong lớp bộ điều hợp) quyết định cách xử lý nó. Là giải pháp thứ hai rò rỉ trách nhiệm ứng dụng ra khỏi lớp ứng dụng, ngoài việc không xác định rõ ràng các cổng đầu vào và đầu ra cho bộ tương tác?

Cổng đầu vào và đầu ra

Xem xét định nghĩa Kiến trúc sạch và đặc biệt là sơ đồ dòng nhỏ mô tả mối quan hệ giữa bộ điều khiển, bộ tương tác ca sử dụng và người trình bày, tôi không chắc liệu tôi có hiểu chính xác "Cổng đầu ra trường hợp sử dụng" là gì không.

Kiến trúc sạch, giống như kiến ​​trúc lục giác, phân biệt giữa các cổng chính (phương thức) và cổng phụ (giao diện được thực hiện bởi bộ điều hợp). Theo luồng truyền thông, tôi hy vọng "Cổng đầu vào ca sử dụng" là cổng chính (do đó, chỉ là một phương thức) và "Cổng đầu ra trường hợp sử dụng" một giao diện sẽ được triển khai, có lẽ là một đối số của hàm tạo lấy bộ điều hợp thực tế, để người tương tác có thể sử dụng nó.

Mã ví dụ

Để tạo một ví dụ mã, đây có thể là mã điều khiển:

Presenter presenter = new Presenter();
Repository repository = new Repository();
UseCase useCase = new UseCase(presenter, repository);
useCase->doSomething();

Giao diện người trình bày:

// Use Case Output Port
interface Presenter
{
    public void present(Data data);
}

Cuối cùng, chính trình tương tác:

class UseCase
{
    private Repository repository;
    private Presenter presenter;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.repository = repository;
        this.presenter = presenter;
    }

    // Use Case Input Port
    public void doSomething()
    {
        Data data = this.repository.getData();
        this.presenter.present(data);
    }
}

Trên trình tương tác gọi người trình bày

Việc giải thích trước đó dường như được xác nhận bởi chính sơ đồ đã nói ở trên, trong đó mối quan hệ giữa bộ điều khiển và cổng đầu vào được biểu thị bằng một mũi tên đặc với đầu "nhọn" (UML cho "liên kết", nghĩa là "có", trong đó bộ điều khiển "có" trường hợp sử dụng), trong khi mối quan hệ giữa người trình bày và cổng đầu ra được biểu thị bằng một mũi tên đặc với đầu "trắng" (UML cho "thừa kế", không phải là "để thực hiện", nhưng có lẽ dù sao đó cũng là ý nghĩa).

Hơn nữa, trong câu trả lời này cho một câu hỏi khác , Robert Martin mô tả chính xác trường hợp sử dụng trong đó người tương tác gọi người trình bày theo yêu cầu đọc:

Nhấp vào bản đồ sẽ khiến cho PlacePinControll được gọi. Nó tập hợp vị trí của nhấp chuột và bất kỳ dữ liệu theo ngữ cảnh nào khác, xây dựng cấu trúc dữ liệu của PlacePinRequest và chuyển nó đến PlacePinInteractor để kiểm tra vị trí của pin, xác thực nó nếu cần, tạo một thực thể Place để ghi lại mã pin, xây dựng một EditPlaceReponse đối tượng và chuyển nó đến EditPlacePresenter, màn hình sẽ hiển thị.

Để làm cho điều này hoạt động tốt với MVC, tôi có thể nghĩ rằng logic ứng dụng mà theo truyền thống sẽ đi vào bộ điều khiển, ở đây được chuyển sang trình tương tác, vì chúng tôi không muốn bất kỳ logic ứng dụng nào bị rò rỉ bên ngoài lớp ứng dụng. Bộ điều khiển trong lớp bộ điều hợp sẽ chỉ gọi trình tương tác và có thể thực hiện một số chuyển đổi định dạng dữ liệu nhỏ trong quy trình:

Phần mềm trong lớp này là một bộ các bộ điều hợp chuyển đổi dữ liệu từ định dạng thuận tiện nhất cho các trường hợp sử dụng và thực thể, sang định dạng thuận tiện nhất cho một số cơ quan bên ngoài như Cơ sở dữ liệu hoặc Web.

từ bài viết gốc, nói về Bộ điều hợp giao diện.

Trên dữ liệu tương tác trả về

Tuy nhiên, vấn đề của tôi với phương pháp này là trường hợp sử dụng phải quan tâm đến bản trình bày. Bây giờ, tôi thấy rằng mục đích của Presentergiao diện là đủ trừu tượng để đại diện cho một số loại trình bày khác nhau (GUI, Web, CLI, v.v.) và nó thực sự chỉ có nghĩa là "đầu ra", đó là một trường hợp sử dụng có thể rất tốt có, nhưng tôi vẫn không hoàn toàn tự tin với nó.

Bây giờ, tìm kiếm trên Web các ứng dụng của kiến ​​trúc sạch, tôi dường như chỉ thấy mọi người diễn giải cổng đầu ra như một phương thức trả về một số DTO. Đây sẽ là một cái gì đó như:

Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);

// I'm omitting the changes to the classes, which are fairly obvious

Điều này hấp dẫn bởi vì chúng tôi chuyển trách nhiệm "gọi" bản trình bày ra khỏi trường hợp sử dụng, vì vậy trường hợp sử dụng không quan tâm đến việc biết phải làm gì với dữ liệu nữa, thay vì chỉ cung cấp dữ liệu. Ngoài ra, trong trường hợp này, chúng tôi vẫn không vi phạm quy tắc phụ thuộc, vì trường hợp sử dụng vẫn không biết gì về lớp bên ngoài.

Tuy nhiên, trường hợp sử dụng không kiểm soát thời điểm khi bản trình bày thực tế được thực hiện nữa (có thể hữu ích, ví dụ để thực hiện các công cụ bổ sung tại thời điểm đó, như ghi nhật ký hoặc hủy bỏ hoàn toàn nếu cần thiết). Ngoài ra, lưu ý rằng chúng tôi đã mất Cổng đầu vào ca sử dụng, vì bây giờ bộ điều khiển chỉ sử dụng getData()phương thức (là cổng đầu ra mới của chúng tôi). Hơn nữa, theo tôi, chúng tôi đang phá vỡ nguyên tắc "nói, đừng hỏi" ở đây, bởi vì chúng tôi đang yêu cầu người tương tác cho một số dữ liệu để làm gì đó với nó, thay vì bảo nó làm điều thực tế trong địa điểm đầu tiên.

Đến điểm

Vì vậy, có bất kỳ một trong hai lựa chọn thay thế này là cách giải thích "chính xác" của Cổng đầu ra ca sử dụng theo Kiến trúc sạch không? Cả hai đều khả thi?


3
Đăng chéo được khuyến khích mạnh mẽ. Nếu đây là nơi bạn muốn câu hỏi của mình tồn tại, thì bạn nên xóa nó khỏi Stack Overflow.
Robert Harvey

Câu trả lời:


48

Kiến trúc sạch đề nghị cho phép một trình tương tác ca sử dụng gọi triển khai thực tế của người trình bày (được chèn, theo sau DIP) để xử lý phản hồi / hiển thị. Tuy nhiên, tôi thấy mọi người thực hiện kiến ​​trúc này, trả lại dữ liệu đầu ra từ trình tương tác và sau đó để bộ điều khiển (trong lớp bộ điều hợp) quyết định cách xử lý nó.

Đó chắc chắn không phải là kiến trúc sạch , hành tây hoặc lục giác . Đó là cái này :

nhập mô tả hình ảnh ở đây

Không phải MVC phải được thực hiện theo cách đó

nhập mô tả hình ảnh ở đây

Bạn có thể sử dụng nhiều cách khác nhau để giao tiếp giữa các mô-đun và gọi nó là MVC . Nói với tôi một cái gì đó sử dụng MVC không thực sự cho tôi biết các thành phần giao tiếp như thế nào. Điều đó không được chuẩn hóa. Tất cả những gì nó nói với tôi là có ít nhất ba thành phần tập trung vào ba trách nhiệm của họ.

Một số trong những cách đó đã được đặt tên khác nhau : nhập mô tả hình ảnh ở đây

Và mỗi một trong số đó có thể được gọi là MVC.

Dù sao, không ai trong số họ thực sự nắm bắt được những gì mà kiến ​​trúc từ thông dụng (Clean, Onion và Hex) đều yêu cầu bạn làm.

nhập mô tả hình ảnh ở đây

Thêm các cấu trúc dữ liệu được xoay xung quanh (và lật ngược nó vì một số lý do) và bạn nhận được :

nhập mô tả hình ảnh ở đây

Một điều cần làm rõ ở đây là mô hình phản hồi không đi qua bộ điều khiển.

Nếu bạn là mắt đại bàng, bạn có thể nhận thấy rằng chỉ các kiến ​​trúc từ thông dụng hoàn toàn tránh được sự phụ thuộc vòng tròn . Điều quan trọng, điều đó có nghĩa là tác động của việc thay đổi mã sẽ không lan truyền bằng cách đạp xe qua các thành phần. Thay đổi sẽ dừng khi nó đạt mã mà không quan tâm đến nó.

Tự hỏi nếu họ lật ngược nó lại để dòng điều khiển sẽ đi qua chiều kim đồng hồ. Thêm vào đó, và những đầu mũi tên "trắng" này, sau này.

Là giải pháp thứ hai rò rỉ trách nhiệm ứng dụng ra khỏi lớp ứng dụng, ngoài việc không xác định rõ ràng các cổng đầu vào và đầu ra cho bộ tương tác?

Vì giao tiếp từ Bộ điều khiển đến Người thuyết trình có nghĩa là đi qua "lớp" ứng dụng, nên có, làm cho Bộ điều khiển thực hiện một phần công việc của Người thuyết trình có khả năng bị rò rỉ. Đây là lời chỉ trích chính của tôi về kiến trúc VIPER .

Tại sao việc phân tách chúng lại quan trọng như vậy có lẽ có thể được hiểu rõ nhất bằng cách nghiên cứu Phân đoạn trách nhiệm truy vấn lệnh .

Cổng đầu vào và đầu ra

Xem xét định nghĩa Kiến trúc sạch và đặc biệt là sơ đồ dòng nhỏ mô tả mối quan hệ giữa bộ điều khiển, bộ tương tác ca sử dụng và người trình bày, tôi không chắc liệu tôi có hiểu chính xác "Cổng đầu ra trường hợp sử dụng" là gì không.

Đó là API mà bạn gửi đầu ra thông qua, cho trường hợp sử dụng cụ thể này. Nó không hơn thế. Trình tương tác cho trường hợp sử dụng này không cần biết, cũng không muốn biết, nếu đầu ra sẽ đến GUI, CLI, nhật ký hoặc loa âm thanh. Tất cả những gì người tương tác cần biết là API rất đơn giản nhất có thể sẽ cho phép nó báo cáo kết quả hoạt động của nó.

Kiến trúc sạch, giống như kiến ​​trúc lục giác, phân biệt giữa các cổng chính (phương thức) và cổng phụ (giao diện được thực hiện bởi bộ điều hợp). Theo luồng truyền thông, tôi hy vọng "Cổng đầu vào ca sử dụng" là cổng chính (do đó, chỉ là một phương thức) và "Cổng đầu ra trường hợp sử dụng" một giao diện sẽ được triển khai, có lẽ là một đối số của hàm tạo lấy bộ điều hợp thực tế, để người tương tác có thể sử dụng nó.

Lý do cổng đầu ra khác với cổng đầu vào là vì nó không được SỞ HỮU bởi lớp mà nó trừu tượng hóa. Đó là, lớp mà nó trừu tượng không được phép ra lệnh thay đổi nó. Chỉ có lớp ứng dụng và tác giả nên quyết định rằng cổng đầu ra có thể thay đổi.

Điều này trái ngược với cổng đầu vào được sở hữu bởi lớp mà nó trừu tượng hóa. Chỉ tác giả lớp ứng dụng mới quyết định xem cổng đầu vào của nó có thay đổi hay không.

Theo các quy tắc này bảo tồn ý tưởng rằng lớp ứng dụng, hoặc bất kỳ lớp bên trong, không biết gì về các lớp bên ngoài.


Trên trình tương tác gọi người trình bày

Việc giải thích trước đó dường như được xác nhận bởi chính sơ đồ đã nói ở trên, trong đó mối quan hệ giữa bộ điều khiển và cổng đầu vào được biểu thị bằng một mũi tên đặc với đầu "nhọn" (UML cho "liên kết", nghĩa là "có", trong đó bộ điều khiển "có" trường hợp sử dụng), trong khi mối quan hệ giữa người trình bày và cổng đầu ra được biểu thị bằng một mũi tên đặc với đầu "trắng" (UML cho "thừa kế", không phải là "để thực hiện", nhưng có lẽ dù sao đó cũng là ý nghĩa).

Điều quan trọng về mũi tên "trắng" đó là nó cho phép bạn làm điều này:

nhập mô tả hình ảnh ở đây

Bạn có thể để dòng kiểm soát đi theo hướng ngược lại của sự phụ thuộc! Điều đó có nghĩa là lớp bên trong không cần phải biết về lớp bên ngoài và bạn vẫn có thể đi sâu vào lớp bên trong và quay trở lại!

Làm điều đó không có gì để làm với việc sử dụng từ khóa "giao diện". Bạn có thể làm điều này với một lớp trừu tượng. Heck bạn có thể làm điều đó với một lớp bê tông (ick) miễn là nó có thể được mở rộng. Thật đơn giản để làm điều đó với một cái gì đó chỉ tập trung vào việc xác định API mà Người trình bày phải triển khai. Mũi tên mở chỉ yêu cầu đa hình. Loại nào là tùy thuộc vào bạn.

Tại sao đảo ngược hướng của sự phụ thuộc đó rất quan trọng có thể được học bằng cách nghiên cứu Nguyên tắc đảo ngược phụ thuộc . Tôi đã ánh xạ nguyên tắc đó lên các sơ đồ này ở đây .

Trên dữ liệu tương tác trả về

Tuy nhiên, vấn đề của tôi với phương pháp này là trường hợp sử dụng phải tự chăm sóc bản trình bày. Bây giờ, tôi thấy rằng mục đích của giao diện Người thuyết trình là đủ trừu tượng để đại diện cho một số loại trình bày khác nhau (GUI, Web, CLI, v.v.) và nó thực sự chỉ có nghĩa là "đầu ra", đó là một trường hợp sử dụng rất có thể có, nhưng tôi vẫn không hoàn toàn tự tin với nó.

Không, đó thực sự là nó. Điểm chắc chắn của các lớp bên trong không biết về các lớp bên ngoài là chúng ta có thể loại bỏ, thay thế hoặc tái cấu trúc các lớp bên ngoài tự tin rằng làm như vậy sẽ không phá vỡ bất cứ thứ gì trong các lớp bên trong. Những gì họ không biết về việc sẽ không làm tổn thương họ. Nếu chúng ta có thể làm điều đó, chúng ta có thể thay đổi những cái bên ngoài thành bất cứ điều gì chúng ta muốn.

Bây giờ, tìm kiếm trên Web các ứng dụng của kiến ​​trúc sạch, tôi dường như chỉ thấy mọi người diễn giải cổng đầu ra như một phương thức trả về một số DTO. Đây sẽ là một cái gì đó như:

Repository repository = new Repository();
UseCase useCase = new UseCase(repository);
Data data = useCase.getData();
Presenter presenter = new Presenter();
presenter.present(data);
// I'm omitting the changes to the classes, which are fairly obvious

Điều này hấp dẫn bởi vì chúng tôi chuyển trách nhiệm "gọi" bản trình bày ra khỏi trường hợp sử dụng, vì vậy trường hợp sử dụng không quan tâm đến việc biết phải làm gì với dữ liệu nữa, thay vì chỉ cung cấp dữ liệu. Ngoài ra, trong trường hợp này, chúng tôi vẫn không vi phạm quy tắc phụ thuộc, vì trường hợp sử dụng vẫn không biết gì về lớp bên ngoài.

Vấn đề ở đây bây giờ là bất cứ điều gì biết cách yêu cầu dữ liệu cũng phải là điều chấp nhận dữ liệu. Trước khi Bộ điều khiển có thể gọi Bộ giao dịch Usecase một cách vui vẻ không biết Mô hình phản hồi sẽ trông như thế nào, nó sẽ đi đến đâu và, heh, làm thế nào để trình bày nó.

Một lần nữa, vui lòng nghiên cứu Phân đoạn trách nhiệm truy vấn lệnh để xem tại sao điều đó quan trọng.

Tuy nhiên, trường hợp sử dụng không kiểm soát thời điểm khi bản trình bày thực tế được thực hiện nữa (có thể hữu ích, ví dụ để thực hiện các công cụ bổ sung tại thời điểm đó, như ghi nhật ký hoặc hủy bỏ hoàn toàn nếu cần thiết). Ngoài ra, lưu ý rằng chúng tôi đã mất Cổng đầu vào ca sử dụng, vì bây giờ bộ điều khiển chỉ sử dụng phương thức getData () (là cổng đầu ra mới của chúng tôi). Hơn nữa, theo tôi, chúng tôi đang phá vỡ nguyên tắc "nói, đừng hỏi" ở đây, bởi vì chúng tôi đang yêu cầu người tương tác cho một số dữ liệu để làm gì đó với nó, thay vì bảo nó làm điều thực tế trong địa điểm đầu tiên.

Đúng! Nói, không hỏi, sẽ giúp giữ cho đối tượng này được định hướng hơn là thủ tục.

Đến điểm

Vì vậy, có bất kỳ một trong hai lựa chọn thay thế này là cách giải thích "chính xác" của Cổng đầu ra ca sử dụng theo Kiến trúc sạch không? Cả hai đều khả thi?

Bất cứ điều gì làm việc là khả thi. Nhưng tôi không nói rằng tùy chọn thứ hai mà bạn trình bày trung thành theo Kiến trúc sạch. Nó có thể là một cái gì đó hoạt động. Nhưng đó không phải là những gì Kiến trúc sạch yêu cầu.


4
Cảm ơn đã dành thời gian để viết một lời giải thích sâu sắc như vậy.
swahnee

1
Tôi đã cố gắng quấn đầu quanh Kiến trúc sạch, và câu trả lời này là một tài nguyên tuyệt vời. Làm rất tốt
Nathan

Câu trả lời tuyệt vời và chi tiết .. Cảm ơn vì điều đó .. Bạn có thể cho tôi một số mẹo (hoặc chỉ ra để giải thích) về việc cập nhật GUI trong khi chạy UseCase, tức là cập nhật thanh tiến trình trong khi tải lên tệp lớn?
Ewoks

1
@Ewoks, như một câu trả lời nhanh cho câu hỏi của bạn, bạn nên xem xét mẫu có thể quan sát được. Trường hợp sử dụng của bạn có thể trả về Chủ đề và Thông báo cho Chủ đề cập nhật tiến độ. Người trình bày sẽ Đăng ký Chủ đề và trả lời Thông báo.
Nathan

7

Trong một cuộc thảo luận liên quan đến câu hỏi của bạn , chú Bob giải thích mục đích của người trình bày trong Kiến trúc sạch của mình:

Cho mẫu mã này:

namespace Some\Controller;

class UserController extends Controller {
    public function registerAction() {
        // Build the Request object
        $request = new RegisterRequest();
        $request->name = $this->getRequest()->get('username');
        $request->pass = $this->getRequest()->get('password');

        // Build the Interactor
        $usecase = new RegisterUser();

        // Execute the Interactors method and retrieve the response
        $response = $usecase->register($request);

        // Pass the result to the view
        $this->render(
            '/user/registration/template.html.twig', 
            array('id' =>  $response->getId()
        );
    }
}

Chú Bob nói thế này:

" Mục đích của người trình bày là tách rời các trường hợp sử dụng khỏi định dạng của UI. Trong ví dụ của bạn, biến phản hồi $ được tạo bởi trình tương tác, nhưng được sử dụng bởi khung nhìn. Điều này kết hợp trình tương tác với khung nhìn. Ví dụ , giả sử rằng một trong các trường trong đối tượng $ reply là một ngày. Trường đó sẽ là một đối tượng ngày nhị phân có thể được hiển thị ở nhiều định dạng ngày khác nhau. Muốn có định dạng ngày rất cụ thể, có lẽ là DD / MM / YYYY. Nếu người tương tác tạo ra định dạng đó thì ai biết quá nhiều về Chế độ xem. Nhưng nếu chế độ xem lấy đối tượng ngày nhị phân thì nó biết quá nhiều về trình tương tác.

"Công việc của người trình bày là thực hiện dữ liệu từ đối tượng phản hồi và định dạng nó cho Chế độ xem. Cả khung nhìn lẫn người tương tác đều không biết về các định dạng của nhau. "

--- Chú Bob

(CẬP NHẬT: ngày 31 tháng 5 năm 2019)

Với câu trả lời đó của chú Bob, tôi nghĩ không quan trọng lắm cho dù chúng tôi có lựa chọn số 1 (cho phép người tương tác sử dụng người trình bày) ...

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... hoặc chúng tôi thực hiện tùy chọn # 2 (để phản hồi trả về của người tương tác, tạo người trình bày bên trong bộ điều khiển, sau đó chuyển phản hồi cho người trình bày) ...

class Controller
{
    public void ExecuteUseCase(Data data)
    {
        Request request = ...
        UseCase useCase = new UseCase(repository);
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        presenter.Show(response);
    }
}

Cá nhân, tôi thích tùy chọn số 1 vì tôi muốn có thể kiểm soát bên trong thời interactor điểm hiển thị dữ liệu và thông báo lỗi, như ví dụ dưới đây:

class UseCase
{
    private Presenter presenter;
    private Repository repository;

    public UseCase(Repository repository, Presenter presenter)
    {
        this.presenter = presenter;
        this.repository = repository;
    }

    public void Execute(Request request)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {...}
        this.presenter.Show(response);
    }
}

... Tôi muốn có thể thực hiện những if/elseđiều này có liên quan đến việc trình bày bên trong interactorchứ không phải bên ngoài trình tương tác.

Nếu mặt khác mà chúng tôi làm tùy chọn # 2, chúng ta sẽ phải lưu trữ các thông báo lỗi (s) trong responseđối tượng, trở về mà responseđối tượng từ interactorđến controller, và làm cho controller phân tích các responseđối tượng ...

class UseCase
{
    public Response Execute(Request request)
    {
        Response response = new Response();
        if (<invalid request>) 
        {
            response.AddError("...");
        }

        if (<there is another error>) 
        {
            response.AddError("another error...");
        }

        if (response.HasNoErrors)
        {
            response.Whatever = ...
        }

        ...
        return response;
    }
}
class Controller
{
    private UseCase useCase;

    public Controller(UseCase useCase)
    {
        this.useCase = useCase;
    }

    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        Response response = useCase.Execute(request);
        Presenter presenter = new Presenter();
        if (response.ErrorMessages.Count > 0)
        {
            if (response.ErrorMessages.Contains(<invalid request>))
            {
                presenter.ShowError("...");
            }
            else if (response.ErrorMessages.Contains("another error")
            {
                presenter.ShowError("another error...");
            }
        }
        else
        {
            presenter.Show(response);
        }
    }
}

Tôi không thích phân tích responsedữ liệu cho các lỗi bên trong controllerbởi vì nếu chúng ta làm việc đó chúng ta đang làm việc dư thừa --- nếu chúng ta thay đổi một cái gì đó trong interactor, chúng ta cũng phải thay đổi một cái gì đó trong controller.

Ngoài ra, nếu sau đó chúng tôi quyết định sử dụng lại interactordữ liệu của chúng tôi để sử dụng bảng điều khiển, chúng tôi phải nhớ sao chép-dán tất cả những gì if/elsetrong controllerứng dụng bảng điều khiển của chúng tôi.

// in the controller for our console app
if (response.ErrorMessages.Count > 0)
{
    if (response.ErrorMessages.Contains(<invalid request>))
    {
        presenterForConsole.ShowError("...");
    }
    else if (response.ErrorMessages.Contains("another error")
    {
        presenterForConsole.ShowError("another error...");
    }
}
else
{
    presenterForConsole.Present(response);
}

Nếu chúng ta sử dụng tùy chọn # 1 chúng ta sẽ có điều này if/else chỉ ở một nơi : các interactor.


Nếu bạn đang sử dụng ASP.NET MVC (hoặc các khung MVC tương tự khác), tùy chọn # 2 là cách dễ dàng hơn để đi.

Nhưng chúng ta vẫn có thể làm tùy chọn số 1 trong loại môi trường đó. Dưới đây là một ví dụ về cách thực hiện tùy chọn # 1 trong ASP.NET MVC:

(Lưu ý rằng chúng ta cần phải có public IActionResult Resultngười trình bày ứng dụng ASP.NET MVC của chúng tôi)

class UseCase
{
    private Repository repository;

    public UseCase(Repository repository)
    {
        this.repository = repository;
    }

    public void Execute(Request request, Presenter presenter)
    {
        if (<invalid request>) 
        {
            this.presenter.ShowError("...");
            return;
        }

        if (<there is another error>) 
        {
            this.presenter.ShowError("another error...");
            return;
        }

        ...
        Response response = new Response() {
            ...
        }
        this.presenter.Show(response);
    }
}
// controller for ASP.NET app

class AspNetController
{
    private UseCase useCase;

    public AspNetController(UseCase useCase)
    {
        this.useCase = useCase;
    }

    [HttpPost("dosomething")]
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new AspNetPresenter();
        useCase.Execute(request, presenter);
        return presenter.Result;
    }
}
// presenter for ASP.NET app

public class AspNetPresenter
{
    public IActionResult Result { get; private set; }

    public AspNetPresenter(...)
    {
    }

    public async void Show(Response response)
    {
        Result = new OkObjectResult(new { });
    }

    public void ShowError(string errorMessage)
    {
        Result = new BadRequestObjectResult(errorMessage);
    }
}

(Lưu ý rằng chúng ta cần phải có public IActionResult Resultngười trình bày ứng dụng ASP.NET MVC của chúng tôi)

Nếu chúng tôi quyết định tạo một ứng dụng khác cho bảng điều khiển, chúng tôi có thể sử dụng lại phần UseCasetrên và chỉ tạo ControllerPresentercho bảng điều khiển:

// controller for console app

class ConsoleController
{    
    public void ExecuteUseCase(Data data)
    {
        Request request = new Request() 
        {
            Whatever = data.whatever,
        };
        var presenter = new ConsolePresenter();
        useCase.Execute(request, presenter);
    }
}
// presenter for console app

public class ConsolePresenter
{
    public ConsolePresenter(...)
    {
    }

    public async void Show(Response response)
    {
        // write response to console
    }

    public void ShowError(string errorMessage)
    {
        Console.WriteLine("Error: " + errorMessage);
    }
}

(Lưu ý rằng chúng tôi KHÔNG CÓ public IActionResult Resulttrong người trình bày ứng dụng bảng điều khiển của chúng tôi)


Cảm ơn sự đóng góp. Tuy nhiên, khi đọc cuộc hội thoại, có một điều tôi không hiểu: ông nói rằng người trình bày nên kết xuất dữ liệu đến từ phản hồi, đồng thời rằng phản hồi không nên được tạo bởi người tương tác. Nhưng ai là người tạo ra phản ứng? Tôi muốn nói rằng trình tương tác sẽ cung cấp dữ liệu cho người trình bày, ở định dạng cụ thể của ứng dụng, mà người trình bày biết, vì lớp bộ điều hợp có thể phụ thuộc vào lớp ứng dụng (chứ không phải theo cách khác).
swahnee

Tôi xin lỗi. Có lẽ nó gây nhầm lẫn vì tôi không bao gồm ví dụ mã từ cuộc thảo luận. Tôi sẽ cập nhật nó để bao gồm ví dụ mã.
Jboy Flaga

Chú Bob không nói rằng phản hồi không nên được tạo ra bởi người tương tác. Phản ứng sẽ được tạo bởi người tương tác . Điều mà chú Bob đang nói là phản hồi được tạo bởi người tương tác sẽ được người trình bày sử dụng. Sau đó, người trình bày sẽ "định dạng nó", đặt phản hồi được định dạng cho chế độ xem, sau đó chuyển chế độ xem đó sang chế độ xem. <br/> Đó là cách tôi hiểu nó.
Jboy Flaga

1
Điều đó có ý nghĩa hơn. Tôi có ấn tượng rằng "view" là từ đồng nghĩa với "người trình bày", vì Clean Architecture không đề cập đến "view" hay "viewmodel", mà tôi tin rằng đó chỉ là các khái niệm MVC, có thể được sử dụng khi triển khai bộ chuyển đổi.
swahnee

2

Một ca sử dụng có thể chứa dữ liệu của người trình bày hoặc trả về, tùy thuộc vào yêu cầu của luồng ứng dụng.

Hãy hiểu một số thuật ngữ trước khi hiểu các luồng ứng dụng khác nhau:

  • Đối tượng miền : Đối tượng miền là vùng chứa dữ liệu trong lớp miền mà các hoạt động logic nghiệp vụ được thực hiện.
  • Mô hình xem : Các đối tượng miền thường được ánh xạ để xem các mô hình trong lớp ứng dụng để làm cho chúng tương thích và thân thiện với giao diện người dùng.
  • Người trình bày : Mặc dù bộ điều khiển trong lớp ứng dụng thường gọi trường hợp sử dụng, nhưng nên ủy thác miền để xem logic ánh xạ mô hình cho lớp riêng biệt (theo Nguyên tắc Trách nhiệm Đơn lẻ), được gọi là Nguyên tắc Trình bày.

Ca sử dụng chứa dữ liệu trả về

Trong một trường hợp thông thường, trường hợp sử dụng chỉ cần trả về một đối tượng miền cho lớp ứng dụng có thể được xử lý thêm trong lớp ứng dụng để làm cho nó thân thiện để hiển thị trong UI.

Do bộ điều khiển chịu trách nhiệm gọi trường hợp sử dụng, nên trong trường hợp này, nó cũng chứa tham chiếu của người trình bày tương ứng để tiến hành miền để xem ánh xạ mô hình trước khi gửi nó để xem được hiển thị.

Đây là một mẫu mã đơn giản hóa:

namespace SimpleCleanArchitecture
{
    public class OutputDTO
    {
        //fields
    }

    public class Presenter 
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    public class Domain
    {
        //fields
    }

    public class UseCaseInteractor
    {
        public Domain Process(Domain domain)
        {
            // additional processing takes place here
            return domain;
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            UseCaseInteractor userCase = new UseCaseInteractor();
            var domain = userCase.Process(new Domain());//passing dummy domain(for demonstration purpose) to process
            var presenter = new Presenter();//presenter might be initiated via dependency injection.

            return new View(presenter.Present(domain));
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

Một ca sử dụng có chứa Người trình bày

Mặc dù không phổ biến, nhưng có thể trường hợp sử dụng có thể cần gọi người trình bày. Trong trường hợp đó thay vì giữ tham chiếu cụ thể của người trình bày, nên xem giao diện (hoặc lớp trừu tượng) là điểm tham chiếu (cần được khởi tạo trong thời gian chạy thông qua tiêm phụ thuộc).

Có miền để xem logic ánh xạ mô hình trong một lớp riêng (thay vì bên trong bộ điều khiển) cũng phá vỡ sự phụ thuộc vòng tròn giữa bộ điều khiển và ca sử dụng (khi tham chiếu đến logic ánh xạ được yêu cầu bởi lớp trường hợp sử dụng).

nhập mô tả hình ảnh ở đây

Dưới đây là đơn giản hóa việc thực hiện luồng điều khiển như được minh họa trong bài viết gốc, cho thấy cách nó có thể được thực hiện. Xin lưu ý rằng không giống như hiển thị trong sơ đồ, vì mục đích đơn giản, UseCaseInteractor là một lớp cụ thể.

namespace CleanArchitectureWithPresenterInUseCase
{
    public class Domain
    {
        //fields
    }

    public class OutputDTO
    {
        //fields
    }

    // Use Case Output Port
    public interface IPresenter
    {
        OutputDTO Present(Domain domain);
    }

    public class Presenter: IPresenter
    {
        public OutputDTO Present(Domain domain)
        {
            // Mapping takes action. Dummy object returned for demonstration purpose
            // Usually frameworks like automapper to the mapping job.
            return new OutputDTO();
        }
    }

    // Use Case Input Port / Interactor   
    public class UseCaseInteractor
    {
        IPresenter _presenter;
        public UseCaseInteractor (IPresenter presenter)
        {
            _presenter = presenter;
        }

        public OutputDTO Process(Domain domain)
        {
            return _presenter.Present(domain);
        }
    }

    // A simple controller. 
    // Usually frameworks like asp.net mvc provides url routing mechanism to reach here through this type of class.
    public class Controller
    {
        public View Action()
        {
            IPresenter presenter = new Presenter();//presenter might be initiated via dependency injection.
            UseCaseInteractor userCase = new UseCaseInteractor(presenter);
            var outputDTO = userCase.Process(new Domain());//passing dummy domain (for demonstration purpose) to process
            return new View(outputDTO);
        }
    }

    // A simple view. 
    // Usually frameworks like asp.net mvc provides mechanism to render html based view through this type of class.
    public class View
    {
        OutputDTO _outputDTO;

        public View(OutputDTO outputDTO)
        {
            _outputDTO = outputDTO;
        }

    }
}

1

Mặc dù tôi thường đồng ý với câu trả lời từ @CandiedOrange, tôi cũng sẽ thấy được lợi ích trong cách tiếp cận khi người tương tác chỉ truy xuất dữ liệu sau đó được bộ điều khiển chuyển đến người trình bày.

Ví dụ, đây là một cách đơn giản để sử dụng các ý tưởng của Kiến trúc sạch (Quy tắc phụ thuộc) trong ngữ cảnh của Asp.Net MVC.

Tôi đã viết một bài đăng trên blog để đi sâu hơn vào cuộc thảo luận này: https://plainionist.github.io/Imcellenceation-Clean-Arch architecture-Contoder-Plenter /


1

Trường hợp sử dụng có chứa người trình bày hoặc trả lại dữ liệu?

Vì vậy, có bất kỳ một trong hai lựa chọn thay thế này là cách giải thích "chính xác" của Cổng đầu ra ca sử dụng theo Kiến trúc sạch không? Cả hai đều khả thi?


Nói ngắn gọn

Có, cả hai đều khả thi miễn là cả hai phương pháp đều xem xét Inversion Of Control giữa lớp doanh nghiệp và cơ chế phân phối. Với cách tiếp cận thứ hai, chúng tôi vẫn có thể giới thiệu IOC bằng cách sử dụng người quan sát, người hòa giải vài mẫu thiết kế khác ...

Với Kiến trúc sạch của mình , nỗ lực của chú Bob là tổng hợp một loạt các kiến ​​trúc đã biết để tiết lộ các khái niệm và thành phần quan trọng để chúng tôi tuân thủ rộng rãi các nguyên tắc OOP.

Sẽ rất hiệu quả nếu coi sơ đồ lớp UML của anh ấy (sơ đồ bên dưới) là thiết kế Kiến trúc sạch độc đáo . Biểu đồ này có thể đã được rút ra vì lợi ích của ví dụ cụ thể ... Tuy nhiên, vì nó là ít trừu tượng hơn đại diện kiến trúc thông thường, ông bị buộc phải lựa chọn bê tông trong đó việc thiết kế cổng đầu ra trình tương tác mà chỉ là một chi tiết thi hành ...

Sơ đồ lớp UML của Bác Bob về Kiến trúc sạch


Theo quan điểm của tôi

Lý do chính tại sao tôi thích trả lại UseCaseResponselà phương pháp này giữ các trường hợp sử dụng của tôi linh hoạt , cho phép cả hai thành phần giữa họ và genericity ( tổng quáthệ cụ thể ). Một ví dụ cơ bản:

// A generic "entity type agnostic" use case encapsulating the interaction logic itself.
class UpdateUseCase implements UpdateUseCaseInterface
{
    function __construct(EntityGatewayInterface $entityGateway, GetUseCaseInterface $getUseCase)
    {
        $this->entityGateway = $entityGateway;
        $this->getUseCase = $getUseCase;
    }

    public function execute(UpdateUseCaseRequestInterface $request) : UpdateUseCaseResponseInterface
    {
        $getUseCaseResponse = $this->getUseCase->execute($request);

        // Update the entity and build the response...

        return $response;
    }
}

// "entity type aware" use cases encapsulating the interaction logic WITH the specific entity type.
final class UpdatePostUseCase extends UpdateUseCase;
final class UpdateProductUseCase extends UpdateUseCase;

Lưu ý rằng nó gần giống với các trường hợp sử dụng UML bao gồm / mở rộng lẫn nhau và được định nghĩa là có thể sử dụng lại trên các đối tượng khác nhau (các thực thể).


Trên dữ liệu tương tác trả về

Tuy nhiên, trường hợp sử dụng không kiểm soát thời điểm khi bản trình bày thực tế được thực hiện nữa (có thể hữu ích, ví dụ để thực hiện các công cụ bổ sung tại thời điểm đó, như ghi nhật ký hoặc hủy bỏ hoàn toàn nếu cần thiết).

Không chắc chắn để hiểu ý của bạn với điều này, tại sao bạn cần phải "kiểm soát" thông tin trình bày? Bạn không kiểm soát nó miễn là bạn không trả lời trường hợp sử dụng?

Ca sử dụng có thể trả về trong phản hồi của nó một mã trạng thái để biết cho lớp khách biết điều gì đã xảy ra chính xác trong quá trình hoạt động của nó. Mã trạng thái phản hồi HTTP đặc biệt phù hợp để mô tả trạng thái hoạt động của trường hợp sử dụng

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.