JavaRush /Blog Java /Random-PL /Projektowanie klas i interfejsów (tłumaczenie artykułu)
fatesha
Poziom 22

Projektowanie klas i interfejsów (tłumaczenie artykułu)

Opublikowano w grupie Random-PL
Projektowanie klas i interfejsów (tłumaczenie artykułu) - 1

Treść

  1. Wstęp
  2. Interfejsy
  3. Znaczniki interfejsu
  4. Interfejsy funkcjonalne, metody statyczne i metody domyślne
  5. Zajęcia abstrakcyjne
  6. Klasy niezmienne (trwałe).
  7. Zajęcia anonimowe
  8. Widoczność
  9. Dziedzictwo
  10. Dziedziczenie wielokrotne
  11. Dziedziczenie i kompozycja
  12. Kapsułkowanie
  13. Zajęcia końcowe i metody
  14. Co dalej
  15. Pobierz kod źródłowy

1. WSTĘP

Bez względu na to, jakiego języka programowania używasz (a Java nie jest wyjątkiem), przestrzeganie zasad dobrego projektowania jest kluczem do pisania czystego, zrozumiałego i weryfikowalnego kodu; a także stworzyć go tak, aby był trwały i łatwo wspierał rozwiązywanie problemów. W tej części samouczka omówimy podstawowe elementy składowe języka Java i przedstawimy kilka zasad projektowania, które pomogą Ci podejmować lepsze decyzje projektowe. Mówiąc dokładniej, omówimy interfejsy i interfejsy wykorzystujące metody domyślne (nowa funkcja w Javie 8), klasy abstrakcyjne i końcowe, klasy niezmienne, dziedziczenie, kompozycję i ponownie omówimy zasady widoczności (lub dostępności), o których pokrótce wspomnieliśmy w Część 1 lekcja „Jak tworzyć i niszczyć obiekty” .

2. INTERFEJSY

W programowaniu obiektowym koncepcja interfejsów stanowi podstawę tworzenia kontraktów . Krótko mówiąc, interfejsy definiują zestaw metod (kontraktów), a każda klasa wymagająca obsługi tego konkretnego interfejsu musi zapewniać implementację tych metod: jest to dość prosty, ale potężny pomysł. Wiele języków programowania ma interfejsy w takiej czy innej formie, ale w szczególności Java zapewnia obsługę tego języka. Przyjrzyjmy się prostej definicji interfejsu w Javie.
package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
void performAction();
}
W powyższym fragmencie interfejs, który wywołaliśmy SimpleInterface, deklaruje tylko jedną metodę o nazwie performAction. Główna różnica między interfejsami a klasami polega na tym, że interfejsy określają, jaki powinien być kontakt (deklarują metodę), ale nie zapewniają ich implementacji. Jednak interfejsy w Javie mogą być bardziej złożone: mogą zawierać zagnieżdżone interfejsy, klasy, liczniki, adnotacje i stałe. Na przykład:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
}
W tym bardziej złożonym przykładzie istnieje kilka ograniczeń, które interfejsy bezwarunkowo nakładają na zagnieżdżone konstrukcje i deklaracje metod wymuszane przez kompilator Java. Po pierwsze, nawet jeśli nie jest to jawnie zadeklarowane, każda deklaracja metody w interfejsie jest publiczna (i może być tylko publiczna). Zatem następujące deklaracje metod są równoważne:
public void performAction();
void performAction();
Warto wspomnieć, że każda pojedyncza metoda w interfejsie jest domyślnie zadeklarowana jako abstrakcyjna i nawet te deklaracje metod są równoważne:
public abstract void performAction();
public void performAction();
void performAction();
Jeśli chodzi o zadeklarowane pola stałe, oprócz tego, że są publiczne , są one również domyślnie statyczne i oznaczone jako final . Dlatego następujące deklaracje są również równoważne:
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
Wreszcie zagnieżdżone klasy, interfejsy lub liczniki, oprócz tego, że są publiczne , są również domyślnie deklarowane jako statyczne . Na przykład te deklaracje są również równoważne:
class InnerClass {
}

static class InnerClass {
}
Wybrany styl jest kwestią osobistych preferencji, ale znajomość tych prostych właściwości interfejsów może uchronić Cię przed niepotrzebnym pisaniem.

3. Znacznik interfejsu

Interfejs znacznika to specjalny rodzaj interfejsu, który nie zawiera metod ani innych zagnieżdżonych konstrukcji. Jak definiuje to biblioteka Java:
public interface Cloneable {
}
Znaczniki interfejsu nie są kontraktami per se, ale są dość przydatną techniką „dołączania” lub „kojarzenia” określonej cechy z klasą. Na przykład w odniesieniu do Cloneable klasa jest oznaczona jako możliwa do sklonowania, ale sposób, w jaki można lub należy to zaimplementować, nie jest częścią interfejsu. Innym bardzo znanym i szeroko stosowanym przykładem znacznika interfejsu jest Serializable:
public interface Serializable {
}
Interfejs ten oznacza klasę jako odpowiednią do serializacji i deserializacji i ponownie nie określa, w jaki sposób można lub należy to zaimplementować. Znaczniki interfejsu mają swoje miejsce w programowaniu obiektowym, chociaż nie spełniają głównego celu interfejsu, jakim jest kontrakt. 

4. INTERFEJSY FUNKCJONALNE, METODY DOMYŚLNE I METODY STATYCZNE

Od czasu wydania Java 8 interfejsy zyskały kilka bardzo interesujących nowych funkcji: metody statyczne, metody domyślne i automatyczną konwersję z lambd (interfejsy funkcjonalne). W sekcji interfejsy podkreśliliśmy fakt, że interfejsy w Javie mogą jedynie deklarować metody, ale nie zapewniają ich implementacji. W przypadku metody domyślnej sytuacja wygląda inaczej: interfejs może oznaczyć metodę słowem kluczowym default i zapewnić jej implementację. Na przykład:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}
Będąc na poziomie instancji, metody domyślne można zastąpić każdą implementacją interfejsu, ale interfejsy mogą teraz zawierać także metody statyczne , na przykład: pakiet com.javacodegeeks.advanced.design;
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}
Można powiedzieć, że zapewnienie implementacji w interfejsie przeczy całemu celowi programowania kontraktowego. Istnieje jednak wiele powodów, dla których te funkcje zostały wprowadzone do języka Java i niezależnie od tego, jak bardzo są przydatne lub zagmatwane, są one dostępne dla Ciebie i Twojego użytku. Interfejsy funkcjonalne to zupełnie inna historia i okazały się bardzo przydatnym dodatkiem do języka. Zasadniczo interfejs funkcjonalny to interfejs z zadeklarowaną tylko jedną abstrakcyjną metodą. RunnableStandardowy interfejs biblioteki jest bardzo dobrym przykładem tej koncepcji.
@FunctionalInterface
public interface Runnable {
    void run();
}
Kompilator Java traktuje interfejsy funkcjonalne w różny sposób i może zamienić funkcję lambda w implementację interfejsu funkcjonalnego, jeśli ma to sens. Rozważmy następujący opis funkcji: 
public void runMe( final Runnable r ) {
    r.run();
}
Aby wywołać tę funkcję w Javie 7 i niższych należy zapewnić implementację interfejsu Runnable(np. przy użyciu klas anonimowych), natomiast w Javie 8 wystarczy zapewnić implementację metody run() przy użyciu składni lambda:
runMe( () -> System.out.println( "Run!" ) );
Dodatkowo adnotacja @FunctionalInterface (adnotacje zostaną szczegółowo omówione w części 5 tutoriala) podpowiada, że ​​kompilator może sprawdzić, czy interfejs zawiera tylko jedną metodę abstrakcyjną, więc jakiekolwiek zmiany wprowadzone w interfejsie w przyszłości nie naruszą tego założenia .

5. ZAJĘCIA STRESZCZONE

Kolejną ciekawą koncepcją wspieraną przez język Java jest koncepcja klas abstrakcyjnych. Klasy abstrakcyjne są nieco podobne do interfejsów w Javie 7 i są bardzo zbliżone do domyślnych metod interfejsu w Javie 8. W przeciwieństwie do zwykłych klas, nie można utworzyć instancji klasy abstrakcyjnej, ale można ją podklasować (więcej szczegółów można znaleźć w sekcji Dziedziczenie). Co ważniejsze, klasy abstrakcyjne mogą zawierać metody abstrakcyjne: specjalny rodzaj metody bez implementacji, podobnie jak interfejs. Na przykład:
package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
}
W tym przykładzie klasa SimpleAbstractClassjest zadeklarowana jako abstrakcyjna i zawiera jedną zadeklarowaną metodę abstrakcyjną. Klasy abstrakcyjne są bardzo przydatne; większość lub nawet niektóre części szczegółów implementacji mogą być współużytkowane przez wiele podklas. Tak czy inaczej, nadal pozostawiają uchylone drzwi i pozwalają dostosować zachowanie właściwe każdej z podklas za pomocą metod abstrakcyjnych. Warto wspomnieć, że w przeciwieństwie do interfejsów, które mogą zawierać jedynie publiczne deklaracje, klasy abstrakcyjne mogą wykorzystywać pełną moc reguł dostępności do kontrolowania widoczności metody abstrakcyjnej.

6. ZAJĘCIA BEZPOŚREDNIE

Niezmienność staje się obecnie coraz ważniejsza w tworzeniu oprogramowania. Pojawienie się systemów wielordzeniowych spowodowało wiele problemów związanych z udostępnianiem danych i równoległością. Ale na pewno pojawił się jeden problem: posiadanie niewielkiego (lub nawet żadnego) stanu zmiennego prowadzi do lepszej rozszerzalności (skalowalności) i łatwiejszego wnioskowania o systemie. Niestety, język Java nie zapewnia przyzwoitej obsługi niezmienności klas. Jednak stosując kombinację technik, możliwe staje się projektowanie klas, które są niezmienne. Przede wszystkim wszystkie pola klasy muszą być ostateczne (oznaczone jako final ). To dobry początek, ale nie gwarantuje. 
package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection<String> collectionOfString;
}
Po drugie, zadbaj o odpowiednią inicjalizację: jeśli pole jest referencją do kolekcji lub tablicy, nie przypisuj tych pól bezpośrednio z argumentów konstruktora, tylko wykonaj kopie. Dzięki temu stan kolekcji lub tablicy nie zostanie zmodyfikowany poza nią.
public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection<String> collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}
I wreszcie zapewnienie odpowiedniego dostępu (gettery). W przypadku kolekcji niezmienność musi być zapewniona jako opakowanie  Collections.unmodifiableXxx: w przypadku tablic jedynym sposobem zapewnienia prawdziwej niezmienności jest dostarczenie kopii zamiast zwracania odniesienia do tablicy. Może to być nie do przyjęcia z praktycznego punktu widzenia, ponieważ jest bardzo zależne od rozmiaru tablicy i może wywierać ogromny nacisk na moduł zbierający elementy bezużyteczne.
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
Nawet ten mały przykład dobrze pokazuje, że niezmienność nie jest jeszcze w Javie obywatelem pierwszej klasy. Sprawy mogą się skomplikować, jeśli niezmienna klasa ma pole odwołujące się do obiektu innej klasy. Klasy te również powinny być niezmienne, ale nie ma sposobu, aby to zapewnić. Istnieje kilka przyzwoitych analizatorów kodu źródłowego Java, takich jak FindBugs i PMD, które mogą bardzo pomóc, sprawdzając kod i wskazując typowe wady programowania Java. Narzędzia te są świetnymi przyjaciółmi każdego programisty Java.

7. ZAJĘCIA ANONIMOWE

W epoce poprzedzającej Java 8 klasy anonimowe były jedynym sposobem na zapewnienie, że klasy będą definiowane na bieżąco i natychmiastowo tworzone. Celem zajęć anonimowych było ograniczenie schematów i zapewnienie krótkiego i łatwego sposobu reprezentowania klas jako rekordu. Przyjrzyjmy się typowemu, staromodnemu sposobowi tworzenia nowego wątku w Javie:
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}
W tym przykładzie implementacja Runnableinterfejsu jest dostarczana natychmiastowo jako klasa anonimowa. Chociaż istnieją pewne ograniczenia związane z klasami anonimowymi, główną wadą ich używania jest bardzo rozwlekła składnia konstrukcji, do której zobowiązuje się Java jako język. Nawet anonimowa klasa, która nic nie robi, wymaga co najmniej 5 linii kodu za każdym razem, gdy jest zapisywana.
new Runnable() {
   @Override
   public void run() {
   }
}
Na szczęście dzięki Javie 8, lambdzie i funkcjonalnym interfejsom wszystkie te stereotypy wkrótce znikną, a pisanie kodu w Javie będzie wyglądać naprawdę zwięźle.
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

8. WIDOCZNOŚĆ

Mówiliśmy już trochę o regułach widoczności i dostępności w Javie w części 1 samouczka. W tej części ponownie powrócimy do tego tematu, ale w kontekście podklas. Projektowanie klas i interfejsów (tłumaczenie artykułu) - 2Widoczność na różnych poziomach umożliwia lub uniemożliwia klasom oglądanie innych klas lub interfejsów (na przykład, jeśli znajdują się w różnych pakietach lub są zagnieżdżone w sobie) lub podklasom wyświetlanie metod, konstruktorów i pól swoich rodziców oraz uzyskiwanie do nich dostępu. W następnej sekcji, dziedziczenie, zobaczymy to w akcji.

9. DZIEDZICZENIE

Dziedziczenie jest jedną z kluczowych koncepcji programowania obiektowego, służącą jako podstawa do konstruowania klasy relacji. W połączeniu z regułami widoczności i dostępności, dziedziczenie umożliwia projektowanie klas w hierarchię, którą można rozszerzać i utrzymywać. Na poziomie koncepcyjnym dziedziczenie w Javie jest implementowane przy użyciu podklas i słowa kluczowego Extends wraz z klasą nadrzędną. Podklasa dziedziczy wszystkie publiczne i chronione elementy klasy nadrzędnej. Dodatkowo podklasa dziedziczy elementy pakietu-prywatnego swojej klasy nadrzędnej, jeśli oba (podklasa i klasa) znajdują się w tym samym pakiecie. Biorąc to pod uwagę, bardzo ważne jest, niezależnie od tego, co próbujesz zaprojektować, trzymanie się minimalnego zestawu metod, które klasa udostępnia publicznie lub swoim podklasom. Przyjrzyjmy się na przykład klasie Parenti jej podklasie, Childaby zademonstrować różnicę w poziomach widoczności i ich efektach.
package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}
Dziedziczenie to bardzo obszerny temat sam w sobie, zawierający wiele drobnych szczegółów specyficznych dla języka Java. Istnieje jednak kilka zasad, których można łatwo przestrzegać i które mogą znacznie pomóc w utrzymaniu zwięzłości hierarchii klas. W Javie każda podklasa może zastąpić wszelkie odziedziczone metody swojego rodzica, chyba że zostanie uznana za ostateczną. Jednakże nie ma specjalnej składni ani słowa kluczowego oznaczającego metodę jako przesłoniętą, co może prowadzić do zamieszania. Właśnie dlatego wprowadzono adnotację @Override : ilekroć Twoim celem jest zastąpienie odziedziczonej metody, użyj adnotacji @Override , aby to zwięźle wskazać. Kolejnym dylematem, przed którym stale stają programiści Java podczas projektowania, jest konstrukcja hierarchii klas (z klasami konkretnymi lub abstrakcyjnymi) w porównaniu z implementacją interfejsów. Zdecydowanie zalecamy, jeśli to możliwe, faworyzowanie interfejsów zamiast klas lub klas abstrakcyjnych. Interfejsy są lżejsze, łatwiejsze w testowaniu i utrzymaniu, a także minimalizują skutki uboczne zmian wdrożeniowych. Wiele zaawansowanych technik programowania, takich jak tworzenie klas proxy w standardowej bibliotece Java, w dużym stopniu opiera się na interfejsach.

10. DZIEDZICZENIE WIELOKROTNE

W przeciwieństwie do C++ i niektórych innych języków, Java nie obsługuje dziedziczenia wielokrotnego: w Javie każda klasa może mieć tylko jednego bezpośredniego rodzica (z klasą Objectna górze hierarchii). Jednakże klasa może implementować wiele interfejsów, dlatego też układanie interfejsów to jedyny sposób na osiągnięcie (lub symulację) wielokrotnego dziedziczenia w Javie.
package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
Implementacja wielu interfejsów ma naprawdę duże możliwości, ale często potrzeba wielokrotnego korzystania z implementacji prowadzi do głębokiej hierarchii klas, co pozwala przezwyciężyć brak obsługi wielokrotnego dziedziczenia w Javie. 
public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
I tak dalej... Najnowsza wersja Java 8 w pewnym stopniu rozwiązuje problem wstrzykiwania metody domyślnej. Ze względu na metody domyślne interfejsy w rzeczywistości zapewniają nie tylko kontrakt, ale także implementację. Dlatego klasy implementujące te interfejsy również automatycznie odziedziczą te zaimplementowane metody. Na przykład:
package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
Należy pamiętać, że dziedziczenie wielokrotne jest bardzo potężnym, ale jednocześnie niebezpiecznym narzędziem. Dobrze znany problem Diamentu Śmierci jest często wymieniany jako główna wada implementacji dziedziczenia wielokrotnego, zmuszająca programistów do bardzo ostrożnego projektowania hierarchii klas. Niestety interfejsy Java 8 z metodami domyślnymi również padają ofiarą tych defektów.
interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
}
Na przykład następujący fragment kodu nie zostanie skompilowany:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
W tym miejscu można śmiało powiedzieć, że Java jako język zawsze starała się unikać skrajnych przypadków programowania obiektowego, ale w miarę ewolucji języka niektóre z tych przypadków nagle zaczęły się pojawiać. 

11. DZIEDZICZENIE I SKŁAD

Na szczęście dziedziczenie nie jest jedynym sposobem projektowania klasy. Inną alternatywą, zdaniem wielu programistów, znacznie lepszą niż dziedziczenie, jest kompozycja. Pomysł jest bardzo prosty: zamiast tworzyć hierarchię klas, należy je składać z innych klas. Spójrzmy na ten przykład:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
Klasa Vehicleskłada się z silnika i kół (oraz wielu innych części, które dla uproszczenia pominięto). Można jednak powiedzieć, że klasa Vehiclejest także silnikiem, więc można ją projektować za pomocą dziedziczenia. 
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
Które rozwiązanie projektowe będzie właściwe? Ogólne podstawowe wytyczne są znane jako zasady IS-A (jest) i HAS-A (zawiera). IS-A to relacja dziedziczenia: podklasa spełnia również specyfikację klasy klasy nadrzędnej i jest odmianą klasy nadrzędnej. podklasa) rozszerza swojego rodzica. Jeśli chcesz wiedzieć, czy jedna jednostka rozszerza drugą, wykonaj test dopasowania - IS -A (jest).") Dlatego HAS-A jest relacją kompozycji: klasa posiada (lub zawiera) obiekt, który W większości przypadków zasada HAS-A działa lepiej niż IS-A z wielu powodów: 
  • Projekt jest bardziej elastyczny;
  • Model jest bardziej stabilny, ponieważ zmiana nie rozprzestrzenia się w hierarchii klas;
  • Klasa i jej skład są luźno powiązane w porównaniu z kompozycją, która ściśle łączy klasę nadrzędną i jej podklasę.
  • Logiczny tok myślenia w klasie jest prostszy, ponieważ wszystkie jej zależności są w niej zawarte, w jednym miejscu. 
Niezależnie od tego, dziedziczenie ma swoje miejsce i rozwiązuje wiele istniejących problemów projektowych na różne sposoby, więc nie należy go zaniedbywać. Projektując model obiektowy, należy pamiętać o tych dwóch alternatywach.

12. ENKAPSULACJA.

Koncepcja enkapsulacji w programowaniu obiektowym polega na ukryciu wszystkich szczegółów implementacji (takich jak tryb działania, metody wewnętrzne itp.) przed światem zewnętrznym. Zaletami enkapsulacji są łatwość konserwacji i łatwość wprowadzania zmian. Wewnętrzna implementacja klasy jest ukryta, praca z danymi klasy odbywa się wyłącznie za pomocą publicznych metod klasy (prawdziwy problem, jeśli tworzysz bibliotekę lub framework używany przez wiele osób). Hermetyzację w Javie osiąga się poprzez reguły widoczności i dostępności. W Javie za najlepszą praktykę uważa się, aby nigdy nie ujawniać pól bezpośrednio, tylko za pomocą metod pobierających i ustawiających (chyba że pola są oznaczone jako ostateczne). Na przykład:
package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
}
Ten przykład przypomina tak zwane komponenty JavaBeans w języku Java: standardowe klasy Java są pisane zgodnie z zestawem konwencji, z których jedna umożliwia dostęp do pól wyłącznie za pomocą metod pobierających i ustawiających. Jak już podkreślaliśmy w części dotyczącej dziedziczenia, prosimy zawsze przestrzegać minimalnego kontraktu reklamowego w klasie, stosując zasady enkapsulacji. Wszystko, co nie powinno być publiczne, powinno stać się prywatne (lub chronione/pakietowe, w zależności od problemu, który rozwiązujesz). Opłaci się to na dłuższą metę, zapewniając swobodę projektowania bez (lub przynajmniej minimalizując) istotnych zmian. 

13. ZAJĘCIA I METODY KOŃCOWE

W Javie istnieje sposób, aby zapobiec temu, aby klasa stała się podklasą innej klasy: druga klasa musi zostać uznana za ostateczną. 
package com.javacodegeeks.advanced.design;

public final class FinalClass {
}
To samo słowo kluczowe final w deklaracji metody zapobiega przesłanianiu metody przez podklasy. 
package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}
Nie ma ogólnych zasad decydujących o tym, czy klasa lub metody powinny być ostateczne, czy nie. Klasy i metody końcowe ograniczają rozszerzalność i bardzo trudno jest przewidzieć, czy klasa powinna być dziedziczona, czy też nie, lub czy metoda powinna być w przyszłości zastępowana. Jest to szczególnie ważne dla twórców bibliotek, ponieważ tego typu decyzje projektowe mogą znacznie ograniczyć możliwości zastosowania biblioteki. Biblioteka standardowa Java zawiera kilka przykładów klas końcowych, z których najsłynniejszą jest klasa String. Decyzja ta została podjęta na wczesnym etapie, aby zapobiec wszelkim próbom deweloperów wymyślenia własnego, „lepszego” rozwiązania w zakresie implementacji string. 

14. CO DALEJ

W tej części lekcji omówiliśmy koncepcje programowania obiektowego w Javie. Przyjrzeliśmy się także szybko programowaniu kontraktowemu, poruszyliśmy niektóre koncepcje funkcjonalne i sprawdziliśmy, jak język ewoluował na przestrzeni czasu. W następnej części lekcji poznamy typy generyczne i to, jak zmieniają one sposób, w jaki podchodzimy do bezpieczeństwa typów w programowaniu. 

15. POBIERZ KOD ŹRÓDŁOWY

Źródło możesz pobrać tutaj - Advanced-Java-part-3 Źródło: Jak projektować klasy an
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION