Hermetyzacja to nie ukrywanie informacji

Słowa są śliskie. Jak ogłosił Humpty Dumpty w książce Lewisa Carrolla Through the Looking Glass: „Kiedy używam słowa, ma ono na myśli dokładnie to, co wybrałem - ani mniej, ani więcej”. Z pewnością powszechne użycie słów hermetyzacja i ukrywanie informacji wydaje się być zgodne z tą logiką. Autorzy rzadko dokonują rozróżnienia między nimi i często bezpośrednio twierdzą, że są takie same.

Czy to sprawia, że ​​tak jest? Nie dla mnie. Gdyby to była tylko kwestia słów, nie napisałbym ani słowa na ten temat. Ale za tymi terminami kryją się dwa odrębne pojęcia, pojęcia zrodzone oddzielnie i najlepiej rozumiane oddzielnie.

Hermetyzacja odnosi się do grupowania danych za pomocą metod, które operują na tych danych. Często definicja ta jest błędnie interpretowana i oznacza, że ​​dane są w jakiś sposób ukryte. W Javie możesz mieć hermetyzowane dane, które nie są w ogóle ukryte.

Jednak ukrywanie danych nie jest pełnym zakresem ukrywania informacji. David Parnas po raz pierwszy przedstawił koncepcję ukrywania informacji około 1972 roku. Twierdził, że podstawowe kryteria modularyzacji systemu powinny dotyczyć ukrywania krytycznych decyzji projektowych. Podkreślił, że ukrywa „trudne decyzje projektowe lub decyzje projektowe, które mogą ulec zmianie”. Ukrywanie informacji w ten sposób izoluje klientów od konieczności dogłębnej znajomości projektu do korzystania z modułu oraz od skutków zmiany tych decyzji.

W tym artykule zbadam różnicę między hermetyzacją a ukrywaniem informacji poprzez opracowanie przykładowego kodu. Dyskusja pokazuje, jak Java ułatwia hermetyzację i bada negatywne konsekwencje hermetyzacji bez ukrywania danych. Przykłady pokazują również, jak ulepszyć projektowanie klas poprzez zasadę ukrywania informacji.

Klasa pozycji

Wraz z rosnącą świadomością ogromnego potencjału bezprzewodowego Internetu, wielu ekspertów oczekuje, że usługi oparte na lokalizacji dadzą szansę na pierwszą bezprzewodową aplikację zabójczą. Dla przykładowego kodu tego artykułu wybrałem klasę reprezentującą położenie geograficzne punktu na powierzchni ziemi. Jako jednostka domeny, klasa o nazwie Positionreprezentuje informacje Global Position System (GPS). Pierwsze cięcie na zajęciach wygląda tak prosto, jak:

public class Position {public double latitude; publiczna podwójna długość geograficzna; }

Klasa zawiera dwa elementy danych: GPS latitudei longitude. Obecnie Positionto nic innego jak mały worek danych. Niemniej jednak Positionjest klasą i Positionobiekty mogą być tworzone przy użyciu tej klasy. Aby wykorzystać te obiekty, klasa PositionUtilityzawiera metody obliczania odległości i kierunku - czyli kierunku - między określonymi Positionobiektami:

public class PositionUtility {public static double distance (Position position1, Position position2) {// Oblicz i zwróć odległość między określonymi pozycjami. } public statyczny podwójny nagłówek (Position position1, Position position2) {// Oblicz i zwróć nagłówek z pozycji 1 do pozycji 2. }}

Pomijam rzeczywisty kod implementacji do obliczania odległości i kursu.

Poniższy kod przedstawia typowe użycie Positioni PositionUtility:

// Utwórz stanowisko reprezentujące mój dom Position myHouse = new Position (); myHouse.latitude = 36,538611; myHouse.longitude = -121,797500; // Utwórz stanowisko reprezentujące lokalną kawiarnię Position coffeeShop = new Position (); coffeeShop.latitude = 36,539722; coffeeShop.longitude = -121.907222; // Użyj PositionUtility, aby obliczyć odległość i drogę z mojego domu // do lokalnej kawiarni. podwójna odległość = PositionUtility.distance (myHouse, coffeeShop); podwójny nagłówek = PositionUtility.heading (myHouse, coffeeShop); // Wydrukuj wyniki System.out.println ("Z mojego domu pod adresem (" + myHouse.latitude + "," + myHouse.longitude + ") do kawiarni pod adresem (" + coffeeShop.latitude + "," + coffeeShop. długość geograficzna + ”) to odległość„ + odległość + ”przy nagłówku„ + nagłówek + ”stopni." );

Kod generuje poniższe dane wyjściowe, które wskazują, że kawiarnia znajduje się na zachód (270,8 stopnia) od mojego domu w odległości 6,09. Późniejsza dyskusja dotyczy braku jednostek odległości.

==================================================== ================= Z mojego domu pod adresem (36.538611, -121.7975) do kawiarni pod adresem (36.539722, -121.907222) jest odległość 6.0873776351893385 na kursie 270,7547022304523 stopni. ==================================================== =================

Position, PositionUtilitya ich użycie kodu jest nieco niepokojące iz pewnością niezbyt zorientowane obiektowo. Ale jak to możliwe? Java jest językiem zorientowanym obiektowo, a kod używa obiektów!

Chociaż kod może wykorzystywać obiekty Java, robi to w sposób przypominający minioną epokę: funkcje narzędziowe działające na strukturach danych. Witamy w 1972 roku! Gdy Prezydent Nixon skulił się nad tajnymi nagraniami taśmowymi, informatycy kodujący w języku proceduralnym Fortran z podekscytowaniem wykorzystali nową Międzynarodową Bibliotekę Matematyki i Statystyki (IMSL) właśnie w ten sposób. Repozytoria kodów, takie jak IMSL, były pełne funkcji do obliczeń numerycznych. Użytkownicy przekazywali dane do tych funkcji w postaci długich list parametrów, które czasami zawierały nie tylko dane wejściowe, ale także wyjściowe struktury danych. (IMSL ewoluował przez lata, a wersja jest teraz dostępna dla programistów Java).

W obecnym projekcie Positionjest prostą strukturą danych i PositionUtilityjest repozytorium funkcji bibliotecznych działających na Positiondanych w stylu IMSL . Jak pokazuje powyższy przykład, współczesne języki obiektowe niekoniecznie wykluczają stosowanie przestarzałych technik proceduralnych.

Grupowanie danych i metod

Kod można łatwo poprawić. Na początek, po co umieszczać dane i funkcje działające na tych danych w oddzielnych modułach? Klasy Java umożliwiają łączenie danych i metod razem:

public class Position {public double distance (Position position) {// Oblicz i zwróć odległość od tego obiektu do określonej // pozycji. } public podwójny nagłówek (pozycja pozycji) {// Oblicz i wróć nagłówek z tego obiektu do określonej // pozycji. } publiczna podwójna szerokość geograficzna; publiczna podwójna długość geograficzna; }

Umieszczenie pozycji danych pozycji i kodu implementacji do obliczania odległości i kursu w tej samej klasie eliminuje potrzebę oddzielnej PositionUtilityklasy. Teraz Positionzaczyna przypominać prawdziwą klasę obiektową. Poniższy kod używa tej nowej wersji, która łączy dane i metody:

Pozycja myHouse = new Position (); myHouse.latitude = 36,538611; myHouse.longitude = -121,797500; Pozycja coffeeShop = nowa pozycja (); coffeeShop.latitude = 36,539722; coffeeShop.longitude = -121.907222; podwójna odległość = myHouse.distance (coffeeShop); podwójny nagłówek = myHouse.heading (coffeeShop); System.out.println ("Z mojego domu pod adresem (" + myHouse.latitude + "," + myHouse.longitude + ") do kawiarni pod adresem (" + coffeeShop.latitude + "," + coffeeShop.longitude + ") to odległość „+ odległość +” z nagłówkiem „+ kurs +„ stopnie ”);

Wynik jest identyczny jak poprzednio, a co ważniejsze, powyższy kod wydaje się bardziej naturalny. Poprzednia wersja przekazywała dwa Positionobiekty do funkcji w oddzielnej klasie narzędziowej w celu obliczenia odległości i kierunku. W tym kodzie obliczenie nagłówka za pomocą wywołania metody util.heading( myHouse, coffeeShop )nie wskazywało wyraźnie kierunku obliczeń. Deweloper musi pamiętać, że funkcja narzędziowa oblicza nagłówek od pierwszego parametru do drugiego.

Dla porównania powyższy kod używa instrukcji myHouse.heading(coffeeShop)do obliczenia tego samego nagłówka. Semantyka wywołania wyraźnie wskazuje, że kierunek przebiega z mojego domu do kawiarni. Konwersja funkcji dwuargumentowej na funkcję heading(Position, Position)jednoargumentową position.heading(Position)jest nazywana funkcją curry . Currying skutecznie specjalizuje funkcję w jej pierwszym argumencie, co skutkuje jaśniejszą semantyką.

Umieszczenie metod wykorzystujących Positiondane klasa w Positionklasie sama sprawia currying funkcje distancei headingmożliwości. Zmiana struktury wywołań funkcji w ten sposób jest istotną przewagą nad językami proceduralnymi. Klasa Positionreprezentuje teraz abstrakcyjny typ danych, który hermetyzuje dane i algorytmy działające na tych danych. Jako typ zdefiniowany przez użytkownika, Positionobiekty są również obywatelami pierwszej klasy, którzy korzystają ze wszystkich zalet systemu typów języka Java.

Funkcją języka, która łączy dane z operacjami wykonanymi na tych danych, jest hermetyzacja. Należy pamiętać, że hermetyzacja nie gwarantuje ani ochrony danych, ani ukrywania informacji. Hermetyzacja nie zapewnia również spójnego projektu klasy. Osiągnięcie tych atrybutów projektowania jakości wymaga technik wykraczających poza hermetyzację zapewnianą przez język. Jak obecnie zaimplementowano, klasa Positionnie zawiera zbędnych lub niezwiązanych ze sobą danych i metod, ale Positionuwidacznia oba latitudei longitudew surowej postaci. To pozwala każdemu klientowi klasy Positionna bezpośrednią zmianę dowolnego wewnętrznego elementu danych bez żadnej interwencji Position. Oczywiście hermetyzacja nie wystarczy.

Programowanie obronne

Aby dokładniej zbadać konsekwencje ujawniania wewnętrznych elementów danych, przypuśćmy, że zdecyduję się dodać trochę programowania obronnego Positionpoprzez ograniczenie szerokości i długości geograficznej do zakresów określonych przez GPS. Szerokość mieści się w zakresie [-90, 90], a w zakresie długości (-180, 180]. Ekspozycja elementów danych latitudei longitudew Position„S bieżącej implementacji powoduje to programowanie w obronie niemożliwe.

Making attributes latitude and longitude private data members of class Position and adding simple accessor and mutator methods, also commonly called getters and setters, provides a simple remedy to exposing raw data items. In the example code below, the setter methods appropriately screen the internal values of latitude and longitude. Rather than throw an exception, I specify performing modulo arithmetic on input values to keep the internal values within specified ranges. For example, attempting to set the latitude to 181.0 results in an internal setting of -179.0 for latitude.

The following code adds getter and setter methods for accessing the private data members latitude and longitude:

public class Position { public Position( double latitude, double longitude ) { setLatitude( latitude ); setLongitude( longitude ); } public void setLatitude( double latitude ) { // Ensure -90 <= latitude <= 90 using modulo arithmetic. // Code not shown. // Then set instance variable. this.latitude = latitude; } public void setLongitude( double longitude ) { // Ensure -180 < longitude <= 180 using modulo arithmetic. // Code not shown. // Then set instance variable. this.longitude = longitude; } public double getLatitude() { return latitude; } public double getLongitude() { return longitude; } public double distance( Position position ) { // Calculate and return the distance from this object to the specified // position. // Code not shown. } public double heading( Position position ) { // Calculate and return the heading from this object to the specified // position. } private double latitude; private double longitude; } 

Using the above version of Position requires only minor changes. As a first change, since the above code specifies a constructor that takes two double arguments, the default constructor is no longer available. The following example uses the new constructor, as well as the new getter methods. The output remains the same as in the first example.

Position myHouse = new Position( 36.538611, -121.797500 ); Position coffeeShop = new Position( 36.539722, -121.907222 ); double distance = myHouse.distance( coffeeShop ); double heading = myHouse.heading( coffeeShop ); System.out.println ( "From my house at (" + myHouse.getLatitude() + ", " + myHouse.getLongitude() + ") to the coffee shop at (" + coffeeShop.getLatitude() + ", " + coffeeShop.getLongitude() + ") is a distance of " + distance + " at a heading of " + heading + " degrees." ); 

Choosing to restrict the acceptable values of latitude and longitude through setter methods is strictly a design decision. Encapsulation does not play a role. That is, encapsulation, as manifested in the Java language, does not guarantee protection of internal data. As a developer, you are free to expose the internals of your class. Nevertheless, you should restrict access and modification of internal data items through the use of getter and setter methods.

Isolating potential change

Protecting internal data is only one of many concerns driving design decisions on top of language encapsulation. Isolation to change is another. Modifying the internal structure of a class should not, if at all possible, affect client classes.

Na przykład wcześniej zauważyłem, że obliczenie odległości w klasie Positionnie wskazuje jednostek. Aby była użyteczna, zgłoszona odległość 6,09 z mojego domu do kawiarni wyraźnie wymaga jednostki miary. Może znam kierunek, ale nie wiem, czy przejść 6,09 metra, przejechać 6,09 mili, czy polecieć 6,09 tysięcy kilometrów.