author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
SOLID: Prinsip Open for Extension, Close for Modification
Mon. Nov 2nd, 2020 12:40 PM5 mins read
SOLID: Prinsip Open for Extension, Close for Modification
Source: Printables.space - Printable Open and Closed Signs

Secara definisi:

Software entities should be open for extension, but closed for modification.

Robert C. Martin

Disini bisnis logic dibungkus menjadi entitas yang bisa di-extend sebanyak apapun tanpa banyak perubahan di entity utama. Disini benefit dari abstraksi sangat terasa. Open-Close Principle ini bisa diterapkan menggunakan Strategy Pattern dan Factory Pattern.

Contoh kasusnya pada pengelompokkan total buku dan jumlah harga buku seperti berikut:

  1. Jika grouping dari request adalah "category", maka:
    • Lakukan query penghitungan total buku berdasarkan filter dari request dan group by category;
    • Lakukan query penghitungan jumlah harga buku berdasarkan filter dari request dan group by category;
    • Lanjutkan logic khusus berdasarkan category lainnya;
  2. Jika grouping dari request adalah "dateReleased", maka:
    • Lakukan query penghitungan total buku berdasarkan filter dari request dan group by dateReleased;
    • Lakukan query penghitungan jumlah harga buku berdasarkan filter dari request dan group by dateReleased;
    • Lanjutkan logic khusus berdasarkan dateReleased lainnya;
  3. Jika grouping dari request adalah "author", maka:
    • Lakukan query penghitungan total buku berdasarkan filter dari request dan group by author;
    • Lakukan query penghitungan jumlah harga buku berdasarkan filter dari request dan group by author;
    • Lanjutkan logic khusus berdasarkan author lainnya;
  4. Print nama group, total buku, dan jumlah harga buku;

Kira-kira design code awalnya seperti ini:

Class BookSummaryService

public class BookSummaryService{
	private final BookRepo bookRepo;

	public BookSummaryService(BookRepo bookRepo){
		this.bookRepo = bookRepo;
	}

	public void printSummary(BookReq req) throws Exception{
		BookSummary books;
		if("category".equals(req.getGrouping())){
			long total = bookRepo.countBookGroupByCategory(req);
			long sum = bookRepo.sumBookPriceGroupByCategory(req);
			//another huge logic about book group by category
			//...
			//
			books = BookSummary.builder()
					.groupName("By Category")
					.sumBookPrice(sum)
					.totalBook(total)
					.build();
		} else if("dateReleased".equals(req.getGrouping())){
			long total = bookRepo.countBookGroupByDateReleased(req);
			long sum = bookRepo.sumBookPriceGroupByDateReleased(req);
			//another huge logic about book group by dateReleased
			//...
			//
			books = BookSummary.builder()
					.groupName("By Release Date")
					.sumBookPrice(sum)
					.totalBook(total)
					.build();
		} else if("author".equals(req.getGrouping())){
			long total = bookRepo.countBookGroupByAuthor(req);
			long sum = bookRepo.sumBookPriceGroupByAuthor(req);
			//another huge logic about book group by author
			//...
			//
			books = BookSummary.builder()
					.groupName("By Author")
					.sumBookPrice(sum)
					.totalBook(total)
					.build();
		} else {
			throw new Exception("No grouping found");
		}

		System.out.println("groupName = " + books.getGroupName());
		System.out.println("total = " + books.getTotalBook());
		System.out.println("sum price = " + books.getSumBookPrice());
	}
}

Code di atas melanggar Open-Close Principle karena setiap penambahan grouping akan selalu terjadi perubahan pada entitas utama. Tentu saja itu akan sangat ribet, susah di-maintain banyak orang, sulit dibaca, dan rawan conflict.

Solusinya bisa dengan menggunakan Strategy Pattern seperti berikut:

Abstract BookGroupStrategy

public interface BookGroupStrategy{
	BookSummary getBookSummary(BookReq req);
}

Concrete Class BookSummaryByCategory

public static class BookSummaryByCategory implements BookGroupStrategy{
	private final BookRepo bookRepo;

	public BookSummaryByCategory(BookRepo bookRepo){
		this.bookRepo = bookRepo;
	}

	@Override
	public BookSummary getBookSummary(BookReq req){
		long total = bookRepo.countBookGroupByCategory(req);
		long sum = bookRepo.sumBookPriceGroupByCategory(req);
		//another huge logic about book group by category
		//...
		//
		return BookSummary.builder()
				.groupName("By Category")
				.sumBookPrice(sum)
				.totalBook(total)
				.build();
	}
}

Concrete Class BookSummaryByAuthor

public static class BookSummaryByAuthor implements BookGroupStrategy{
	private final BookRepo bookRepo;

	public BookSummaryByAuthor(BookRepo bookRepo){
		this.bookRepo = bookRepo;
	}

	@Override
	public BookSummary getBookSummary(BookReq req){
		long total = bookRepo.countBookGroupByAuthor(req);
		long sum = bookRepo.sumBookPriceGroupByAuthor(req);
		//another huge logic about book group by author
		//...
		//
		return BookSummary.builder()
				.groupName("By Author")
				.sumBookPrice(sum)
				.totalBook(total)
				.build();
	}
}

Concrete Class BookSummaryByReleasedDate

public static class BookSummaryByReleasedDate implements BookGroupStrategy{
	private final BookRepo bookRepo;

	public BookSummaryByReleasedDate(BookRepo bookRepo){
		this.bookRepo = bookRepo;
	}

	@Override
	public BookSummary getBookSummary(BookReq req){
		long total = bookRepo.countBookGroupByDateReleased(req);
		long sum = bookRepo.sumBookPriceGroupByDateReleased(req);
		//another huge logic about book group by dateReleased
		//...
		//
		return BookSummary.builder()
				.groupName("By Release Date")
				.sumBookPrice(sum)
				.totalBook(total)
				.build();
	}
}

Class BookSummaryService

public class BookSummaryService{
	private final BookRepo bookRepo;

	public BookSummaryService(BookRepo bookRepo){
		this.bookRepo = bookRepo;
	}

	public void printSummary(BookReq req) throws Exception{
		BookGroupStrategy strategy;
		if("category".equals(req.getGrouping())){
			strategy = new BookSummaryByCategory(bookRepo);
		} else if("dateReleased".equals(req.getGrouping())){
			strategy = new BookSummaryByReleasedDate(bookRepo);
		} else if("author".equals(req.getGrouping())){
			strategy = new BookSummaryByAuthor(bookRepo);
		} else {
			throw new Exception("No grouping found");
		}

		BookSummary books = strategy.getBookSummary(req);
		System.out.println("groupName = " + books.getGroupName());
		System.out.println("total = " + books.getTotalBook());
		System.out.println("sum price = " + books.getSumBookPrice());
	}
}

Dengan Strategy Pattern, code di atas jadi lebih gampang di-maintanance. Tiap ada penambahan logic grouping tinggal menuju masing-masing class aja tanpa mengganggu class lainnya.

Tapi code di atas masih bisa disederhanakan lagi menggunakan Simple Factory Pattern. Code-nya jadi seperti ini:

Factory Class BookGroupFactory

public static class BookGroupFactory{
	private final BookRepo bookRepo;

	public BookGroupFactory(BookRepo bookRepo){
		this.bookRepo = bookRepo;
	}

	public BookGroupStrategy buildStrategy(String grouping) throws Exception{
		if("category".equals(grouping)){
			return new BookSummaryByCategory(bookRepo);
		} else if("dateReleased".equals(grouping)){
			return new BookSummaryByReleasedDate(bookRepo);
		} else if("author".equals(grouping)){
			return new BookSummaryByAuthor(bookRepo);
		} else {
			throw new Exception("No grouping found");
		}
	}
}

Class BookSummaryService

public class BookSummaryService{
	private final BookRepo bookRepo;

	public BookSummaryService(BookRepo bookRepo){
		this.bookRepo = bookRepo;
	}

	public void printSummary(BookReq req) throws Exception{
		BookGroupFactory bookGroupFactory = new BookGroupFactory(bookRepo);
		BookGroupStrategy strategy = bookGroupFactory.buildStrategy(req.getGrouping());

		BookSummary books = strategy.getBookSummary(req);
		System.out.println("groupName = " + books.getGroupName());
		System.out.println("total = " + books.getTotalBook());
		System.out.println("sum price = " + books.getSumBookPrice());
	}
}

Dengan Simple Factory Pattern, logic untuk mendapatkan concrete object dari BookGroupStrategy dipisah dari entitas utama. Oleh karena itu tiap penambahan extension tinggal lakukan perubahan di Factory Class saja tanpa mengganggu entitas utama. Entitas utama hanya tau pakai aja.

Dengan Open-Close Principle kompleksitas code bisa lebih disederhanakan karena masing-masing kompleksitas dipecah jadi lebih spesifik. Jika ada varian baru, kita tidak mengubah class yang sudah ada, melainkan membuat class baru dengan mengimplementasi interface yang sama. Setiap perubahan yang dilakukan diharapkan tidak mengganggu code yang lainnya. Unit testing pun jadi lebih mudah karena code-nya sudah dipisah-pisah, business logic-nya lebih fokus pada task masing-masing. Misalkan di masa depan ada penambahan logic grouping, misalnya group by publisher, group by sex gender, dan lainnya tinggal extend dari BookGroupStrategy dan tambahkan class-nya di bagian BookGroupFactory. Seandainya penambahan tersebut dikerjakan oleh orang berbeda, itu dapat mengurangi conflict saat develop. Misalkan penambahan group by publisher dikerjakan oleh Tika, dan group by sex gender dikerjakan oleh Tiwy. Masing-masing Tika dan Tiwy hanya fokus pada Class yang didevelop masing-masing tanpa saling ganggu. Ini ga akan mengakibatkan conflict yang panjang karena kelasnya terpisah. Semua perubahan tersebut efektif ga akan mengganggu logic di entitas utama tanpa senggol-senggolan.

Prinsip SOLID lainnya: