Dengan Decorator Pattern kita bisa menambah behavior baru kepada objek originalnya sebanyak mungkin. Sehingga behavior-behavior tadi membentuk struktur baru berdasarkan behavior-behavior sebelumnya.
Decorator Design Pattern merupakan Structural Design Pattern yang bisa menambahkan behavior baru kepada objek originalnya dengan membungkusnya ke dalam special wrapper objects dengan tipe yang sama.
Design Pattern
Use Case
Daripada pusing-pusing langsung liat use case berikut aja😵. Di sini gw akan membuat use case berdasarkan game action/shooting seperti Call of Duty, Battlefield, atau Far Cry. Yaitu algoritma senjata beserta attachment-attachment yang bisa mengubah behavior senjata tersebut. Requirement-nya kurang lebih seperti berikut:
- Kita butuh object Weapon yang menampung struktur senjata tersebut seperti akurasi, damage, range, kapasitas amunisi, dan peredam;
- Object weapon tersebut strukturnya bisa berubah berdasarkan attachment yang ditambahkan oleh user;
- Attachment-nya berupa suppressor, extended rapid magazine dan long range scope;
- Setiap penambahan suppressor, statistik akurasi berkurang 2 point, damage berkurang 1 point, dan peredamnya aktif;
- Setiap penambahan extended rapid magazine, statistik damage bertambah 1 point, dan kapasitas peluru bertambah 2 kali lipat;
- Setiap penambahan long range scope, statistik akurasi bertambah 3 point, dan range bertambah 3 point;
- User dapat memberikan request lebih dari satu attachments pada setiap Weapon untuk digabungkan;
- Misalkan user memberikan attachments suppressor dan long range scope saja, maka struktur objek tersebut hanya berubah sesuai algoritma pada suppressor dan long range scope saja;
Contoh Code
Kita coba dengan membuat Weapon AK47 yang awalnya dibuat dengan struktur sebagai berikut:
- akurasi:8
- damage:9
- kapasitas amunisi: 30
- range: 6
- silenced: false
User kemudian memberikan request additional Items berupa sebuah Suppressor, Extended Rapid Magazine, dan Long Range Scope sekaligus. Code yang akan dihasilkan adalah final Weapon dengan struktur sebagai berikut:
- akurasi:9
- damage:10
- kapasitas amunisi: 60
- range: 9
- silenced: true
Weapon Interface
public interface Weapon{
int getAccuracy();
int getDamage();
int getRange();
int getMagazineCapacity();
boolean isSilence();
}
AKFortySeven Class
public class AKFortySeven implements Weapon{
private final int accuracy;
private final int damage;
private final int range;
private final int magazineCapacity;
private final boolean silence;
public AKFortySeven(int accuracy, int damage, int range, int magazineCapacity, boolean silence){
this.accuracy = accuracy;
this.damage = damage;
this.range = range;
this.magazineCapacity = magazineCapacity;
this.silence = silence;
}
@Override
public int getAccuracy(){
return accuracy;
}
@Override
public int getDamage(){
return damage;
}
@Override
public int getRange(){
return range;
}
@Override
public boolean isSilence(){
return silence;
}
@Override
public int getMagazineCapacity(){
return magazineCapacity;
}
}
Contoh penggunaan
public static void main(String[] args){
List<String> additionalItems = Arrays.asList("suppressor", "extendedRapidMagazine", "longRangeScope");
Weapon ak47 = new AKFortySeven(8, 9, 6, 30, false);
for(String item : additionalItems){
ak47 = installAttachment(ak47, item);
}
System.out.println("ak47.getAccuracy() = " + ak47.getAccuracy());
System.out.println("ak47.isSilence() = " + ak47.isSilence());
System.out.println("ak47.getDamage() = " + ak47.getDamage());
System.out.println("ak47.getRange() = " + ak47.getRange());
System.out.println("ak47.getMagazineCapacity() = " + ak47.getMagazineCapacity());
}
private static Weapon installAttachment(Weapon weapon, String attachment){
if("suppressor".equalsIgnoreCase(attachment)){
return new AKFortySeven(ak47.getAccuracy() - 2, ak47.getDamage() - 1, ak47.getRange(),
ak47.getMagazineCapacity(), true);
}
if("extendedRapidMagazine".equalsIgnoreCase(attachment)){
return new AKFortySeven(ak47.getAccuracy(), ak47.getDamage() + 2, ak47.getRange(),
ak47.getMagazineCapacity() * 2, ak47.isSilence());
}
if("longRangeScope".equalsIgnoreCase(attachment)){
return new AKFortySeven(ak47.getAccuracy() + 3, ak47.getDamage(), ak47.getRange() + 3,
ak47.getMagazineCapacity(), ak47.isSilence());
}
return weapon;
}
Masalah
Code di atas sudah berjalan sesuai requirement. Namun kita akan menemukan kendala ketika jenis attachment tersebut terus bertambah, misalnya ingin menambahkan attachment Grip, Long Barrel, dan lainnya. Kita akan melakukan banyak perubahan pada code yang sudah ada. Begitu juga misalkan ada revisi point, perubahan juga akan dilakukan pada class yang sama. Maintenance code jadi lebih sulit.
Solusi
Sayangnya kita tidak dapat menggunakan behavioral pattern di sini seperti Strategy ataupun State Pattern, karena problemnya bukan di behavior, tapi struktur object tersebut. Sekarang kita coba refactor menggunakan Decorator Pattern😎.
Untuk Weapon Interface dan AKFortySeven class tidak perlu perubahan pada code sebelumnya. Kita hanya butuh menambahkan beberapa special wrapper objects untuk menampung attachments tadi.
SuppressorWeapon Class
public class SuppressorWeapon implements Weapon{
private final Weapon weapon;
public SuppressorWeapon(Weapon weapon){
this.weapon = weapon;
}
@Override
public int getAccuracy(){
return weapon.getAccuracy() - 2;
}
@Override
public int getDamage(){
return weapon.getDamage() - 1;
}
@Override
public int getRange(){
return weapon.getRange();
}
@Override
public boolean isSilence(){
return true;
}
@Override
public int getMagazineCapacity(){
return weapon.getMagazineCapacity();
}
}
ExtendedRapidMagazineWeapon Class
public class ExtendedRapidMagazineWeapon implements Weapon{
private final Weapon weapon;
public ExtendedRapidMagazineWeapon(Weapon weapon){
this.weapon = weapon;
}
@Override
public int getAccuracy(){
return weapon.getAccuracy();
}
@Override
public int getDamage(){
return weapon.getDamage() + 2;
}
@Override
public int getRange(){
return weapon.getRange();
}
@Override
public boolean isSilence(){
return weapon.isSilence();
}
@Override
public int getMagazineCapacity(){
return weapon.getMagazineCapacity() * 2;
}
}
LongRangeScopeWeapon Class
public class LongRangeScopeWeapon implements Weapon{
private final Weapon weapon;
public LongRangeScopeWeapon(Weapon weapon){
this.weapon = weapon;
}
@Override
public int getAccuracy(){
return weapon.getAccuracy() + 3;
}
@Override
public int getDamage(){
return weapon.getDamage();
}
@Override
public int getRange(){
return weapon.getRange() + 3;
}
@Override
public boolean isSilence(){
return weapon.isSilence();
}
@Override
public int getMagazineCapacity(){
return weapon.getMagazineCapacity();
}
}
Contoh penggunaan
public static void main(String[] args){
List<String> additionalItems = Arrays.asList("suppressor", "extendedRapidMagazine", "longRangeScope");
Weapon ak47 = new AKFortySeven(8, 9, 6, 30, false);
for(String item : additionalItems){
ak47 = installAttachment(ak47, item);
}
System.out.println("ak47.getAccuracy() = " + ak47.getAccuracy());
System.out.println("ak47.isSilence() = " + ak47.isSilence());
System.out.println("ak47.getDamage() = " + ak47.getDamage());
System.out.println("ak47.getRange() = " + ak47.getRange());
System.out.println("ak47.getMagazineCapacity() = " + ak47.getMagazineCapacity());
}
private static Weapon installAttachment(Weapon weapon, String attachment){
if("suppressor".equalsIgnoreCase(attachment)){
return new SuppressorWeapon(weapon);
}
if("extendedRapidMagazine".equalsIgnoreCase(attachment)){
return new ExtendedRapidMagazineWeapon(weapon);
}
if("longRangeScope".equalsIgnoreCase(attachment)){
return new LongRangeScopeWeapon(weapon);
}
return weapon;
}
Pada code di atas, kita tinggal membungkus objek Weapon pada special objek attachment yang diinginkan. Special objek tersebut juga mengimplementasi interface Weapon. Jadi misalkan kita punya objek Weapon AK47, lalu kita ingin memasangkan attachment suppressor sehingga akan terjadi adjustment behavior pada objek, maka cukup bungkus objek AK47 tersebut dengan Decorator SuppressorWeapon. Setiap penambahan attachments kita tinggal bikin beberapa special wrapper objects lainnya, seperti Fore Grip atau Long Barrel untuk melakukan dekorasi terhadap objek originalnya. Contohnya bisa seperti berikut:
Weapon ak47 = new AKFortySeven(8, 9, 6, 30, false);
Weapon longBarrelAk47 = new LongBarrelWeapon(ak47);
Weapon foreGripAk47 = new ForeGripWeapon(ak47);
Weapon foreGripAndLongBarrelAk47 = new ForeGripWeapon(longBarrelAk47);
Kalau ada perubahan pun tinggal ubah di masing-masing special objeknya aja.
Kenapa menggunakan Decorator Pattern?
Decorator pattern digunakan ketika ingin menambahkan behavior baru pada struktur objek sebelumnya tanpa mengubah struktur pada objek aslinya. Dengan Decorator Pattern, user cukup membungkus objek original ke dalam spesial objek seperti Suppressor, Extended Rapid Magazine dan Long Range Scope di atas tanpa harus capek-capek bikin objek baru dari awal tiap melakukan perubahan.
Verdict
Decorator Pattern merupakan structural pattern karena dia bertugas mengubah struktur objek lewat special object wrapper yang mengimplementasi objek yang sama. Dinamakan "special object" karena sebenarnya itu bukan objek biasa, tapi hanya sebagai wrapper dari abstrak sejenis. Disini kita menerapkan Dependency Injection Principle. Gw sendiri sebenarnya sering banget menggunakan design pattern ini, tapi bukan gw yang buat, melainkan menggunakan Decorator Pattern yang sudah ada pada Java😁. Yaitu ketika menggunakan Immutable Collections sebagai constant. Collections.unmodifiableList(), Collections.unmodifiableSet(), dan Collections.unmodifiableMap() merupakan decorator pattern yang bertugas mengubah behavior mutable List, Set, maupun Map menjadi immutable.