Là lập trình chức năng là một thay thế khả thi cho các mẫu tiêm phụ thuộc?


21

Gần đây tôi đã đọc một cuốn sách có tên Lập trình chức năng trong C # và tôi nhận thấy rằng bản chất bất biến và không trạng thái của lập trình hàm thực hiện các kết quả tương tự với các mẫu tiêm phụ thuộc và thậm chí có thể là một cách tiếp cận tốt hơn, đặc biệt là về kiểm thử đơn vị.

Tôi sẽ đánh giá cao nếu bất cứ ai có kinh nghiệm với cả hai phương pháp đều có thể chia sẻ suy nghĩ và kinh nghiệm của họ để trả lời câu hỏi chính: Lập trình chức năng có thể thay thế cho mô hình tiêm phụ thuộc không?


10
Điều này không có ý nghĩa nhiều với tôi, sự bất biến không loại bỏ sự phụ thuộc.
Telastyn 10/03/2015

Tôi đồng ý rằng nó không loại bỏ sự phụ thuộc. Có lẽ sự hiểu biết của tôi là không chính xác, nhưng tôi đã suy luận điều đó bởi vì nếu tôi không thể thay đổi đối tượng ban đầu, thì bắt buộc tôi phải chuyển nó (tiêm nó) cho bất kỳ chức năng nào sử dụng nó.
Matt Cashatt 10/03/2015


5
Ngoài ra còn có Cách đưa các lập trình viên OO vào lập trình chức năng yêu thương , đây thực sự là một phân tích chi tiết về DI từ cả góc độ OO và FP.
Robert Harvey

1
Câu hỏi này, các bài viết mà nó liên kết đến và câu trả lời được chấp nhận cũng có thể hữu ích: stackoverflow.com/questions/11276319/ Đá Bỏ qua từ Monad đáng sợ. Như Runar chỉ ra trong câu trả lời của mình, đây không phải là một khái niệm phức tạp trong trường hợp này (chỉ là một chức năng).
itsbruce

Câu trả lời:


27

Quản lý phụ thuộc là một vấn đề lớn trong OOP vì hai lý do sau:

  • Sự kết hợp chặt chẽ của dữ liệu và mã.
  • Sử dụng phổ biến các tác dụng phụ.

Hầu hết các lập trình viên OO coi việc kết hợp chặt chẽ giữa dữ liệu và mã là hoàn toàn có lợi, nhưng nó đi kèm với một chi phí. Quản lý luồng dữ liệu qua các lớp là một phần không thể tránh khỏi của lập trình trong bất kỳ mô hình nào. Kết hợp dữ liệu và mã của bạn thêm một vấn đề nữa là nếu bạn muốn sử dụng một hàm tại một điểm nhất định, bạn phải tìm cách đưa đối tượng của nó đến điểm đó.

Sử dụng các tác dụng phụ tạo ra những khó khăn tương tự. Nếu bạn sử dụng một tác dụng phụ cho một số chức năng, nhưng muốn có thể trao đổi việc thực hiện nó, bạn không có lựa chọn nào khác ngoài việc loại bỏ sự phụ thuộc đó.

Hãy xem xét như một ví dụ về một chương trình spam giúp loại bỏ các trang web cho các địa chỉ email sau đó gửi email cho họ. Nếu bạn có tư duy DI, ngay bây giờ bạn đang nghĩ về các dịch vụ bạn sẽ gói gọn đằng sau các giao diện và dịch vụ nào sẽ được đưa vào đâu. Tôi sẽ để lại thiết kế đó như một bài tập cho người đọc. Nếu bạn có tư duy FP, ngay bây giờ bạn đang nghĩ đến đầu vào và đầu ra cho lớp chức năng thấp nhất, như:

  • Nhập địa chỉ trang web, xuất văn bản của trang đó.
  • Nhập văn bản của một trang, xuất ra một danh sách các liên kết từ trang đó.
  • Nhập văn bản của trang, xuất danh sách địa chỉ email trên trang đó.
  • Nhập danh sách địa chỉ email, xuất danh sách địa chỉ email đã bị xóa.
  • Nhập địa chỉ email, xuất email spam cho địa chỉ đó.
  • Nhập email spam, xuất các lệnh SMTP để gửi email đó.

Khi bạn nghĩ về mặt đầu vào và đầu ra, không có phụ thuộc chức năng, chỉ có phụ thuộc dữ liệu. Đó là những gì làm cho chúng dễ dàng để kiểm tra đơn vị. Lớp tiếp theo của bạn sắp xếp để đầu ra của một chức năng được đưa vào đầu vào của lớp tiếp theo và có thể dễ dàng trao đổi các triển khai khác nhau khi cần.

Theo một nghĩa rất thực, lập trình chức năng tự nhiên thúc đẩy bạn luôn đảo ngược các phụ thuộc chức năng của mình và do đó bạn thường không phải thực hiện bất kỳ biện pháp đặc biệt nào để làm như vậy sau khi thực tế. Khi bạn làm như vậy, các công cụ như các hàm bậc cao hơn, các bao đóng và ứng dụng một phần sẽ giúp thực hiện dễ dàng hơn với ít nồi hơi hơn.

Lưu ý rằng bản thân nó không phụ thuộc mà có vấn đề. Đó là sự phụ thuộc chỉ ra sai cách. Lớp tiếp theo có thể có chức năng như:

processText = spamToSMTP . emailAddressToSpam . removeEmailDups . textToEmailAddresses

Lớp này hoàn toàn ổn khi có các phụ thuộc được mã hóa cứng như thế này, vì mục đích duy nhất của nó là dán các chức năng của lớp thấp hơn lại với nhau. Hoán đổi một triển khai cũng đơn giản như tạo ra một thành phần khác nhau:

processTextFancy = spamToSMTP . emailAddressToFancySpam . removeEmailDups . textToEmailAddresses

Sự tái tạo dễ dàng này được thực hiện bằng cách thiếu tác dụng phụ. Các chức năng lớp dưới hoàn toàn độc lập với nhau. Lớp tiếp theo có thể chọn lớp processTextthực sự được sử dụng dựa trên một số cấu hình người dùng:

actuallyUsedProcessText = if (config == "Fancy") then processTextFancy else processText

Một lần nữa, không phải là một vấn đề bởi vì tất cả các phụ thuộc chỉ ra một cách. Chúng ta không cần phải đảo ngược một số phụ thuộc để có được tất cả chúng theo cùng một cách, bởi vì các hàm thuần túy đã buộc chúng ta phải làm như vậy.

Lưu ý rằng bạn có thể làm cho điều này được kết hợp nhiều hơn bằng cách chuyển configxuống lớp thấp nhất thay vì kiểm tra nó ở trên cùng. FP không ngăn cản bạn làm điều này, nhưng nó có xu hướng làm cho nó khó chịu hơn rất nhiều nếu bạn cố gắng.


3
"Sử dụng các tác dụng phụ tạo ra những khó khăn tương tự. Nếu bạn sử dụng một tác dụng phụ cho một số chức năng, nhưng muốn có thể trao đổi việc thực hiện nó, bạn không có lựa chọn nào khác ngoài việc loại bỏ sự phụ thuộc đó." Tôi không nghĩ tác dụng phụ có liên quan đến điều này. Nếu bạn muốn trao đổi việc triển khai trong Haskell, bạn vẫn phải thực hiện tiêm phụ thuộc . Loại bỏ các lớp loại và bạn đang chuyển qua một giao diện làm đối số đầu tiên cho mọi hàm.
Doval

2
Mấu chốt của vấn đề là hầu hết mọi ngôn ngữ đều buộc bạn phải tham chiếu mã cứng đến các mô-đun mã khác, vì vậy cách duy nhất để trao đổi triển khai là sử dụng công văn động ở mọi nơi và sau đó bạn bị mắc kẹt khi giải quyết các phụ thuộc của mình trong thời gian chạy. Một hệ thống mô-đun sẽ cho phép bạn thể hiện biểu đồ phụ thuộc tại thời điểm kiểm tra kiểu.
Doval

@ Doval - Cảm ơn những bình luận thú vị và kích thích tư duy của bạn. Tôi có thể đã hiểu lầm bạn, nhưng tôi có đúng khi suy luận từ các ý kiến ​​của bạn rằng nếu tôi sử dụng một kiểu lập trình chức năng theo kiểu DI (theo nghĩa C # truyền thống), thì tôi sẽ tránh được sự thất vọng có thể xảy ra liên quan đến thời gian chạy giải quyết phụ thuộc?
Matt Cashatt 11/03/2015

@MatthewPatrickCashatt Đây không phải là vấn đề về phong cách hay mô hình, mà là về các tính năng ngôn ngữ. Nếu ngôn ngữ không hỗ trợ các mô-đun như những thứ hạng nhất, bạn sẽ phải thực hiện một số hình thức gửi động phụ thuộc và tiêm phụ thuộc để thực hiện hoán đổi, bởi vì không có cách nào để thể hiện tĩnh phụ thuộc. Nói cách khác, nếu chương trình C # của bạn sử dụng chuỗi, nó có phần phụ thuộc được mã hóa cứng System.String. Một hệ thống mô-đun sẽ cho phép bạn thay thế System.Stringbằng một biến để việc lựa chọn thực hiện chuỗi không được mã hóa cứng, nhưng vẫn được giải quyết tại thời điểm biên dịch.
Doval

8

Lập trình chức năng là một thay thế khả thi cho các mẫu tiêm phụ thuộc?

Điều này đánh tôi như một câu hỏi kỳ lạ. Phương pháp tiếp cận lập trình chức năng chủ yếu là tiếp tuyến với tiêm phụ thuộc.

Chắc chắn, có trạng thái bất biến có thể đẩy bạn không "gian lận" bằng cách có tác dụng phụ hoặc sử dụng trạng thái lớp như một hợp đồng ngầm giữa các chức năng. Nó làm cho việc truyền dữ liệu rõ ràng hơn, mà tôi cho là hình thức tiêm phụ thuộc cơ bản nhất. Và khái niệm lập trình chức năng của việc truyền các hàm xung quanh làm cho điều đó dễ dàng hơn rất nhiều.

Nhưng nó không loại bỏ sự phụ thuộc. Hoạt động của bạn vẫn cần tất cả dữ liệu / hoạt động họ cần khi trạng thái của bạn có thể thay đổi. Và bạn vẫn cần phải có được những phụ thuộc ở đó bằng cách nào đó. Vì vậy, tôi sẽ không nói rằng các phương pháp lập trình chức năng thay thế DI cả, vì vậy không có cách nào khác.

Nếu bất cứ điều gì, họ vừa chỉ cho bạn thấy mã OO tồi tệ như thế nào có thể tạo ra sự phụ thuộc ngầm mà các lập trình viên hiếm khi nghĩ tới.


Cảm ơn một lần nữa vì đã đóng góp cho cuộc trò chuyện, Telastyn. Như bạn đã chỉ ra, câu hỏi của tôi không được xây dựng tốt (lời tôi nói), nhưng nhờ vào phản hồi ở đây, tôi bắt đầu hiểu rõ hơn một chút về những gì đang diễn ra trong não tôi về tất cả những điều này: Tất cả chúng ta đều đồng ý (Tôi nghĩ) rằng thử nghiệm đơn vị có thể là một cơn ác mộng với DI. Thật không may, việc sử dụng DI, đặc biệt là với các bộ chứa IoC có thể tạo ra một dạng ác mộng gỡ lỗi mới nhờ vào việc nó giải quyết các phụ thuộc khi chạy. Tương tự như DI, ​​FP làm cho việc kiểm tra đơn vị dễ dàng hơn, nhưng không có vấn đề phụ thuộc thời gian chạy.
Matt Cashatt 11/03/2015

(tiếp tục từ trên). . Đây là cách hiểu hiện tại của tôi. Xin vui lòng cho tôi biết nếu tôi bị mất dấu. Tôi không ngại thừa nhận rằng tôi chỉ là một phàm nhân trong số những người khổng lồ ở đây!
Matt Cashatt 11/03/2015

@MatthewPatrickCashatt - DI không nhất thiết ngụ ý các vấn đề phụ thuộc thời gian chạy, như bạn lưu ý, thật kinh khủng.
Telastyn 11/03/2015

7

Câu trả lời nhanh cho câu hỏi của bạn là: Không .

Nhưng như những người khác đã khẳng định, câu hỏi kết hôn với hai khái niệm có phần không liên quan.

Hãy làm điều này từng bước một.

Kết quả DI trong phong cách không chức năng

Trong cốt lõi của lập trình hàm là các hàm thuần túy - các hàm ánh xạ đầu vào thành đầu ra, vì vậy bạn luôn nhận được cùng một đầu ra cho một đầu vào đã cho.

DI thường có nghĩa là đơn vị của bạn không còn tinh khiết vì đầu ra có thể thay đổi tùy thuộc vào việc tiêm. Chẳng hạn, trong hàm sau:

const bookSeats = ( seatCount, getBookedSeatCount ) => { ... }

getBookedSeatCount(một hàm) có thể thay đổi mang lại các kết quả khác nhau cho cùng một đầu vào đã cho. Điều này làm cho bookSeatskhông tinh khiết là tốt.

Có những trường hợp ngoại lệ cho điều này - bạn có thể tiêm một trong hai thuật toán sắp xếp thực hiện cùng ánh xạ đầu vào-đầu ra, mặc dù sử dụng các thuật toán khác nhau. Nhưng đây là những ngoại lệ.

Một hệ thống không thể thuần túy

Thực tế là một hệ thống không thể thuần túy cũng bị bỏ qua như nhau vì nó được khẳng định trong các nguồn lập trình chức năng.

Một hệ thống phải có tác dụng phụ với các ví dụ rõ ràng là:

  • Giao diện người dùng
  • Cơ sở dữ liệu
  • API (trong kiến ​​trúc máy khách-máy chủ)

Vì vậy, một phần trong hệ thống của bạn phải liên quan đến các tác dụng phụ và phần đó cũng có thể liên quan đến phong cách bắt buộc hoặc phong cách OO.

Mô hình lõi-vỏ

Mượn các điều khoản từ bài nói chuyện tuyệt vời về ranh giới của Gary Bernhardt , một kiến ​​trúc hệ thống (hoặc mô-đun) tốt sẽ bao gồm hai lớp:

  • Cốt lõi
    • Hàm tinh khiết
    • Phân nhánh
    • Không phụ thuộc
  • Vỏ
    • Không tinh khiết (tác dụng phụ)
    • Không phân nhánh
    • Phụ thuộc
    • Có thể là bắt buộc, liên quan đến phong cách OO, vv

Điểm mấu chốt là 'chia nhỏ' hệ thống thành phần nguyên chất (phần lõi) và phần không tinh khiết (phần vỏ).

Mặc dù đưa ra một giải pháp hơi thiếu sót (và kết luận), bài viết này của Mark Seemann đề xuất khái niệm rất giống nhau. Việc triển khai Haskell đặc biệt sâu sắc vì nó cho thấy tất cả có thể được thực hiện bằng cách sử dụng FP.

DI và FP

Sử dụng DI là hoàn toàn hợp lý ngay cả khi phần lớn ứng dụng của bạn là thuần túy. Điều quan trọng là giới hạn DI trong vỏ không tinh khiết.

Một ví dụ sẽ là sơ khai API - bạn muốn API thực sự trong sản xuất, nhưng sử dụng sơ khai trong thử nghiệm. Tuân thủ mô hình shell-core sẽ giúp ích rất nhiều ở đây.

Phần kết luận

Vì vậy, FP và DI không phải là chính xác thay thế. Bạn có thể có cả hai trong hệ thống của mình và lời khuyên là đảm bảo tách biệt giữa phần nguyên chất và không tinh khiết của hệ thống, nơi mà FP và DI cư trú tương ứng.


Khi bạn đề cập đến mô hình lõi-vỏ, làm thế nào người ta không đạt được sự phân nhánh trong vỏ? Tôi có thể nghĩ ra nhiều ví dụ trong đó một ứng dụng sẽ cần phải làm một điều không tinh khiết hoặc một điều khác dựa trên một giá trị. Đây có phải là quy tắc không phân nhánh áp dụng trong các ngôn ngữ như Java không?
jrahhali

@jrahhali Vui lòng xem Talk của Gary Bernhardt để biết chi tiết (được liên kết trong câu trả lời).
Izhaki

một
jk.

1

Từ quan điểm OOP, các hàm xem có thể được coi là giao diện đơn phương thức.

Giao diện là một hợp đồng mạnh hơn một chức năng.

Nếu bạn đang sử dụng một phương pháp chức năng và thực hiện nhiều DI thì so với việc sử dụng phương pháp OOP, bạn sẽ nhận được nhiều ứng cử viên hơn cho mỗi phụ thuộc.

void DoStuff(Func<DateTime> getDateTime) {}; //Anything that satisfies the signature can be injected.

đấu với

void DoStuff(IDateTimeProvider dateTimeProvider) {}; //Only types implementing the interface can be injected.

3
Bất kỳ lớp nào cũng có thể được bọc để thực hiện giao diện, vì vậy "hợp đồng mạnh hơn" không mạnh hơn nhiều. Quan trọng hơn là cung cấp cho mỗi chức năng một loại khác nhau làm cho nó không thể thực hiện thành phần chức năng.
Doval

Lập trình hàm không có nghĩa là "Lập trình với các hàm bậc cao hơn", nó đề cập đến một khái niệm rộng lớn hơn, các hàm bậc cao hơn chỉ là một kỹ thuật hữu ích trong mô hình.
Jimmy Hoffa
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.