Командният модел в Java

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.