Terakhir gw bikin tulisan tentang design pattern sekitar 1 tahun yang lalu, abis itu ga gw lanjutin dan mulai menulis topik lainnya. Bukannya apa-apa, tapi nyari contoh real use case yang gampang dipahami itu susah😅. Kadang contoh yang sering ditemui saat googling itu terlalu kompleks untuk dipahami, atau terlalu sederhana sehingga poinnya jadi ga dapet. Makanya gw inisiatif bikin tulisan sendiri dan nyari use case sendiri sambil berlatih menulis. Sampai akhirnya sekitar seminggu yang lalu gw baru ngeh kalau ObjectMapper di library Jackson itu menggunakan design pattern Prototype. Gw jadi terinspirasi lagi buat bikin tulisan design pattern Prototype dan ngelanjutin beberapa design pattern yang belum sempat gw tulis. Prototype Design Pattern ditandai dengan adanya sebuah method pada objek untuk melakukan trigger duplikasi.
Prototype Design Pattern adalah Creational Design Pattern yang membuat sebuah objek bisa menduplikasi objek itu sendiri tanpa harus bergantung terhadap objek originalnya.
Design Pattern
Use Case
Kita mau membuat Generator untuk generate serial number yang bisa digunakan untuk generate order number, product code, ataupun invoice number. Konfigurasi Generator tersebut bisa diubah-ubah secara fleksibel sesuai kebutuhan serial number yang diinginkan. Btw, ini hanya contoh ya, untuk algoritma bikin serial number yang sesungguhnya tentu ga sesimple ini😅.
Contoh Code
Interface Generator
public interface Generator{
Generator withPrefix(String prefix);
Generator withNumberLengthInEachBracketNumber(int numberLengthInEachBracketNumber);
Generator withTotalBracketNumber(int totalBracketNumber);
Generator withDelimiter(String delimiter);
Generator withSuffix(String suffix);
String generate();
}
Class SerialNumberGenerator
public class SerialNumberGenerator implements Generator{
private String prefix;
private int numberLengthInEachBracketNumber;
private int totalBracketNumber;
private String delimiter;
private String suffix;
public SerialNumberGenerator(){
this.totalBracketNumber = 1;
this.numberLengthInEachBracketNumber = 3;
this.delimiter = "-";
this.suffix = "";
this.prefix = "";
}
@Override
public Generator withPrefix(String prefix){
if(prefix == null){
return this;
}
this.prefix = prefix;
return this;
}
@Override
public Generator withNumberLengthInEachBracketNumber(int numberLengthInEachBracketNumber){
if(numberLengthInEachBracketNumber <= 0){
return this;
}
this.numberLengthInEachBracketNumber = numberLengthInEachBracketNumber;
return this;
}
@Override
public Generator withTotalBracketNumber(int totalBracketNumber){
if(totalBracketNumber <= 0){
return this;
}
this.totalBracketNumber = totalBracketNumber;
return this;
}
@Override
public Generator withDelimiter(String delimiter){
if(delimiter == null){
return this;
}
this.delimiter = delimiter;
return this;
}
@Override
public Generator withSuffix(String suffix){
if(suffix == null){
return this;
}
this.suffix = suffix;
return this;
}
@Override
public String generate(){
StringJoiner joiner = new StringJoiner(this.delimiter);
if(!this.prefix.isEmpty()){
joiner.add(this.prefix);
}
for(int i = 0; i < this.totalBracketNumber; i++){
StringBuilder builder = new StringBuilder();
for(int j = 0; j < this.numberLengthInEachBracketNumber; j++){
builder.append(ThreadLocalRandom.current().nextInt(1, 10));
}
joiner.add(builder);
}
if(!this.suffix.isEmpty()){
joiner.add(this.suffix);
}
return joiner.toString();
}
}
Contoh penggunaan
public static void main(String[] args){
Generator prdGenerator = new SerialNumberGenerator()
.withDelimiter("-")
.withPrefix("prd")
.withSuffix("xxx")
.withNumberLengthInEachBracketNumber(5)
.withTotalBracketNumber(2);
Generator ordGenerator = new SerialNumberGenerator()
.withDelimiter("-")
.withPrefix("ord")
.withSuffix("xxx")
.withNumberLengthInEachBracketNumber(5)
.withTotalBracketNumber(3);
String prdGenerated = prdGenerator.generate();
System.out.println("result = " + prdGenerated);
String ordGenerated = ordGenerator.generate();
System.out.println("result = " + ordGenerated);
String prdGenerated2 = prdGenerator.generate();
System.out.println("result = " + prdGenerated2);
String ordGenerated2 = ordGenerator.generate();
System.out.println("result = " + ordGenerated2);
}
Pada code di atas, kita membuat beberapa konfigurasi untuk generation seperti delimiter, prefix, suffix, total bracket number, dan number length in each bracket. Lalu kita menyediakan method generate untuk generate serial number sesuai konfigurasi yang ditulis.
Masalah
Kita harus menulis ulang konfigurasi yang mirip untuk generate product code dan order number. Perbedaannya hanya terletak pada prefix dan total bracket number saja. Suffix, delimiter, dan number length in each bracket sama persis dengan konfigurasi product code. Misalkan konfigurasi yang mirip itu digunakan di banyak tempat tentu agak ribet. Sebenarnya bisa saja konfigurasi pada product code dimutasi lalu objek yang sama dipakai untuk generate order number. Tapi itu melanggar single responsibility principle. Generator untuk product code jadi ga bisa dipakai beberapa kali kalau konfigurasinya dimutasi. Alternatif lainnya bisa saja kita rebuild objek tersebut dengan objek baru lalu state dari objek originalnya kita pakai pada objek baru yang dibuat. Tapi masalahnya dengan cara seperti itu, kita rebuild-nya di client code sehingga orang yang menggunakan generator tersebut harus repot melakukan rebuild objek. Selain itu, cara tersebut artinya membuat kita harus mengekspoitasi private field dari generator ke luar class. Padahal dalam hal ini kita ga perlu megeksploitasi private field generator ke luar entah itu dengan menjadikan field-nya public maupun menggunakan getters, karena private field tersebut hanya dibutuhkan oleh method generate saja. Untuk itu Prototype bisa jadi solusi dalam kasus ini.
Solusi
Kita akan memodifikasi Generator di atas seperti berikut untuk mengimplementasi Prototype Design Pattern😎.
Interface Generator
public interface Generator{
Generator withPrefix(String prefix);
Generator withNumberLengthInEachBracketNumber(int numberLengthInEachBracketNumber);
Generator withTotalBracketNumber(int totalBracketNumber);
Generator withDelimiter(String delimiter);
Generator withSuffix(String suffix);
String generate();
Generator copy();
}
Class SerialNumberGenerator
public class SerialNumberGenerator implements Generator{
private String prefix;
private int numberLengthInEachBracketNumber;
private int totalBracketNumber;
private String delimiter;
private String suffix;
public SerialNumberGenerator(){
this.totalBracketNumber = 1;
this.numberLengthInEachBracketNumber = 3;
this.delimiter = "-";
this.suffix = "";
this.prefix = "";
}
private SerialNumberGenerator(SerialNumberGenerator generator){
this();
this.prefix = generator.prefix;
this.numberLengthInEachBracketNumber = generator.numberLengthInEachBracketNumber;
this.totalBracketNumber = generator.totalBracketNumber;
this.delimiter = generator.delimiter;
this.suffix = generator.suffix;
}
@Override
public Generator withPrefix(String prefix){
if(prefix == null){
return this;
}
this.prefix = prefix;
return this;
}
@Override
public Generator withNumberLengthInEachBracketNumber(int numberLengthInEachBracketNumber){
if(numberLengthInEachBracketNumber <= 0){
return this;
}
this.numberLengthInEachBracketNumber = numberLengthInEachBracketNumber;
return this;
}
@Override
public Generator withTotalBracketNumber(int totalBracketNumber){
if(totalBracketNumber <= 0){
return this;
}
this.totalBracketNumber = totalBracketNumber;
return this;
}
@Override
public Generator withDelimiter(String delimiter){
if(delimiter == null){
return this;
}
this.delimiter = delimiter;
return this;
}
@Override
public Generator withSuffix(String suffix){
if(suffix == null){
return this;
}
this.suffix = suffix;
return this;
}
@Override
public String generate(){
StringJoiner joiner = new StringJoiner(this.delimiter);
if(!this.prefix.isEmpty()){
joiner.add(this.prefix);
}
for(int i = 0; i < this.totalBracketNumber; i++){
StringBuilder builder = new StringBuilder();
for(int j = 0; j < this.numberLengthInEachBracketNumber; j++){
builder.append(ThreadLocalRandom.current().nextInt(1, 10));
}
joiner.add(builder);
}
if(!this.suffix.isEmpty()){
joiner.add(this.suffix);
}
return joiner.toString();
}
@Override
public Generator copy(){
return new SerialNumberGenerator(this);
}
}
Contoh penggunaan
public static void main(String[] args){
Generator prdGenerator = new SerialNumberGenerator()
.withDelimiter("-")
.withPrefix("prd")
.withSuffix("xxx")
.withNumberLengthInEachBracketNumber(5)
.withTotalBracketNumber(2)
;
Generator ordGenerator = prdGenerator.copy()
.withPrefix("ord")
.withTotalBracketNumber(3);
String prdGenerated = prdGenerator.generate();
System.out.println("result = " + prdGenerated);
String ordGenerated = ordGenerator.generate();
System.out.println("result = " + ordGenerated);
String prdGenerated2 = prdGenerator.generate();
System.out.println("result = " + prdGenerated2);
String ordGenerated2 = ordGenerator.generate();
System.out.println("result = " + ordGenerated2);
}
Sekarang kita menambahkan method copy() pada Generator untuk membuat objek Generator bisa menduplikasi dirinya sendiri menjadi objek baru tanpa ada dependency ke objek original. Kita bisa reuse konfigurasi untuk generate product code dengan menduplikasi objek dan melakukan sedikit modifikasi konfigurasi agar bisa digunakan untuk generate order number. Misalkan konfigurasi tersebut juga digunakan untuk generate serial number lainnya yang juga mirip, maka tinggal duplikasi saja objeknya tanpa mengganggu objek original. Objek original tetap bekerja seperti sebelumnya secara independen. Code kita jadi selaras dengan Single Responsibility Principle.
Prototype Manager
Misalkan konfigurasi suffix, delimiter, dan number length in each bracket itu sangat common dan sering dipakai untuk beberapa jenis generator, maka kita bisa membuat Prototype Manager seperti berikut.
Prototype Manager in SerialNumberGenerator
private static final Generator COMMON = new SerialNumberGenerator()
.withDelimiter("-")
.withNumberLengthInEachBracketNumber(5)
.withSuffix("xxx");
public static Generator getCommonGenerator(){
return COMMON.copy();
}
Contoh penggunaan
public static void main(String[] args){
Generator prdGenerator = SerialNumberGenerator.getCommonGenerator()
.withPrefix("prd")
.withTotalBracketNumber(2);
Generator ordGenerator = SerialNumberGenerator.getCommonGenerator()
.withPrefix("ord")
.withTotalBracketNumber(3);
Generator invGenerator = SerialNumberGenerator.getCommonGenerator()
.withPrefix("inv")
.withTotalBracketNumber(5);
String prdGenerated = prdGenerator.generate();
System.out.println("result = " + prdGenerated);
String ordGenerated = ordGenerator.generate();
System.out.println("result = " + ordGenerated);
String invGenerated = invGenerator.generate();
System.out.println("result = " + invGenerated);
String prdGenerated2 = prdGenerator.generate();
System.out.println("result = " + prdGenerated2);
String ordGenerated2 = ordGenerator.generate();
System.out.println("result = " + ordGenerated2);
String invGenerated2 = invGenerator.generate();
System.out.println("result = " + invGenerated2);
}
Dengan Prototype Manager kita melakukan cache common konfigurasi tersebut dan menggunakan method getCommonGenerator() yang bertugas menduplikasi common konfigurasi tersebut untuk kemudian di-reuse.
Kenapa menggunakan Prototype Design Pattern?
Prototype Design Pattern digunakan ketika ada kasus saat kita membutuhkan beberapa objek yang common yang bisa di-reuse tanpa dependency antar objek. Jadi ketika kita menduplikasi objek tersebut, kita tinggal mengeksekusi method untuk duplikasi tanpa harus menulis ulang objek dari awal, atau mengeksploitasi private field ke luar tanpa urgensi, ataupun melakukan duplikasi di client code. Oh ya, sebenarnya bisa juga kita menggunakan constructor langsung dengan menjadikannya public constructor untuk duplikasi objek tanpa harus bikin method copy(). Hanya saja, dengan begitu kita perlu bikin null-checking di dalam constructor tersebut karena bisa saja user mengirimkan value null ke constructor dan rentan terhadap exception yang tidak diharapkan. Code di constructor jadi agak rumit dong. Sedangkan kalau menggunakan method copy() lebih simple karena tidak perlu pakai keyword new
dan lebih aman karena menggunakan instance method dari objek original yang dibuat langsung tanpa passing value apapun ke parameter argument method tersebut.
Verdict
Prototype Design Pattern termasuk design pattern yang cukup simple sehingga cukup mudah dipahami. Dengan Prototype Design Pattern client code jadi lebih simple dan bersih karena kita hanya butuh eksekusi satu method saja untuk duplikasi objek. Method tersebut biasanya dinamai dengan nama seperti copy(), cloning(), atau duplicate() tergantung selera masing-masing. Kita juga berhasil menerapkan Single Responsibility Priniciple dengan design pattern ini. Contoh library yang menggunakan design pattern ini adalah library Jackson Databind pada class ObjectMapper. Kita bisa menduplikasi konfigurasi untuk mapping json atau object dengan mengeksekusi method copy().