Service Layer menggunakan Command Pattern

Eko Kurniawan Khannedy 14 Juli 2017

Service Layer menggunakan Command Pattern

Di dunia Java, kita pasti terbiasa dengan membuat Service Layer dalam aplikasi. Biasanya Service Layer yang kita buat, rata-rata menggunakan Facade Pattern. Entah dari mana asalnya, sejak zaman EJB, rata-rata programmer Java selalu menggunakan Facade Pattern jika membuat Service Layer.

Image

Jika masih bingung apa itu Facade Pattern, mungkin bisa baca dulu di wikipedia tentang Facade Pattern. Intinya, biasanya yang kita lakukan adalah mengenkapsulasi bisnis logic dalam sebuah class, biasanya kita beri nama class nya menjadi class XxxService. Misal :

package com.idspring.commandpattern.service; import com.idspring.commandpattern.entity.Cart; import com.idspring.commandpattern.model.service.AddProductToCartRequest; import com.idspring.commandpattern.model.service.CreateNewCartRequest; import com.idspring.commandpattern.service.command.RemoveProductFromCartCommand; import reactor.core.publisher.Mono; /** * @author Eko Kurniawan Khannedy * @since 08/07/17 */ public interface CartService { Mono<Cart> createNewCart(CreateNewCartRequest request); Mono<Cart> removeProductFromCart(RemoveProductFromCartCommand request); Mono<Cart> addProductToCart(AddProductToCartRequest request); Mono<Cart> updateProductInCart(AddProductToCartRequest request); }

Problem Dengan Facade Pattern

Sebenarnya tidak ada masalah dengan menggunakan Facade Pattern. Sampai pada titik Service Class yang kita buat sudah semakin besar. Misal, berikut adalah contoh implementasi Service Class untuk Cart.

package com.idspring.commandpattern.service.impl; import com.idspring.commandpattern.entity.Cart; import com.idspring.commandpattern.entity.CartItem; import com.idspring.commandpattern.entity.Product; import com.idspring.commandpattern.model.service.AddProductToCartRequest; import com.idspring.commandpattern.model.service.CreateNewCartRequest; import com.idspring.commandpattern.model.service.RemoveProductFromCartRequest; import com.idspring.commandpattern.model.service.UpdateProductInCartRequest; import com.idspring.commandpattern.repository.CartRepository; import com.idspring.commandpattern.repository.ProductRepository; import com.idspring.commandpattern.service.CartService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; /** * @author Eko Kurniawan Khannedy * @since 08/07/17 */ @Service public class CartServiceImpl implements CartService { @Autowired private CartRepository cartRepository; @Autowired private ProductRepository productRepository; @Override public Mono<Cart> createNewCart(CreateNewCartRequest request) { Cart cart = newCart(request.getCartId()); return cartRepository.save(cart); } @Override public Mono<Cart> removeProductFromCart(RemoveProductFromCartRequest request) { return cartRepository.findById(request.getCartId()) .map(cart -> findCartItemAndRemoveIt(cart, request.getProductId())) .flatMap(cart -> cartRepository.save(cart)); } @Override public Mono<Cart> addProductToCart(AddProductToCartRequest request) { return Mono.zip( objects -> addOrUpdateProductInCart((Cart) objects[0], (Product) objects[1], request.getQuantity()), cartRepository.findById(request.getCartId()), productRepository.findById(request.getProductId()) ).flatMap(cart -> cartRepository.save(cart)); } @Override public Mono<Cart> updateProductInCart(UpdateProductInCartRequest request) { return cartRepository.findById(request.getCartId()) .map(cart -> updateCartItemQuantity(cart, request)); } private Cart newCart(String id) { return Cart.builder() .id(id) .build(); } private Cart findCartItemAndRemoveIt(Cart cart, String productId) { CartItem cartItem = findItemInCart(cart, productId); return removeItemFromCart(cart, cartItem); } private CartItem findItemInCart(Cart cart, String productId) { return cart.getItems().stream() .filter(cartItem -> cartItem.getId().equals(productId)) .findFirst().get(); } private Cart removeItemFromCart(Cart cart, CartItem cartItem) { cart.getItems().remove(cartItem); return cart; } private Cart updateCartItemQuantity(Cart cart, UpdateProductInCartRequest request) { cart.getItems().stream() .filter(cartItem -> cartItem.getId().equals(request.getProductId())) .forEach(cartItem -> cartItem.setQuantity(cartItem.getQuantity() + request.getQuantity())); return cart; } private Cart addOrUpdateProductInCart(Cart cart, Product product, Integer quantity) { if (isCartContainProduct(cart, product)) { incrementProductQuantity(cart, product, quantity); } else { addNewProductToCart(cart, product, quantity); } return cart; } private boolean isCartContainProduct(Cart cart, Product product) { return cart.getItems().stream() .anyMatch(cartItem -> cartItem.getId().equals(product.getId())); } private void incrementProductQuantity(Cart cart, Product product, Integer quantity) { cart.getItems().stream() .filter(cartItem -> cartItem.getId().equals(product.getId())) .forEach(cartItem -> cartItem.setQuantity(cartItem.getQuantity() + quantity)); } private void addNewProductToCart(Cart cart, Product product, Integer quantity) { CartItem item = CartItem.builder() .id(product.getId()) .name(product.getName()) .price(product.getPrice()) .quantity(quantity) .build(); cart.getItems().add(item); } }

Semain banyak code yang terdapat di dalam sebuah class, maka ada beberapa problem yang bakal kita temui, diantaranya.

  • Code akan lebih sulit untuk dibaca, contoh diatas mungkin kita perlu melihat lebih detail, method x itu digunakan oleh method mana, dan apakah ada method yang digunakan oleh lebih dari satu method.
  • Lebih gampang konflik, semakin banyak code dalam sebuah class, kemungkinan konflik antar developer ketika mengubah code akan semakin besar.
  • Lebih gampang membuat kekacauan. Mungkin ada satu method yang ternyata dipake oleh lebih dari satu method. Jika programmer tidak disiplin membuat unit test, dia mungkin secara tidak sadar bisa membuat kekacauan untuk fitur yang lain yang tidak dia test.

Bayangkan sample code diatas barulah 100-an baris, bayangkan jika service class yang dibuat sudah mencapai 1000-an baris. Pusingnya bukan main untuk membaca codenya, apalagi jika programmer yang membuat code nya tidak mengerti konsep clean code, bisa-bisa udah kayak spageti, acak-acakan, semrawut, akhirnya kita malah pengen nge-rewrite :D

Apa itu Command Pattern

Oke, sekarang kita sudah tahu, problem apa yang kita temui saat menggunakan Facade Pattern. Sekarang kita bahas alternative lain yang bisa kita gunakan sebagai Service Layer, yaitu Command Pattern.

Image

Sederhananya method-method yang kita buat tadi di Service Class, kita pindahkan menjadi Command Class.

Jadi jika tadi kita memili 4 method dalam CartService, sekarang berarti kita ubah menjadi kelas command, yaitu CreateNewCardCommand, AddProductToCartCommand, UpdateProductInCartCommand, dan RemoveProductFromCartCommand. Wew, kok jadi makin banyak class? Yup betul, memang makin jadi makin banyak class, karena tiap action kita representasikan sebagai Command Class.

Dalam implementasinya, biasanya agar standard cara kerjanya, dibuat Interface Command sebaga base class untuk semua Command Class, misal :

package com.idspring.commandpattern.service; import com.idspring.commandpattern.model.service.ServiceRequest; import reactor.core.publisher.Mono; /** * @author Eko Kurniawan Khannedy * @since 30/06/17 */ public interface Command<RESULT, REQUEST extends ServiceRequest> { Mono<RESULT> execute(REQUEST request); }

Setelah itu baru dibuat tiap Action Command, misal berikut salah satu contoh implementasi untuk AddProductToCartCommand.

package com.idspring.commandpattern.service.command; import com.idspring.commandpattern.entity.Cart; import com.idspring.commandpattern.model.service.AddProductToCartRequest; import com.idspring.commandpattern.service.Command; /** * @author Eko Kurniawan Khannedy * @since 30/06/17 */ public interface AddProductToCartCommand extends Command<Cart, AddProductToCartRequest> { }
package com.idspring.commandpattern.service.command.impl; import com.idspring.commandpattern.entity.Cart; import com.idspring.commandpattern.entity.CartItem; import com.idspring.commandpattern.entity.Product; import com.idspring.commandpattern.model.service.AddProductToCartRequest; import com.idspring.commandpattern.repository.CartRepository; import com.idspring.commandpattern.repository.ProductRepository; import com.idspring.commandpattern.service.AbstractCommand; import com.idspring.commandpattern.service.command.AddProductToCartCommand; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; /** * @author Eko Kurniawan Khannedy * @since 30/06/17 */ @Component public class AddProductToCartCommandImpl extends AbstractCommand<Cart, AddProductToCartRequest> implements AddProductToCartCommand { @Autowired private ProductRepository productRepository; @Autowired private CartRepository cartRepository; @Override public Mono<Cart> doExecute(AddProductToCartRequest request) { return Mono.zip( objects -> addOrUpdateProductInCart((Cart) objects[0], (Product) objects[1], request.getQuantity()), cartRepository.findById(request.getCartId()), productRepository.findById(request.getProductId()) ).flatMap(cart -> cartRepository.save(cart)); } private Cart addOrUpdateProductInCart(Cart cart, Product product, Integer quantity) { if (isCartContainProduct(cart, product)) { incrementProductQuantity(cart, product, quantity); } else { addNewProductToCart(cart, product, quantity); } return cart; } private boolean isCartContainProduct(Cart cart, Product product) { return cart.getItems().stream() .anyMatch(cartItem -> cartItem.getId().equals(product.getId())); } private void incrementProductQuantity(Cart cart, Product product, Integer quantity) { cart.getItems().stream() .filter(cartItem -> cartItem.getId().equals(product.getId())) .forEach(cartItem -> cartItem.setQuantity(cartItem.getQuantity() + quantity)); } private void addNewProductToCart(Cart cart, Product product, Integer quantity) { CartItem item = CartItem.builder() .id(product.getId()) .name(product.getName()) .price(product.getPrice()) .quantity(quantity) .build(); cart.getItems().add(item); } }

Benefit Menggunakan Command Pattern

Jika diperhatikan, memang jika menggunakan Command Pattern, class yang kita buat akan menjadi lebih banyak, Yang tadinya ada 1 CartService class, sekarang menjadi 4 CartCommand class.

Tapi menurut saya, lebih baik banyak class dan tiap class tidak terlalu banyak code, maksimal 100 baris. Dibandingkan dengan class sedikit, tapi tiap class lebih dari 1000 baris, itu sangat pusing untuk di maintain.

Secara singkat berikut beberapa keuntungan menggunakan Command Pattern.

  • Single responsibility principle, yup jika pernah dengar tentang konsep ini adalah, tiap class hanya bertanggung jawab melakukan satu kewajiban. Jika menggunakan Facade Pattern, maka secara tidak langsung kita meminta service class untuk melakukan lebih dari satu pekerjaan, contohnya di CartService sebelumnya, kita membebankan 4 pekerjaan ke satu class Service.
  • Mudah untuk dikembangkan, walaupun banyak programmer, untuk mengembangkan fitur jika menggunakan Command Pattern sangatlah mudah, karena hanya tinggal membaut Command Class baru. Dan tidak akan bentrok dengan Command Class lain.
  • Code gampang untuk di baca. Yup, karena tiap Command Class kemungkinan sedikit baris codenya, jadi akan mudah untuk dibaca dan dimaintain. Tidak mudah mess juga, karena kita tahu kalo semua method yang ada di class itu hanya digunakan oleh satu action.

Problem dengan Command Pattern

Banyaknya class menurut saya bukanlah problem, karena itu sebenernya hanya memecah yang tadinya 1 class 1000 baris, menjadi 10 class dengan 100 baris. Jadi intinya sama saja.

Lantas apa problem yang akan kita temui jika menggunakan Command Pattern? Salah satu problem-nya adalah; akan ada banyak duplicate code yang terjadi. Misal saja ada code yang sama di antar Command. Tapi kan mudah aja, buat saja Helper / Utilities Class jika ada code yang sama? Yup benar, tapi bagaimana jika yang membuat duplicate code nya adalah programmer yang berbeda? Dia membuat code, tapi ternyata dia tidak tahu kalo programmer lain sudah membuat code yang sama?

Jadi untuk duplicate code, harus ada static code analyzer nya di projectnya, misal menggunakan Sonar. Sehingga jika ada duplicate code, bisa dilakukan refactoring agar project tetap bagus.

Contoh Project

Berikut adalah salah satu contoh Spring Boot Project yang saya buat menggunakan Command Pattern untuk Service Layer nya.

https://github.com/khannedy/spring-command-pattern

Referensi