Most

Wzorce projektowe

  |   8 min czytania

Zastosowanie

Most (ang. Bridge) (wzorzec strukturalny) ma za zadanie oddzielić abstrakcję obiektu od jego implementacji dzięki czemu istnieje możliwość modyfikacji implementacji bez dokonywania zmian w kodzie klasy abstrakcji i na odwrót. Obiekt abstrakcji nie jest odpowiedzialny za sposób realizacji metod abstrakcji, ponieważ wywołuje on implementacje tych metod na wstrzykniętym obiekcie. Ukrywa on implementacje przed klientem oraz ułatwia rozbudowę, a także sprawia, że kod staje się czytelniejszy. Jak sama nazwa wskazuje jest on mostem - łącznikiem między abstrakcją, a implementacją. Implementacja logiki może być inicjalizowana dynamicznie.

Ograniczenia

Ze względu na podobieństwa w strukturze, wzorzec ten może być mylony ze wzorcami Adapter i Strategia, które mają inny cel. Należy zatem się upewnić, czy wybór wzorca Most jest właściwy. Poza tym może powstać zbyt wiele klas abstrakcji oraz implementacji.

Użycie

Wzorzec ten może przypominać Adapter jednakże główna różnica polega na tym, że Adapter stosowany jest do istniejącego niekompatybilnego kodu, natomiast Most używany jest w trakcie tworzenia i rozszerzania kodu. Ponadto używa podobnej struktury jak Strategia, jednakże celem wzorca Strategia jest przede wszystkim dynamiczna zmiana zachowania obiektu w zależności od sytuacji. Most warto użyć tam gdzie istnieje potrzeba odspeparowania implementacji od podmiotu przy jednoczesnym ukryciu szczegółów przed klientem. Wzorzec ten kieruje się zasadą kompozycja ponad dziedziczenie.

Implementacja

Klasy abstrakcji implementują interfejs Abstraction oraz zawierają referencje do wstrzykniętej implementacji Implementor. Klient tworzy obiekt klasy abstrakcji wraz ze wstrzyknięciem obiektu konkretnej klasy logiki, która implementuje interfejs Implementor.

Most diagram

Poniższy listing przedstawia implementację interfejsów Abstraction oraz Implementor.

public class Abstraction1 implements Abstraction {
	
    private Implementor implementor;
    //other fields

    public Abstraction1(Implementor implementor) {
        this.implementor = implementor;
    }

    @Override
    public void operation1() {
        implementor.operation1();
    }

    @Override
    public void operation2() {
        implementor.operation2();
    }

    @Override
    public void operation3() {
        //do some work
    }
}

public class Implementor1 implements Implementor {
	
    @Override
    public void operation1() {
        //some work
    }

    @Override
    public void operation2() {
        //some work
    }
}

//implementation of other implementors in similar way

public interface Abstraction {
	
    void operation1();
    void operation2();
    void operation3();
}

public interface Implementor {
	
    void operation1();
    void operation2();
}

Wybór implementacji podejmowany jest na podstawie oczekiwań systemu co przedstawia się następująco.

//some case to use Implementor1
Abstraction abstraction1 = new Abstraction1(new Implementor1());
abstraction1.operation1(); //runs operation1 from Implementor1

//some case to use Implementor2
Abstraction abstraction2 = new Abstraction2(new Implementor2());
abstraction2.operation2(); //runs operation2 from Implementor2

Przykład

Aplikacja Writer umożliwia zarządzanie dokumentami tekstowymi DocumentWriter oraz arkuszami kalkulacyjnymi SheetWriter w pamięci urządzenia. W zależnosci od wybranego typu dokumentu, zmienia się graficzny interfejs użytkownika (kolor, przybornik edytora itp). Aplikacja jest dostępna na różne wersje API, w związku z czym dla telefonów od Lollipop, wymagane jest pytanie użytkownika aplikacji o pozwolenie systemowe (np.: zapis do pamięci). Aplikacja musi zatem być elastyczna w sprawie graficznego interfejsu oraz zarządzania pozwoleniami czy pamiecią systemu. Poniższy listing przedstawia rozwiązanie tego problemu przy użyciu wzorca Most.

public class DocumentWriter implements Writer {

    private StorageManager storageManager;

    public DocumentWriter(StorageManager storageManager) {
        this.storageManager = storageManager;
    }

    @Override
    public void openDocument(String name) {
        storageManager.readFile(path);
    }

    @Override
    public void closeDocument() {
        //if is save checked then save file
        storageManager.writeFile(content);
        showEditor(false);
    }

    @Override
    public void createDocument(String name) {
        storageManager.writeFile(name, "");
        showEditor(true);
    }

    @Override
    public void deleteDocument(String name) {
        storageManager.deleteFile(name);
    }

    @Override
    public void showDocuments() {
        File[] files = storageManager.readFolder();
        //draw files list with specific theme
    }

    @Override
    public void showEditor(boolean isShowed) {
        if(isShowed) {
            //draw editor with specific theme
        }
        else {
            //just close editor
        }
    }
}

public class SheetWriter implements Writer {

    private StorageManager storageManager;

    public DocumentWriter(StorageManager storageManager) {
        this.storageManager = storageManager;
    }

    //implements Writer method in similiar way to DocumentWriter
    //use own style for editor windows
}

public class LollipopStorageManager implements StorageManager {

    private String currentFile;
    private String folderPath = "/path/to/lollipop/folder"; //get path in specific way

    @Override
    public void readFile(String name) {
        //check READ_EXTERNAL_STORAGE permissions
        //if OK, open file
        currentFile = name;
        //else show permissions dialog
    }

    @Override
    public File[] readFolder() {
        //check READ_EXTERNAL_STORAGE permissions
        //if OK, return files List
        File folder = new File(folderPath);
        return folder.listFiles;
        //else show permissions dialog
    }

    @Override
    public void writeFile(String name, String content) {
        //check WRITE_EXTERNAL_STORAGE permissions
        //if OK, write file
        //else show permissions dialog
    }

    @Override
    public void writeFile(String content) {
        //check WRITE_EXTERNAL_STORAGE permissions
        //just write content to current file
        //else show permissions dialog
    }

    @Override
    public void deleteFile(String name) {
        //check WRITE_EXTERNAL_STORAGE permissions
        //if OK, open file
        File file = new File(name);
        file.delete();
        //else show permissions dialog
    }
}

public class UnderLollipopStorageManager implements StorageManager {

    private String currentFile;
    private String folderPath = "/path/to/underlollipop/folder"; //get path in specific way

    @Override
    public void readFile(String name) {
        //just open file
        currentFile = name;
    }

    @Override
    public File[] readFolder(String name) {
        File folder = new File(name);
        return folder.listFiles;
    }

    @Override
    public void writeFile(String name, String content) {
        //just write file with content
    }

    @Override
    public void writeFile(String content) {
        //just write content to current file
    }

    @Override
    public void deleteFile(String name) {
        File file = new File(name);
        file.delete();
    }
}

public interface Writer {

    void openDocument(String name);
    void closeDocument();
    void createDocument(String name);
    void deleteDocument(String name);
    void showDocuments();
    void showEditor(boolean isShowed);
}

public interface StorageManager {

    void readFile(String name);
    File[] readFolder();
    void writeFile(String name, String content);
    void deleteFile(String name);
}

W przypadku rozszerzenia aplikacji o nowy typ dokumentu lub obsługę po stronie API, wystarczy dodać kolejny typ abstrakcji Abstraction lub implementacje Implementor. Takie podejście zapewnia dobrze zorganizowaną strukturę projektu, a implementacja może być dynamicznie zmieniana.

//check conditions and decide
StorageManager storageManager = new LollipopStorageManager();
Writer writer = new DocumentWriter(storageManager);

//do some actions
writer.showDocuments();
writer.openDocumnet("Document1");
writer.closeDocument();

//choosen abstraction has changed
writer = new SheetWriter(storageManager);
//do some actions

Biblioteki

Frameworki wstrzykiwania zależności mogą być pomocne w obsłudze wzorca Most, jednakże ze względu na prostotę wzorca oraz mnogość problemów, implementacja wzorca spoczywa na barkach programisty.