Tại sao liên kết chặt chẽ giữa các chức năng và dữ liệu xấu?


38

Tôi tìm thấy trích dẫn này trong " Niềm vui của Clojure " trên trang. 32, nhưng ai đó đã nói điều tương tự với tôi trong bữa tối tuần trước và tôi cũng đã nghe thấy nó ở những nơi khác:

Nhược điểm của lập trình hướng đối tượng là sự kết hợp chặt chẽ giữa chức năng và dữ liệu.

Tôi hiểu tại sao khớp nối không cần thiết là xấu trong một ứng dụng. Ngoài ra, tôi rất thoải mái khi nói rằng nên tránh trạng thái đột biến và kế thừa, ngay cả trong Lập trình hướng đối tượng. Nhưng tôi không thấy lý do tại sao việc dán các chức năng trên các lớp vốn đã xấu.

Ý tôi là, việc thêm một chức năng vào một lớp có vẻ như gắn thẻ thư trong Gmail hoặc dán tệp vào thư mục. Đây là một kỹ thuật tổ chức giúp bạn tìm lại. Bạn chọn một số tiêu chí, sau đó đặt những thứ như nhau. Trước OOP, các chương trình của chúng tôi có khá nhiều phương thức trong tệp. Ý tôi là, bạn phải đặt các chức năng ở đâu đó. Tại sao không tổ chức chúng?

Nếu đây là một cuộc tấn công che giấu các loại, tại sao họ không nói rằng việc hạn chế loại đầu vào và đầu ra cho một chức năng là sai? Tôi không chắc liệu tôi có thể đồng ý với điều đó không, nhưng ít nhất tôi đã quen thuộc với các đối số an toàn và loại đối số. Điều này nghe với tôi như một mối quan tâm chủ yếu riêng biệt.

Chắc chắn, đôi khi mọi người hiểu sai và đưa chức năng vào lớp sai. Nhưng so với những sai lầm khác, điều này có vẻ như là một bất tiện rất nhỏ.

Vì vậy, Clojure có không gian tên. Việc gắn một hàm trên một lớp trong OOP khác với việc gắn một hàm trong một không gian tên trong Clojure như thế nào và tại sao nó lại tệ đến vậy? Hãy nhớ rằng, các chức năng trong một lớp không nhất thiết chỉ hoạt động đối với các thành viên của lớp đó. Nhìn vào java.lang.StringBuilder - nó hoạt động trên bất kỳ loại tham chiếu nào, hoặc thông qua tự động đấm bốc, trên bất kỳ loại nào.

PS Trích dẫn này tham khảo một cuốn sách mà tôi chưa đọc: Lập trình đa hướng trong Leda: Timothy Budd, 1995 .


20
Tôi tin rằng người viết đơn giản là không hiểu đúng về OOP và chỉ cần thêm một lý do để nói Java là xấu và Clojure là tốt. / rant
Euphoric

6
Các phương thức sơ thẩm (không giống như các hàm miễn phí hoặc các phương thức mở rộng) không thể được thêm vào từ các mô-đun khác. Điều này trở nên hạn chế hơn khi bạn xem xét các giao diện chỉ có thể được thực hiện bằng các phương thức thể hiện. Bạn không thể định nghĩa một giao diện và một lớp trong các mô-đun khác nhau và sau đó sử dụng mã từ mô-đun thứ ba để liên kết chúng lại với nhau. Một cách tiếp cận linh hoạt hơn, như các lớp loại của haskell sẽ có thể làm điều đó.
CodeInChaos

4
@Euphoric Tôi tin rằng người viết đã hiểu, nhưng cộng đồng Clojure dường như thích tạo ra một người rơm của OOP và đốt nó như một hình nộm cho tất cả các tệ nạn lập trình trước khi chúng tôi có bộ sưu tập rác tốt, nhiều bộ nhớ, bộ xử lý nhanh và nhiều không gian đĩa. Tôi ước họ sẽ bỏ đập trên OOP và nhắm vào các nguyên nhân thực sự: ví dụ kiến ​​trúc Von Neuman.
GlenPeterson

4
Ấn tượng của tôi là hầu hết những lời chỉ trích về OOP thực sự là sự chỉ trích về OOP như được thực hiện trong Java. Không phải vì đó là một người đàn ông rơm có chủ ý, mà bởi vì đó là những gì họ liên kết với OOP. Có những vấn đề khá giống nhau với những người phàn nàn về việc gõ tĩnh. Hầu hết các vấn đề không phải là cố hữu trong khái niệm, mà chỉ là sai sót trong việc thực hiện phổ biến khái niệm đó.
CodeInChaos

3
Tiêu đề của bạn không phù hợp với nội dung câu hỏi của bạn. Thật dễ dàng để giải thích tại sao sự kết hợp chặt chẽ giữa các chức năng và dữ liệu là xấu, nhưng văn bản của bạn đặt câu hỏi "OOP có làm điều này không?", "Nếu vậy, tại sao?" và "Đây có phải là một điều xấu?". Cho đến nay, bạn đã đủ may mắn để nhận được câu trả lời liên quan đến một hoặc nhiều câu hỏi trong số ba câu hỏi này và không ai cho rằng câu hỏi đơn giản hơn trong tiêu đề.
itbruce

Câu trả lời:


34

Về lý thuyết, khớp nối dữ liệu chức năng lỏng lẻo giúp dễ dàng thêm nhiều chức năng hơn để làm việc trên cùng một dữ liệu. Mặt trái của nó là làm cho việc thay đổi cấu trúc dữ liệu trở nên khó khăn hơn, đó là lý do tại sao trong thực tế, mã chức năng được thiết kế tốt và mã OOP được thiết kế tốt có mức độ khớp rất giống nhau.

Lấy một biểu đồ chu kỳ có hướng (DAG) làm cấu trúc dữ liệu mẫu. Trong lập trình chức năng, bạn vẫn cần một số trừu tượng để tránh lặp lại chính mình, vì vậy bạn sẽ tạo một mô-đun với các chức năng để thêm và xóa các nút và cạnh, tìm các nút có thể truy cập từ một nút nhất định, tạo sắp xếp tôpô, v.v. được kết hợp chặt chẽ với dữ liệu, mặc dù trình biên dịch không thực thi nó. Bạn có thể thêm một nút một cách khó khăn, nhưng tại sao bạn lại muốn? Sự gắn kết trong một mô-đun ngăn chặn khớp nối chặt chẽ trong toàn hệ thống.

Ngược lại, về phía OOP, bất kỳ chức năng nào ngoài các hoạt động DAG cơ bản sẽ được thực hiện trong các lớp "xem" riêng biệt, với đối tượng DAG được truyền vào dưới dạng tham số. Thật dễ dàng để thêm nhiều chế độ xem như bạn muốn hoạt động trên dữ liệu DAG, tạo ra cùng một mức phân tách dữ liệu chức năng như bạn thấy trong chương trình chức năng. Trình biên dịch sẽ không ngăn bạn nhồi nhét mọi thứ vào một lớp, nhưng các đồng nghiệp của bạn thì sẽ.

Thay đổi mô hình lập trình không thay đổi các thực hành trừu tượng, gắn kết và khớp nối tốt nhất, nó chỉ thay đổi các thực hành mà trình biên dịch giúp bạn thực thi. Trong lập trình chức năng, khi bạn muốn khớp dữ liệu chức năng, nó được thi hành theo thỏa thuận của quý ông hơn là trình biên dịch. Trong OOP, phân tách chế độ xem mô hình được thi hành theo thỏa thuận của quý ông thay vì trình biên dịch.


13

Trong trường hợp bạn không biết nó đã hiểu rõ điều này: Các khái niệm về hướng đối tượng và sự đóng cửa là hai mặt của cùng một đồng tiền. Điều đó nói rằng, đóng cửa là gì? Nó lấy (các) biến hoặc dữ liệu từ phạm vi xung quanh và liên kết với nó bên trong hàm hoặc từ phối cảnh OO, bạn thực hiện điều tương tự khi bạn chuyển một cái gì đó vào hàm tạo để sau này bạn có thể sử dụng phần dữ liệu trong một chức năng thành viên của trường hợp đó. Nhưng lấy những thứ từ phạm vi xung quanh không phải là một điều tốt đẹp để làm - phạm vi xung quanh càng lớn thì càng phải làm điều này (mặc dù thực tế, một số điều xấu thường là cần thiết để hoàn thành công việc). Việc sử dụng các biến toàn cục đang đưa điều này đến mức cực đoan, trong đó các hàm trong một chương trình đang sử dụng các biến ở phạm vi chương trình - thực sự rất xấu. Cómô tả tốt ở nơi khác về lý do tại sao các biến toàn cầu là xấu.

Nếu bạn làm theo các kỹ thuật OO, về cơ bản bạn đã chấp nhận rằng mọi mô-đun trong chương trình của bạn sẽ có một mức độ xấu tối thiểu nhất định. Nếu bạn thực hiện một cách tiếp cận chức năng để lập trình, bạn đang hướng đến một lý tưởng trong đó không có mô-đun nào trong chương trình của bạn sẽ chứa ác đóng cửa, mặc dù bạn vẫn có thể có một số, nhưng nó sẽ ít hơn nhiều so với OO.

Đó là nhược điểm của OO - nó khuyến khích loại dữ liệu này, khớp dữ liệu để hoạt động thông qua việc đóng tiêu chuẩn (một loại lý thuyết lập trình cửa sổ bị phá vỡ ).

Điểm cộng duy nhất là, nếu bạn biết rằng bạn sẽ sử dụng nhiều lần đóng để bắt đầu, OO ít nhất cung cấp cho bạn một khung ý tưởng để giúp tổ chức cách tiếp cận đó để lập trình viên trung bình có thể hiểu được. Cụ thể, các biến được đóng lại là rõ ràng trong hàm tạo chứ không chỉ được thực hiện trong một hàm đóng. Các chương trình chức năng sử dụng nhiều lần đóng thường khó hiểu hơn chương trình OO tương đương, mặc dù không nhất thiết phải kém thanh lịch :)


8
Trích dẫn trong ngày: "một số điều xấu thường là cần thiết để hoàn thành công việc"
GlenPeterson

5
Bạn đã không thực sự giải thích tại sao những điều bạn gọi là xấu xa là xấu xa; bạn chỉ đang gọi họ là ác. Giải thích tại sao họ xấu xa và bạn có thể có câu trả lời cho câu hỏi của quý ông.
Robert Harvey

2
Đoạn cuối của bạn lưu câu trả lời mặc dù. Theo bạn, đó có thể là điểm cộng duy nhất, nhưng đó không phải là chuyện nhỏ. Chúng tôi được gọi là "lập trình viên trung bình" thực sự chào đón một số lượng lễ nhất định, chắc chắn đủ để cho chúng tôi biết những gì đang xảy ra.
Robert Harvey

Nếu OO và bao đóng là đồng nghĩa, tại sao có quá nhiều ngôn ngữ OO không cung cấp hỗ trợ rõ ràng cho chúng? Trang wiki C2 mà bạn trích dẫn thậm chí còn gây tranh cãi nhiều hơn (và ít đồng thuận hơn) so với bình thường đối với trang web đó.
itbruce

1
@itsbruce Chúng được làm phần lớn không cần thiết. Các biến sẽ được "đóng lại" thay vào đó trở thành các biến lớp được truyền vào đối tượng.
Izkata

7

Đó là về khớp nối kiểu :

Một hàm được xây dựng trong một đối tượng để làm việc trên đối tượng đó không thể được sử dụng trên các loại đối tượng khác.

Trong Haskell, bạn viết các hàm để làm việc với các lớp loại - vì vậy có nhiều loại đối tượng khác nhau mà bất kỳ hàm đã cho nào cũng có thể hoạt động, miễn là nó là một loại của lớp đã cho hoạt động.

Các chức năng độc lập cho phép tách rời như vậy mà bạn không nhận được khi bạn tập trung viết các chức năng của mình để hoạt động bên trong loại A vì sau đó bạn không thể sử dụng chúng nếu bạn không có phiên bản loại A, mặc dù chức năng có thể mặt khác, đủ chung để được sử dụng trên một thể hiện loại B hoặc thể hiện loại C.


3
Không phải đó là toàn bộ điểm của giao diện sao? Để cung cấp những thứ cho phép loại B và loại C trông giống với chức năng của bạn, để nó có thể hoạt động trên nhiều loại?
Random832

2
@ Random832 hoàn toàn, nhưng tại sao lại nhúng một hàm bên trong một kiểu dữ liệu nếu không hoạt động với kiểu dữ liệu đó? Câu trả lời: Đó là lý do duy nhất để nhúng một hàm vào một kiểu dữ liệu. Bạn không thể viết gì ngoài các lớp tĩnh và làm cho tất cả các hàm của bạn không quan tâm đến kiểu dữ liệu mà chúng được gói gọn để làm cho chúng tách rời hoàn toàn khỏi kiểu sở hữu của chúng, nhưng tại sao thậm chí còn bận tâm đặt chúng vào một kiểu? Phương pháp tiếp cận chức năng cho biết: Đừng bận tâm, hãy viết các chức năng của bạn để làm việc với các giao diện sắp xếp và sau đó không có lý do gì để gói chúng với dữ liệu của bạn.
Jimmy Hoffa

Bạn vẫn phải thực hiện các giao diện.
Random832

2
@ Random832 các giao diện là các loại dữ liệu; họ không cần chức năng gói gọn trong chúng. Với các chức năng miễn phí, tất cả các giao diện cần phải trích xuất là dữ liệu họ cung cấp cho các chức năng để chống lại.
Jimmy Hoffa

2
@ Random832 để liên quan đến các đối tượng trong thế giới thực rất phổ biến trong OO, hãy nghĩ về giao diện của một cuốn sách: Nó trình bày thông tin (dữ liệu), đó là tất cả. Bạn có chức năng miễn phí của trang lật hoạt động chống lại loại loại có trang, chức năng này hoạt động chống lại tất cả các loại sách, tin tức, các trục chính áp phích tại K-Mart, thiệp chúc mừng, thư, bất cứ thứ gì được ghim lại với nhau trong góc. Nếu bạn triển khai trang lần lượt với tư cách là thành viên của cuốn sách, bạn sẽ bỏ lỡ tất cả những điều bạn có thể sử dụng trang bật, như một chức năng miễn phí, nó không bị ràng buộc; nó chỉ ném một PartyFoulException vào bia.
Jimmy Hoffa

4

Trong Java và các hóa thân tương tự của OOP, các phương thức cá thể (không giống như các hàm miễn phí hoặc phương thức mở rộng) không thể được thêm vào từ các mô-đun khác.

Điều này trở nên hạn chế hơn khi bạn xem xét các giao diện chỉ có thể được thực hiện bằng các phương thức thể hiện. Bạn không thể định nghĩa một giao diện và một lớp trong các mô-đun khác nhau và sau đó sử dụng mã từ mô-đun thứ ba để liên kết chúng lại với nhau. Một cách tiếp cận linh hoạt hơn, như các lớp loại của Haskell sẽ có thể làm điều đó.


Bạn có thể làm điều đó một cách dễ dàng trong Scala. Tôi không quen với Go, nhưng AFAIK bạn cũng có thể làm điều đó ở đó. Trong Ruby, việc thêm các phương thức vào các đối tượng sau khi thực tế để làm cho chúng phù hợp với một số giao diện là một thực tế khá phổ biến. Những gì bạn mô tả có vẻ giống như một hệ thống loại được thiết kế tồi hơn bất kỳ thứ gì thậm chí liên quan từ xa đến OO. Giống như một thử nghiệm tư duy: câu trả lời của bạn sẽ khác như thế nào khi nói về các loại dữ liệu trừu tượng thay vì các đối tượng? Tôi không tin rằng nó sẽ tạo ra bất kỳ sự khác biệt nào, điều này sẽ chứng minh rằng lập luận của bạn không liên quan đến OO.
Jörg W Mittag

1
@ JörgWMittag Tôi nghĩ bạn có nghĩa là kiểu dữ liệu đại số. Và CodInChaos, Haskell rất rõ ràng không khuyến khích những gì bạn đề xuất. Nó được gọi là một ví dụ mồ côi và đưa ra các cảnh báo về GHC.
jozefg

3
@ JörgWMittag Ấn tượng của tôi là nhiều người chỉ trích OOP chỉ trích hình thức OOP được sử dụng trong Java và các ngôn ngữ tương tự với cấu trúc lớp cứng nhắc và tập trung vào các phương thức cá thể. Ấn tượng của tôi về câu nói đó là nó chỉ trích sự tập trung vào các phương thức cá thể và không thực sự áp dụng cho các hương vị khác của OOP, giống như những gì golang sử dụng.
CodeInChaos

2
@CodesInChaos Sau đó, có lẽ làm rõ điều này là "lớp tĩnh dựa trên OO"
jozefg

@jozefg: Tôi đang nói về các loại dữ liệu trừu tượng. Tôi thậm chí không thấy các kiểu dữ liệu đại số có liên quan từ xa đến cuộc thảo luận này.
Jörg W Mittag

3

Định hướng đối tượng về cơ bản là về Trừu tượng dữ liệu thủ tục (hoặc Trừu tượng dữ liệu chức năng nếu bạn loại bỏ các tác dụng phụ là một vấn đề trực giao). Theo một nghĩa nào đó, Lambda Tính là ngôn ngữ hướng đối tượng thuần túy và lâu đời nhất, vì nó chỉ cung cấp Trừu tượng dữ liệu chức năng (vì nó không có bất kỳ cấu trúc nào ngoài các chức năng).

Chỉ hoạt động của một đơn đối tượng có thể kiểm tra biểu diễn dữ liệu của đối tượng đó. Thậm chí không các đối tượng khác cùng loại có thể làm điều đó. (Đây là sự khác biệt chính giữa Object-Oriented dữ liệu trừu tượng và trừu tượng dữ liệu loại: với ADTs, các đối tượng cùng loại có thể kiểm tra biểu diễn dữ liệu của nhau, chỉ có các đại diện của các đối tượng khác loại được ẩn.)

Điều này có nghĩa là một số đối tượng cùng loại có thể có các biểu diễn dữ liệu khác nhau. Ngay cả cùng một đối tượng có thể có các biểu diễn dữ liệu khác nhau tại các thời điểm khác nhau. (Ví dụ, trong Scala, Maps và Sets chuyển đổi giữa một mảng và một Trie băm tùy thuộc vào số lượng các yếu tố bởi vì đối với số lượng rất nhỏ tuyến tính tìm kiếm trong một mảng là nhanh hơn so với tìm kiếm logarit trong một cây tìm kiếm bởi vì trong những yếu tố không đổi rất nhỏ .)

Từ bên ngoài một đối tượng, bạn không nên, bạn không thể biết biểu diễn dữ liệu của nó. Điều đó trái ngược với khớp nối chặt chẽ.


Tôi có các lớp trong OOP chuyển đổi cấu trúc dữ liệu nội bộ tùy theo hoàn cảnh để các thể hiện đối tượng của các lớp này có thể sử dụng các biểu diễn dữ liệu rất khác nhau cùng một lúc. Dữ liệu cơ bản ẩn và đóng gói tôi muốn nói? Vậy Map trong Scala khác với lớp Map được thực hiện đúng cách như thế nào (ẩn dữ liệu và đóng gói dữ liệu) trong một ngôn ngữ OOP?
Marjan Venema

Trong ví dụ của bạn, việc đóng gói dữ liệu của bạn với các hàm truy cập trong một lớp (và do đó kết hợp chặt chẽ các chức năng đó với dữ liệu đó) thực sự cho phép bạn ghép một cách lỏng lẻo các trường hợp của lớp đó với phần còn lại của chương trình. Bạn đang bác bỏ điểm trung tâm của trích dẫn - rất hay!
GlenPeterson

2

Khớp nối chặt chẽ giữa dữ liệu và chức năng là không tốt vì bạn muốn có thể thay đổi độc lập với nhau và khớp nối chặt chẽ khiến việc này trở nên khó khăn vì bạn không thể thay đổi cái này mà không biết và có thể thay đổi cái khác.

Bạn muốn các dữ liệu khác nhau được trình bày cho hàm không yêu cầu bất kỳ thay đổi nào trong chức năng và tương tự bạn muốn có thể thay đổi chức năng mà không cần bất kỳ thay đổi nào đối với dữ liệu mà nó đang hoạt động để hỗ trợ các thay đổi chức năng đó.


1
Vâng tôi muốn nó. Nhưng kinh nghiệm của tôi là khi bạn gửi dữ liệu đến một chức năng không tầm thường mà nó không được thiết kế rõ ràng để xử lý, chức năng đó có xu hướng bị phá vỡ. Tôi không chỉ đề cập đến an toàn loại, mà là bất kỳ điều kiện dữ liệu nào mà tác giả của chức năng không lường trước được. Nếu chức năng này cũ và thường được sử dụng, bất kỳ thay đổi nào cho phép dữ liệu mới truyền qua nó đều có khả năng phá vỡ nó đối với một số dạng dữ liệu cũ vẫn cần hoạt động. Mặc dù việc tách rời có thể là lý tưởng cho các chức năng so với dữ liệu, nhưng thực tế việc tách rời đó có thể khó khăn và nguy hiểm.
GlenPeterson
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.