author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Contoh Decorator Design Pattern
Sunday Aug 8th, 2021 06:37 pm6 mins read
Java, Tips & Tutorial, Design Pattern
Contoh Decorator Design Pattern
Source: PNGEgg - House painter and decorator Painting Facade

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 pattern yang bisa menambahkan behavior baru kepada objek originalnya secara dinamis dengan menempatkannya ke dalam special wrapper objects.

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){
		switch(item){
			case "suppressor":
				ak47 = new AKFortySeven(ak47.getAccuracy() - 2, ak47.getDamage() - 1, ak47.getRange(),
						ak47.getMagazineCapacity(), true);
				break;
			case "extendedRapidMagazine":
				ak47 = new AKFortySeven(ak47.getAccuracy(), ak47.getDamage() + 2, ak47.getRange(),
						ak47.getMagazineCapacity() * 2, ak47.isSilence());
				break;
			case "longRangeScope":
				ak47 = new AKFortySeven(ak47.getAccuracy() + 3, ak47.getDamage(), ak47.getRange() + 3,
						ak47.getMagazineCapacity(), ak47.isSilence());
				break;
		}
	}
	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());
}

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){
		switch(item){
			case "suppressor":
				ak47 = new SuppressorWeapon(ak47);
				break;
			case "extendedRapidMagazine":
				ak47 = new ExtendedRapidMagazineWeapon(ak47);
				break;
			case "longRangeScope":
				ak47 = new LongRangeScopeWeapon(ak47);
				break;
		}
	}
	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());
}

Pada code di atas, kita tinggal membungkus objek Weapon pada special objek attachment yang diinginkan. Special objek tersebut juga mengimplementasi interface Weapon. Sekarang setiap penambahan attachments kita tinggal bikin beberapa special wrapper objects lainnya, seperti Grip atau Long Barrel untuk melakukan dekorasi terhadap objek originalnya. Kalau ada perubahan pun tinggal ubah di masing-masing special objeknya aja.

Improvement

Bukankah pada post sebelumnya gw pernah bilang switch/case itu bad practice? Apakah ada cara yang lebih praktis? Tentu saja ada😎. Gw menggunakan switch/case pada kasus di atas agar lebih mudah dipahami oleh orang-orang pada umumnya. Agar lebih elegan, kita bisa menggunakan hashing map algorithm seperti berikut:

private static final Map<String, UnaryOperator<Weapon>> map = Map.of(
		"suppressor", SuppressorWeapon::new,
		"extendedRapidMagazine", ExtendedRapidMagazineWeapon::new,
		"longRangeScope", LongRangeScopeWeapon::new);

public static void main(String[] args){
	List<String> additionalItems = Arrays.asList("suppressor", "extendedRapidMagazine", "longRangeScope");
	Weapon ak47 = new AK47(8, 9, 6, 30, false);
	for(String item : additionalItems){
		UnaryOperator<Weapon> weaponItem = map.get(item);
		ak47 = weaponItem.apply(ak47);
	}
}

Kita menampung objek tersebut dengan constant Map. Value-nya berupa UnaryOperator agar bisa menampung parameter dan return objek Weapon. Kita bisa menggunakan lambda style disini. Setelah di-get value-nya, tinggal apply function tersebut menggunakan objek Weapon sebelumnya. Oh ya, kode Map seperti di atas hanya bisa dilakukan jika menggunakan Java 9 ke atas. Untuk pengguna Java 8 bisa membuat Map mirip seperti di atas dengan membaca artikel gw sebelumnya tentang Immutable Collections.

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, bukan behavior 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. 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.unmodfiableList(), Collections.unmodfiableSet(), dan Collections.unmodfiableMap() merupakan decorator pattern yang bertugas mengubah mutable List, Set, maupun Map menjadi immutable.