author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
SOLID: Prinsip Single Responsibility
Sun. Nov 1st, 2020 11:49 PM6 mins read
SOLID: Prinsip Single Responsibility
Source: Bing Image Creator - chibi character holding a heavy "RESPONSIBILITY"

Secara definisi:

Single-responsibility principle (SRP) is a computer-programming principle that states that every module, class or function in a computer program should have responsibility over a single part of that program's functionality, which it should encapsulate.

Robert C. Martin

Kalau di-bahasa-indonesia-kan kurang lebih seperti ini: Single Responsibility adalah prinsip yang setiap modul, kelas atau fungsinya hanya bertanggung jawab terhadap satu part fungsionalitas saja yang di-engkapsulasi. Secara definisi memang agak rancu makna responsibility yang dimaksud cukup luas, dan ga hanya gw yang bingung, beberapa pendapat juga bilang begitušŸ¤£. Tapi secara praktiknya yang gw pahami adalah scope dari responsibility-nya tergantung masalah yang akan di-solve pada class. Disini penamaan class cukup penting, itu yang menjadi acuan masalah yang akan di-solve. Tujuannya untuk mengurangi kompleksitas saat terjadi perubahan. Yang penting rancangan class yang dihasilkan saling keterkaitannya sebatas method dan field dalam class itu sendiri (cohesion). Makanya butuh analisa yang cukup mendalam dalam menerapkan prinsip ini. Tulisan tentang Single Responsibility juga sering gw singgung di postingan tentang Mutable Objects dan Global Variables.

Contohnya pada kasus penyimpanan data buku. Dalam sebuah kelas BookService terdapat sebuah API method untuk melakukan saveBook dengan algoritma seperti berikut:

  1. Check apakah Id dari Author tersebut sudah ada:
    • Jika sudah ada, maka ambil Id Author tersebut sebagai Id Author yang akan dipasangkan dengan Book nanti;
    • Jika belum ada, maka lakukan save Author terlebih dahulu dengan nama Author: "unknown" dan ambil Id Author tersebut sebagai Id Author yang akan dipasangkan dengan Book nanti;
  2. Construct objek Book beserta propertinya;
  3. Simpan buku;

Kira-kira code-nya seperti ini:

public class BookService{
	private final BookRepo bookRepo;
	private final AuthorRepo authorRepo;

	public BookService(BookRepo bookRepo, AuthorRepo authorRepo){
		this.bookRepo = bookRepo;
		this.authorRepo = authorRepo;
	}

	public void saveBook(int authorId, String bookName){
		boolean existedAuthor = authorRepo.checkAuthorId(authorId);
		if(!existedAuthor){
			saveAuthor("unknown", authorId);
		}
		Book book = new Book();
		book.setAuthorId(authorId);
		book.setName(bookName);
		bookRepo.save(book);
	}

	private Author saveAuthor(String authorName, int authorId){
		Author author = new Author();
		author.setName(authorName);
		author.setAuthorId(authorId);
		return authorRepo.save(author);
	}
}

Code di atas melanggar Single Responsibility Code karena terdapat dua tanggungjawab yang ditangani oleh BookService, yaitu melakukan penyimpanan buku dan melakukan pengecekan serta penyimpanan author. Sesuai penamaan Class-nya, seharusnya BookService hanya melakukan logika bisnis yang berhubungan dengan buku saja. Solusinya logika tentang pengecekan dan penyimpanan author dikerjakan oleh Class berbeda. BookService menjadi seperti berikut:

public class BookService{
	private final BookRepo bookRepo;
	private final AuthorService authorService;

	public BookService(BookRepo bookRepo, AuthorService authorService){
		this.bookRepo = bookRepo;
		this.authorService = authorService;
	}

	public void saveBook(int authorId, String bookName) throws Exception{
		authorService.saveIfNotExist(authorId);

		Book book = new Book();
		book.setAuthorId(authorId);
		book.setName(bookName);
		bookRepo.save(book);
	}
}

AuthorService menjadi seperti berikut:

public class AuthorService{

	private final AuthorRepo authorRepo;

	public AuthorService(AuthorRepo authorRepo){
		this.authorRepo = authorRepo;
	}

	public void saveIfNotExist(int authorId){
		boolean existedAuthor = authorRepo.checkAuthorId(authorId);
		if(!existedAuthor){
			Author author = new Author();
			author.setName("unknown");
			author.setAuthorId(authorId);
			authorRepo.save(author);
		}
	}
}

Dengan begitu, setiap ada perubahan logic pada Author hanya dilakukan pada kelas AuthorService. Kelas Book hanya tinggal menggunakan AuthorService secara composition tanpa harus tahu kompleksitas dibalik logic AuthorService tersebut.

Single Responsibility tidak hanya diperuntukkan pada Class saja. Method juga berlaku hal yang sama. Masih menggunakan contoh kasus yang sama, misalkan ada tambahan validasi sebagai berikut:

  1. Jika nama bukunya null, maka buku tidak jadi disimpan;
  2. Jika buku dengan authorId dan nama buku yang sama sudah ada, maka buku tidak jadi disimpan;
  3. Jika publishernya bukan null, maka publisherName-nya diisi sesuai nilainya;
  4. Jika publishernya null, maka publisherName-nya diisini "Anonym";

Code-nya jadi seperti berikut:

public class BookService{
	private final BookRepo bookRepo;
	private final AuthorService authorService;

	public BookService(BookRepo bookRepo, AuthorService authorService){
		this.bookRepo = bookRepo;
		this.authorService = authorService;
	}

	public void saveBook(int authorId, String bookName, String publisher) throws Exception{
		if(bookName == null) throw new Exception("Book Name is null");
		Book bookByAuthorIdAndBookName = bookRepo.findByAuthorIdAndBookName(authorId, bookName);
		if(bookByAuthorIdAndBookName != null){
			throw new Exception("Duplicate Book");
		}
		authorService.saveIfNotExist(authorId);
		String publisherName;
		if(publisher != null){
			publisherName = publisher;
		} else {
			publisherName = "Anonym";
		}

		Book book = new Book();
		book.setAuthorId(authorId);
		book.setName(bookName);
		book.setPublisherName(publisherName);
		bookRepo.save(book);
	}
}

Code di atas melanggar Single Responsibility karena di dalam method saveBook terdapat tiga responsibility, yaitu logic pengecekan buku, conditional publisher, dan penyimpanan buku. Oleh karena itu method pengecekan buku dan conditional publisher perlu dipisah menjadi seperti berikut:

public class BookService{
	private final BookRepo bookRepo;
	private final AuthorService authorService;

	public BookService(BookRepo bookRepo, AuthorService authorService){
		this.bookRepo = bookRepo;
		this.authorService = authorService;
	}

	public void saveBook(int authorId, String bookName, String publisher) throws Exception{
		validateBook(authorId, bookName);
		authorService.saveIfNotExist(authorId);
		String publisherName = getPublisherName(publisher);

		Book book = new Book();
		book.setAuthorId(authorId);
		book.setName(bookName);
		book.setPublisherName(publisherName);
		bookRepo.save(book);
	}

	private String getPublisherName(String publisher){
		if(publisher != null){
			return publisher;
		} else {
			return "Anonym";
		}
	}

	private void validateBook(int authorId, String bookName) throws Exception{
		if(bookName == null) throw new Exception("Book Name is null");
		Book bookByAuthorIdAndBookName = bookRepo.findByAuthorIdAndBookName(authorId, bookName);
		if(bookByAuthorIdAndBookName != null){
			throw new Exception("Duplicate Book");
		}
	}
}

Dengan begini masing-masing method hanya akan bertanggungjawab pada masing-masing tugasnya. Ga ada lagi code campur sari di dalamnya.

private void print(Book book){
	Integer authorId = null;
	LocalDate releasedDate = null;
	if(book != null){
		authorId = book.getAuthorId;
		releasedDate = book.getReleased();
	}
	System.out.println("authorId = " + authorId);
	System.out.println("releasedDate = " + releasedDate);
}

Code di atas melanggar Single Responsibility Principle karena di dalam block conditional book terdapat lebih dari satu variable assignment. Untuk itu perlu dipecah assignment-nya jadi masing-masing assignment.

private void print(Book book){
	Integer authorId = book != null ? book.getAuthorId : null;
	LocalDate releasedDate = book != null ? book.getReleased() : null;
	System.out.println("authorId = " + authorId);
	System.out.println("releasedDate = " + releasedDate);
}

Sekarang code-nya jadi lebih enak dipahami.

Contoh lainnya misalkan kita ingin mengupdate tanggal rilis buku, lalu tampilkan buku berdasarkan author dari buku tersebut:

public Map<Integer, List<Book>> releaseBooksByAuthor(List<Integer> bookIds){
	List<Book> books = bookRepo.findByBookIds(bookIds);
	Map<Integer, List<Book>> booksByAuthor = new HashMap<>();
	for(Book book : books){
		book.setReleased(LocalDate.now());
		List<Book> bookList = booksByAuthor.computeIfAbsent(book.getAuthorId(), b -> new ArrayList<>());
		bookList.add(book);
	}
	return booksByAuthor;
}	

Ini juga melanggar Single Responsibility Principle karena terdapat lebih dari satu fungsional pada method di atas, yaitu update released date dan grouping buku. Kita perlu refactor seperti berikut:

public Map<Integer, List<Book>> releaseBooksByAuthor(List<Integer> bookIds){
	List<Book> books = updateReleaseBooks(bookIds);
	return groupBooksByAuthor(books);
}

private Map<Integer, List<Book>> groupBooksByAuthor(List<Book> books){
	Map<Integer, List<Book>> booksByAuthor = new HashMap<>();
	for(Book book : books){
		List<Book> bookList = booksByAuthor.computeIfAbsent(book.getAuthorId(), b -> new ArrayList<>());
		bookList.add(book);
	}
	return booksByAuthor;
}

private List<Book> updateReleaseBooks(List<Integer> bookIds){
	List<Book> books = bookRepo.findByBookIds(bookIds);
	for(Book book : books){
		book.setReleased(LocalDate.now());
	}
	return books;
}

Beberapa orang menggabungkan code untuk update buku dan grouping karena merasa itu lebih cepat dibanding memecah code tersebut dan looping 2x. Padahal itu Premature Optimization. Justru akan membuat code semakin sulit di-refactor saat logic-nya mulai kompleks. Time Complexity menggunakan looping list yang sama sebanyak 2x tetap O(n). Ini juga pernah gw bahas di tulisan tentang Big O.

Benefit dari Single Responsibility ini adalah code jadi lebih rapi. Class atau method hanya bertanggungjawab pada masing-masing tugasnya sesuai penamaannya. Masing-masing class dan method mempunyai tugas yang lebih spesifik. Jumlah line pada method yang menerapkan Single Responsibility akan berkurang dan cenderung membuat code lebih enak di-maintain dan dibaca. Walaupun Class dan Method jadi lebih banyak, tapi sebanding dengan kemudahan maintain-nya. Secara teori banyak Class dan banyak Method memang berpengaruh terhadap performance. Tapi perbandingannya hanya nanoseconds. Siapa yang peduli performance yang hanya cepat sekian nanoseconds? Lagian jaman sekarang resource seperti Memory dan Hard disk makin murah dengan ukuran yang gede. Lebih worth it maintain banyak Class atau Method dengan jumlah line yang sedikit daripada sedikit Class atau Method dengan jumlah line yang panjang banget.

Prinsip SOLID lainnya: