Table of Contents

Design patterns - Command

Obiective

Scopul acestui laborator este familiarizarea cu folosirea design pattern-ului comportamental Command.

Introducere

În laboratoarele precedent am prezentat pattern-uri ce vă ajută în realizarea unei arhitecturi mai decuplate, modulare și extensibile a aplicațiilor:

În acest laborator vom Command, un pattern comportamental care decuplează obiectele care execută anumite acțiuni de obiectele care le invocă.

Design pattern-ul Command incapsulează un apel cu tot cu parametri într-o clasă cu interfată generică. Acesta este Behavioral pentru că modifică interacțiunea dintre componente, mai exact felul în care se efectuează apelurile.

Acest pattern este recomandat în următoarele cazuri:

Exemple de utilizare:

Funcționare și necesitate

În esentă, Command pattern (așa cum v-ați obișnuit și lucrând cu celelate Pattern-uri pe larg cunoscute) presupune încapsularea unei informații referitoare la acțiuni/comenzi folosind un wrapper pentru a “ține minte această informație” și pentru a o folosi ulterior. Astfel, un astfel de wrapper va deține informații referitoare la tipul acțiunii respective (în general un asemenea wrapper va expunde o metodă execute(), care va descrie comportamentul pentru acțiunea respectivă).

Mai mult încă, când vorbim de Command Pattern, în terminologia OOP o să întâlniți deseori și noțiunea de Invoker. Invoker-ul este un middleware ca funcționalitate care realizează managementul comenzilor. Practic, un Client, care vrea să facă anumite acțiune, va instanția clase care implementează o interfață Command. Ar fi incomod ca, în cazul în care aceste instanțieri de comenzi provin din mai multe locuri, acest management de comenzi să se facă local, în fiecare parte (din rațiuni de economie, nu vrem să duplicăm cod). Invoker-ul apare ca o necesitate de a centraliza acest proces și de a realiza intern management-ul comenzilor (le ține într-o listă, ține cont de eventuale dependințe între ele, totul în funcție de context).

Un client (generic spus, un loc de unde se lansează comenzi) instanțiază comenzile și le pasează Invoker-ului. Din acest motiv Invoker-ul este un middleware între client și receiver, fiindcă acesta va apela execute pe fiecare Command, în funcție de logica să internă.

Recomandare: La Referinte aveti un link catre un post pe StackOverflow, pentru a intelege mai bine de ce aveti nevoie de Pattern-ul Command si de ce nu lansati comenzi pur si simplu.

Structura

Ideea principală este de a crea un obiect de tip Command care va reține parametrii pentru comandă. Comandantul reține o referință la comandă și nu la componenta comandată. Comanda propriu-zisă este anunțată obiectului Command (de către comandant) prin execuția unei metode specificate asupra lui. Obiectul Command este apoi responsabil de trimiterea (dispatch) comenzii către obiectele care o îndeplinesc (comandați).

Fig. 1: Diagrama de stări pentru CommandPattern

Tipuri de componente (roluri):

În Java, se pot folosi atât interfețe cât și clase abstracte, pentru Command, depinzând de situație (e.g. clasă abstractă dacă știm sigur ca obiectele de tip Command nu mai au nevoie să extindă și alte clase).

În diagrama din figure 1, comandantul este clasa Invoker care conține o referință la o instanță (command) a clasei (Command). Invoker va apela metoda abstractă execute() pentru a cere îndeplinirea comenzii. ConcreteCommand reprezintă o implementare a interfeței Command, iar în metoda execute() va apela metoda din Receiver corespunzătoare acelei acțiuni/comenzi.

Exemplu

Diagrama de secvență din figure 2 prezintă apelurile în cadrul unei aplicație de editare a imaginilor, ce este structurată folosind pattern-ul Command. În cadrul acesteia, Receiver-ul este Image, iar comenzile BlurCommand și CropCommand modifică starea acesteia. Structurând aplicația în felul acesta, este foarte ușor de implementat un mecanism de undo/redo, fiind suficient să menținem în Invoker o listă cu obiectele de tip Command aplicate imaginii.

Fig. 2: Diagrama de secvență pentru comenzile de prelucrare a imaginilor

Pe wikipedia puteți analiza exemplul PressSwitch. Flow-ul pentru acesta este ilustrat în figure 3  Fig. 3: Diagrama de secvență pentru comenzile de aprindere/stingere a switch-ului

Exerciții

Implementați folosind patternul Command un editor de diagrame foarte simplificat. Scheletul de cod conține o parte din clase și câteva teste.

Componentele principale ale programului:

Task 1 - Implementarea comenzilor (4p)

Implementați 5 tipuri de comenzi, pentru următoarele acțiuni:

Comenzile primesc în constructor referința către DiagramCanvas și alte argumente necesare lor. De exemplu, comanda pentru schimbarea culorii trebuie sa primească și culoarea nouă și indexul componentei.

Pentru acest task nu este nevoie să implementați și metoda undo(), doar execute().

Comenzile implementează în afară de metodele interfeței și metoda toString() pentru a afișa comanda. Recomandăm folosirea IDE-ului pentru a o genera.

Task 2 - Testarea comenzilor (2p)

Scheletul conține în clasa Test metode pentru a testa comportamentul comenzilor. O parte sunt deja implementate, iar o parte trebuie implementate.

Task 3 - Undo/redo (2p)

Implementați în comenzi și în Invoker mecanismul de undo/redo al comenzilor. Recomandăm în Invoker sa folosiți două structuri de date, una care să mențină comenzile efectuate, iar una pentru comenzile făcute undo.

Task 4 - Test undo/redo (2p)

Scheletul conține în clasa Test o metodă pentru o verificare simplă a corectitudinii implementării undo și redo. Completați metoda testComplexUndoRedo în care să faceți multiple undo-uri și redo-uri pentru diverse tipuri de comenzi.

Resurse

Referințe