Trước OOP, các thành viên cấu trúc dữ liệu có bị bỏ mặc không?


44

Khi cấu trúc dữ liệu (ví dụ: hàng đợi) được triển khai bằng ngôn ngữ OOP, một số thành viên của cấu trúc dữ liệu cần phải ở chế độ riêng tư (ví dụ: số lượng mục trong hàng đợi).

Một hàng đợi cũng có thể được thực hiện bằng ngôn ngữ thủ tục bằng cách sử dụng structmột tập hợp các hàm hoạt động trên struct. Tuy nhiên, trong một ngôn ngữ thủ tục, bạn không thể làm cho các thành viên của một structtư nhân. Các thành viên của cấu trúc dữ liệu được triển khai theo ngôn ngữ thủ tục có được công khai không, hoặc có một số mẹo để đặt chúng ở chế độ riêng tư?


75
"Một số thành viên của cấu trúc dữ liệu cần phải riêng tư" Có một sự khác biệt lớn giữa "có lẽ nên" và "cần phải có". Đưa cho tôi một ngôn ngữ OO và tôi đảm bảo tôi có thể tạo một hàng đợi hoạt động hoàn toàn tốt ngay cả với tất cả các thành viên và phương thức của nó được công khai, miễn là bạn không lạm dụng tất cả sự tự do đó.
8bittree

48
Để tạo ra một vòng quay khác với những gì @ 8bittree đã nói, việc mọi thứ công khai đều ổn nếu những người sử dụng mã của bạn đủ kỷ luật để tuân theo giao diện bạn đã đặt ra. Cấu trúc thành viên tư nhân xuất hiện do những người không thể giữ mũi của họ ra khỏi nơi họ không thuộc về.
Blrfl

20
Ý của bạn là "trước khi đóng gói trở nên phổ biến"? Đóng gói khá phổ biến trước khi các ngôn ngữ OO trở nên phổ biến.
Frank Hileman

6
@FrankHileman Tôi nghĩ đó thực sự là cốt lõi của câu hỏi: OP muốn biết liệu đóng gói có tồn tại trong các ngôn ngữ thủ tục hay không, trước Simula / Smalltalk / C ++
dcorking

18
Tôi xin lỗi trước nếu điều này xảy ra là hạ thấp, tôi không có ý đó. Bạn cần học một số ngôn ngữ khác. Ngôn ngữ lập trình không dành cho máy móc để chạy, chúng dành cho lập trình viên suy nghĩ . Họ nhất thiết phải định hình cách bạn nghĩ. Đơn giản là bạn sẽ không có câu hỏi này nếu bạn dành bất kỳ thời gian đáng kể nào để làm việc với JavaScript / Python / Ocaml / Clojure, ngay cả khi bạn đã làm Java cả ngày trong công việc hàng ngày. Khác với một dự án nguồn mở C ++ mà tôi làm việc (hầu hết là C dù sao) Tôi thực sự đã sử dụng một ngôn ngữ với các công cụ sửa đổi truy cập từ khi học đại học và tôi đã không bỏ lỡ chúng.
Jared Smith

Câu trả lời:


139

OOP không phát minh ra đóng gói và không đồng nghĩa với đóng gói. Nhiều ngôn ngữ OOP không có bộ sửa đổi truy cập kiểu C ++ / Java. Nhiều ngôn ngữ không phải OOP có các kỹ thuật khác nhau có sẵn để cung cấp đóng gói.

Một cách tiếp cận cổ điển để đóng gói là đóng cửa , như được sử dụng trong lập trình chức năng . Điều này cũ hơn đáng kể so với OOP nhưng theo cách tương đương. Ví dụ: trong JavaScript, chúng tôi có thể tạo một đối tượng như thế này:

function Adder(x) {
  this.add = function add(y) {
    return x + y;
  }
}

var plus2 = new Adder(2);
plus2.add(7);  //=> 9

plus2Đối tượng trên không có thành viên nào cho phép truy cập trực tiếp vào x- nó hoàn toàn được gói gọn. Các add()phương pháp là một đóng cửa so với xbiến.

Các C ngôn ngữ hỗ trợ một số loại đóng gói thông qua nó tập tin tiêu đề cơ chế, đặc biệt là con trỏ đục kỹ thuật. Trong C, có thể khai báo tên cấu trúc mà không xác định thành viên của nó. Tại thời điểm đó, không có biến nào của loại cấu trúc đó có thể được sử dụng, nhưng chúng ta có thể sử dụng các con trỏ tới cấu trúc đó một cách tự do (vì kích thước của một con trỏ cấu trúc được biết đến tại thời điểm biên dịch). Ví dụ: xem xét tệp tiêu đề này:

#ifndef ADDER_H
#define ADDER_H

typedef struct AdderImpl *Adder;

Adder Adder_new(int x);
void Adder_free(Adder self);
int Adder_add(Adder self, int y);

#endif

Bây giờ chúng ta có thể viết mã sử dụng giao diện Adder này mà không cần truy cập vào các trường của nó, ví dụ:

Adder plus2 = Adder_new(2);
if (!plus2) abort();
printf("%d\n", Adder_add(plus2, 7));  /* => 9 */
Adder_free(plus2);

Và đây sẽ là chi tiết thực hiện được gói gọn:

#include "adder.h"

struct AdderImpl { int x; };

Adder Adder_new(int x) {
  Adder self = malloc(sizeof *self);
  if (!self) return NULL;
  self->x = x;
  return self;
}

void Adder_free(Adder self) {
  free(self);
}

int Adder_add(Adder self, int y) {
  return self->x + y;
}

Ngoài ra còn có lớp ngôn ngữ lập trình mô-đun , tập trung vào các giao diện cấp mô-đun. Gia đình ngôn ngữ ML bao gồm. OCaml bao gồm một cách tiếp cận thú vị cho các mô-đun được gọi là functor . OOP bị lu mờ và phần lớn là lập trình mô-đun, nhưng nhiều lợi thế có chủ đích của OOP là về tính mô đun hơn là hướng đối tượng.

Ngoài ra còn có quan sát rằng các lớp trong các ngôn ngữ OOP như C ++ hoặc Java thường không được sử dụng cho các đối tượng (theo nghĩa các thực thể giải quyết các hoạt động thông qua liên kết động / công văn muộn) mà chỉ dành cho các loại dữ liệu trừu tượng (trong đó chúng tôi xác định giao diện chung ẩn chi tiết thực hiện nội bộ). Bài viết về Tìm hiểu trừu tượng dữ liệu, được xem xét lại (Cook, 2009) thảo luận về sự khác biệt này chi tiết hơn.

Nhưng có, nhiều ngôn ngữ không có cơ chế đóng gói nào. Trong các ngôn ngữ này, các thành viên cấu trúc được để lại công khai. Nhiều nhất, sẽ có một quy ước đặt tên không khuyến khích sử dụng. Ví dụ, tôi nghĩ Pascal không có cơ chế đóng gói hữu ích.


11
Thấy lỗi trong Adder self = malloc(sizeof(Adder));? Có một lý do con trỏ typedef-ing và sizeof(TYPE)thường được tán thành.
Ded repeatator

10
Bạn không thể chỉ viết sizeof(*Adder), bởi vì *Adderkhông phải là một loại, cũng như *int *không phải là một loại. Biểu thức T t = malloc(sizeof *t)là cả thành ngữ và chính xác. Xem chỉnh sửa của tôi.
wchargein

4
Pascal có các biến đơn vị không thể nhìn thấy từ bên ngoài đơn vị đó. Thực tế, các biến đơn vị tương đương với private staticcác biến trong Java. Tương tự như vậy với C, bạn có thể sử dụng các con trỏ mờ để truyền dữ liệu trong Pascal mà không cần khai báo nó là gì. MacOS cổ điển đã sử dụng rất nhiều con trỏ mờ vì các phần công khai và riêng tư của một bản ghi (cấu trúc dữ liệu) có thể được truyền cùng nhau. Tôi nhớ Window Manager thực hiện rất nhiều điều này vì các phần của Window Record là công khai nhưng một số thông tin nội bộ cũng được đưa vào.
Michael Storesin

6
Có lẽ một ví dụ tốt hơn Pascal là Python, hỗ trợ hướng đối tượng nhưng không đóng gói, sử dụng các quy ước đặt tên như _private_memberoutput_property_, hoặc các kỹ thuật nâng cao hơn để tạo các đối tượng không thể thay đổi.
Mephy

11
Có một xu hướng khó chịu trong tài liệu của 3M để trình bày mọi nguyên tắc thiết kế như một nguyên tắc thiết kế OO . Văn học (không mang tính hàn lâm) có xu hướng vẽ một bức tranh về "thời kỳ đen tối" nơi mọi người đều làm sai, và sau đó các học viên OOP mang đến ánh sáng. Theo như tôi có thể nói, điều này chủ yếu xuất phát từ sự thiếu hiểu biết. Ví dụ, theo như tôi có thể nói, Bob Martin đã cho lập trình chức năng một cái nhìn nghiêm túc chỉ một vài năm trước đây.
Derek Elkins

31

Đầu tiên, là thủ tục so với hướng đối tượng không liên quan gì đến công khai và riêng tư. Rất nhiều ngôn ngữ hướng đối tượng không có khái niệm về kiểm soát truy cập.

Thứ hai, trong "C" - mà hầu hết mọi người sẽ gọi là thủ tục và không hướng đối tượng, có rất nhiều thủ thuật bạn có thể sử dụng để làm cho mọi thứ trở nên riêng tư. Một cái rất phổ biến là sử dụng các con trỏ mờ (ví dụ void *). Hoặc - bạn có thể chuyển tiếp khai báo một đối tượng và chỉ không xác định nó trong tệp tiêu đề.

foo.h:

struct queue;
struct queue* makeQueue();
void add2Queue(struct queue* q, int value);
...

foo.c:

struct queue {
    int* head;
    int* head;
};
struct queue* makeQueue() { .... }
void add2Queue(struct queue* q, int value) { ... }

Nhìn vào SDK Windows! Nó sử dụng HANDLE và UINT_PTR và những thứ tương tự như là các thẻ điều khiển chung cho bộ nhớ được sử dụng trong API - làm cho việc triển khai ở chế độ riêng tư một cách hiệu quả.


1
Mẫu của tôi đã chứng minh một cách tiếp cận (C) tốt hơn - sử dụng các cấu trúc được khai báo phía trước. để sử dụng phương pháp void * tôi sẽ sử dụng typedefs: Trong tệp .h nói typedef void * queue, và sau đó ở mọi nơi chúng ta có hàng đợi cấu trúc chỉ cần nói hàng đợi; Sau đó, trong tệp .c, đổi tên hàng đợi struct thành struct queueImpl và bất kỳ đối số nào trở thành hàng đợi (không phải là hàng đợi struct *) và dòng mã đầu tiên cho mỗi chức năng đó trở thành struct queueImpl * qi = (struct queueImpl *) q
Lewis Pringle

7
Hừm. Nó làm cho nó ở chế độ riêng tư vì bạn không thể truy cập (đọc hoặc ghi) bất kỳ trường nào của 'hàng đợi' từ bất kỳ nơi nào ngoài việc triển khai (tệp foo.c). Những gì bạn có nghĩa là riêng tư? BTW - điều đó đúng với CẢ HAI typedef void * apporach và cách tiếp cận cấu trúc khai báo phía trước (tốt hơn)
Lewis Pringle

5
Tôi phải thú nhận đã gần 40 năm kể từ khi tôi đọc cuốn sách trên smalltalk-80, nhưng tôi không nhớ bất kỳ khái niệm nào về các thành viên dữ liệu công khai hoặc riêng tư. Tôi nghĩ CLOS cũng không có khái niệm như vậy. Đối tượng Pascal không có khái niệm như vậy. Tôi nhớ lại Simula đã làm (có lẽ là nơi Stroustrup có ý tưởng) và hầu hết các ngôn ngữ OO kể từ C ++ đều có nó. Dù sao đi nữa - chúng tôi đồng ý đóng gói và dữ liệu riêng tư là những ý tưởng tốt. Ngay cả người hỏi ban đầu cũng rõ ràng về điểm đó. Anh ta chỉ hỏi - làm thế nào oldies thực hiện đóng gói trong các ngôn ngữ tiền C ++.
Lewis Pringle

5
@LewisPringle không có đề cập đến các thành viên dữ liệu công khai trong Smalltalk-80 vì tất cả "biến thể hiện" (thành viên dữ liệu) là riêng tư, trừ khi bạn sử dụng phản ánh. AFAIU Smalltalkers viết một trình truy cập cho mọi biến họ muốn công khai.
dcorking

4
Ngược lại, @LewisPringle, tất cả các "phương thức" của Smalltalk đều là công khai (có những quy ước vụng về để đánh dấu chúng là riêng tư)
dcorking 19/07/18

13

"Các loại dữ liệu mờ" là một khái niệm nổi tiếng khi tôi học văn bằng máy tính 30 năm trước. Chúng tôi không bao gồm OOP vì nó không được sử dụng phổ biến vào thời điểm đó và "lập trình chức năng" được coi là chính xác hơn.

Modula-2 đã hỗ trợ trực tiếp cho họ, xem https://www.modula2.org/reference/modules.php .

Lewis Pringle đã giải thích về cách khai báo một cấu trúc có thể được sử dụng trong C. Không giống như Mô-đun 2, một chức năng của nhà máy phải được cung cấp để tạo đối tượng. ( Các phương thức ảo cũng dễ thực hiện trong C bằng cách có thành viên đầu tiên của một cấu trúc là một con trỏ tới một cấu trúc khác có chứa các con trỏ hàm cho các phương thức.)

Thông thường quy ước cũng được sử dụng. Ví dụ: không nên truy cập bất kỳ trường nào bắt đầu bằng miền _ _ bên ngoài tệp sở hữu dữ liệu. Điều này đã được thực thi dễ dàng bằng cách tạo ra các công cụ kiểm tra tùy chỉnh.

Mỗi dự án quy mô lớn mà tôi đã làm việc, (trước khi tôi chuyển sang C ++ thì C #) đã có một hệ thống để ngăn chặn dữ liệu "riêng tư" bị truy cập bởi mã sai. Nó chỉ là một chút ít tiêu chuẩn hơn bây giờ.


9

Lưu ý rằng có nhiều ngôn ngữ OO không có khả năng tích hợp để đánh dấu các thành viên riêng tư. Điều này có thể được thực hiện theo quy ước, mà không cần trình biên dịch để thực thi quyền riêng tư. Ví dụ, mọi người sẽ thường tiền tố các biến riêng tư với dấu gạch dưới.

Có các kỹ thuật để làm cho việc truy cập các biến "riêng tư" trở nên khó khăn hơn, phổ biến nhất là thành ngữ PIMPL . Điều này đặt các biến riêng tư của bạn trong một cấu trúc riêng biệt, chỉ với một con trỏ được phân bổ trong các tệp tiêu đề công khai của bạn. Điều này có nghĩa là một sự bổ sung thêm và một diễn viên để có được bất kỳ biến riêng tư nào, một cái gì đó giống như ((private_impl)(obj->private))->actual_value, gây khó chịu, vì vậy trong thực tế hiếm khi được sử dụng.


4

Cấu trúc dữ liệu không có "thành viên", chỉ có các trường dữ liệu (giả sử đó là loại bản ghi). Tầm nhìn thường được đặt cho toàn bộ loại. Tuy nhiên, điều đó có thể không giới hạn như bạn nghĩ, bởi vì các chức năng không phải là một phần của bản ghi.

Hãy quay lại và tìm hiểu một chút về lịch sử ở đây ...

Mô hình lập trình chi phối trước OOP được gọi là lập trình có cấu trúc . Mục tiêu chính ban đầu của việc này là để tránh sử dụng các câu lệnh nhảy không có cấu trúc ("goto" s). Đây là mô hình hướng dòng điều khiển (trong khi OOP thiên về dữ liệu hơn), nhưng nó vẫn là một phần mở rộng tự nhiên của nó để cố gắng giữ dữ liệu có cấu trúc logic giống như mã.

Một phần khác của lập trình có cấu trúc là ẩn thông tin , ý tưởng rằng việc triển khai cấu trúc của mã (có khả năng thay đổi khá thường xuyên) nên được tách biệt khỏi giao diện (lý tưởng là sẽ không thay đổi gần như nhiều). Bây giờ là giáo điều, nhưng vào thời xa xưa, nhiều người thực sự coi mọi nhà phát triển nên tìm hiểu chi tiết về toàn bộ hệ thống, vì vậy đây thực sự là một ý tưởng gây tranh cãi. Phiên bản gốc của Tháng huyền thoại của Brook thực sự lập luận chống lại việc che giấu thông tin.

Các ngôn ngữ lập trình sau này được thiết kế rõ ràng là ngôn ngữ lập trình có cấu trúc tốt (ví dụ Modula-2 và Ada) thường bao gồm thông tin ẩn như một khái niệm cơ bản, được xây dựng xung quanh một số loại khái niệm về một cơ sở gắn kết của các hàm (và bất kỳ loại, hằng, và đối tượng họ có thể yêu cầu). Trong Modula-2, chúng được gọi là "Mô-đun", trong "Gói" của Ada. Rất nhiều ngôn ngữ OOP hiện đại gọi cùng một khái niệm là "không gian tên". Các không gian tên này là nền tảng tổ chức phát triển trong các ngôn ngữ này và cho hầu hết các mục đích có thể được sử dụng tương tự như các lớp OOP (tất nhiên không có hỗ trợ thực sự cho việc thừa kế).

Vì vậy, trong Modula-2 và Ada (83), bạn có thể khai báo bất kỳ thường trình, loại, hằng hoặc đối tượng trong một không gian tên riêng tư hoặc công khai, nhưng nếu bạn có một loại bản ghi, không có cách nào (dễ dàng) để khai báo một số trường bản ghi công khai và những người khác riêng tư. Toàn bộ hồ sơ của bạn là công khai, hoặc không.


Tôi đã dành khá nhiều thời gian làm việc tại Ada. Ẩn chọn lọc (một phần của kiểu dữ liệu) là điều chúng tôi đã làm mọi lúc; trong gói chứa, bạn sẽ tự xác định loại là riêng tư hoặc riêng tư có giới hạn; giao diện gói sẽ hiển thị các chức năng / quy trình công cộng để nhận và / hoặc đặt các trường nội bộ. Những thói quen đó tất nhiên sẽ cần phải có một tham số của kiểu riêng. Tôi đã không sau đó và bây giờ không xem xét điều này khó khăn.
David

Ngoài ra, hầu hết các ngôn ngữ OO của AFAIK đều hoạt động theo cùng một cách trong phần mềm, tức là myWidget.getFoo () thực sự được triển khai dưới dạng getFoo (myWidget). Việc object.method()gọi chỉ là đường cú pháp. IMHO quan trọng - xem Nguyên tắc truy cập / tham khảo thống nhất của Meyer - nhưng vẫn chỉ là cú pháp cú pháp.
David

@David - Đó là lý lẽ của cộng đồng Ada trong nhiều năm trong kỷ nguyên Ada 95. Tôi tin rằng cuối cùng họ đã nhượng bộ và chứng minh lập luận của riêng mình bằng cách cho phép object.method()như một hình thức thay thế method(object, ...) cho những người không thể thực hiện bước nhảy vọt về mặt khái niệm.
TED

0

Trong C, bạn có thể chuyển các con trỏ tới các kiểu khai báo nhưng không xác định như những người khác đã nói, có hiệu lực hạn chế quyền truy cập vào tất cả các trường.

Bạn cũng có thể có các chức năng riêng tư và công khai trên cơ sở mô-đun. Các hàm được khai báo tĩnh trong tệp nguồn không hiển thị bên ngoài, ngay cả khi bạn cố đoán tên của chúng. Tương tự, bạn có thể có các biến toàn cục ở cấp độ tệp tĩnh, thường là thực tiễn xấu nhưng cho phép cách ly trên cơ sở mô-đun.

Có lẽ rất quan trọng để nhấn mạnh rằng hạn chế truy cập như một quy ước được tiêu chuẩn hóa tốt hơn là một cấu trúc được thi hành bằng ngôn ngữ hoạt động tốt (xem Python). Trên hết, việc hạn chế quyền truy cập vào các trường đối tượng sẽ chỉ bảo vệ lập trình viên khi có nhu cầu thay đổi giá trị của dữ liệu bên trong một đối tượng sau khi tạo. Mà đã là một mùi mã. Có thể cho rằng, consttừ khóa của C và đặc biệt là C ++ cho các phương thức và đối số hàm là một trợ giúp lớn hơn cho lập trình viên so với Java khá kém final.


Tính năng duy nhất mà C có đặc biệt để che giấu thông tin là staticdữ liệu và hoạt động toàn cầu (có nghĩa là chúng không được trình bày cho trình liên kết để sử dụng từ các phần tổng hợp khác). Bạn có thể tranh luận một cách hợp lý bất kỳ sự hỗ trợ nào mà C đã có đối với các hoạt động thiết kế phần mềm tốt ngoài việc đó là một vụ hack và không phải là một phần của thiết kế ban đầu của ngôn ngữ vào năm 1972.
TED

0

Nếu định nghĩa về Công khai của bạn là khả năng truy cập triển khai và dữ liệu / thuộc tính thông qua mã của riêng bạn tại bất kỳ thời điểm nào, câu trả lời chỉ đơn giản là: . Tuy nhiên, nó được trừu tượng hóa bằng nhiều phương tiện khác nhau - tùy thuộc vào ngôn ngữ.

Tôi hy vọng điều này ngắn gọn trả lời câu hỏi của bạn.


-1

Đây là một ví dụ phản biện rất đơn giản: trong Java, interfaces định nghĩa các đối tượng, nhưng classes thì không. A classđịnh nghĩa một Kiểu dữ liệu trừu tượng, không phải là một đối tượng.

Ergo, bất cứ khi nào bạn sử dụng privatetrong classJava, bạn có một ví dụ về cấu trúc dữ liệu với các thành viên riêng không hướng đối tượng.


7
Câu trả lời này tất nhiên là đúng về mặt kỹ thuật, nhưng nó hoàn toàn không thể hiểu được đối với bất kỳ ai chưa biết ADT là gì và chúng khác với các vật thể như thế nào.
amon

1
Tôi đã học được điều gì đó từ câu trả lời này.
littleO

3
Các giao diện không "xác định" các đối tượng; họ chỉ định hợp đồng cho các hoạt động / hành vi mà các đối tượng có thể thực hiện hoặc thực hiện. Giống như sự kế thừa thường được mô tả bởi một mối quan hệ và thành phần bởi một mối quan hệ, các giao diện thường được mô tả bằng cách có thể thực hiện các mối quan hệ.
code_dredd
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.