author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Contoh Composite Design Pattern
Wednesday May 17th, 2023 08:30 pm7 mins read
Contoh Composite Design Pattern
Source: freepik - company tree structure

Composite Design Pattern bentuknya seperti hierarki objek pada Tree Structures. Design pattern ini biasanya terdiri dari satu interface yang disebut Component yang menjadi abstraksi untuk beberapa class implementation. Beberapa implementation dari interface Component sebagai class tunggal yang disebut Leaf. Lalu beberapa implementation dari interface Component yang menampung beberapa class implementation dari Component yang disebut Composite. Kedua jenis implementation itu sama-sama mengimplementasi interface Component yang sama sehingga implementation Composite tadi bisa menampung baik Leaf maupun Composite. Implementation Composite tersebut bertugas menghandle kedua jenis implementation Component masing-masing sebagai sebuah hierarki Tree Stuctures. Jadi objek-objek Leaf yang ditampung pada Composite itu bisa dihandle masing-masing satu-persatu. Agak kompleks memang design pattern satu ini🤯. Gw aja mikirin contoh use case-nya aja perlu mikir keras😵.

Composite Design Pattern adalah Structural Design Pattern yang bertugas menyusun beberapa objek menjadi sebuah hierarki seperti pada Tree Structures dan masing-masing objek tersebut bisa dihandle seperti objek tunggal.

Composite Design Pattern

Use Case

Kita akan membuat beberapa objek seputar hierarki struktur militer. Hierarki tertinggi pada militer adalah Perwira dan Inspektur Sarjana yang menjadi atasan dari Bintara. Seorang Perwira juga bisa menjadi atasan dari Perwira lainnya, seperti Perwira tinggi yang menjadi atasan dari Perwira lain. Lalu logikanya adalah ketika atasan mendapatkan bonus, maka bawahan-bawahannya akan mendapatkan bonus yang sama tapi dibagi sama rata dengan sesama bawahan lainnya atau prorate. Lalu bawahan dari bawahannya juga akan mendapatkan bonus yang sama seperti atasannya langsung tapi juga dibagi rata dengan sesama rekannya secara prorate. Misalnya Perwira Kiki dapat bonus Rp1 miliiar. Perwira Kiki punya dua bawahan langsung, Perwira Elmi dan dr. Joni. Maka masing-masing mereka akan mendapatkan Rp500 juta. Lalu Perwira Elmi punya bawahan Bintara Andi, Bintara Budi, Bintara Coki, dan Bintara Dodi. Maka masing-masing mereka akan mendapatkan Rp125 juta. Kemudian dr. Joni juga memiliki bawahan Bintara Fiki, Bintara Gani, Bintara Heri, dan Bintara Indi. Maka masing-masing mereka juga akan mendapatkan Rp125 juta. Contoh hierarkinya adalah seperti berikut:

  • Perwira Kiki mendapatkan bonus Rp1 miliar;
    • Perwira Elmi mendapatkan bonus Rp500 juta;
      • Bintara Andi mendapatkan bonus Rp125 juta;
      • Bintara Budi mendapatkan bonus Rp125 juta;
      • Bintara Coki mendapatkan bonus Rp125 juta;
      • Bintara Dodi mendapatkan bonus Rp125 juta;
    • dr. Joni mendapatkan bonus Rp500 juta;
      • Bintara Fiki mendapatkan bonus Rp125 juta;
      • Bintara Gani mendapatkan bonus Rp125 juta;
      • Bintara Heri mendapatkan bonus Rp125 juta;
      • Bintara Indi mendapatkan bonus Rp125 juta;

Contoh code

Interface Military

public interface Military{
	void shareBonus(BigDecimal bonus);

	BigDecimal getBonus();

	default String print(){
		NumberFormat formatter = NumberFormat.getInstance(new Locale("id_ID"));
		return rank() + ' ' + name() + " (Rp" + formatter.format(getBonus()) + ") ";
	}

	String name();

	String rank();
}

Class Bintara

public class Bintara implements Military{
	private BigDecimal bonus;
	private final String name;

	public Bintara(String name){
		this.name = name;
	}

	@Override
	public void shareBonus(BigDecimal bonus){
		this.bonus = bonus;
	}

	@Override
	public BigDecimal getBonus(){
		return this.bonus;
	}

	@Override
	public String name(){
		return this.name;
	}

	@Override
	public String rank(){
		return "Bintara";
	}
}

Class Perwira

public class Perwira implements Military{
	private BigDecimal bonus;
	private final String name;

	public Perwira(String name){
		this.name = name;
	}

	@Override
	public BigDecimal getBonus(){
		return this.bonus;
	}

	@Override
	public String name(){
		return this.name;
	}

	@Override
	public String rank(){
		return "Perwira";
	}

	@Override
	public void shareBonus(BigDecimal bonus){
		this.bonus = bonus;
	}
}

Class UndergraduateInspector

public class UndergraduteInspector implements Military{
	private BigDecimal bonus;
	private final String name;
	private final String title;

	public UndergraduteInspector(String title, String name){
		this.name = name;
		this.title = title;
	}

	@Override
	public BigDecimal getBonus(){
		return this.bonus;
	}

	@Override
	public String name(){
		return this.name;
	}

	@Override
	public String rank(){
		return this.title;
	}

	@Override
	public void shareBonus(BigDecimal bonus){
		this.bonus = bonus;
	}
}

Contoh penggunaan

public static void main(String[] args){
	BigDecimal bonus = BigDecimal.valueOf(1_000_000_000);
	Military kiki = new Perwira("Kiki");
	kiki.shareBonus(bonus);
	System.out.println(kiki.print());
	Military elmi = new Perwira("Elmi");
	Military joni = new UndergraduteInspector("dr.", "Joni");
	List<Military> kikiSubordinates = Arrays.asList(elmi, joni);
	BigDecimal kikiSubordinateBonus = kiki.getBonus().divide(BigDecimal.valueOf(kikiSubordinates.size()), RoundingMode.HALF_EVEN);
	for(Military kikiSubordinate : kikiSubordinates){
		kikiSubordinate.shareBonus(kikiSubordinateBonus);
	}

	Military andi = new Bintara("Andi");
	Military budi = new Bintara("Budi");
	Military coki = new Bintara("Coki");
	Military dodi = new Bintara("Dodi");
	List<Military> elmiSubordinates = Arrays.asList(andi, budi, coki, dodi);
	BigDecimal elmiSubordinateBonus = elmi.getBonus().divide(BigDecimal.valueOf(elmiSubordinates.size()), RoundingMode.HALF_EVEN);
	System.out.println('\t' + elmi.print());
	for(Military elmiSubordinate : elmiSubordinates){
		elmiSubordinate.shareBonus(elmiSubordinateBonus);
		System.out.println("\t\t" + elmiSubordinate.print());
	}

	Military fiki = new Bintara("Fiki");
	Military gani = new Bintara("Gani");
	Military heri = new Bintara("Heri");
	Military indi = new Bintara("Indi");
	List<Military> joniSubordinates = Arrays.asList(fiki, gani, heri, indi);
	BigDecimal joniSubordinateBonus = joni.getBonus().divide(BigDecimal.valueOf(joniSubordinates.size()), RoundingMode.HALF_EVEN);
	System.out.println('\t' + joni.print());
	for(Military joniSubordinate : joniSubordinates){
		joniSubordinate.shareBonus(joniSubordinateBonus);
		System.out.println("\t\t" + joniSubordinate.print());
	}
}

Kita mendaftarkan masing-masing hierarki menggunakan List di client code. Kita juga menulis logic bonus dan print masing-masing hierarki di client code.

Masalah

Karena semuanya ditulis di client code, tentu ribet memaintain code seperti ini. Selain itu logic juga diekspos di client code, sehingga melanggar Single Responsibility Principle.

Solusi

Permasalahan di atas bisa diselesaikan menggunakan Composite Design Pattern😎. Untuk mempermudah pemahaman berikut ini Class Diagram dari use case ini:

Class Diagram Military Composite Design Pattern
Class Diagram Military Composite Design Pattern

Interface Military

public interface Military{
	void shareBonus(BigDecimal bonus);

	BigDecimal getBonus();

	default String printAll(){
		return print(0);
	}

	default String print(int tab){
		StringBuilder tabBuilder = new StringBuilder();
		for(int i = 0; i < tab; i++){
			tabBuilder.append('\t');
		}
		NumberFormat formatter = NumberFormat.getInstance(new Locale("id_ID"));
		return tabBuilder + rank() + ' ' + name() + " (Rp" + formatter.format(getBonus()) + ")\n";
	}

	String name();

	String rank();
}

Interface SuperiorMilitary

public interface SuperiorMilitary extends Military{
	void insertSubordinates(Military... militaries);

	List<Military> getSubordinates();

	@Override
	default String print(int tab){
		StringBuilder builder = new StringBuilder(Military.super.print(tab));
		List<Military> subordinates = getSubordinates();
		for(Military subordinate : subordinates){
			builder.append(subordinate.print(tab + 1));
		}
		return builder.toString();
	}

	@Override
	default void shareBonus(BigDecimal bonus){
		List<Military> subordinates = getSubordinates();
		BigDecimal finalBonus = bonus.divide(BigDecimal.valueOf(subordinates.isEmpty() ? 1 : subordinates.size()), RoundingMode.HALF_EVEN);
		for(Military subordinate : subordinates){
			subordinate.shareBonus(finalBonus);
		}
	}
}

Class Bintara

public class Bintara implements Military{
	private BigDecimal bonus;
	private final String name;

	public Bintara(String name){
		this.name = name;
	}

	@Override
	public void shareBonus(BigDecimal bonus){
		this.bonus = bonus;
	}

	@Override
	public BigDecimal getBonus(){
		return this.bonus;
	}

	@Override
	public String name(){
		return this.name;
	}

	@Override
	public String rank(){
		return "Bintara";
	}
}

Class Perwira

public class Perwira implements SuperiorMilitary{
	private BigDecimal bonus;
	private final String name;
	private final List<Military> subordinates = new ArrayList<>();

	public Perwira(String name){
		this.name = name;
	}

	@Override
	public BigDecimal getBonus(){
		return this.bonus;
	}

	@Override
	public String name(){
		return this.name;
	}

	@Override
	public String rank(){
		return "Perwira";
	}

	@Override
	public void insertSubordinates(Military... militaries){
		Collections.addAll(this.subordinates, militaries);
	}

	@Override
	public List<Military> getSubordinates(){
		return this.subordinates;
	}

	@Override
	public void shareBonus(BigDecimal bonus){
		this.bonus = bonus;
		SuperiorMilitary.super.shareBonus(bonus);
	}
}

Class UndergraduateInspector

public class UndergraduteInspector implements SuperiorMilitary{
	private BigDecimal bonus;
	private final String name;
	private final String title;
	private final List<Military> subordinates = new ArrayList<>();

	public UndergraduteInspector(String title, String name){
		this.name = name;
		this.title = title;
	}

	@Override
	public BigDecimal getBonus(){
		return this.bonus;
	}

	@Override
	public String name(){
		return this.name;
	}

	@Override
	public String rank(){
		return this.title;
	}

	@Override
	public void insertSubordinates(Military... militaries){
		Collections.addAll(this.subordinates, militaries);
	}

	@Override
	public List<Military> getSubordinates(){
		return this.subordinates;
	}

	@Override
	public void shareBonus(BigDecimal bonus){
		this.bonus = bonus;
		SuperiorMilitary.super.shareBonus(bonus);
	}
}

Contoh penggunaan

public static void main(String[] args){
	Military andi = new Bintara("Andi");
	Military budi = new Bintara("Budi");
	Military coki = new Bintara("Coki");
	Military dodi = new Bintara("Dodi");
	SuperiorMilitary elmi = new Perwira("Elmi");
	elmi.insertSubordinates(andi, budi, coki, dodi);

	Military fiki = new Bintara("Fiki");
	Military gani = new Bintara("Gani");
	Military heri = new Bintara("Heri");
	Military indi = new Bintara("Indi");
	SuperiorMilitary joni = new UndergraduteInspector("dr.", "Joni");
	joni.insertSubordinates(fiki, gani, heri, indi);

	SuperiorMilitary kiki = new Perwira("Kiki");
	kiki.insertSubordinates(elmi, joni);

	kiki.shareBonus(BigDecimal.valueOf(1_000_000_000));
	System.out.println(kiki.printAll());
}

Object Diagram dari design di atas jadi seperti berikut:

Object Diagram Military Composite Design Pattern
Object Diagram Military Composite Design Pattern

Class Bintara (Leaf) mengimplementasi interface Military (Component). Interface SuperiorMilitary (Composite) meng-extend interface Military. Class Perwira dan class UndergraduateInspector sama-sama mengimplementasi interface SuperiorMilitary. Keduanya juga sama-sama menampung list objek dari interface Military yang menjadi bawahannya langsung.

Beberapa contoh di internet ada yang membuat Component dan Composite disatukan. Tapi menurut gw itu melanggar Interface Segregation Principle karena di sini Component itu untuk abstraksi yang general, dan Composite untuk abstraksi spesifik yang bisa menampung beberapa Component. Selain itu juga melanggar Liskov Substitution Principle karena jika Component dan Composite disatukan, maka Component tersebut akan otomatis by default juga bisa menampung dan mendaftarkan Component lain. Padahal implementasi Component tersebut ada yang sifatnya objek tunggal, ga ada bawahan sama sekali. Ga semua implementasi Component itu bisa menampung dan mendaftarkan Component lain. Makanya di sini gw memisahkan antara interface Component dan Composite, yaitu Military dan SuperiorMilitary. Jadi Military khusus interface yang general, dan SuperiorMilitary jadi sub-interface dari Military yang spesifik bisa melakukan insertion Component.

Kenapa menggunakan Composite Design Pattern?

Dengan Composite Design Pattern penggunaannya di client code jadi lebih simple. Kita memiliki single implementation untuk bawahan dan grouped implementation untuk atasan dari abstraksi yang sama yang bisa digunakan berbarengan. Antar implementation tersebut interchangable, kita bisa register siapa bawahan siapa tanpa menampilkan kompleksitasnya ke client code. Kita hanya perlu register masing-masing bawahan ke atasannya. Pada contoh di atas, jika perwira Kiki juga memiliki bawahan perwira yang juga memiliki bawahan, kita tinggal register lagi implementasinya ke perwira Kiki, sisanya akan dihandle oleh masing-masing implementasi. Lalu untuk share bonus dan print hierarkinya kita hanya perlu eksekusi method pada Military interface itu saja. Kita tidak perlu mengeksploitasi semua logic-nya ke luar.

Verdict

Composite Design Pattern ini lumayan kompleks. Design pattern ini terdiri dari interface Component sebagai abstraksi paling umum, Leaf sebagai implementasi Component tunggal, dan Composite sebagai implementasi Component yang dapat menampung beberapa Component. Sejauh ini gw belum pernah menemukan kasus sekompleks ini dunia kerja. Ini juga gw agak susah mencari contoh kasus yang sederhana. Mungkin karena kekompleksan ini, jadi kasusnya cukup langka di dunis nyata😅.