Czy przed OOP członkowie struktury danych byli publicznie dostępni?

44

Gdy struktura danych (na przykład kolejka) jest implementowana przy użyciu języka OOP, niektórzy członkowie struktury danych muszą być prywatni (na przykład liczba elementów w kolejce).

Kolejka może być również zaimplementowana w języku proceduralnym przy użyciu structzestawu funkcji działających w systemie struct. Jednak w języku proceduralnym nie można uczynić członków structprywatnymi. Czy członkowie struktury danych zaimplementowani w języku proceduralnym pozostali publiczni, czy też istnieje jakiś sposób na uczynienie ich prywatnymi?

Krzysztof
źródło
75
„niektórzy członkowie struktury danych muszą być prywatni” Istnieje duża różnica między „prawdopodobnie powinno być” a „musi być”. Daj mi język OO, a gwarantuję, że mogę stworzyć kolejkę, która działa idealnie, nawet gdy wszyscy jej członkowie i metody są publiczne, o ile nie będziesz nadużywać tej wolności.
8bittree
48
Aby rzucić okiem na to, co powiedział @ 8bittree, upublicznienie wszystkiego jest w porządku, jeśli ludzie używający twojego kodu są wystarczająco zdyscyplinowani, aby trzymać się interfejsu, który ustaliłeś. Konstrukcja członka prywatnego powstała z powodu ludzi, którzy nie mogli trzymać nosa z dala od miejsca, do którego nie należą.
Blrfl
20
Miałeś na myśli „zanim enkapsulacja stała się popularna”? Hermetyzacja była dość popularna, zanim języki OO stały się popularne.
Frank Hileman
6
@FrankHileman Myślę, że to właściwie sedno pytania: OP chce wiedzieć, czy w językach proceduralnych istniało enkapsulacja, przed Simula / Smalltalk / C ++
dcorking
18
Z góry przepraszam, jeśli okaże się to protekcjonalne, nie mam na myśli tego. Musisz nauczyć się innych języków. Języki programowania nie są przeznaczone do uruchamiania maszyn, lecz do myślenia programistów . Z konieczności kształtują twój sposób myślenia. Po prostu nie miałbyś tego pytania, gdybyś spędził znaczący czas na pracy z JavaScript / Python / Ocaml / Clojure, nawet jeśli robiłeś Javę przez cały dzień w swojej pracy. Poza jednym projektem open source C ++, nad którym pracuję (w każdym razie głównie C), tak naprawdę nie używałem języka z modyfikatorami dostępu od czasów college'u i nie przegapiłem ich.
Jared Smith

Odpowiedzi:

139

OOP nie wynalazł enkapsulacji i nie jest synonimem enkapsulacji. Wiele języków OOP nie ma modyfikatorów dostępu w stylu C ++ / Java. Wiele języków innych niż OOP oferuje różne techniki oferujące enkapsulację.

Klasycznym podejściem do enkapsulacji są zamknięcia , stosowane w programowaniu funkcjonalnym . Jest to znacznie starsze niż OOP, ale jest w pewnym sensie równoważne. Np. W JavaScript możemy stworzyć taki obiekt:

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

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

Powyższy plus2obiekt nie ma elementu, który umożliwiałby bezpośredni dostęp do x- jest całkowicie zamknięty. add()Metoda jest zamknięcie na xzmiennej.

Język C obsługuje pewne rodzaje enkapsulacji za pomocą mechanizmu pliku nagłówkowego , w szczególności technikę nieprzezroczystego wskaźnika . W C można zadeklarować nazwę struktury bez definiowania jej członków. W tym momencie nie można użyć żadnej zmiennej typu tej struktury, ale możemy swobodnie używać wskaźników do tej struktury (ponieważ rozmiar wskaźnika struktury jest znany w czasie kompilacji). Weźmy na przykład ten plik nagłówkowy:

#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

Możemy teraz napisać kod korzystający z tego interfejsu Addera, bez dostępu do jego pól, np .:

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

I oto byłyby całkowicie zamknięte szczegóły implementacji:

#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;
}

Istnieje również klasa modułowych języków programowania , która koncentruje się na interfejsach na poziomie modułu. Rodzina języków ML włącznie OCaml zawiera ciekawe podejście do modułów zwanych funktorami . OOP przyćmiło i w dużej mierze przysłoniło programowanie modułowe, ale wiele rzekomych zalet OOP dotyczy bardziej modułowości niż orientacji obiektowej.

Istnieje również spostrzeżenie, że klasy w językach OOP, takich jak C ++ lub Java, często nie są używane dla obiektów (w sensie podmiotów, które rozwiązują operacje poprzez późne wiązanie / dynamiczne wysyłanie), a jedynie do abstrakcyjnych typów danych (w których definiujemy publiczny interfejs, który ukrywa wewnętrzne szczegóły wdrożenia). Artykuł „ Understanding Data Abstraction, Revisited” (Cook, 2009) omawia tę różnicę bardziej szczegółowo.

Ale tak, wiele języków nie ma żadnego mechanizmu enkapsulacji. W tych językach członkowie struktury są publicznie dostępni. Co najwyżej istniałaby konwencja nazewnictwa zniechęcająca do użycia. Np. Myślę, że Pascal nie miał użytecznego mechanizmu enkapsulacji.

amon
źródło
11
Widzisz błąd w Adder self = malloc(sizeof(Adder));? Jest powód, dla którego typowanie wskaźników sizeof(TYPE)jest na ogół niezadowolone.
Deduplicator
10
Nie możesz po prostu pisać sizeof(*Adder), ponieważ *Addernie jest typem, tak jak *int *nie jest typem. Wyrażenie T t = malloc(sizeof *t)jest zarówno idiomatyczne, jak i poprawne. Zobacz moją edycję.
wchargin
4
Pascal miał zmienne jednostkowe, których nie można było zobaczyć spoza tej jednostki. W rzeczywistości zmienne jednostkowe były równoważne private staticzmiennym w Javie. Podobnie do C można użyć nieprzejrzystych wskaźników do przekazywania danych w Pascalu bez deklarowania, co to było. Klasyczny MacOS używał wielu nieprzejrzystych wskaźników, ponieważ publiczne i prywatne części rekordu (struktura danych) mogą być przekazywane razem. Pamiętam, że Menedżer okien robił wiele z tego, ponieważ niektóre części rekordu okna były publiczne, ale niektóre informacje wewnętrzne zostały również uwzględnione.
Michael Shopsin
6
Być może lepszym przykładem niż Pascal jest Python, który obsługuje orientację obiektową, ale nie zawiera enkapsulacji, odwołując się do konwencji nazewnictwa, takich jak _private_memberi output_property_, lub bardziej zaawansowanych technik tworzenia obiektów możliwych do przypisania.
Mephy,
11
W literaturze OOD występuje irytująca tendencja do przedstawiania każdej zasady projektowania jako zasady projektowania OO . (Nieakademicka) literatura OOD ma tendencję do malowania obrazu „średniowiecza”, w którym wszyscy robili wszystko źle, a następnie praktykujący OOP przynoszą światło. O ile mi wiadomo, wynika to głównie z ignorancji. Na przykład, o ile wiem, Bob Martin nadał poważny wygląd programowaniu funkcjonalnemu zaledwie kilka lat temu.
Derek Elkins,
31

Po pierwsze, podejście proceduralne kontra obiektowe nie ma nic wspólnego z publicznym czy prywatnym. Wiele języków obiektowych nie ma pojęcia o kontroli dostępu.

Po drugie, w „C” - które większość ludzi nazwałaby proceduralnymi, a nie obiektowymi, istnieje wiele sztuczek, których można użyć, aby skutecznie uczynić rzeczy prywatnymi. Bardzo często stosuje się nieprzezroczyste wskaźniki (np. Void *). Lub - możesz przekazać deklarację obiektu i po prostu nie definiować go w pliku nagłówkowym.

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) { ... }

Spójrz na Windows SDK! Wykorzystuje HANDLE i UINT_PTR, a takie rzeczy są ogólnymi uchwytami pamięci używanej w interfejsach API - skutecznie czyniąc implementacje prywatnymi.

Lewis Pringle
źródło
1
Moja próbka wykazała lepsze podejście (C) - używając struktur deklarowanych w przód. aby zastosować podejście void *, użyłbym typedefs: w pliku .h powiedz typedef void * queue, a następnie wszędzie, gdzie mieliśmy strukturę, po prostu powiedz kolejkę; Następnie w pliku .c zmień nazwę kolejki strukturalnej na struct queueImpl, a wszelkie argumenty staną się kolejką (bez kolejki strukturalnej *), a pierwszy wiersz kodu dla każdej takiej funkcji staje się struct queueImpl * qi = (struct queueImpl *) q
Lewis Pringle
7
Hmm To sprawia, że ​​jest prywatny, ponieważ nie można uzyskać dostępu (odczytu lub zapisu) do żadnych pól „kolejki” z dowolnego miejsca poza implementacją (plik foo.c). Co jeszcze miałeś na myśli przez prywatny? BTW - tak jest zarówno ZARÓWNO typedef void * apporach, jak i (lepsze) podejście do deklarowania struktury
Lewis Pringle
5
Muszę wyznać, że minęło prawie 40 lat, odkąd przeczytałem książkę na smalltalk-80, ale nie pamiętam żadnych pojęć o publicznych lub prywatnych członkach danych. Myślę, że CLOS również nie miał takiego pojęcia. Obiekt Pascal nie miał takiego pojęcia. Pamiętam, że Simula to zrobiła (prawdopodobnie skąd pomysł Stroustrupa) i większość języków OO od C ++. W każdym razie - zgadzamy się na hermetyzację, a prywatne dane to dobre pomysły. Nawet pierwotny pytający był w tej kwestii jasny. Po prostu pytał - w jaki sposób oldies robiły enkapsulację w językach wcześniejszych niż C ++.
Lewis Pringle
5
@LewisPringle nie ma wzmianki o publicznych elementach danych w Smalltalk-80, ponieważ wszystkie „zmienne instancji” (elementy danych) są prywatne, chyba że użyjesz refleksji. AFAIU Smalltalkers piszą akcesor dla każdej zmiennej, którą chcą upublicznić.
dcorking
4
@LewisPringle dla kontrastu, wszystkie „metody” Smalltalk (elementy funkcyjne) są publiczne (istnieją niezdarne konwencje oznaczania ich jako prywatne)
dorking
13

„Nieprzezroczyste typy danych” były dobrze znaną koncepcją, gdy 30 lat temu uzyskałem stopień informatyki. Nie obejmowaliśmy OOP, ponieważ nie było to wtedy w powszechnym użyciu, a „programowanie funkcjonalne” uznano za bardziej poprawne.

Modula-2 miała dla nich bezpośrednie wsparcie, patrz https://www.modula2.org/reference/modules.php .

Lewis Pringle już wyjaśnił, w jaki sposób można deklarować strukturę w C w przód. W przeciwieństwie do Module-2, do utworzenia obiektu trzeba było zapewnić funkcję fabryki. ( Metody wirtualne były również łatwe do wdrożenia w C , ponieważ pierwszy element struktury był wskaźnikiem innej struktury zawierającej wskaźniki funkcji do metod).

Często stosowano także konwencję. Na przykład żadne pole zaczynające się od „_” nie powinno być dostępne poza plikiem, który był właścicielem danych. Łatwo to wymusić dzięki stworzeniu niestandardowych narzędzi sprawdzających.

Każdy projekt na dużą skalę, nad którym pracowałem (zanim przeszedłem do C ++, a następnie C #) posiadał system zapobiegający dostępowi do „prywatnych” danych przez niewłaściwy kod. To było tylko trochę mniej znormalizowane niż obecnie.

Łan
źródło
9

Uwaga: istnieje wiele języków OO bez wbudowanej możliwości oznaczania członków jako prywatnych. Można to zrobić umownie, bez potrzeby kompilatora do egzekwowania prywatności. Na przykład ludzie często poprzedzają prywatne zmienne znakiem podkreślenia.

Istnieją techniki utrudniające dostęp do zmiennych „prywatnych”, z których najczęstszym jest idiom PIMPL . To umieszcza twoje prywatne zmienne w osobnej strukturze, z tylko wskaźnikiem przydzielonym w twoich publicznych plikach nagłówkowych. Oznacza to dodatkowe dereferencje i obsadę, aby uzyskać dowolne zmienne prywatne, coś takiego ((private_impl)(obj->private))->actual_value, co staje się denerwujące, więc w praktyce jest rzadko używane.

Karl Bielefeldt
źródło
4

Struktury danych nie miały „elementów”, tylko pola danych (zakładając, że był to typ rekordu). Widoczność była zwykle ustawiana dla całego typu. Jednak może to nie być tak ograniczające, jak myślisz, ponieważ funkcje nie były częścią rekordu.

Cofnijmy się i zdobądźmy trochę historii tutaj ...

Dominujący paradygmat programowania przed OOP nazwano programowaniem strukturalnym . Początkowym głównym celem tego było uniknięcie stosowania nieustrukturyzowanych instrukcji skoku („goto”). Jest to paradygmat zorientowany na przepływ sterowania (podczas gdy OOP jest bardziej zorientowany na dane), ale nadal było naturalnym rozszerzeniem jego próby logicznej struktury danych, tak jak kod.

Kolejnym odgałęzieniem programowania strukturalnego było ukrywanie informacji , pomysł, że implementacje struktury kodu (które prawdopodobnie będą się zmieniać dość często) powinny być oddzielone od interfejsu (co idealnie nie zmieni się prawie tak bardzo). Teraz jest to dogmat, ale w dawnych czasach wiele osób uważało, że lepiej jest, aby każdy programista poznał szczegóły całego systemu, więc był to kiedyś kontrowersyjny pomysł. Oryginalna edycja Miesiąca Mitycznego Człowieka Brook'a faktycznie sprzeciwiła się ukrywaniu informacji.

Późniejsze języki programowania zaprojektowane wyraźnie jako dobre Języki programowania strukturalnego (na przykład Modula-2 i Ada) generalnie zawierały ukrywanie informacji jako podstawową koncepcję, zbudowaną wokół pewnego rodzaju koncepcji spójnego ułatwienia funkcji (i wszelkich typów, stałych i obiekty, których mogą potrzebować). W Modula-2 były one nazywane „modułami”, w Adzie „pakietami”. Wiele współczesnych języków OOP nazywa tę samą koncepcję „przestrzeniami nazw”. Te przestrzenie nazw były organizacyjną podstawą rozwoju w tych językach i dla większości celów mogą być używane podobnie jak klasy OOP (oczywiście bez rzeczywistego wsparcia dla dziedziczenia).

Tak więc w Modula-2 i Adzie (83) możesz zadeklarować dowolną procedurę, typ, stałą lub obiekt w przestrzeni nazw prywatnej lub publicznej, ale jeśli masz typ rekordu, nie ma (łatwego) sposobu na zadeklarowanie niektórych pól rekordów jako publicznych i inne prywatne. Albo cała twoja płyta jest publiczna, albo nie.

PRZETRZĄSAĆ
źródło
Spędziłem sporo czasu pracując w Adzie. Selektywne ukrywanie (części typu danych) było czymś, co robiliśmy cały czas; w pakiecie zawierającym zdefiniowałbyś sam typ jako prywatny lub ograniczony prywatny; interfejs pakietu odsłoniłby funkcje / procedury publiczne w celu uzyskania i / lub ustawienia pól wewnętrznych. Procedury te musiałyby oczywiście przyjmować parametr typu prywatnego. Nie wtedy i nie uważam tego za trudne.
David
Ponadto, większość języków OO AFAIK działa w ten sam sposób pod maską, tj. MyWidget.getFoo () jest naprawdę zaimplementowany jako getFoo (myWidget). object.method()Inwokacja jest po prostu cukier syntaktyczny. Ważne IMHO - patrz zasada jednolitego dostępu / odniesienia Meyera - ale wciąż tylko cukier syntaktyczny.
David
@David - taki był argument społeczności Ady od lat w erze Ady 95. Wierzę, że w końcu poddali się i udowodnili swój argument, dopuszczając object.method()jako alternatywną formę method(object, ...) dla ludzi, którzy po prostu nie mogli dokonać skoku koncepcyjnego.
TED,
0

W C można już przekazywać wskaźniki do zadeklarowanych, ale niezdefiniowanych typów, jak powiedzieli inni, w efekcie ograniczając dostęp do wszystkich pól.

Możesz także mieć funkcje prywatne i publiczne na zasadzie moduł-moduł. Funkcje zadeklarowane jako statyczne w pliku źródłowym nie są widoczne na zewnątrz, nawet jeśli spróbujesz odgadnąć ich nazwę. Podobnie, możesz mieć statyczne zmienne globalne na poziomie pliku, co jest ogólnie złą praktyką, ale pozwala na izolację na poziomie modułu.

Prawdopodobnie ważne jest podkreślenie, że ograniczenie dostępu jako dobrze znormalizowana konwencja, a nie konstrukcja wymuszona językiem, działa dobrze (patrz Python). Co więcej, ograniczenie dostępu do pól obiektowych zawsze chroni programistę tylko wtedy, gdy istnieje potrzeba zmiany wartości danych wewnątrz obiektu po utworzeniu. Który już jest zapachem kodu. Prawdopodobnie constsłowo kluczowe C, a w szczególności C ++ dla metod i argumentów funkcyjnych, jest znacznie większą pomocą dla programisty niż raczej słaba Java final.

Kafein
źródło
Jedyną funkcją C, która była specjalnie przeznaczona do ukrywania informacji, były staticglobalne dane i operacje (co oznaczało, że nie zostały przedstawione linkerowi do użycia z innych kompilacji). Można w sposób wiarygodny argumentować, że C wsparło dobre praktyki projektowania oprogramowania, poza tym był to w zasadzie hack i nie był częścią oryginalnego projektu języka z 1972 r.
TED
0

Jeśli twoja definicja Public to możliwość dostępu do implementacji i danych / właściwości za pomocą własnego kodu w dowolnym momencie, odpowiedź brzmi: Tak . Został jednak wyodrębniony na różne sposoby - w zależności od języka.

Mam nadzieję, że zwięźle odpowiedziała na twoje pytanie.

RobMac
źródło
-1

Oto bardzo prosty kontrprzykład: w Javie interfacedefiniują obiekty, ale classnie. A classdefiniuje abstrakcyjny typ danych, a nie obiekt.

Ergo, za każdym razem, gdy używasz privatew classJavie, masz przykład struktury danych z elementami prywatnymi, która nie jest zorientowana obiektowo.

Jörg W Mittag
źródło
7
Ta odpowiedź jest oczywiście poprawna technicznie, ale jest całkowicie niezrozumiała dla każdego, kto jeszcze nie wie, czym są ADT i czym różnią się od obiektów.
amon
1
Nauczyłem się czegoś z tej odpowiedzi.
littleO
3
Interfejsy nie „definiują” obiektów; oni określić kontrakty dla operacji / zachowań, które obiekty mogą zrobić lub wykonać. Podobnie jak dziedziczenie jest ogólnie opisać za pomocą to związek i składu przez ma związek, interfejsy są ogólnie opisał może zrobić relacji.
code_dredd