2016-02-20 17 views
5

Wprowadzam wzorzec szablonu w Javie. Załóżmy następujący fragment kodu:Haczyki ochronne w szablonie

public abstract class A { 

    public final void work() { 
    doPrepare(); 
    doWork(); 
    } 

    protected abstract void doPrepare(); 

    protected abstract void doWork(); 
} 

public final class B extends A { 

    @Override 
    protected abstract void doPrepare() { /* do stuff here */ } 

    @Override 
    protected abstract void doWork() { /* do stuff here */ } 
} 

public final class Main { 

    public static void main(String[] args) { 
    B b = new B(); 
    b.work(); 
    } 
} 

uważam, że to problem, który z nich jest w stanie łatwo przypadkowo wywołać b.doWork() zamiast zawsze dzwoni b.work(). Jakie jest najbardziej eleganckie rozwiązanie, jeśli to możliwe, aby "ukryć" haczyki?

+0

"jeden jest w stanie łatwo przypadkowo wywołać b.doWork()". Naprawdę? Jest chroniony, więc mogą go wywoływać tylko instancje A lub B. –

+0

Niestety, ponieważ jest on chroniony, wszystkie klasy z pakietu mogą go wywoływać. Dzielenie pakietów jest drogą do zrobienia, ale wolałbym unikać pakietów 1- lub 2-klasowych. –

+0

Nie! Opisujesz domyślny modyfikator dostępu (tzn. Brak). Chronione oznacza, że ​​"tylko ja lub klasy, które mnie przedłużają, mogą z tego korzystać" –

Odpowiedz

2

Najprostszą i bardziej oczywistą rzeczą, jaką możesz zrobić, to użyć hierarchii A -> B z innego pakietu.

Jeśli z jakiegoś powodu nie możesz tego zrobić, możesz użyć interfejsu i uczynić zawijanie klasy klasy B klasą, która faktycznie pochodzi z A. Coś takiego:

public interface Worker { 

    void work(); 
} 

public abstract class A implements Worker { 

    @Override 
    public final void work() { 
     doPrepare(); 
     doWork(); 
    } 

    protected abstract void doPrepare(); 

    protected abstract void doWork(); 
} 

public static class B implements Worker { // does not extend A, but implements Worker 

    private final A wrapped = new A() { // anonymous inner class, so it extends A 

     @Override 
     protected void doPrepare() { 
      System.out.println("doPrepare"); 
     } 

     @Override 
     protected void doWork() { 
      System.out.println("doWork"); 
     } 
    }; 

    @Override 
    public void work() { // delegate implementation to descendant from A 
     this.wrapped.work(); 
    } 
} 

ten sposób chroniony metody pozostają ukryte w anonimowej klasy wewnętrznej, która rozciąga się od A, który jest prywatny końcowy członkiem B. Kluczem jest to, że B nie rozciąga się na A, ale implementuje metodę Worker interfejsu work() poprzez delegowanie do anonimowej klasy wewnętrznej.Tak więc nawet w przypadku, gdy metoda B jest wywoływana z klasy należącej do tego samego pakietu, metody chronione nie będą widoczne.

B b = new B(); 
b.work(); // this method is accessible via the Work interface 

wyjściowa:

doPrepare 
doWork 
+0

Próbowałem i dostosowałem mój kod, aby podążać za tym podejściem i działa jak urok, dziękuję. Działa również z lekami generycznymi. Jedyną wadą, jaką znalazłem, jest to, że dla każdej metody w klasie abstrakcyjnej A należy podać opakowanie; czegoś, czego możesz uniknąć, kiedy używasz dziedziczenia. Ale jest to z pewnością przyjemny kompromis. –

3

Rzeczywiście używasz wzorca szablonu. Istnieją dwie główne rzeczy można zrobić, aby „ukryć” dane z zewnętrznych użytkowników:

1) Zrozumieć swój access modifiers:

    Access Levels 
Modifier | Class | Package | Subclass | World | 
-------------------------------------------------- 
public  | Y  | Y  | Y  | Y  | 
protected | Y  | Y  | Y  | N  | 
no modifier | Y  | Y  | N  | N  | 
private  | Y  | N  | N  | N  | 

brakuje zdecydowanych hakerów zastraszanie ich drogę w przeszłość tych zabezpieczeń z refleksji, mogą one utrzymuj przypadkowych programistów od przypadkowego wywoływania metod, które najlepiej trzymać z daleka. Kodiści to docenią. Nikt nie chce używać zagraconego API.

Obecnie b.doWork() jest protected. Więc będzie to widoczne tylko w twoim main(), jeśli twoja main() jest w klasie A (to nie jest), podklasa B (to nie jest), lub w tym samym pakiecie (nie jest jasne, nie wysłałeś żadnych wskazówek dotyczących pakietu (ów) twój kod znajduje się w).

To nasuwa się pytanie, kogo próbujesz chronić przed wyświetleniem b.doWork()? Jeśli tworzony przez Ciebie pakiet jest czymś, co tylko Ty się rozwijasz, może nie być tak źle. Jeśli wiele osób tupie w tym pakiecie z wieloma innymi klasami, nie jest prawdopodobne, że będą one dokładnie zaznajomione z klasami A i B. Jest tak w przypadku, gdy nie chcesz, aby wszystko kręciło się tam, gdzie każdy może je zobaczyć. Tutaj byłoby miło zezwolić na dostęp tylko do klasy i podklasy. Ale nie ma modyfikatora, który umożliwia dostęp do podklasy bez umożliwienia dostępu do pakietu. Dopóki podklasy jesteś protected jest najlepsze, co możesz zrobić. Mając to na uwadze, nie należy tworzyć pakietów z dużą liczbą klas. Utrzymuj paczki szczelne i skupione. W ten sposób większość ludzi będzie pracować poza pakietem, w którym rzeczy wyglądają ładnie i prosto.

Krótko mówiąc, jeśli chcesz, aby main() nie zadzwonił pod numer b.doWork(), umieść main() w innym pakiecie.

package some.other.package 

public final class Main { 
... 
} 

2) Wartość tego następnego kawałka porad trudno będzie zrozumieć z aktualnym kodem ponieważ chodzi o rozwiązywanie problemów, które dopiero pojawią się, jeśli kod jest rozszerzony na. Ale to naprawdę jest ściśle związana z ideą ochrony ludzi przed widzeniem rzeczy jak b.doWork()

What does "program to interfaces, not implementations" mean?

W kodzie, co to oznacza to, że byłoby lepiej, gdyby główny wyglądał następująco:

public static void main(String[] args) { 
    A someALikeThingy = new B(); // <-- note use of type A 
    someALikeThingy.work();  //The name is silly but shows we've forgotten about B 
           //and know it isn't really just an A :) 
} 

Czemu? Co ma wspólnego korzystanie z typu A i typu B? Cóż, A jest bliżej bycia interfejsem . Uwaga: tutaj nie chodzi mi tylko o to, co otrzymujesz, gdy używasz słowa kluczowego interface.Mam na myśli typ B jest konkretną implementacją typu A i jeśli nie koniecznie muszę napisać kod przeciwko B Naprawdę nie chciałbym, ponieważ kto wie, co dziwne nie A rzeczy B ma. Trudno to zrozumieć, ponieważ kod w głównej wersji jest krótki: & słodki. Ponadto, różne inne publiczne interfejsy nie są tak różne. Oba mają tylko jedną publiczną metodę: work(). Wyobraź sobie, że main() był długi i skomplikowany, Wyobraź sobie, że B zawierał wiele różnych metod, które nie są dostępne pod A. Teraz wyobraź sobie, że musisz stworzyć klasę C, którą chcesz obsłużyć. Czy nie byłoby pocieszające zobaczyć, że podczas gdy główna była karmiona B, o ile każda linia główna wie, że B jest po prostu prostą rzeczą podobną do. Z wyjątkiem części po new może to być prawda i zaoszczędzić od zrobienia czegoś na rzecz main(), ale aktualizację tego new. Czyż nie jest tak, że używanie silnie napisanego języka może czasami być miłe?

<rant>
nawet jeśli uważasz, że część aktualizacji po new z B() jest zbyt dużo pracy, że nie jesteś sam. Ta szczególna obsesja jest tym, co dało nam dependency injection movement, która zrodziła kilka frameworków, które naprawdę bardziej dotyczyły automatyzacji budowy obiektów niż zwykły zastrzyk, który każdy konstruktor może ci pozwolić.
</rant >

Aktualizacja:

widzę z waszych komentarzy, które są niechętne do tworzenia małych ciasnych pakiety. W takim przypadku rozważ porzucenie wzoru szablonu opartego na dziedziczeniu na rzecz wzorca strategii opartego na składzie. Nadal masz polimorfizm. Po prostu nie dzieje się za darmo. Niewiele więcej pisania kodu i pośrednictwa. Ale możesz zmienić stan nawet w czasie wykonywania.

Będzie to wydawać się sprzeczne z intuicją, ponieważ teraz doWork() musi być publiczne. Jaka to jest ochrona? Teraz możesz ukryć B całkowicie za lub nawet wewnątrz AHandler. Jest to klasa przyjazna dla człowieka, która zawiera kod szablonu, który jest tylko szablonem, a tylko eksponuje work(). A może po prostu być interface, więc AHandler nadal nie wie, że ma B. Takie podejście jest popularne wśród tłumu uzależnienia, ale z pewnością nie ma uniwersalnego uroku. Niektórzy robią to ślepo za każdym razem, gdy drażni ich wielu. Możesz przeczytać więcej na ten temat tutaj: Prefer composition over inheritance?

+0

Dziękuję za wyczerpującą odpowiedź. Postaram się odpowiedzieć na wszystkie pomysły: 1) Modyfikatory dostępu i struktura paczek były pierwszymi rzeczami, które rozważałem na długo przed opublikowaniem pytania. Mój pakiet jest już mały, a jego dalsze dzielenie dałoby mi pakiety 1- lub 2-klasowe, o czym wspomniałem w jednym z moich poprzednich komentarzy. To nie jest rozwiązanie w mojej obecnej sytuacji, ale może być dla kogoś innego w przyszłości. –

+0

2) Jeśli chodzi o podejście interfejsu, ma dwa wady. Po pierwsze, nie działa dobrze w przypadku leków generycznych (mógłbym podać przykład). Po drugie, uniemożliwia napisanie: 'Wynik r = nowy B (args) .work();', który uważam za ładniejszy niż nieco wymuszony 'A a = nowy B (args); Wynik r = a.work(); '. To jest znowu coś, czego próbowałem i nadal szukałem czegoś lepszego, choć dla niektórych może być do przyjęcia. –

+0

3) Kompozycja jest naprawdę odpowiedzią. Skoczyłem na dziedziczenie ze względu na semantykę moich zajęć i zapomniałem rozważyć tę opcję. Zaznaczam odpowiedź pana Schaffnera jako poprawną, ponieważ stanowi ona przykład, i utrzymuje ograniczony (chroniony) dostęp do haków; ale polecam innym, aby przejrzeli opublikowane linki. –