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
.
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.