Hoàn toàn bất biến và lập trình hướng đối tượng


43

Trong hầu hết các ngôn ngữ OOP, các đối tượng thường có thể thay đổi với một tập hợp ngoại lệ giới hạn (ví dụ: bộ dữ liệu và chuỗi trong python). Trong hầu hết các ngôn ngữ chức năng, dữ liệu là bất biến.

Cả hai đối tượng có thể thay đổi và bất biến đều mang lại một danh sách toàn bộ ưu điểm và nhược điểm của riêng chúng.

Có những ngôn ngữ cố gắng kết hôn với cả hai khái niệm như ví dụ scala nơi bạn có (được tuyên bố rõ ràng) dữ liệu có thể thay đổi và không thay đổi (vui lòng sửa cho tôi nếu tôi sai, kiến ​​thức về scala của tôi bị hạn chế nhiều hơn).

Câu hỏi của tôi là: Liệu hoàn chỉnh (! Sic) không thay đổi -ie không có đối tượng có thể đột biến khi nó đã được created- thực hiện bất kỳ ý nghĩa trong bối cảnh OOP?

Có thiết kế hoặc thực hiện của một mô hình như vậy?

Về cơ bản, là (hoàn toàn) bất biến và OOP đối lập hay trực giao?

Động lực: Trong OOP bạn thường hoạt động trên dữ liệu, thay đổi (biến đổi) thông tin cơ bản, giữ các tham chiếu giữa các đối tượng đó. Ví dụ, một đối tượng của lớp Personvới một thành viên fathertham chiếu đến một Personđối tượng khác . Nếu bạn thay đổi tên của người cha, điều này sẽ hiển thị ngay lập tức cho đối tượng con mà không cần cập nhật. Không thay đổi, bạn sẽ cần phải xây dựng các đối tượng mới cho cả cha và con. Nhưng bạn sẽ có ít kerfuffle hơn với các đối tượng được chia sẻ, đa luồng, GIL, v.v.


2
Tính không thay đổi có thể được mô phỏng bằng ngôn ngữ OOP, bằng cách chỉ hiển thị các điểm truy cập đối tượng dưới dạng phương thức hoặc thuộc tính chỉ đọc không làm thay đổi dữ liệu. Tính không thay đổi hoạt động giống nhau trong các ngôn ngữ OOP giống như trong bất kỳ ngôn ngữ chức năng nào, ngoại trừ việc bạn có thể thiếu một số tính năng ngôn ngữ chức năng.
Robert Harvey

4
Khả năng tương tác không phải là một thuộc tính của các ngôn ngữ OOP như C # và Java, cũng không phải là bất biến. Bạn chỉ định khả năng biến đổi hoặc bất biến bằng cách bạn viết lớp.
Robert Harvey

9
Giả định của bạn dường như là tính đột biến là một tính năng cốt lõi của hướng đối tượng. Không phải vậy. Mutility chỉ đơn giản là một tài sản của các đối tượng hoặc giá trị. Hướng đối tượng bao gồm một số khái niệm nội tại (đóng gói, đa hình, kế thừa, v.v.) không liên quan nhiều đến đột biến và bạn vẫn sẽ nhận được lợi ích của các tính năng đó. ngay cả khi bạn làm mọi thứ bất biến.
Robert Harvey

2
@MichaelT Câu hỏi không phải là làm cho những thứ cụ thể trở nên biến đổi, mà là làm cho tất cả mọi thứ trở nên bất biến.

Câu trả lời:


43

OOP và bất biến gần như hoàn toàn trực giao với nhau. Tuy nhiên, lập trình bắt buộc và bất biến là không.

OOP có thể được tóm tắt bởi hai tính năng cốt lõi:

  • Đóng gói : Tôi sẽ không truy cập trực tiếp vào nội dung của các đối tượng mà thay vào đó giao tiếp thông qua một giao diện cụ thể (các phương thức trực tuyến) với đối tượng này. Giao diện này có thể ẩn dữ liệu nội bộ từ tôi. Về mặt kỹ thuật, điều này đặc trưng cho lập trình mô-đun chứ không phải OOP. Truy cập dữ liệu qua giao diện được xác định gần tương đương với loại dữ liệu trừu tượng.

  • Công văn động : Khi tôi gọi một phương thức trên một đối tượng, phương thức được thực thi sẽ được giải quyết trong thời gian chạy. (Ví dụ, trong OOP dựa trên lớp, tôi có thể gọi một sizephương thức trên một IListthể hiện, nhưng cuộc gọi có thể được giải quyết thành một triển khai trong một LinkedListlớp). Công văn động là một cách để cho phép hành vi đa hình.

Đóng gói có ý nghĩa ít hơn mà không có tính đột biến (không có trạng thái bên trong có thể bị hỏng bởi sự can thiệp bên ngoài), nhưng nó vẫn có xu hướng làm cho sự trừu tượng dễ dàng hơn ngay cả khi mọi thứ đều bất biến.

Một chương trình bắt buộc bao gồm các câu lệnh được thực hiện tuần tự. Một tuyên bố có tác dụng phụ như thay đổi trạng thái của chương trình. Với sự bất biến, trạng thái không thể thay đổi (tất nhiên, một trạng thái mới có thể được tạo ra). Do đó, lập trình mệnh lệnh về cơ bản là không tương thích với tính bất biến.

Bây giờ xảy ra rằng OOP trong lịch sử luôn được kết nối với lập trình mệnh lệnh (Simula dựa trên Algol) và tất cả các ngôn ngữ OOP chính thống đều có nguồn gốc bắt buộc (C ++, Java, C #, đấm đều bắt nguồn từ C). Điều này không ngụ ý rằng bản thân OOP sẽ là bắt buộc hoặc có thể thay đổi, điều này chỉ có nghĩa là việc triển khai OOP bằng các ngôn ngữ này cho phép khả năng biến đổi.


2
Cảm ơn bạn rất nhiều, đặc biệt là định nghĩa của hai tính năng cốt lõi.
Hyperboreus

Công văn động không phải là một tính năng cốt lõi của OOP. Không thực sự là Đóng gói (như bạn đã thừa nhận).
Ngừng làm hại Monica

5
@OrangeDog Có, không có định nghĩa được chấp nhận phổ biến về OOP, nhưng tôi cần một định nghĩa để làm việc. Vì vậy, tôi đã chọn một cái gì đó gần với sự thật mà tôi có thể nhận được mà không cần viết một bài luận văn về nó. Tuy nhiên, tôi coi công văn động là tính năng phân biệt chính duy nhất của OOP với các mô hình khác. Một cái gì đó trông giống như OOP nhưng thực sự có tất cả các cuộc gọi được giải quyết tĩnh thực sự chỉ là lập trình mô-đun với đa hình ad-hoc. Các đối tượng là một cặp phương thức và dữ liệu, và tương đương với các bao đóng.
amon

2
"Đối tượng là một cặp phương thức và dữ liệu" sẽ làm. Tất cả những gì bạn cần là một cái gì đó không liên quan gì đến sự bất biến.
Ngừng làm hại Monica

3
@CodeYogi Ẩn dữ liệu là loại đóng gói phổ biến nhất. Tuy nhiên, cách dữ liệu được lưu trữ bên trong bởi một đối tượng không phải là chi tiết triển khai duy nhất cần được ẩn đi. Điều quan trọng không kém là ẩn cách thức giao diện công cộng được triển khai, ví dụ: liệu tôi có sử dụng bất kỳ phương thức trợ giúp nào không. Các phương pháp trợ giúp như vậy cũng nên riêng tư, nói chung. Vì vậy, để tóm tắt: đóng gói là một nguyên tắc, trong khi ẩn dữ liệu là một kỹ thuật đóng gói.
amon

25

Lưu ý, có một văn hóa giữa các lập trình viên hướng đối tượng, nơi mọi người cho rằng nếu bạn đang thực hiện OOP rằng hầu hết các đối tượng của bạn sẽ có thể thay đổi, nhưng đó là một vấn đề riêng biệt với việc liệu OOP có yêu cầu khả năng biến đổi hay không. Ngoài ra, văn hóa đó dường như đang dần thay đổi theo hướng bất biến hơn, do sự tiếp xúc của mọi người với lập trình chức năng.

Scala là một minh họa thực sự tốt rằng tính đột biến không cần thiết cho hướng đối tượng. Trong khi Scala hỗ trợ khả năng biến đổi, việc sử dụng nó không được khuyến khích. Thành ngữ Scala là rất nhiều đối tượng hướng và cũng gần như hoàn toàn bất biến. Nó chủ yếu cho phép khả năng tương thích với Java và vì trong một số trường hợp, các đối tượng bất biến không hiệu quả hoặc không ổn định để làm việc với.

Ví dụ, so sánh danh sách Scaladanh sách Java . Danh sách bất biến của Scala chứa tất cả các phương thức đối tượng giống như danh sách có thể thay đổi của Java. Thực tế, vì Java sử dụng các hàm tĩnh cho các hoạt động như sắp xếp và Scala thêm các phương thức kiểu chức năng như map. Tất cả các đặc điểm của đóng gói, kế thừa và đa hình OOP có sẵn ở dạng quen thuộc với các lập trình viên hướng đối tượng và được sử dụng một cách thích hợp.

Sự khác biệt duy nhất bạn sẽ thấy là khi bạn thay đổi danh sách, bạn sẽ nhận được một đối tượng mới. Điều đó thường yêu cầu bạn sử dụng các mẫu thiết kế khác với các đối tượng có thể thay đổi, nhưng nó không yêu cầu bạn phải từ bỏ OOP hoàn toàn.


17

Tính không thay đổi có thể được mô phỏng bằng ngôn ngữ OOP, bằng cách chỉ hiển thị các điểm truy cập đối tượng dưới dạng phương thức hoặc thuộc tính chỉ đọc không làm thay đổi dữ liệu. Tính không thay đổi hoạt động giống nhau trong các ngôn ngữ OOP giống như trong bất kỳ ngôn ngữ chức năng nào, ngoại trừ việc bạn có thể thiếu một số tính năng ngôn ngữ chức năng.

Giả định của bạn dường như là tính đột biến là một tính năng cốt lõi của hướng đối tượng. Nhưng khả năng biến đổi chỉ đơn giản là một thuộc tính của các đối tượng hoặc giá trị. Hướng đối tượng bao gồm một số khái niệm nội tại (đóng gói, đa hình, kế thừa, v.v.) có ít hoặc không liên quan gì đến đột biến, và bạn vẫn sẽ nhận được lợi ích của các tính năng đó, ngay cả khi bạn biến mọi thứ thành bất biến.

Không phải tất cả các ngôn ngữ chức năng đều yêu cầu sự bất biến. Clojure có một chú thích cụ thể cho phép các loại có thể thay đổi và hầu hết các ngôn ngữ chức năng "thực tế" có một cách để chỉ định các loại có thể thay đổi.

Một câu hỏi tốt hơn để hỏi có thể là "Liệu sự bất biến hoàn toàn có ý nghĩa trong lập trình mệnh lệnh ?" Tôi muốn nói câu trả lời rõ ràng cho câu hỏi đó là không. Để đạt được sự bất biến hoàn toàn trong lập trình mệnh lệnh, bạn sẽ phải từ bỏ những thứ như forvòng lặp (vì bạn sẽ phải thay đổi một biến vòng lặp) để ủng hộ đệ quy, và bây giờ dù sao bạn cũng lập trình theo cách chức năng.


Cảm ơn bạn. Bạn có thể vui lòng giải thích một chút đoạn cuối của bạn ("rõ ràng" có thể là một chút chủ quan).
Hyperboreus

Đã làm ....
Robert Harvey

1
@Hyperboreus Có nhiều cách để đạt được đa hình. Subtyping với công văn động, đa hình tĩnh ad-hoc (hay còn gọi là quá tải chức năng) và đa hình tham số (hay còn gọi là generic) là những cách phổ biến nhất để làm điều đó, và tất cả các cách đều có điểm mạnh và điểm yếu. Các ngôn ngữ OOP hiện đại kết hợp cả ba cách này, trong khi Haskell chủ yếu dựa vào đa hình tham số và đa hình ad-hoc.
amon

3
@RobertHarvey Bạn nói rằng bạn cần khả năng biến đổi vì bạn cần lặp (nếu không bạn phải sử dụng đệ quy). Trước khi tôi bắt đầu sử dụng Haskell hai năm trước, tôi cũng nghĩ rằng mình cũng cần các biến có thể thay đổi. Tôi chỉ nói có những cách khác để "lặp" (ánh xạ, gấp, lọc, v.v.). Khi bạn thực hiện vòng lặp khỏi bảng, tại sao bạn lại cần các biến có thể thay đổi?
cimmanon

1
@RobertHarvey Nhưng đây chính xác là điểm của ngôn ngữ lập trình: Những gì được tiếp xúc với bạn và không phải là những gì đang diễn ra dưới mui xe. Cái sau là khả năng đáp ứng của trình biên dịch hoặc trình thông dịch, không phải của nhà phát triển ứng dụng. Nếu không thì quay lại lắp ráp.
Hyperboreus

5

Việc phân loại các đối tượng là đóng gói các giá trị hoặc thực thể thường rất hữu ích, với điểm khác biệt là nếu một thứ gì đó là một giá trị, mã giữ tham chiếu đến nó sẽ không bao giờ thấy thay đổi trạng thái của nó theo bất kỳ kiểu nào mà bản thân mã không khởi tạo. Ngược lại, mã chứa tham chiếu đến một thực thể có thể mong đợi nó thay đổi theo những cách nằm ngoài sự kiểm soát của chủ sở hữu tham chiếu.

Mặc dù có thể sử dụng giá trị đóng gói bằng cách sử dụng các đối tượng thuộc loại có thể thay đổi hoặc không thay đổi, một đối tượng chỉ có thể hoạt động như một giá trị nếu áp dụng ít nhất một trong các điều kiện sau:

  1. Không có tài liệu tham khảo nào về đối tượng sẽ được tiếp xúc với bất cứ điều gì có thể thay đổi trạng thái được gói gọn trong đó.

  2. Người giữ ít nhất một trong số các tham chiếu đến đối tượng biết tất cả các cách sử dụng mà bất kỳ tham chiếu còn lại nào có thể được đặt.

Vì tất cả các trường hợp của các loại bất biến tự động đáp ứng yêu cầu đầu tiên, sử dụng chúng làm giá trị là dễ dàng. Đảm bảo rằng một trong hai yêu cầu được đáp ứng khi sử dụng các loại có thể thay đổi, ngược lại, khó khăn hơn nhiều. Trong khi các tham chiếu đến các loại bất biến có thể được tự do chuyển qua như một phương tiện đóng gói trạng thái được gói gọn trong đó, chuyển qua trạng thái được lưu trữ trong các loại có thể thay đổi đòi hỏi phải xây dựng các đối tượng bao bọc bất biến, hoặc sao chép trạng thái được đóng gói bởi các đối tượng riêng tư vào các đối tượng khác hoặc được cung cấp bởi hoặc được xây dựng cho người nhận dữ liệu.

Các loại không thay đổi hoạt động rất tốt để truyền các giá trị và thường ít nhất có thể sử dụng được để thao túng chúng. Họ không tốt lắm, tuy nhiên, trong việc xử lý các thực thể. Điều gần nhất mà người ta có thể có đối với một thực thể trong một hệ thống với các kiểu hoàn toàn bất biến là một hàm, với trạng thái của hệ thống, sẽ báo cáo các thuộc tính của một phần nào đó hoặc tạo ra một thể hiện trạng thái hệ thống mới giống như một cung cấp một ngoại trừ một số phần cụ thể sẽ khác nhau trong một số thời trang có thể lựa chọn. Hơn nữa, nếu mục đích của một thực thể là giao diện một số mã với một cái gì đó tồn tại trong thế giới thực, thì thực thể đó có thể tránh việc phơi bày trạng thái có thể thay đổi.

Ví dụ: nếu một người nhận được một số dữ liệu qua kết nối TCP, người ta có thể tạo ra một đối tượng "trạng thái của thế giới" mới bao gồm dữ liệu đó trong bộ đệm của mình mà không ảnh hưởng đến bất kỳ tham chiếu nào đến "trạng thái cũ" của thế giới, nhưng các bản sao cũ của trạng thái thế giới không bao gồm lô dữ liệu cuối cùng sẽ bị lỗi và không nên được sử dụng vì chúng sẽ không còn phù hợp với trạng thái của ổ cắm TCP trong thế giới thực.


4

Trong c # một số loại là bất biến như chuỗi.

Điều này dường như hơn nữa cho thấy rằng sự lựa chọn đã được xem xét mạnh mẽ.

Để chắc chắn rằng hiệu suất thực sự đòi hỏi phải sử dụng các loại không thay đổi nếu bạn phải sửa đổi loại đó hàng trăm ngàn lần. Đó là lý do tại sao nó được đề xuất sử dụng StringBuilderlớp thay vì stringlớp trong trường hợp này.

Tôi đã thực hiện một thử nghiệm với trình lược tả và sử dụng loại không thay đổi thực sự đòi hỏi nhiều CPU và RAM hơn.

Nó cũng trực quan nếu bạn xem xét rằng để sửa đổi chỉ một chữ cái trong chuỗi 4000 ký tự, bạn phải sao chép mọi ký tự trong một khu vực khác của RAM.


6
Thường xuyên sửa đổi dữ liệu bất biến không cần phải chậm một cách thảm khốc như với stringsự kết hợp lặp đi lặp lại . Đối với hầu hết tất cả các loại dữ liệu / trường hợp sử dụng, một cấu trúc bền vững hiệu quả có thể (thường đã được) phát minh ra. Hầu hết những người có hiệu suất gần như bằng nhau, ngay cả khi các yếu tố không đổi đôi khi tồi tệ hơn.

@delnan Tôi cũng nghĩ rằng đoạn cuối của câu trả lời là về một chi tiết triển khai hơn là về khả năng biến đổi (im).
Hyperboreus

@Hyperboreus: bạn có nghĩ rằng tôi nên xóa phần đó không? Nhưng làm thế nào một chuỗi có thể thay đổi nếu nó là bất biến? Ý tôi là .. theo ý kiến ​​của tôi, nhưng chắc chắn tôi có thể sai, đó có thể là lý do chính tại sao đối tượng không phải là bất biến.
Hồi sinh

1
@Revious Bằng không có nghĩa. Để lại nó, vì vậy nó gây ra cuộc thảo luận và ý kiến ​​và quan điểm thú vị hơn.
Hyperboreus

1
@Revious Có, đọc sẽ chậm hơn, mặc dù không chậm như thay đổi một string(đại diện truyền thống). Một "chuỗi" (trong đại diện mà tôi đang nói đến) sau 1000 lần sửa đổi sẽ giống như một chuỗi được tạo mới (nội dung modulo); không có cấu trúc dữ liệu liên tục hữu ích hoặc được sử dụng rộng rãi làm suy giảm chất lượng sau các hoạt động X. Phân mảnh bộ nhớ không phải là vấn đề nghiêm trọng (bạn có nhiều phân bổ, vâng, nhưng phân mảnh cũng không phải là vấn đề trong bộ thu gom rác hiện đại)

0

Hoàn toàn bất biến của mọi thứ không có ý nghĩa nhiều trong OOP, hoặc hầu hết các mô hình khác cho vấn đề đó, vì một lý do rất lớn:

Mỗi chương trình hữu ích đều có tác dụng phụ.

Một chương trình không gây ra bất cứ điều gì để thay đổi, là vô giá trị. Bạn cũng có thể không chạy nó, vì hiệu ứng sẽ giống hệt nhau.

Ngay cả khi bạn nghĩ rằng bạn không thay đổi bất cứ điều gì và chỉ đơn giản là tóm tắt một danh sách các số bạn nhận được bằng cách nào đó, hãy xem xét rằng bạn cần phải làm gì đó với kết quả - cho dù bạn in nó ra đầu ra tiêu chuẩn, hãy ghi nó vào một tệp, hoặc bất cứ nơi nào. Và điều đó liên quan đến việc đột biến một bộ đệm và thay đổi trạng thái của hệ thống.

Nó có thể có nhiều ý nghĩa để hạn chế khả năng biến đổi đối với các bộ phận cần có thể thay đổi. Nhưng nếu hoàn toàn không có gì cần thay đổi, thì bạn không làm gì đáng làm cả.


4
Tôi không thấy câu trả lời của bạn liên quan đến câu hỏi như thế nào vì tôi không giải quyết các ngôn ngữ chức năng thuần túy. Lấy erlang làm ví dụ: Dữ liệu không thay đổi, không có sự phân công phá hủy, không gây ấn tượng về tác dụng phụ. Ngoài ra, bạn có trạng thái trong một ngôn ngữ chức năng, chỉ có trạng thái "chảy" qua các chức năng, không giống như các chức năng hoạt động trên trạng thái. Trạng thái thay đổi, nhưng nó không đột biến tại chỗ, nhưng trạng thái trong tương lai sẽ thay thế trạng thái hiện tại. Sự bất biến không phải là về việc bộ nhớ đệm có bị thay đổi hay không, mà là về việc những đột biến này có thể nhìn thấy từ bên ngoài hay không.
Hyperboreus

Và một nhà nước trong tương lai thay thế trạng thái hiện tại như thế nào , chính xác? Trong một chương trình OO, trạng thái đó là một thuộc tính của một đối tượng ở đâu đó. Việc thay thế trạng thái đòi hỏi phải thay đổi một đối tượng (hoặc thay thế nó bằng một đối tượng khác, điều này đòi hỏi phải thay đổi đối tượng tham chiếu đến nó (hoặc thay thế nó bằng một đối tượng khác, mà ... eh. Bạn nhận được điểm)). Bạn có thể nghĩ ra một số loại hack đơn điệu, trong đó mọi hành động kết thúc tạo ra một ứng dụng hoàn toàn mới ... nhưng ngay cả khi đó, trạng thái hiện tại của chương trình phải được ghi lại ở đâu đó.
cHao 17/03/2016

7
-1. Điều này là không đúng. Bạn đang nhầm lẫn các tác dụng phụ với đột biến và trong khi chúng thường được đối xử giống nhau bởi các ngôn ngữ chức năng, thì chúng lại khác nhau. Mỗi chương trình hữu ích đều có tác dụng phụ; không phải mọi chương trình hữu ích đều có đột biến.
Michael Shaw

@Michael: Khi nói đến OOP, đột biến và tác dụng phụ đan xen nhau đến mức chúng không thể tách rời thực tế. Nếu bạn không có đột biến, thì bạn không thể có tác dụng phụ nếu không có số lượng lớn tin tặc.
cHao


-2

Tôi nghĩ nó phụ thuộc vào việc định nghĩa OOP của bạn có phải là nó sử dụng kiểu truyền tin nhắn hay không.

Các hàm thuần túy không phải biến đổi bất cứ thứ gì vì chúng trả về các giá trị mà bạn có thể lưu trữ trong các biến mới.

var brandNewVariable = pureFunction(foo);

Với kiểu truyền tin nhắn, bạn bảo một đối tượng lưu trữ dữ liệu mới thay vì hỏi nó dữ liệu mới nào bạn nên lưu trữ trong một biến mới.

sameOldObject.changeMe(foo);

Có thể có các đối tượng và không làm biến đổi chúng, bằng cách làm cho các phương thức của nó các hàm thuần túy xảy ra để sống bên trong đối tượng thay vì bên ngoài.

var brandNewVariable = nonMutatingObject.askMe(foo);

Nhưng không thể trộn lẫn phong cách truyền thông điệp và các đối tượng bất biến.

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.