Làm thế nào để kiên trì phù hợp với một ngôn ngữ chức năng thuần túy?


18

Làm thế nào mô hình sử dụng các trình xử lý lệnh để đối phó với sự bền bỉ phù hợp với một ngôn ngữ chức năng thuần túy, nơi chúng ta muốn làm cho mã liên quan đến IO càng mỏng càng tốt?


Khi triển khai Thiết kế hướng tên miền theo ngôn ngữ hướng đối tượng, người ta thường sử dụng mẫu Lệnh / Trình xử lý để thực hiện các thay đổi trạng thái. Trong thiết kế này, các trình xử lý lệnh nằm trên các đối tượng miền của bạn và chịu trách nhiệm cho logic liên quan đến tính bền vững nhàm chán như sử dụng kho lưu trữ và xuất bản các sự kiện miền. Các trình xử lý là bộ mặt công khai của mô hình miền của bạn; mã ứng dụng như UI gọi trình xử lý khi cần thay đổi trạng thái của các đối tượng miền.

Một bản phác thảo trong C #:

public class DiscardDraftDocumentCommandHandler : CommandHandler<DiscardDraftDocument>
{
    IDraftDocumentRepository _repo;
    IEventPublisher _publisher;

    public DiscardDraftCommandHandler(IDraftDocumentRepository repo, IEventPublisher publisher)
    {
        _repo = repo;
        _publisher = publisher;
    }

    public override void Handle(DiscardDraftDocument command)
    {
        var document = _repo.Get(command.DocumentId);
        document.Discard(command.UserId);
        _publisher.Publish(document.NewEvents);
    }
}

Các documentđối tượng miền có trách nhiệm thực hiện các quy tắc kinh doanh (như "người sử dụng nên có quyền loại bỏ các tài liệu" hoặc "bạn không thể loại bỏ một tài liệu đó đã bị loại bỏ") và để tạo ra các sự kiện miền chúng ta cần phải xuất bản ( document.NewEventssẽ là một IEnumerable<Event>và có thể sẽ chứa một DocumentDiscardedsự kiện).

Đây là một thiết kế đẹp - dễ dàng mở rộng (bạn có thể thêm các trường hợp sử dụng mới mà không thay đổi mô hình miền của mình, bằng cách thêm trình xử lý lệnh mới) và không biết cách các đối tượng được duy trì (bạn có thể dễ dàng trao đổi kho lưu trữ NHibernate cho Mongo kho lưu trữ hoặc trao đổi nhà xuất bản RabbitMQ cho nhà xuất bản EventStore) giúp dễ dàng kiểm tra bằng cách sử dụng hàng giả và giả. Nó cũng tuân theo sự phân tách mô hình / khung nhìn - trình xử lý lệnh không biết liệu nó được sử dụng bởi một công việc hàng loạt, GUI hay API REST.


Trong một ngôn ngữ hoàn toàn có chức năng như Haskell, bạn có thể mô hình hóa trình xử lý lệnh như thế này:

newtype CommandHandler = CommandHandler {handleCommand :: Command -> IO Result)
data Result a = Success a | Failure Reason
type Reason = String

discardDraftDocumentCommandHandler = CommandHandler handle
    where handle (DiscardDraftDocument documentID userID) = do
              document <- loadDocument documentID
              let result = discard document userID :: Result [Event]
              case result of
                   Success events -> publishEvents events >> return result
                   -- in an event-sourced model, there's no extra step to save the document
                   Failure _ -> return result
          handle _ = return $ Failure "I expected a DiscardDraftDocument command"

Đây là phần tôi đang đấu tranh để hiểu. Thông thường, sẽ có một số loại mã 'trình bày' gọi vào trình xử lý lệnh, như GUI hoặc API REST. Vì vậy, bây giờ chúng tôi có hai lớp trong chương trình của chúng tôi cần thực hiện IO - trình xử lý lệnh và khung nhìn - đó là một điều không lớn trong Haskell.

Theo như tôi có thể nhận ra, có hai lực lượng đối lập ở đây: một là tách biệt mô hình / khung nhìn và hai là cần phải duy trì mô hình. Cần phải có mã IO để duy trì mô hình ở đâu đó , nhưng tách mô hình / khung nhìn nói rằng chúng ta không thể đặt nó trong lớp trình bày với tất cả các mã IO khác.

Tất nhiên, trong một ngôn ngữ "bình thường", IO có thể (và không) xảy ra ở bất cứ đâu. Thiết kế tốt chỉ ra rằng các loại IO khác nhau được giữ riêng biệt, nhưng trình biên dịch không thực thi nó.

Vậy: làm thế nào để chúng ta điều hòa sự tách biệt mô hình / khung nhìn với mong muốn đẩy mã IO đến tận cùng của chương trình, khi mô hình cần được duy trì? Làm thế nào để chúng ta giữ hai loại IO khác nhau , nhưng vẫn cách xa tất cả các mã thuần túy?


Cập nhật : Tiền thưởng hết hạn sau chưa đầy 24 giờ. Tôi không cảm thấy rằng một trong những câu trả lời hiện tại đã giải quyết được câu hỏi của tôi. Nhận xét về ngọn lửa của @ Ptharien acid-statecó vẻ đầy hứa hẹn, nhưng đó không phải là câu trả lời và nó thiếu chi tiết. Tôi ghét những điểm này để lãng phí!


1
Có lẽ sẽ hữu ích khi xem xét thiết kế của các thư viện kiên trì khác nhau trong Haskell; đặc biệt, acid-statedường như gần với những gì bạn đang mô tả .
Ngọn lửa của Ptharien

1
acid-statetrông khá tuyệt, cảm ơn vì liên kết đó Về mặt thiết kế API, nó dường như vẫn bị ràng buộc IO; câu hỏi của tôi là làm thế nào một khung kiên trì phù hợp với một kiến ​​trúc lớn hơn. Bạn có biết bất kỳ ứng dụng nguồn mở nào sử dụng acid-statecùng với một lớp trình bày và thành công trong việc giữ hai phần riêng biệt không?
Benjamin Hodgson

Các QueryUpdatemonads được khá xa rời IO, trên thực tế. Tôi sẽ cố gắng đưa ra một ví dụ đơn giản trong một câu trả lời.
Ngọn lửa của Ptharien

Có nguy cơ lạc đề, đối với bất kỳ độc giả nào đang sử dụng mẫu Command / Handler theo cách này, tôi thực sự khuyên bạn nên kiểm tra Akka.NET. Các mô hình diễn viên cảm thấy như một phù hợp tốt ở đây. Có một khóa học tuyệt vời cho nó về Pluralsight. (Tôi thề tôi chỉ là một fanboy, không phải bot quảng cáo.)
RJB

Câu trả lời:


6

Cách chung để tách các thành phần trong Haskell là thông qua các ngăn máy biến áp đơn nguyên. Tôi giải thích điều này chi tiết hơn dưới đây.

Hãy tưởng tượng chúng ta đang xây dựng một hệ thống có một số thành phần quy mô lớn:

  • một thành phần nói chuyện với đĩa hoặc cơ sở dữ liệu (mô hình con)
  • một thành phần thực hiện các phép biến đổi trên miền (mô hình) của chúng tôi
  • một thành phần tương tác với người dùng (xem)
  • một thành phần mô tả kết nối giữa khung nhìn, mô hình và mô hình con (bộ điều khiển)
  • một thành phần khởi động toàn bộ hệ thống (trình điều khiển)

Chúng tôi quyết định rằng chúng tôi cần giữ cho các thành phần này được ghép lỏng lẻo để duy trì kiểu mã tốt.

Do đó, chúng tôi mã hóa từng thành phần của chúng tôi một cách đa hình, sử dụng các lớp MTL khác nhau để hướng dẫn chúng tôi:

  • mọi hàm trong mô hình con là loại MonadState DataState m => Foo -> Bar -> ... -> m Baz
    • DataState là một đại diện thuần túy của một ảnh chụp nhanh về trạng thái cơ sở dữ liệu hoặc lưu trữ của chúng tôi
  • mọi chức năng trong mô hình là thuần túy
  • mọi chức năng trong khung nhìn là kiểu MonadState UIState m => Foo -> Bar -> ... -> m Baz
    • UIState là một đại diện thuần túy của một ảnh chụp nhanh về trạng thái giao diện người dùng của chúng tôi
  • mọi chức năng trong bộ điều khiển là loại MonadState (DataState, UIState) m => Foo -> Bar -> ... -> m Baz
    • Lưu ý rằng bộ điều khiển có quyền truy cập vào cả trạng thái của khung nhìn và trạng thái của mô hình con
  • trình điều khiển chỉ có một định nghĩa, main :: IO ()đó là công việc gần như tầm thường khi kết hợp các thành phần khác vào một hệ thống
    • khung nhìn và mô hình con sẽ cần phải được nâng lên cùng loại trạng thái như bộ điều khiển sử dụng zoomhoặc một bộ kết hợp tương tự
    • mô hình là thuần túy, và do đó có thể được sử dụng mà không bị hạn chế
    • cuối cùng, mọi thứ đều tồn tại (một loại tương thích với) StateT (DataState, UIState) IO, sau đó được chạy với nội dung thực tế của cơ sở dữ liệu hoặc bộ lưu trữ để sản xuất IO.

1
Đây là lời khuyên tuyệt vời, và chính xác những gì tôi đang tìm kiếm. Cảm ơn!
Benjamin Hodgson

2
Tôi đang tiêu hóa câu trả lời này. Xin vui lòng làm rõ vai trò của 'mô hình con' trong kiến ​​trúc này? Làm thế nào để nó "nói chuyện với đĩa hoặc cơ sở dữ liệu" mà không thực hiện IO? Tôi đặc biệt bối rối về ý của bạn bởi " DataStatelà một đại diện thuần túy của một ảnh chụp nhanh về trạng thái cơ sở dữ liệu hoặc lưu trữ của chúng tôi". Có lẽ bạn không có nghĩa là tải toàn bộ cơ sở dữ liệu vào bộ nhớ!
Benjamin Hodgson

1
Tôi hoàn toàn thích nhìn thấy suy nghĩ của bạn về việc triển khai C # của logic này. Đừng cho rằng tôi có thể mua chuộc bạn bằng một upvote? ;-)
RJB

1
@RJB Thật không may, bạn sẽ phải hối lộ nhóm phát triển C # để cho phép các loại ngôn ngữ cao hơn, bởi vì không có chúng, kiến ​​trúc này sẽ hơi thất bại.
Ngọn lửa của Ptharien

4

Vậy: làm thế nào để chúng ta điều hòa sự tách biệt mô hình / khung nhìn với mong muốn đẩy mã IO đến tận cùng của chương trình, khi mô hình cần được duy trì?

Mô hình có nên được kiên trì? Trong nhiều chương trình, việc lưu mô hình là bắt buộc vì trạng thái không thể đoán trước, mọi thao tác có thể làm thay đổi mô hình theo bất kỳ cách nào, vì vậy cách duy nhất để biết trạng thái của mô hình là truy cập trực tiếp vào mô hình.

Nếu trong kịch bản của bạn, chuỗi các sự kiện (các lệnh đã được xác thực và chấp nhận) luôn có thể tạo trạng thái, thì đó là các sự kiện cần được duy trì, không nhất thiết phải là trạng thái. Trạng thái luôn có thể được tạo bằng cách phát lại các sự kiện.

Phải nói rằng, thường thì trạng thái được lưu trữ, nhưng chỉ như một ảnh chụp nhanh / bộ đệm để tránh phát lại các lệnh, không phải là dữ liệu chương trình thiết yếu.

Vì vậy, bây giờ chúng tôi có hai lớp trong chương trình của chúng tôi cần thực hiện IO - trình xử lý lệnh và khung nhìn - đó là một điều không lớn trong Haskell.

Sau khi lệnh được chấp nhận, sự kiện được truyền đến hai đích (bộ lưu trữ sự kiện và hệ thống báo cáo) nhưng ở cùng một lớp của chương trình.

Xem Ngoài ra
tổ chức sự kiện Sourcing
Háo hức đọc thức chiết khấu


2
Tôi quen thuộc với việc tìm nguồn cung ứng sự kiện (tôi đang sử dụng nó trong ví dụ của tôi ở trên!) Và để tránh bị chẻ sợi tóc tôi vẫn nói rằng tìm nguồn cung ứng sự kiện là một cách tiếp cận vấn đề của sự kiên trì. Trong mọi trường hợp, tìm nguồn cung ứng sự kiện không làm giảm nhu cầu tải lên các đối tượng miền của bạn trong trình xử lý lệnh . Trình xử lý lệnh không biết liệu các đối tượng đến từ luồng sự kiện, ORM hay thủ tục được lưu trữ - nó chỉ lấy nó từ kho lưu trữ.
Benjamin Hodgson

1
Sự hiểu biết của bạn dường như kết hợp khung nhìn và trình xử lý lệnh với nhau để tạo ra nhiều IO. Hiểu biết của tôi là người xử lý tạo ra sự kiện và không có hứng thú nữa. Khung nhìn trong trường hợp này hoạt động như một mô-đun riêng biệt (ngay cả khi về mặt kỹ thuật trong cùng một ứng dụng) và không được kết hợp với trình xử lý lệnh.
FMJaguar

1
Tôi nghĩ rằng chúng ta có thể đang nói chuyện với mục đích chéo. Khi tôi nói 'view', tôi đang nói về toàn bộ lớp trình bày, có thể là API REST hoặc hệ thống điều khiển chế độ xem mô hình. (Tôi đồng ý rằng khung nhìn nên được tách rời khỏi mô hình trong mẫu MVC.) Về cơ bản tôi có nghĩa là "bất kỳ cuộc gọi nào vào trình xử lý lệnh".
Benjamin Hodgson

2

Bạn đang cố gắng đặt không gian vào ứng dụng chuyên sâu IO của mình cho tất cả các hoạt động không phải IO; Thật không may, các ứng dụng CRUD điển hình như bạn nói về làm ít hơn IO.

Tôi nghĩ rằng bạn hiểu rõ sự phân tách có liên quan, nhưng khi bạn đang cố gắng đặt mã IO liên tục ở một số lớp cách xa mã trình bày, thì thực tế chung của vấn đề nằm ở bộ điều khiển của bạn ở đâu đó bạn nên gọi cho lớp kiên trì, có thể cảm thấy quá gần với bài thuyết trình của bạn với bạn - nhưng đó chỉ là sự trùng hợp ngẫu nhiên trong loại ứng dụng đó có chút khác với nó.

Trình bày và kiên trì tạo nên về cơ bản toàn bộ loại ứng dụng tôi nghĩ bạn đang mô tả ở đây.

Nếu bạn nghĩ trong đầu về một ứng dụng tương tự có nhiều logic kinh doanh và xử lý dữ liệu phức tạp trong đó, tôi nghĩ bạn sẽ thấy mình có thể tưởng tượng cách tách biệt khỏi IO hiện tại và các công cụ IO bền bỉ như vậy nó không cần biết gì về cả Vấn đề bạn gặp phải bây giờ chỉ là vấn đề nhận thức do cố gắng xem giải pháp cho vấn đề trong một loại ứng dụng không có vấn đề đó để bắt đầu.


1
Bạn đang nói rằng các hệ thống CRUD sẽ kết hợp với sự kiên trì và trình bày. Điều này có vẻ hợp lý với tôi; tuy nhiên tôi đã không đề cập đến CRUD. Tôi đặc biệt hỏi về DDD, nơi bạn có các đối tượng kinh doanh phù hợp với các tương tác phức tạp, một lớp kiên trì (trình xử lý lệnh) và một lớp trình bày trên đó. Làm thế nào để bạn giữ hai lớp IO riêng biệt trong khi duy trì lớp bọc IO mỏng ?
Benjamin Hodgson

1
NB, tên miền tôi mô tả trong câu hỏi thể rất phức tạp. Có lẽ việc loại bỏ tài liệu nháp phải chịu một số kiểm tra quyền liên quan hoặc nhiều phiên bản của cùng một bản nháp có thể cần phải xử lý hoặc cần gửi thông báo hoặc hành động cần được người dùng khác phê duyệt hoặc dự thảo thông qua một số giai đoạn vòng đời trước khi hoàn thành ...
Benjamin Hodgson

2
@BenjaminHodgson Tôi thực sự khuyên bạn không nên trộn lẫn DDD hoặc các phương pháp thiết kế OO vốn có khác vào tình huống này trong đầu bạn, điều đó sẽ chỉ gây nhầm lẫn. Mặc dù có, bạn có thể tạo đối tượng như bit và bobble trong FP thuần túy, các phương pháp thiết kế dựa trên chúng không nhất thiết phải là tầm với đầu tiên của bạn. Trong kịch bản mà bạn mô tả tôi sẽ hình dung như tôi đã đề cập ở trên, một bộ điều khiển giao tiếp giữa hai IO và mã thuần: Trình bày IO đi vào và được yêu cầu từ bộ điều khiển, bộ điều khiển chuyển mọi thứ xuống các phần thuần túy và đến các phần bền.
Jimmy Hoffa

1
@BenjaminHodgson bạn có thể tưởng tượng một bong bóng nơi tất cả các mã thuần túy của bạn sống, với tất cả các lớp và sự huyền ảo mà bạn có thể muốn trong bất kỳ thiết kế nào bạn đánh giá cao. Điểm vào của bong bóng này sẽ là một mảnh nhỏ mà tôi đang gọi là "bộ điều khiển" (có lẽ không chính xác), giao tiếp giữa cách trình bày, sự kiên trì và các mảnh thuần túy. Bằng cách này, sự kiên trì của bạn không biết gì về cách trình bày hoặc thuần túy và ngược lại - và điều này giữ cho công cụ IO của bạn trong lớp mỏng này bên trên bong bóng của hệ thống thuần túy của bạn.
Jimmy Hoffa

2
@BenjaminHodgson Cách tiếp cận "đối tượng thông minh" này mà bạn nói đến vốn dĩ là một cách tiếp cận tồi đối với FP, vấn đề với các đối tượng thông minh trong FP là chúng kết hợp quá nhiều và khái quát quá ít. Bạn kết thúc với dữ liệu và chức năng gắn liền với dữ liệu đó, trong đó FP thích dữ liệu của bạn khớp nối lỏng lẻo với chức năng để bạn có thể thực hiện các chức năng của mình để được khái quát hóa và sau đó chúng sẽ hoạt động trên nhiều loại dữ liệu. Hãy đọc câu trả lời của tôi ở đây: lập trình
viên.stackexchange.com

1

Gần như tôi có thể hiểu câu hỏi của bạn (mà tôi có thể không, nhưng tôi nghĩ rằng tôi sẽ ném 2 xu của mình), vì bạn không nhất thiết phải có quyền truy cập vào các đối tượng, bạn cần phải có cơ sở dữ liệu đối tượng của riêng mình. hết hạn theo thời gian).

Lý tưởng nhất là bản thân các đối tượng có thể được tăng cường để lưu trữ trạng thái của chúng để khi chúng được "chuyển xung quanh", các bộ xử lý lệnh khác nhau sẽ biết chúng đang làm việc với cái gì.

Nếu điều đó là không thể, (icky icky), cách duy nhất là có một số khóa giống như DB thông thường, mà bạn có thể sử dụng để lưu trữ thông tin trong một cửa hàng được thiết lập để có thể chia sẻ giữa các lệnh khác nhau - và hy vọng, "mở" giao diện và / hoặc mã để bất kỳ người viết lệnh nào khác cũng sẽ chấp nhận giao diện của bạn để lưu và xử lý thông tin meta.

Trong khu vực của các máy chủ tệp, samba có các cách khác nhau để lưu trữ những thứ như danh sách truy cập và luồng dữ liệu thay thế, tùy thuộc vào những gì hệ điều hành máy chủ cung cấp. Lý tưởng nhất, samba đang được lưu trữ trên một hệ thống tệp cung cấp các thuộc tính mở rộng trên các tệp. Ví dụ 'xfs' trên 'linux' - nhiều lệnh hơn đang sao chép các thuộc tính mở rộng cùng với một tệp (theo mặc định, hầu hết các tiện ích trên linux "lớn lên" sẽ giống như các thuộc tính mở rộng).

Một giải pháp thay thế - hoạt động cho nhiều quy trình samba từ những người dùng khác nhau hoạt động trên các tệp chung (đối tượng), là nếu hệ thống tệp không hỗ trợ đính kèm tài nguyên trực tiếp vào tệp như với các thuộc tính mở rộng, đang sử dụng mô-đun thực hiện một lớp hệ thống tệp ảo để mô phỏng các thuộc tính mở rộng cho các quy trình samba. Chỉ samba biết về nó, nhưng nó có lợi thế khi hoạt động khi định dạng đối tượng không hỗ trợ nó, nhưng vẫn hoạt động với những người dùng samba khác nhau (bộ xử lý lệnh), những người thực hiện một số công việc trên tệp dựa trên trạng thái trước đó. Nó sẽ lưu trữ thông tin meta trong cơ sở dữ liệu chung cho hệ thống tệp giúp kiểm soát kích thước của cơ sở dữ liệu (và không '

Nó có thể không hữu ích cho bạn nếu bạn cần thêm thông tin cụ thể cho việc triển khai bạn đang làm việc, nhưng về mặt khái niệm, cùng một lý thuyết có thể được áp dụng cho cả hai bộ vấn đề. Vì vậy, nếu bạn đang tìm kiếm các thuật toán và phương pháp để làm những gì bạn muốn, điều đó có thể giúp ích. Nếu bạn cần kiến ​​thức cụ thể hơn trong một số khung cụ thể, thì có lẽ không hữu ích lắm ... ;-)

BTW - lý do tôi đề cập đến 'tự hết hạn' - là không rõ ràng nếu bạn biết những vật thể nào ở ngoài đó và chúng tồn tại bao lâu. Nếu bạn không có cách trực tiếp để biết khi nào một đối tượng bị xóa, bạn phải cắt metaDB của riêng mình để ngăn không cho nó điền thông tin meta cũ hoặc cũ mà người dùng đã xóa từ lâu đối tượng.

Nếu bạn biết khi nào các đối tượng hết hạn / bị xóa, thì bạn đang ở phía trước của trò chơi và có thể hết hạn nó khỏi metaDB của bạn cùng một lúc, nhưng không rõ ràng nếu bạn có tùy chọn đó.

Chúc mừng!


1
Đối với tôi, đây dường như là một câu trả lời cho một câu hỏi hoàn toàn khác. Tôi đang tìm kiếm lời khuyên liên quan đến kiến ​​trúc trong lập trình đơn thuần, trong bối cảnh thiết kế hướng tên miền. Bạn có thể làm rõ quan điểm của bạn xin vui lòng?
Benjamin Hodgson

Bạn đang hỏi về sự kiên trì dữ liệu trong một mô hình lập trình hoàn toàn chức năng. Trích dẫn Wikipedia: "Chức năng thuần túy là một thuật ngữ trong điện toán được sử dụng để mô tả các thuật toán, cấu trúc dữ liệu hoặc ngôn ngữ lập trình loại trừ các sửa đổi phá hủy (cập nhật) của các thực thể trong môi trường chạy chương trình." ==== Theo định nghĩa, việc lưu giữ dữ liệu là không liên quan và không có tác dụng gì với việc sửa đổi không có dữ liệu. Nói đúng ra không có câu trả lời cho câu hỏi của bạn. Tôi đã cố gắng giải thích lỏng lẻo hơn về những gì bạn đã viết.
Astara
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.