Contoh Command Design Pattern

Yang udah pernah menggunakan Clean Architecture mungkin udah familiar dengan design pattern ini, terutama ketika declare input boundary. Kita membungkus sebuah action ke dalam satu objek. Selain itu, ketika kita melakukan pembungkusan logic ke dalam runnable juga termasuk Command Pattern. Design pattern yang satu ini memang cukup populer di kalangan software engineer. Termasuk salah satu design pattern yang gampang dipahami.
Command Design Pattern adalah Behavioral Design Pattern yang membungkus request menjadi objek tunggal yang menyediakan informasi tentang request tersebut untuk ditransformasikan menjadi method arguments, eksekusi request, antrian proses, dan undoable operations.
Design Pattern
Use Case
Kita akan membuat system registrasi user beserta rolenya pada sebuah toko:
- Pertama akan dilakukan pengecekan apakah username sudah ada atau belum;
- Jika ada maka akan error;
- Jika ga ada maka insert ke system dan kirim notifikasi ke user lainnya;
- Ini bisa digunakan beberapa kali pada use case lainnya, misalnya saat pendaftaran toko baru by default juga akan registrasi user baru dengan role "ADMIN";
Contoh Code
public class UserEntity{
private final Long storeId;
private final String username;
private final String role;
public UserEntity(Long storeId, String username, String role){
this.storeId = storeId;
this.username = username;
this.role = role;
}
public Long getStoreId(){
return storeId;
}
public String getUsername(){
return username;
}
public String getRole(){
return role;
}
}public interface UserRepository{
UserEntity save(UserEntity user);
boolean existsByUsername(String username);
}public class UserSqlRepository implements UserRepository{
@Override
public UserEntity save(UserEntity user) {
return null;
}
@Override
public boolean existsByUsername(String username){
return false;
}
}public interface NotificationService{
void sendNotification(String message);
}public class PushNotificationService implements NotificationService{
@Override
public void sendNotification(String message){
System.out.println(message);
}
}private static void registerUser(Long storeId, String username, String role){
UserRepository userRepository = new UserSqlRepository();
NotificationService notificationService = new PushNotificationService();
if (userRepository.existsByUsername(username)) {
throw new IllegalArgumentException(username);
}
userRepository.save(new UserEntity(storeId, username, role));
notificationService.sendNotification("New user registered: " + username);
}private static void registerStore(){
//logic register store
Long generatedNewStoreId = 999L;
String storeUsername = "admin";
registerUser(generatedNewStoreId, storeUsername, "ADMIN");
}public static void main(){
//register user
registerUser(1L, "ferry", "STAFF");
registerUser(1L, "ferry99", "STAFF");
//register store and default user
registerStore();
}Masalah
Logic registrasi hanya dibungkus ke dalam sebuah method. Di sini client yang melakukan konfigurasi dependency dan sejenisnya di dalam method tersebut. Saat action tersebut digunakan beberapa kali, kita kembali construct konfigurasinya. Begitu juga saat registrasi toko, konfigurasi tersebut di-setup lagi tanpa bisa di-reuse konfigurasinya. Logicnya langsung dieksekusi saat method tersebut dipanggil tanpa bisa ditunda.
Solusi
Sekarang kita coba membuat Command Design Pattern😎.
public interface RegisterUserCommand{
void execute(Long storeId, String username, String role);
}public class DefaultRegisterUserCommand implements RegisterUserCommand{
private final UserRepository userRepository;
private final NotificationService notificationService;
public DefaultRegisterUserCommand(UserRepository userRepository, NotificationService notificationService){
this.userRepository = userRepository;
this.notificationService = notificationService;
}
@Override
public void execute(Long storeId, String username, String role){
if (userRepository.existsByUsername(username)) {
throw new IllegalArgumentException(username);
}
userRepository.save(new UserEntity(storeId, username, role));
notificationService.sendNotification("New user registered: " + username);
}
}private static void registerStore(RegisterUserCommand registerUserCommand){
//logic register store
Long generatedNewStoreId = 999L;
String storeUsername = "admin";
registerUserCommand.execute(generatedNewStoreId, storeUsername, "ADMIN");
}public static void main(){
//register user
RegisterUserCommand registerUserCommand = new DefaultRegisterUserCommand(new UserSqlRepository(),
new PushNotificationService());
registerUserCommand.execute(1L, "ferry", "STAFF");
registerUserCommand.execute(1L, "ferry99", "STAFF");
//register store and default user
registerStore(registerUserCommand);
}Kita membungkus logicnya ke dalam Command Interface beserta implementasinya. Kita bisa setup konfigurasinya sekali lalu reuse berulang kali. Bahkan kita bisa menunda eksekusinya selama method execute() belum di-trigger. Kita juga bisa menggunakannya sebagai parameter untuk digunakan kembali pada method registerStore() dan dieksekusi di sana tanpa perlu konfigurasi ulang. Dengan menggunakan interface pada Command kita bisa membuat beberapa varian registrasi ke depannya seperti registrasi menggunakan SSO atau third party lainnya.
Kenapa menggunakan Command Design Pattern?
Command Design Pattern digunakan ketika ingin menjadikan sebuah action sebagai parameter. Oleh karena itu kita membungkusnya menjadi sebuah objek. Dengan begitu kita bisa mengirim objek yang berisi action yang fleksibel. Selain itu, command pattern juga bisa dimanfaatkan untuk antrian eksekusi, karena trigger proses eksekusinya bisa dilakukan belakangan seperti pada Runnable di Java.
Verdict
Contoh Command Pattern yang paling sering dilakukan adalah ketika melakukan multi-threading menggunakan Runnable, di sana kita membungkus sebuah action ke dalam objek runnable sebelum dieksekusi nanti secara concurrent. Ini juga diimplementasi pada Clean Architecture untuk membungkus logic pada class use case. Sekilas memang terlihat seperti Strategy Design Pattern, tapi beda tujuan. Command Design Pattern lebih spesifik tentang membungkus algoritma ke dalam sebuah objek yang memiliki method berupa perintah untuk mentrigger eksekusi algoritma tersebut. Sedangkan Strategy Design Pattern lebih general tentang penggunaan varian dari beberapa algoritma agar fleksibel dan maintainable. Dengan Command Pattern kita telah menerapkan Single Responsibility dan Open/Close Principle.
