Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
W ramach mini projektu bazującego na RMI (ang. Remote Method Invocation) narzuciłem sobie zadanie, zaimplementowania wzorca polecenie. Zacznijmy od omówienia problemów jakie sprawnie rozwiązuje wzorzec Command. Wzorzec ten możemy wykorzystać w sytuacjach takich jak:
Jak to wygląda na diagramie ?
ConcreteCommand jest to klasa pośrednicząca między nadawcą i odbiorcą komendy. W deklaracji klasy znajduje atrybut będący odbiorcą. Dzięki temu komenda wewnątrz metody execute jest wstanie wykonać coś za jego pośrednictem. W ten sposób wyeliminowaliśmy tight-coupling między nadawcą i odbiorcą. Na marginesie, ważne jest, aby odbiorca był ustawiany przez konstruktor albo wstrzykiwany. Uniknie to tworzenia dla każdego polecenia oddzielnego obiektu odbiorcy.
CommandExecutor odpowiada za wykonywanie komend wszelakiego typu. Wewnątrz metody executeCommand zostaje wywołana metoda execute aktualnie ustawionej komendy.
Poniżej zamieszczam kod jak to wygląda u mnie:
public interface Command<T> { T execute() throws InterruptedException, NotBoundException, RemoteException; }
Używam tutaj typu generycznego, ponieważ tak jest uniwersalnie, komenda może mieć przecież różne typy zwracane albo nie mieć żadnego (void).
public class CalculateInvertedMatrixParallelCommand implements Command<double[][]>{ String UNIQUE_BINDING_NAME = "matrix.calculator"; private final MatrixCalculator matrixCalculator; private final double[][] matrix; private final int threads; public CalculateInvertedMatrixParallelCommand(double[][] matrix, int threads) throws RemoteException, NotBoundException { this.matrix = matrix; this.threads = threads; Registry registry = LocateRegistry.getRegistry(2732); this.matrixCalculator = (MatrixCalculator) registry.lookup(UNIQUE_BINDING_NAME); } @Override public double[][] execute() throws InterruptedException, RemoteException { return matrixCalculator.invertParallel(matrix, threads); } }
Zatrzymajmy się tutaj na chwilę, bo cała magia dzieje się tutaj. Jak nazwa komendy mówi, polecenie zwraca obliczaną równolegle macierz odwrotną. To co się dzieję w konstruktorze nazwałbym… wstrzykiwaniem przez RMI. Łączymy się tutaj z zdalnym obiektem kalkulatora macierzy, a następnie inicjalizujemy go jako naszego odbiorcę polecenia. Następnie w metodzie execute zwracamy wynik. Fajne nie?
Będzie jeszcze ciekawiej, ale póki co, dla formalności, poniżej znajduje się kod klasy wykonującej polecenia:
public class OperationExecutor {
Command<?> command;
void setCommand(Command<?> command) {
this.command = command;
}
Object executeOperation() throws InterruptedException, NotBoundException, RemoteException {
return this.command.execute();
}
}
Musiałem tutaj użyć typu wieloznacznego (ang. wildcard), ponieważ w czasie kompilacji nie jest wiadome jaki typ zwracany będzie mieć ustawiana komenda.
Dobra, a co w przypadku gdybyśmy chcieli np. chcieli zmierzyć czas wykonywanej operacji? Możemy zrobić coś na zasadzie:
public static void main(String[] args) throws {
OperationExecutor executor = new OperationExecutor();
Command<Integer> addCommand = new SomeCommand();
executor.setCommand(addCommand);
Instant start = Instant.now();
executor.executeOperation();
Instant end = Instant.now();
System.out.println("Elapsed time: " + Duration.between(start, end));
}
Takie rozwiązanie wymusza na kliencie lub odbiorcy implementacje mierzenia czasu. Gryzie się to trochę z literami S i O z akronimu SOLID (ang. Single Responsibility Principle, Open-Closed Principle). Dodawanie nowych funkcjonalności do którejkolwiek z tych klas sprowadza się do zwiększania ilości odpowiedzialności jakie klasa posiada oraz dodawanie kolejnych funkcjonalności równa się w ingerencję w istniejący kod, co może skutkować błędami w innych częściach systemu. Co zrobić by nie ingerować w strukturę tych klas? Trzeba nasze polecenie udekorować! Przyjrzyjmy się zatem definicji wzorca Dekorator:
Wzorzec Dekorator pozwala na dynamiczne przydzielenie danemu obiektowi nowych zachowań. Dekoratory dają elastyczność podobną do tej, jaką daje dziedziczenie, oferując jednak w zamian znacznie rozszerzoną funkcjonalność.
Tak brzmi definicja, ale niezbyt wiele z niej wynika. Co tak naprawdę mamy zrobić?Moglibyśmy po prostu zrobić klasę która rozszerza wskazaną komendę i dodaje odpowiednio nowe zachowania.
Na pierwszy rzut oka takie rozwiązanie jest w porządku, ale jeśli przybyłoby więcej wariantów i chcielibyśmy robić ich kombinacje? Na przykład:
Licząc z komendą, która mierzy czas wykonania operacji mamy już 4 warianty. To są aż 24 kombinacje!
Przykład reprezentuje aplikacje z interfejsem graficznym. Każdy składnik/komponent implementuje interfejs Renderable. Chcemy dodatkowo mieć możliwość stylowania komponentów. Przykład do czego prowadzi takie podejście jak wyżej znajduje się na trzecim slajdzie.
Jedną z dobrych praktyk w programowaniu obiektowym jest preferowanie kompozycji nad dziedziczeniem. Zamiast dziedziczyć bezpośrednio po elemencie, stworzymy klasę implementującą ten sam interfejs. Będziemy w ten sposób w stanie “opakować” starą komendę, nową. Nowa komenda przez kompozycję będzie miała dostęp do starej komendy, ustawimy ją przez konstruktor w ten sposób:
public class TimedCommand<T> implements Command<T> {
private final Command<T> command;
public TimedCommand(Command<T> command) {
this.command = command;
}
}
Teraz dodajmy dodatkową funkcjonalność tej komendy.
public class TimedCommand<T> implements Command<T> {
private final Command<T> command;
private Duration executionTime;
public TimedCommand(Command<T> command) {
this.command = command;
}
@Override
public T execute() {
Instant start = Instant.now();
T result = command.execute();
Instant end = Instant.now();
executionTime = Duration.between(start, end);
return result;
}
public Duration getExecutionTime() {
return executionTime;
}
}
Przetestujmy to w praktyce:
public static void main( String[] args ) throws NotBoundException, RemoteException, InterruptedException {
double[][] matrix = generateMatrix(2000);
OperationExecutor executor = new OperationExecutor();
Command<double[][]> parallelCommand = new CalculateInvertedMatrixParallelCommand(matrix, 8);
Command<double[][]> sequentialCommand = new CalculateInvertedMatrixSequentialCommand(matrix);
TimedCommand<double[][]> timedParallelCommand = new TimedCommand<>(parallelCommand);
TimedCommand<double[][]> timedSequentialCommand = new TimedCommand<>(sequentialCommand);
executor.setCommand(timedParallelCommand);
matrix = (double[][]) executor.executeOperation();
System.out.println(timedParallelCommand.getExecutionTime());
executor.setCommand(timedSequentialCommand);
matrix = (double[][]) executor.executeOperation();
System.out.println(timedSequentialCommand.getExecutionTime());
}
}
Jest to kod klienta korzystającego z naszego systemu komend. Na początku inicjalizowana jest macierz z losowymi wartościami do policzenia. Następnie tworzymy obiekt wykonawcy, obiekty poleceń, które chcemy wykonać. Jeśli dodatkowo chcemy sprawdzić ile czasu zajmuje wykonanie danego polecenia to tworzymy odpowiedni dekorator, który od teraz będzie opakowywać naszą komendę. Następnie polecenia wykonujemy, a uzyskany czas pozyskujemy poprzez getter dekoratora.
W artykule omówiłem jak działają wzorce projektowe Decorator i Command. Dodatkowo przedstawiłem przykład świetnej synergii pomiędzy nimi. Gorąco zachęcam do przetestowania u siebie. Kod źródłowy znajduję się tutaj: https://github.com/newmon13/PRIR_RMI_Project.git