Chúng ta có thể thực sự sử dụng tính bất biến trong OOP mà không mất tất cả các tính năng OOP chính không?


11

Tôi thấy những lợi ích của việc làm cho các đối tượng trong chương trình của tôi trở nên bất biến. Khi tôi thực sự suy nghĩ sâu sắc về một thiết kế tốt cho ứng dụng của mình, tôi thường tự nhiên thấy nhiều đối tượng của mình là bất biến. Nó thường đến điểm mà tôi muốn có tất cả các đối tượng của mình bất biến.

Câu hỏi này liên quan đến cùng một ý tưởng nhưng không có câu trả lời nào cho thấy đâu là cách tiếp cận tốt đối với tính bất biến và khi nào thực sự sử dụng nó. Có một số mẫu thiết kế bất biến tốt? Ý tưởng chung dường như là "làm cho các đối tượng trở nên bất biến trừ khi bạn thực sự cần chúng để thay đổi", điều này là vô ích trong thực tế.

Kinh nghiệm của tôi là sự bất biến thúc đẩy mã của tôi ngày càng nhiều hơn đến mô hình chức năng và sự tiến triển này luôn xảy ra:

  1. Tôi bắt đầu cần các cấu trúc dữ liệu liên tục (theo nghĩa chức năng) như danh sách, bản đồ, v.v.
  2. Thật vô cùng bất tiện khi làm việc với các tham chiếu chéo (ví dụ: nút cây tham chiếu đến con của nó trong khi trẻ tham chiếu cha mẹ của chúng) khiến tôi không sử dụng tham chiếu chéo nào cả, điều này một lần nữa làm cho cấu trúc dữ liệu và mã của tôi có nhiều chức năng hơn.
  3. Kế thừa dừng lại để có ý nghĩa và tôi bắt đầu sử dụng thành phần thay thế.
  4. Toàn bộ ý tưởng cơ bản của OOP như đóng gói bắt đầu tan rã và các đối tượng của tôi bắt đầu trông giống như các hàm.

Tại thời điểm này, tôi thực tế không sử dụng gì từ mô hình OOP nữa và chỉ có thể chuyển sang ngôn ngữ chức năng thuần túy. Vì vậy, câu hỏi của tôi: có một cách tiếp cận nhất quán để thiết kế OOP bất biến tốt hay luôn luôn là khi bạn đưa ý tưởng bất biến đến tiềm năng tối đa của nó, bạn luôn kết thúc việc lập trình bằng ngôn ngữ chức năng không cần bất cứ điều gì từ thế giới OOP nữa? Có hướng dẫn tốt nào để quyết định lớp nào sẽ bất biến và lớp nào có thể thay đổi để đảm bảo rằng OOP không bị sụp đổ?

Để thuận tiện, tôi sẽ cung cấp một ví dụ. Chúng ta hãy có một ChessBoardbộ sưu tập các quân cờ bất biến (mở rộng lớp trừu tượngPiece). Từ quan điểm OOP, một mảnh có trách nhiệm tạo ra các bước di chuyển hợp lệ từ vị trí của nó trên bảng. Nhưng để tạo ra các bước di chuyển, mảnh cần có một tham chiếu đến bảng của nó trong khi bảng cần phải có tham chiếu đến các mảnh của nó. Vâng, có một số thủ thuật để tạo các tài liệu tham khảo chéo bất biến này tùy thuộc vào ngôn ngữ OOP của bạn nhưng chúng rất khó quản lý, tốt hơn là không có phần nào để tham khảo bảng của nó. Nhưng sau đó, mảnh không thể tạo ra di chuyển của nó vì nó không biết trạng thái của bảng. Sau đó, mảnh trở thành một cấu trúc dữ liệu giữ loại mảnh và vị trí của nó. Sau đó, bạn có thể sử dụng một hàm đa hình để tạo ra các bước di chuyển cho tất cả các loại mảnh. Điều này là hoàn toàn có thể đạt được trong lập trình chức năng nhưng hầu như không thể trong OOP nếu không kiểm tra loại thời gian chạy và các thực tiễn OOP xấu khác ... Sau đó,


3
Tôi thích câu hỏi cơ bản, nhưng tôi có một thời gian khó khăn với các chi tiết. Ví dụ, tại sao thừa kế dừng lại có ý nghĩa khi bạn tối đa hóa sự bất biến?
Martin Ba

1
Đối tượng của bạn không có phương pháp?
Ngừng làm hại Monica


4
"Từ quan điểm OOP, một mảnh có trách nhiệm tạo ra các bước di chuyển hợp lệ từ vị trí của nó trên bảng." - chắc chắn là không, thiết kế một mảnh như thế có lẽ sẽ làm tổn thương SRP.
Doc Brown

1
@lishaak Bạn "đấu tranh để giải thích vấn đề với sự kế thừa bằng những thuật ngữ đơn giản" bởi vì đó không phải là vấn đề; thiết kế kém là một vấn đề. Kế thừa là bản chất của OOP, là điều kiện thiết yếu nếu bạn muốn. Rất nhiều cái gọi là "vấn đề với sự kế thừa" thực sự là vấn đề với các ngôn ngữ không có cú pháp ghi đè rõ ràng và chúng ít gặp vấn đề hơn trong các ngôn ngữ được thiết kế tốt hơn. Không có phương thức ảo và đa hình, bạn không có OOP; bạn có lập trình thủ tục với cú pháp đối tượng hài hước. Vì vậy, không có gì lạ khi bạn không thấy lợi ích của OO nếu bạn tránh nó!
Mason Wheeler

Câu trả lời:


24

Chúng ta có thể thực sự sử dụng tính bất biến trong OOP mà không mất tất cả các tính năng OOP chính không?

Đừng xem tại sao không. Đã làm điều đó trong nhiều năm trước khi Java 8 có tất cả các chức năng. Bạn đã từng nghe về String chưa? Đẹp và bất biến kể từ khi bắt đầu.

  1. Tôi bắt đầu cần các cấu trúc dữ liệu liên tục (theo nghĩa chức năng) như danh sách, bản đồ, v.v.

Cũng cần những thứ đó. Vô hiệu hóa các trình lặp của tôi bởi vì bạn đã thay đổi bộ sưu tập trong khi tôi đang đọc nó chỉ là thô lỗ.

  1. Thật vô cùng bất tiện khi làm việc với các tham chiếu chéo (ví dụ: nút cây tham chiếu đến con của nó trong khi trẻ tham chiếu cha mẹ của chúng) khiến tôi không sử dụng tham chiếu chéo nào cả, điều này một lần nữa làm cho cấu trúc dữ liệu và mã của tôi có nhiều chức năng hơn.

Tài liệu tham khảo thông tư là một loại địa ngục đặc biệt. Bất biến sẽ không cứu bạn khỏi nó.

  1. Kế thừa dừng lại để có ý nghĩa và tôi bắt đầu sử dụng thành phần thay thế.

Vâng, tôi ở đây với bạn nhưng không thấy nó phải làm gì với sự bất biến. Lý do tôi thích sáng tác không phải vì tôi yêu mô hình chiến lược năng động, bởi vì nó cho phép tôi thay đổi mức độ trừu tượng của mình.

  1. Toàn bộ ý tưởng cơ bản của OOP như đóng gói bắt đầu tan rã và các đối tượng của tôi bắt đầu trông giống như các hàm.

Tôi rùng mình khi nghĩ ý tưởng của bạn về "OOP như đóng gói" là gì. Nếu nó liên quan đến getters và setters thì chỉ xin vui lòng ngừng gọi đóng gói đó bởi vì nó không phải. Nó chưa bao giờ. Đó là hướng dẫn lập trình theo hướng Aspect. Một cơ hội để xác nhận và một nơi để đặt một điểm dừng là tốt nhưng đó không phải là đóng gói. Đóng gói là bảo vệ quyền của tôi để không biết hoặc quan tâm những gì đang xảy ra bên trong.

Các đối tượng của bạn được cho là trông giống như các chức năng. Chúng là túi chức năng. Chúng là các túi chức năng di chuyển cùng nhau và xác định lại chính chúng cùng nhau.

Lập trình chức năng đang thịnh hành tại thời điểm này và mọi người đang rũ bỏ một số quan niệm sai lầm về OOP. Đừng để điều đó làm bạn bối rối khi tin rằng đây là sự kết thúc của OOP. Chức năng và OOP có thể sống với nhau khá độc đáo.

  • Lập trình chức năng đang được chính thức về bài tập.

  • OOP là chính thức về con trỏ chức năng.

Thực sự là nó. Dykstra nói với chúng tôi gotolà có hại vì vậy chúng tôi đã chính thức về nó và tạo lập trình có cấu trúc. Cứ như vậy, hai mô hình này là tìm cách hoàn thành công việc trong khi tránh những cạm bẫy đến từ việc làm những việc rắc rối này một cách tình cờ.

Hãy để tôi chỉ cho bạn một số thứ:

f n (x)

Đó là một chức năng. Đó thực sự là một sự liên tục của các chức năng:

f 1 (x)
f 2 (x)
...
f n (x)

Đoán làm thế nào chúng ta thể hiện điều đó trong các ngôn ngữ OOP?

n.f(x)

Điều đó ít nlựa chọn việc thực hiện fđược sử dụng VÀ nó quyết định một số hằng số được sử dụng trong hàm đó là gì (mà thực sự có nghĩa là điều tương tự). Ví dụ:

f 1 (x) = x + 1
f 2 (x) = x + 2

Đó là điều tương tự đóng cửa cung cấp. Khi các bao đóng tham chiếu đến phạm vi kèm theo của chúng, các phương thức đối tượng đề cập đến trạng thái thể hiện của chúng. Đối tượng có thể đóng một cái tốt hơn. Một bao đóng là một hàm duy nhất được trả về từ một hàm khác. Một constructor trả về một tham chiếu đến toàn bộ các hàm:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Bạn đã đoán nó:

n.g(x)

f và g là các hàm thay đổi cùng nhau và di chuyển xung quanh cùng nhau. Vì vậy, chúng tôi dán chúng trong cùng một túi. Đây là những gì một đối tượng thực sự là. Giữ nliên tục (không thay đổi) chỉ có nghĩa là dễ dàng hơn để dự đoán những gì sẽ làm khi bạn gọi chúng.

Bây giờ chỉ là cấu trúc. Cách tôi nghĩ về OOP là một loạt những điều nhỏ nói với những điều nhỏ nhặt khác. Hy vọng đến một nhóm nhỏ những điều nhỏ nhặt. Khi tôi code tôi tưởng tượng mình NHƯ đối tượng. Tôi nhìn mọi thứ từ quan điểm của đối tượng. Và tôi cố gắng để lười biếng vì vậy tôi không làm việc quá mức đối tượng. Tôi nhận những tin nhắn đơn giản, làm việc với chúng một chút và gửi những tin nhắn đơn giản đến những người bạn thân nhất của tôi. Khi tôi hoàn thành với đối tượng đó, tôi nhảy vào một cái khác và nhìn mọi thứ từ quan điểm của nó.

Thẻ trách nhiệm lớp học là người đầu tiên dạy tôi nghĩ theo cách đó. Người đàn ông tôi đã bối rối về họ hồi đó nhưng chết tiệt nếu họ không còn liên quan đến ngày hôm nay.

Chúng ta hãy có ChessBoard như một bộ sưu tập các quân cờ bất biến (mở rộng lớp trừu tượng). Từ quan điểm OOP, một mảnh có trách nhiệm tạo ra các bước di chuyển hợp lệ từ vị trí của nó trên bảng. Nhưng để tạo ra các bước di chuyển, mảnh cần có một tham chiếu đến bảng của nó trong khi bảng cần phải có tham chiếu đến các mảnh của nó.

Tranh cãi! Một lần nữa với các tài liệu tham khảo tròn không cần thiết.

Làm thế nào về: Một ChessBoardDataStructurebiến xy dây thành tham chiếu mảnh. Những mảnh đó có một phương thức lấy x, y và một đặc biệt ChessBoardDataStructurevà biến nó thành một bộ sưu tập các nhãn hiệu mới ChessBoardDataStructure. Sau đó chuyển nó thành một cái gì đó có thể chọn di chuyển tốt nhất. Bây giờ ChessBoardDataStructurecó thể là bất biến và các mảnh cũng vậy. Theo cách này, bạn chỉ có một con tốt trắng trong bộ nhớ. Chỉ có một số tài liệu tham khảo cho nó ở đúng vị trí xy. Đối tượng định hướng, chức năng, và bất biến.

Đợi đã , chúng ta đã nói về cờ vua chưa?


6
Mọi người nên đọc cái này Và sau đó đọc về Tìm hiểu trừu tượng dữ liệu, được xem lại bởi William R. Cook . Và sau đó đọc lại. Và sau đó đọc Đề xuất của Cook về các định nghĩa hiện đại, đơn giản về "Đối tượng" và "Hướng đối tượng" . Ném vào " Ý tưởng lớn là" nhắn tin "của Alan Kay , định nghĩa
Jörg W Mittag

1
Và cuối cùng nhưng không kém phần Hiệp ước Orlando .
Jörg W Mittag

1
@Euphoric: Tôi nghĩ rằng đó là một công thức khá chuẩn phù hợp với các định nghĩa tiêu chuẩn cho "lập trình chức năng", "lập trình mệnh lệnh", "lập trình logic", v.v. Nếu không, C là ngôn ngữ chức năng vì bạn có thể mã hóa FP trong đó, một ngôn ngữ logic, bởi vì bạn có thể mã hóa lập trình logic trong đó, một ngôn ngữ động, bởi vì bạn có thể mã hóa kiểu gõ động trong đó, ngôn ngữ diễn viên, bởi vì bạn có thể mã hóa một hệ thống diễn viên trong đó, v.v. Và Haskell là ngôn ngữ mệnh lệnh lớn nhất thế giới vì bạn thực sự có thể đặt tên cho các tác dụng phụ, lưu trữ chúng trong các biến, chuyển chúng thành Từ
Jörg W Mittag

1
Tôi nghĩ rằng chúng ta có thể sử dụng những ý tưởng tương tự như những ý tưởng trong bài báo thiên tài của Matthias Felleisen về khả năng biểu đạt ngôn ngữ. Việc chuyển một chương trình OO từ, giả sử, Java sang C♯ chỉ có thể được thực hiện chỉ bằng các phép biến đổi cục bộ, nhưng để chuyển nó sang C đòi hỏi phải tái cấu trúc toàn cầu (về cơ bản, bạn cần phải giới thiệu một cơ chế gửi tin nhắn và chuyển hướng tất cả các hàm gọi qua nó), do đó Java và C♯ có thể biểu thị OO và C có thể "chỉ" mã hóa nó.
Jörg W Mittag

1
@lishaak Bởi vì bạn chỉ định nó cho những thứ khác nhau. Điều này giữ cho nó ở một mức độ trừu tượng và ngăn ngừa trùng lặp lưu trữ thông tin vị trí mà nếu không có thể trở nên không nhất quán. Nếu bạn chỉ đơn giản là bực bội khi gõ thêm thì hãy dán nó vào một phương thức để bạn chỉ gõ một lần..Một phần không cần phải nhớ nó ở đâu nếu bạn nói nó ở đâu. Bây giờ các mảnh chỉ lưu trữ thông tin như màu sắc, di chuyển và hình ảnh / viết tắt, luôn luôn đúng và không thay đổi.
candied_orange

2

Theo tôi, các khái niệm hữu ích nhất được OOP đưa vào dòng chính là:

  • Mô đun hóa đúng.
  • Đóng gói dữ liệu.
  • Phân tách rõ ràng giữa giao diện riêng tư và công cộng.
  • Cơ chế rõ ràng cho khả năng mở rộng mã.

Tất cả những lợi ích này cũng có thể được nhận ra mà không cần các chi tiết triển khai truyền thống, như kế thừa hoặc thậm chí các lớp. Ý tưởng ban đầu của Alan Kay về một "hệ thống hướng đối tượng" đã sử dụng "thông điệp" thay vì "phương thức", và gần với Erlang hơn là C ++. Nhìn vào Go đi với nhiều chi tiết triển khai OOP truyền thống, nhưng vẫn cảm thấy hợp lý hướng đối tượng.

Nếu bạn sử dụng các đối tượng bất biến, bạn vẫn có thể sử dụng hầu hết các tính năng OOP truyền thống: giao diện, công văn động, đóng gói. Bạn không cần setters và thường không cần getters cho các đối tượng đơn giản hơn. Bạn cũng có thể gặt hái những lợi ích của tính bất biến: bạn luôn chắc chắn rằng một đối tượng không thay đổi trong thời gian đó, không sao chép phòng thủ, không có cuộc đua dữ liệu và thật dễ dàng để làm cho các phương thức trở nên thuần túy.

Hãy xem cách Scala cố gắng kết hợp các phương pháp tiếp cận bất biến và FP với OOP. Phải thừa nhận rằng đó không phải là ngôn ngữ đơn giản và thanh lịch nhất. Mặc dù vậy, nó thực sự thành công. Ngoài ra, hãy xem Kotlin cung cấp nhiều công cụ và cách tiếp cận cho một hỗn hợp tương tự.

Vấn đề thông thường khi thử một cách tiếp cận khác với những người sáng tạo ngôn ngữ đã nghĩ trong N năm trước là 'sự không phù hợp trở kháng' với thư viện chuẩn. OTOH cả hệ sinh thái Java và .NET hiện có hỗ trợ thư viện chuẩn hợp lý cho các cấu trúc dữ liệu bất biến; Tất nhiên, thư viện của bên thứ ba cũng tồn tại.

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.