Fasada

Wzorce projektowe

  |   6 min czytania

Zastosowanie

Fasada (ang. Facade) (wzorzec strukturalny) ujednolica oraz upraszcza dostęp do złożonego systemu lub jego części poprzez udostępnienie uporządkowanego interfejsu programistycznego. Wzorzec ten czerpię inspiracje z architektury - fasady, czyli głównej i często efektownej elewacji budynku, która pełni role reprezentacyjną. Wzorzec ten analogicznie pełni rolę “nakładki” dodając kolejny wyższy poziom abstrakcji. Nie rozszerza on zatem funkcjonalności systemu, ale sprawia, że użycie jego funkcji jest prostsze, a tworzony kod czytelniejszy. Fasada sprawdza się wszędzie tam gdzie wykonanie operacji w systemie pociąga za sobą sekwencje wykonań operacji w różnych częściach systemu oraz w określonej kolejności. Jest często stosowanym wzorcem i stanowi nieodłączny element niejednego refaktoringu. Programista używający Fasady nie musi wiedzieć jak dokładnie działa dana operacja, co jednak nie zwalnia go z czytania załączonej dokumentacji.

Ograniczenia

Fasada może przechowywać referencje do obiektów, dzięki czemu istnieje silna pokusa, aby jednostkowe operacje mogły być również dostępne dla programisty. Takie podejście przeczy jednak idei wzorca, który ukrywa szczegóły implementacji. Przy projektowaniu Fasady należy więc przeanalizować jakie jednostkowe operacje z jej obiektów powinny być dostępne w interfejsie, a które ukryte w taki sposób, aby zmiana stanu obiektów nie wpływała na poprawne działanie Fasady. Co więcej Fasada może przyjmować zbyt wielką odpowiedzialność i mieć za dużo zależności co prowadzi do stworzenia super klasy (god object).

Użycie

Można pokusić się o stwierdzenie, że praktycznie każde API czy framework jest Fasadą, tzn. pełni rolę pośrednika między skomplikowanym system, a klientem zapewniając dostęp do prostych w użyciu lecz często skomplikowanych “w środku” funkcji. Fasada jest zatem prawie wszędzie.

Implementacja

Tworzenie Fasady sprowadza się przede wszystkim do agregacji powiązanych ze sobą obiektów oraz udostępnienie metod publicznych, które implementują szczegółowe wykonanie żądanej operacji.

Fasada diagram

Poniższy listing przedstawia przykładową implementacje Fasady operującej na obiektach dwóch typów, klas Utils oraz Enumach.

public class Facade {
    
    private Element1 element1;
    private Element2 element2;

    public Facade() {
        element1 = new Element1();
        element2 = new Element2();
    }

    public void operation1(Action action) {
        if(action == Action.INCREASE)
            element1.increase();
        else if(action == Action.DECREASE)
            element1.decrease();
        else if(action == Action.DOUBLE)
            element1.makeDouble();
        else
            element2.restart();
    }

    public void operation2() {
    	if(Utils.isAccessProvided()) {
            element2.startAction(element1.getState());
            element1.refreshState();
        }
        else
            Utils.printError();
    }
}

Przykład

Aplikacja jest mockupem odtwarzacza muzyki (Klient), którzy korzysta z autorskiego MediaPlayer (Fasada). Programista tworząc aplikacje odtwarzacza ma za zadanie przygotować zrozumiały interfejs, który pozwoli innemu programiście w prosty i krótki sposób wywołać podstawowe funkcje odtwarzacza takie jak play, pause, stop, zmiana utworów czy regulacja głośności (metody Fasady). Wiedza na temat sposobu działania tak przygotowanego API nie jest niezbędna, aby programista mógł zaimplementować podstawowe funkcje odtwarzacza. W przypadku niejasności czy też zaawansowanych operacji powinien on sięgnąć do dokumentacji oraz kodu źródłowego. Odpowiednio zaprogramowany MediaPlayer komunikuje się z różnymi innymi elementami systemu i modułami zewnętrznymi, deleguje im poszczególne zadania oraz zarządza całym procesem. Każdorazowe, ręczne tworzenie podstawowych funkcji odtwarzacza może być czasoschłonne, nieczytalne i wykryczać poza możliwości początkującego programisty, zatem zastosowanie Fasady jest tu wysoce pożądane. Udostępnia ona w klarowny sposób funkcje systemu, ukrywając przy tym mechanizm działania. Poniższy listing przedstawia Fasadę MediaPlayer.

public class MediaPlayer {

    private FileManager fileManager;
    private HardwarePlayer hardwarePlayer;
    private SongFile songFile;
    private List<SongFile> songs;
    private int currentSong;
    private int currentVolume;
    private boolean isPlaying;

    public MediaPlayer(Source hardwareSource, boolean isSdCard) {
        fileManager = new FileManager(isSdCard);
        hardwarePlayer = new HardwarePlayer(hardwareSource);
        songs = new ArrayList<>();
        currentSong = 0;
        currentVolume = 5;
        isPlaying = false;
        createPlaylist();
    }

    public void play() {
        loadSong();
        hardwarePlayer.play();
        isPlaying = true;
    }

    public void pause() {
        hardwarePlayer.pause();
        isPlaying = false;
    }

    public void stop() {
        hardwarePlayer.stop();
        currentSong = 0;
        isPlaying = false;
    }

    public void increaseVolume() {
        if(currentVolume < 10)
            currentVolume++;
        changeVolume();
    }

    public void decreaseVolume() {
        if(currentVolume > 0)
            currentVolume--;
        changeVolume();
    }

    public void nextSong() {
        if(currentSong == songs.size() + 1)
            currentSong = 0;
        else
            currentSong++;
        changeSong();
    }

    public void previousSong() {
        if(currentSong == 0)
            currentSong = songs.size() - 1;
        else
            currentSong--;
        changeSong();
    }

    public void loadSong() {
        SongBytes songBytes = Converter.convertToBytes(songFile);
        hardwarePlayer.load(songBytes);
    }

    public void createPlaylist() {
        List<File> files = fileManager.readSongsList();
        songs = Converter.convertToSongFiles(files);
    }
    
    public boolean isPlaying() {
        return isPlaying;
    }

    private void changeVolume() {
        hardwarePlayer.pause();
        hardwarePlayer.setVolume(currentVolume);
        hardwarePlayer.start();
    }

    private void changeSong() {
        songFile = songs.get(currentSong);
        hardwarePlayer.stop();
        play();
    }

    //other methods
}

Tak przygotowana Fasada wraz z zaimplementowanymi modułami, umożliwia w łatwy sposób podpięcie akcji pod interfejs użytkownika co przekłada się również na czytelność kodu.

Biblioteki

Implementacja Fasady jest tak różna jak realizacja danego problemu, w związku z czym nie ma jednej zasady jej tworzenia, a biblioteki usprawniające proces implementacji Fasady w systemie nie mają racji bytu. Każda zewnętrzna biblioteka (np.: Retrofit, Glide) jest Fasadą w tworzonym oprogramowaniu.