Có còn hợp lệ để nói về mô hình thiếu máu trong bối cảnh lập trình chức năng không?


40

Hầu hết các mẫu thiết kế DDD chiến thuật thuộc về mô hình hướng đối tượng, và mô hình thiếu máu mô tả các tình huống khi tất cả các logic kinh doanh được đưa vào dịch vụ chứ không phải là đối tượng do đó làm cho chúng một loại DTO. Nói cách khác, mô hình thiếu máu là một từ đồng nghĩa của phong cách thủ tục, không được khuyên dùng cho mô hình phức tạp.

Tôi không phải là rất có kinh nghiệm trong lập trình chức năng thuần túy, nhưng tôi muốn biết làm thế nào DDD phù hợp vào FP mô và liệu 'mô hình thiếu máu' hạn vẫn còn tồn tại trong trường hợp đó.

Cập nhật : Recentlry xuất bản cuốn sáchvideo về chủ đề này.


1
Nếu bạn đang nói những gì tôi nghĩ rằng bạn đang nói ở đây, của DTO bị thiếu máu, nhưng trước tiên-lớp đối tượng trong DDD, và rằng có một sự tách biệt tự nhiên giữa của DTO và các dịch vụ mà xử lý chúng. Tôi đồng ý về nguyên tắc. Blog này cũng vậy , rõ ràng.
Robert Harvey


1
"Liệu thuật ngữ" mô hình thiếu máu "có còn tồn tại trong trường hợp đó không" Câu trả lời ngắn, thuật ngữ mô hình thiếu máu được đặt ra trong bối cảnh của OO. Nói về một mô hình thiếu máu trong bối cảnh của FP không có ý nghĩa gì cả. Có thể có sự tương đương trong ý nghĩa của việc mô tả thế nào là thành ngữ FP, nhưng nó không liên quan gì đến các mô hình thiếu máu.
plalx

5
Eric Evans đã từng được hỏi những gì anh ta nói với những người buộc tội anh ta rằng những gì anh ta mô tả trong cuốn sách của anh ta chỉ là thiết kế hướng đối tượng tốt, và anh ta trả lời rằng đó không phải là một lời buộc tội, đó là sự thật, DDD chỉ là tốt, anh ta chỉ viết xuống một số công thức và mô hình và đặt tên cho chúng, để dễ dàng theo dõi chúng và nói về chúng. Vì vậy, không có gì ngạc nhiên khi DDD được liên kết với OOD. Câu hỏi rộng hơn sẽ là, là gì nút giao thông và sự khác biệt giữa OOD và Chi cục Kiểm lâm, mặc dù bạn sẽ phải xác định những gì bạn có nghĩa là bằng cách "lập trình chức năng" đầu tiên.
Jörg W Mittag

2
@ JörgWMittag: Ý bạn là khác với định nghĩa thông thường? Có rất nhiều nền tảng minh họa, Haskell là nền tảng rõ ràng nhất.
Robert Harvey

Câu trả lời:


24

Cách mô tả vấn đề "mô hình thiếu máu" không được dịch tốt cho FP. Đầu tiên nó cần phải được khái quát phù hợp. Thực chất, một mô hình thiếu máu là một mô hình chứa đựng kiến ​​thức về cách sử dụng đúng cách mà mô hình không được gói gọn trong chính mô hình đó. Thay vào đó, kiến ​​thức đó được lan truyền xung quanh một đống dịch vụ liên quan. Những dịch vụ đó chỉ nên là khách hàng của mô hình, nhưng do thiếu máu nên họ phải chịu trách nhiệm về nó. Ví dụ: xem xét một Accountlớp không thể được sử dụng để kích hoạt hoặc hủy kích hoạt tài khoản hoặc thậm chí tra cứu thông tin về tài khoản trừ khi được xử lý thông qua một AccountManagerlớp. Tài khoản phải chịu trách nhiệm cho các hoạt động cơ bản trên nó, không phải một số lớp quản lý bên ngoài.

Trong lập trình chức năng, một vấn đề tương tự tồn tại khi các kiểu dữ liệu không thể hiện chính xác những gì chúng được cho là mô hình hóa. Giả sử chúng ta cần xác định một loại đại diện cho ID người dùng. Một định nghĩa "thiếu máu" sẽ nói rằng ID người dùng là các chuỗi. Điều đó khả thi về mặt kỹ thuật, nhưng gặp phải vấn đề lớn vì ID người dùng không được sử dụng như các chuỗi tùy ý. Thật vô nghĩa khi ghép chúng hoặc cắt bỏ các chuỗi con của chúng, Unicode không thực sự quan trọng và chúng có thể dễ dàng nhúng vào URL và các bối cảnh khác với các giới hạn về ký tự và định dạng nghiêm ngặt.

Giải quyết vấn đề này thường xảy ra trong một vài giai đoạn. Cách cắt đầu tiên đơn giản là nói "Chà, a UserIDđược biểu diễn tương đương với một chuỗi, nhưng chúng là các loại khác nhau và bạn không thể sử dụng một trong những nơi bạn mong đợi khác." Haskell (và một số ngôn ngữ chức năng đã nhập khác) cung cấp tính năng này thông qua newtype:

newtype UserID = UserID String

Điều này xác định một UserIDhàm mà khi được cung cấp sẽ Stringxây dựng một giá trị được xử lý như một UserIDhệ thống loại, nhưng vẫn chỉ là một Stringthời gian chạy. Bây giờ các hàm có thể khai báo rằng chúng yêu cầu UserIDthay vì một chuỗi; sử dụng UserIDs nơi mà trước đây bạn đang sử dụng các bộ bảo vệ chuỗi chống lại mã nối hai UserIDs với nhau. Hệ thống loại đảm bảo không thể xảy ra, không cần kiểm tra.

Điểm yếu ở đây là mã vẫn có thể lấy bất kỳ tùy ý Stringnào "hello"và xây dựng một UserIDtừ nó. Các bước tiếp theo bao gồm tạo hàm "hàm tạo thông minh" mà khi được cung cấp một chuỗi sẽ kiểm tra một số bất biến và chỉ trả về UserIDnếu chúng hài lòng. Sau đó, hàm tạo "câm" UserIDđược đặt ở chế độ riêng tư, vì vậy nếu khách hàng muốn, UserIDhọ phải sử dụng hàm tạo thông minh, do đó ngăn các ID người dùng không đúng định dạng xuất hiện.

Thậm chí các bước tiếp theo xác định UserIDloại dữ liệu theo cách không thể xây dựng một kiểu không đúng hoặc "không đúng", chỉ đơn giản là theo định nghĩa. Chẳng hạn, định nghĩa UserIDmột danh sách các chữ số:

data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine
data UserID = UserID [Digit]

Để xây dựng một UserIDdanh sách các chữ số phải được cung cấp. Đưa ra định nghĩa này, thật tầm thường khi cho thấy rằng không thể UserIDtồn tại mà không thể được thể hiện trong một URL. Việc xác định các mô hình dữ liệu như thế này trong Haskell thường được hỗ trợ bởi các tính năng hệ thống loại nâng cao như Kiểu dữ liệuKiểu dữ liệu đại số tổng quát (GADTs) , cho phép hệ thống loại xác định và chứng minh nhiều bất biến hơn về mã của bạn. Khi dữ liệu được tách rời khỏi hành vi, định nghĩa dữ liệu của bạn là phương tiện duy nhất bạn phải thực thi hành vi.


2
Và những gì về tổng hợp và gốc tổng hợp bảo vệ bất biến, thì những cái cuối cùng cũng có thể được thể hiện và dễ dàng hiểu được bởi các nhà phát triển sau đó? Đối với tôi, phần có giá trị nhất của DDD là ánh xạ trực tiếp mô hình kinh doanh vào mã. Và bạn trả lời là chính xác về điều đó.
Pavel Voronin

2
Bài phát biểu hay, nhưng không có câu trả lời cho câu hỏi OP.
SerG

10

Ở một mức độ lớn, tính bất biến khiến không cần thiết phải kết hợp chặt chẽ các chức năng của bạn với dữ liệu của bạn khi OOP ủng hộ. Bạn có thể tạo bao nhiêu bản sao tùy thích, thậm chí tạo các cấu trúc dữ liệu phái sinh, trong mã bị loại bỏ khỏi mã gốc, mà không sợ cấu trúc dữ liệu gốc bất ngờ thay đổi từ bên dưới bạn.

Tuy nhiên, một cách tốt hơn để thực hiện so sánh này có lẽ là xem xét các chức năng bạn đang phân bổ cho lớp mô hình so với lớp dịch vụ . Mặc dù trông không giống như trong OOP, nhưng đó là một lỗi khá phổ biến trong FP khi cố gắng nhồi nhét những gì nên có nhiều mức độ trừu tượng vào một chức năng.

Theo tôi biết, không ai gọi nó là mô hình thiếu máu, vì đó là thuật ngữ OOP, nhưng hiệu quả là như nhau. Bạn có thể và nên sử dụng lại các hàm chung bất cứ nơi nào có thể áp dụng, nhưng đối với các hoạt động phức tạp hơn hoặc dành riêng cho ứng dụng, bạn cũng nên cung cấp một bộ hàm phong phú chỉ để làm việc với mô hình của mình. Tạo các lớp trừu tượng thích hợp là thiết kế tốt trong bất kỳ mô hình nào.


2
"Ở một mức độ lớn, tính bất biến khiến không cần thiết phải kết hợp chặt chẽ các chức năng của bạn với dữ liệu của bạn khi OOP ủng hộ.": Một lý do khác để ghép dữ liệu và quy trình là thực hiện đa hình mặc dù công văn động.
Giorgio

2
Ưu điểm chính của hành vi ghép nối với dữ liệu trong ngữ cảnh của DDD là cung cấp giao diện liên quan đến kinh doanh có ý nghĩa; nó luôn luôn trong tầm tay. Chúng tôi có một cách tự nhiên để tự viết mã (ít nhất đó là những gì tôi đã quen) và đó là chìa khóa để giao tiếp thành công với các chuyên gia kinh doanh. Làm thế nào sau đó nó được thực hiện trong FP? Có lẽ đường ống giúp, nhưng những gì khác? Không phải bản chất chung của FP làm cho các yêu cầu kinh doanh khó khăn hơn để đảo ngược kỹ sư từ mã?
Pavel Voronin

7

Khi sử dụng DDD trong OOP, một trong những lý do chính để đưa logic nghiệp vụ vào chính các đối tượng miền là logic nghiệp vụ thường được áp dụng bằng cách thay đổi trạng thái của đối tượng. Điều này có liên quan đến đóng gói: Employee.RaiseSalarycó thể làm thay đổi salarytrường của Employeetrường hợp, không nên giải quyết công khai.

Trong FP, tránh đột biến, vì vậy bạn sẽ thực hiện hành vi này bằng cách tạo một RaiseSalaryhàm lấy một thể hiện hiện có Employeevà trả về một thể hiện mới Employee với mức lương mới. Vì vậy, không có đột biến liên quan: chỉ đọc từ đối tượng ban đầu và tạo đối tượng mới. Vì lý do này, một RaiseSalaryhàm như vậy không cần phải được định nghĩa là một phương thức trên Employeelớp, nhưng có thể sống ở bất cứ đâu.

Trong trường hợp này, việc tách dữ liệu khỏi hành vi trở nên tự nhiên: một cấu trúc đại diện cho Employeedữ liệu dưới dạng dữ liệu (hoàn toàn thiếu máu), trong khi một (hoặc một số) mô-đun chứa các chức năng hoạt động trên dữ liệu đó (bảo toàn tính bất biến).

Lưu ý rằng khi bạn kết hợp dữ liệu và hành vi như trong DDD, bạn thường vi phạm Nguyên tắc Trách nhiệm duy nhất (SRP): Employeecó thể cần thay đổi nếu quy tắc thay đổi tiền lương thay đổi; nhưng nó cũng có thể cần thay đổi nếu các quy tắc tính toán tiền thưởng thay đổi. Với cách tiếp cận tách rời, đây không phải là trường hợp, vì bạn có thể có một vài mô-đun, mỗi mô-đun có một trách nhiệm.

Vì vậy, như thường lệ, cách tiếp cận FP cung cấp tính mô đun / khả năng kết hợp lớn hơn.


-1

Tôi nghĩ rằng cốt lõi của vấn đề là một mô hình thiếu máu với tất cả logic miền trong các dịch vụ hoạt động trên mô hình về cơ bản là lập trình thủ tục - trái ngược với lập trình OO "thực" nơi bạn có các đối tượng "thông minh" và không chỉ chứa dữ liệu nhưng cũng là logic gắn chặt nhất với dữ liệu.

Và sự tương phản tương tự tồn tại với lập trình chức năng: FP "thực" có nghĩa là sử dụng các hàm như các thực thể hạng nhất, được truyền xung quanh dưới dạng tham số, cũng như được xây dựng khi đang bay và trả về làm giá trị trả về. Nhưng khi bạn không sử dụng tất cả sức mạnh đó và chỉ có các chức năng hoạt động trên các cấu trúc dữ liệu được truyền xung quanh chúng, thì bạn sẽ ở cùng một nơi: về cơ bản bạn đang thực hiện lập trình thủ tục.


5
Vâng, về cơ bản đó là những gì OP nói trong câu hỏi của anh ấy. Cả hai bạn dường như đã bỏ lỡ điểm mà bạn vẫn có thể có thành phần chức năng.
Robert Harvey

-3

Tôi muốn biết làm thế nào DDD phù hợp với mô hình FP

Tôi nghĩ là có, nhưng phần lớn là một cách tiếp cận chiến thuật để chuyển đổi giữa các Đối tượng Giá trị bất biến hoặc như một cách để kích hoạt các phương thức trên các thực thể. (Trường hợp phần lớn logic vẫn sống trong thực thể.)

và liệu thuật ngữ 'mô hình thiếu máu' có còn tồn tại trong trường hợp đó hay không.

Chà, nếu bạn có nghĩa là "theo cách tương tự với OOP truyền thống", thì nó sẽ giúp bỏ qua các chi tiết triển khai thông thường và quay trở lại vấn đề cơ bản: Chuyên gia tên miền của bạn sử dụng ngôn ngữ nào? Có gì ý định chụp của bạn từ người dùng của bạn?

Giả sử họ nói về các quá trình xâu chuỗi và các chức năng với nhau, thì có vẻ như các hàm (hoặc ít nhất là các đối tượng "do-er") về cơ bản các đối tượng miền của bạn!

Vì vậy, trong trường hợp đó, một "mô hình thiếu máu" có lẽ sẽ xảy ra khi bạn "chức năng" không thực sự thực thi, và thay vào đó chỉ là chòm sao của siêu dữ liệu được giải thích bởi một dịch vụ mà hiện các công việc thực tế.


1
Một mô hình thiếu máu sẽ xảy ra khi bạn chuyển các loại dữ liệu trừu tượng xung quanh, chẳng hạn như bộ dữ liệu, bản ghi hoặc danh sách, cho các chức năng khác nhau để xử lý. Bạn không cần bất cứ thứ gì kỳ lạ như một "chức năng không thực thi" (bất kể đó là gì).
Robert Harvey

Do đó, dấu ngoặc kép quanh "các chức năng", để nhấn mạnh mức độ không phù hợp của nhãn khi chúng bị thiếu máu.
Darien

Nếu bạn đang mỉa mai, đó là một chút tinh tế.
Robert Harvey
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.