1. Общ преглед
Командният модел е модел на поведенчески дизайн и е част от официалния списък на дизайнерските модели на GoF. Най-просто казано, шаблонът възнамерява да капсулира в обект всички данни, необходими за извършване на дадено действие (команда), включително какъв метод да се извика, аргументите на метода и обекта, към който принадлежи методът.
Този модел ни позволява да отделяме обекти, които произвеждат командите от техните потребители , така че затова моделът е известен като модел производител-потребител.
В този урок ще научим как да реализираме командния модел в Java, като използваме обектно-ориентирани и обектно-функционални подходи, и ще видим в какви случаи на употреба може да бъде полезен.
2. Обектно-ориентирано изпълнение
При класическото изпълнение командният модел изисква внедряване на четири компонента: Command, Receiver, Invoker и Client .
За да разберем как работи моделът и ролята, която играе всеки компонент, нека създадем основен пример.
Да предположим, че искаме да разработим приложение за текстови файлове. В такъв случай трябва да внедрим цялата функционалност, необходима за извършване на някои операции, свързани с текстови файлове, като отваряне, писане, запазване на текстов файл и т.н.
И така, трябва да разделим приложението на четирите компонента, споменати по-горе.
2.1. Командни класове
Командата е обект, чиято роля е да съхранява цялата информация, необходима за изпълнение на действие , включително метода за извикване, аргументите на метода и обекта (известен като получател), който реализира метода.
За да получите по-точна представа за това как работят командните обекти, нека започнем да разработваме прост команден слой, който включва само един-единствен интерфейс и две реализации:
@FunctionalInterface public interface TextFileOperation { String execute(); }
public class OpenTextFileOperation implements TextFileOperation { private TextFile textFile; // constructors @Override public String execute() { return textFile.open(); } }
public class SaveTextFileOperation implements TextFileOperation { // same field and constructor as above @Override public String execute() { return textFile.save(); } }
В този случай интерфейсът TextFileOperation дефинира API на командните обекти и двете реализации, OpenTextFileOperation и SaveTextFileOperation, изпълняват конкретните действия. Първият отваря текстов файл, докато вторият записва текстов файл.
Ясно е да се види функционалността на команден обект: командите TextFileOperation капсулират цялата информация, необходима за отваряне и запазване на текстов файл, включително обекта на получателя, методите за извикване и аргументите (в този случай не се изискват аргументи, но биха могли да бъдат).
Струва си да се подчертае, че компонентът, който изпълнява файловите операции, е приемникът ( екземпляр TextFile ) .
2.2. Класът на приемника
Приемникът е обект, който извършва набор от сплотени действия . Това е компонентът, който изпълнява действителното действие, когато се извика методът execute () на командата .
В този случай трябва да дефинираме клас на приемник, чиято роля е да моделира обекти TextFile :
public class TextFile { private String name; // constructor public String open() { return "Opening file " + name; } public String save() { return "Saving file " + name; } // additional text file methods (editing, writing, copying, pasting) }
2.3. Класът Invoker
Инвокерът е обект, който знае как да изпълни дадена команда, но не знае как е изпълнена командата. Той знае само интерфейса на командата.
В някои случаи повикващият също съхранява и поставя на опашка команди, освен да ги изпълнява. Това е полезно за внедряване на някои допълнителни функции, като например запис на макро или функционалност за отмяна и повторение.
В нашия пример става очевидно, че трябва да има допълнителен компонент, отговорен за извикването на командните обекти и тяхното изпълнение чрез метода execute () на командите . Точно тук влиза в игра класът на призоваващите .
Нека да разгледаме основно изпълнение на нашия инвокер:
public class TextFileOperationExecutor { private final List textFileOperations = new ArrayList(); public String executeOperation(TextFileOperation textFileOperation) { textFileOperations.add(textFileOperation); return textFileOperation.execute(); } }
Класът TextFileOperationExecutor е само тънък слой абстракция, който отделя командните обекти от техните потребители и извиква метода, капсулиран в командните обекти TextFileOperation .
В този случай класът също съхранява командните обекти в Списък . Разбира се, това не е задължително при изпълнението на модела, освен ако не се наложи да добавим допълнителен контрол към процеса на изпълнение на операциите.
2.4. Клиентският клас
Клиентът е обект, който контролира процеса на изпълнение на командите, като посочва какви команди да се изпълняват и на какви етапи от процеса да се изпълняват.
Така че, ако искаме да бъдем ортодоксални с формалната дефиниция на шаблона, трябва да създадем клиентски клас, като използваме типичния основен метод:
public static void main(String[] args) { TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); textFileOperationExecutor.executeOperation( new OpenTextFileOperation(new TextFile("file1.txt")))); textFileOperationExecutor.executeOperation( new SaveTextFileOperation(new TextFile("file2.txt")))); }
3. Обектно-функционална реализация
Досега използвахме обектно-ориентиран подход за реализиране на командния модел, който е добре.
От Java 8 можем да използваме обектно-функционален подход, базиран на ламбда изрази и препратки към методи, за да направим кода малко по-компактен и по-малко подробен .
3.1. Използване на ламбда изрази
Тъй като TextFileOperation интерфейсът е функционален интерфейс, ние можем да предаваме командни обекти под формата на ламбда изрази на повикващия , без да се налага да създаваме екземпляри TextFileOperation изрично:
TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); textFileOperationExecutor.executeOperation(() -> "Opening file file1.txt"); textFileOperationExecutor.executeOperation(() -> "Saving file file1.txt");
The implementation now looks much more streamlined and concise, as we've reduced the amount of boilerplate code.
Even so, the question still stands: is this approach better, compared to the object-oriented one?
Well, that's tricky. If we assume that more compact code means better code in most cases, then indeed it is.
As a rule of thumb, we should evaluate on a per-use-case basis when to resort to lambda expressions.
3.2. Using Method References
Similarly, we can use method references for passing command objects to the invoker:
TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); TextFile textFile = new TextFile("file1.txt"); textFileOperationExecutor.executeOperation(textFile::open); textFileOperationExecutor.executeOperation(textFile::save);
In this case, the implementation is a little bit more verbose than the one that uses lambdas, as we still had to create the TextFile instances.
4. Conclusion
В тази статия научихме ключовите концепции на командния модел и как да приложим шаблона в Java с помощта на обектно-ориентиран подход и комбинация от ламбда изрази и референции на методи.
Както обикновено, всички примери за кодове, показани в този урок, са достъпни в GitHub.