Single Responsibility Principle

The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change

Single Responsibility Principle (RSP) pertama kali dicetuskan oleh Robert C. Martin dengan bunyi “The Single Responsibility Principle (SRP) states that each software module should have one and only one reason to change” kalau kita artikan ke dalam bahasa indonesia SRP menyatakan bahwa setiap module software / perangkat lunak / Module/ Class / Fungsi harus mempunyai satu tugas atau satu fungsi DAN hanya memiliki satu alasan untuk mengubah. Di sini ada dua pernyataan, yang pertama harus satu fungsi/task dan yang kedua hanya memiliki satu alasan ketika kita mengubahnya, dengan kata lain sebuah Class harus lah dibatasi dampak dari perubahaan tersebut dan dampak nya itu harus seminim mungkin. Terdengar lebih membingungkan dan mungkin kita perlu mengesampingkan literaturnya dan mencoba memahami dengan praktikya. Tetapi kita coba pahami dengan analagi dunia nyata.

Seorang asisten rumah tangga hanya mengerjakan pekerjaan rumah tangga maka sudah tepat bahwa asisten rumah tangga memegang prinsip SRP tetapi faktanya banyak asisten rumah tangga yang menjadikan fungsi nya menjadi banyak hal, tidak sedikit asisten rumah tangga harus mengasuh anak dan membereskan kebun. Artinya bahwa Asisten rumah tangga yang mengerjakan pekerjaan tersebut telah melanggar prinsip SRP. Karena ada tiga pekerjaan yang dilakukan dan ketika kita ingin menggantikan salah satu fungsi nya kita punya tiga alasan untuk hal itu.

Contoh lain dari dunia nyata adalah Mobil. Mobil tidak melanggar SRP, karena setiap komponen yang ada pada mobil bertanggung jawab terhadap fungsinya sendiri. Sebagai contoh, jika mobil tersebut memiliki kendala atau masalah terhadap ban maka yang perlu kita ganti hanyalah ban bahkan kita bisa menggunakan ban merek lain. Lalu apa contoh yang melanggar SRP yaitu Swiss Army Knife. Fungsi pisau yang ada pada Swiss Army Knife sangat beragam, tidak ada masalah dengan hal itu tetapi bagai mana jika kita ingin mengganti salah satu dari pisau itu?

Untuk memahami dalam dunia programming. Kita akan membuat contoh menggunakan bahasa python. Di bawah ini adalah contoh Class untuk melakukan transformasi atau convert format dari satu format ke format lain. Terdapat satu fungsi dalam Class tersebut yang fungsi csv_to_json dimana fungsinya untuk mengubah data dari file csv ke file json.

import csv
import json

class FormatConverter:
    def csv_to_json(self, file_csv, file_json):
        raw_csv = []
        with open(file_csv, newline='') as csvfile:
            reader = csv.DictReader(csvfile, delimiter=';')
            for line in reader:
                raw_csv.append(line)

        with open(file_json, 'w') as jsonFile:
            json.dump(raw_csv, jsonFile)

if __name__ == '__main__': 
     print(FormatConverter().csv_to_json('cod_coverage.csv', 'cod_coverage.json'))

Pada fungsi csv_to_json terdapat beberapa pekerjaan yaitu membaca file csv, memasukan hasil baca csv ke dalam variable raw_csv kemudian menulis kembali ke dalam format json. Berdasarkan SRP, menulis code ini melanggar aturan SRP dimana terdapat tiga pekerjaan dalam satu fungsi dan kita memiliki tiga alasan untuk mengubahnya. Padahal seharusnya fungsi dan alasannya harus lah satu. Jadi apa yang harus kita refactor code.

class FormatConverter:

    def csv_to_json(self, file_csv, file_json):
        data_from_csv = read_file_csv(file_csv)
        write_file_json(data_from_csv)
        
    def read_file_csv(self, file):
        data = []
        with open(file, newline='') as f:
            reader = csv.DictReader(f, delimiter=';')
            for line in reader:
                data.append(line)
        return data

    def write_file_json(self, file, data):
        with open(file, 'w') as f:
            json.dump(data, f)

Pada class FormatConverter kita memisahkan fungsi membaca file csv dan menulis file json hal ini untuk memudahkan kita ketika harus membuat fungsi convert dari format lain tanpa harus menulis ulang atau menduplikasi code nya. Tentunya hal di atas masih belum ideal kalau kita liat fungsi read_file_csv terdapat dua fungsi yaitu membaca file dan membaca format csv bagaimana kalo kita ingin membuat membaca file dan membaca format json? tetapi dari sini kita bisa mempelajari bahwa pentingnya memegang prinsip SRP.

SRP bukan hanya Fungsi saja

Seperti yang sudah disebutkan diawal bahwa SRP tidak hanya tentang fungsi tetapi berlaku dengan class, module dan bahkan di level yang lebih tinggi lagi seperti library atau Add On dan seperti kata uncle Bob bahwa SRP berlaku untuk semua komponen perangkat lunak. Untuk lebih memahaminya lagi kita langsung pada contoh yang lebih kompleks.

Kita akan membuat sebuah Class UserRegister, dimana tugasnya untuk menghandle registrasi. Terdapat tiga method utama yaitu register, get_user_by_email dan send_email. Data User disimpan dalam bentuk list karena masih bingung dengan database yang akan digunakan sedangkan untuk validasinya harus di pastikan bahwa tidak boleh ada email yang sama dan antara attribute password dan repassword harus sama. Program yang dibuat pun harus bisa mengirimkan email tapi untuk sementara cukup menampilkan string saja. Code yang dibuat kurang lebih seperti ini.

class UserRegister:
    USERS = []

    def __init__(self):
        self.USERS = []

    def register(self, email, password, repassword):
        is_exist_user = self.get_user_by_email(email)
        
        if is_exist_user:
            raise Exception("User sudah terdaftar")

        if password != repassword:
            raise Exception("Password tidak sama")
        
        self.email = email
        self.password = hashlib.md5(b'{password}').hexdigest()
        self.USERS.append({'email': email, 'password': password})
        self.send_email("Mengirim email: Terima Kasih sudah mendaftar")
        
    def get_user_by_email(self, email):
        for user in self.USERS:
            if email in user['email']:
                return True
        return False

    def send_email(self, message):
        print(message)
        #using Email Service seperti mailgun atau sendgrid

Kalu kita review kembali code kita maka bisa kita nyatakan code tersebut sukses dibuat dan tidak ada masalah secara fungsional. Tetapi berdasarkan SRP bahwa code diatas melanggar prinsip SRP. Code di atas bisa kita sebut dengan God Object dimana class tersebut dapat melakukan apapun dan mengetahui apapun. Pada Class UserRegister memiliki kemampuan untuk menyimpan data, memvalidasi data, mengirim email dan itu dilakukan hanya oleh satu Class saja. Bagaimana jika class lain membutuhkan fungsi untuk mengambil data user pada class UserRegister apakah kita perlu menambah methodnya padahal tidak memerlukan method untuk mengirim email dan bagaimana jika class atau method lain membutuhkan method untuk mengirim email apakah perlu method untuk menyimpan user. Dan jika ingin merefactor code tersebut maka kita punya banyak alasan untuk mengubahnya seperti, mengganti data penyimpanan, mengganti email service dll.

Mari kita terapkan SRP pada code di atas. Pertama kita harus memikirkan apa hubungan atau class – class yang terlibat itu seperti kita mempunyai seorang asisten rumah tangga dengan fungsi mengasuh bayi, mengepel, memotong rumput, mencuci piring, mengantar anak, belanja, dll. Maka kita pisahkan ada yang disebut dengan bagian bersih – bersih rumah, ada yang bekerja sebagai baby sitter dan ada juga yang bagian tukang kebun. Begitu juga dengan code di atas, kita pisahkan fungsi – fungsi tersebut berdasarkan tanggung jawabnya. Kita memerlukan class User dimana class tersebut berisi attribute email dan password, lalu kita memerlukan class UserRepository dimana fungsinya untuk melakukan manipulasi data user pada tempat penyimpanan atau database. Kemudian kita juga perlu membuat class untuk mengirim Email. Beginilah hasil refactor nya.

class User:
    def __init__(self, email=None, password=None):
        self.email = email
        self.password = password

Class User hanya fokus pada attribute yang kita perlukan tidak lebih dan tidak kurang

class IRepository:

    def create(self, *args, **kwargs):
        raise NotImplemented

    def get_one_by(self, attribute, value):
        raise NotImplemented

class UserRepository(IRepository):
    USERS = []
    def create(self, email, password):
        user = User()
        user.email = email
        self.password = hashlib.md5(b'{password}').hexdigest()
        self.USERS.append({'email': email, 'password': password})
        return True
    
    def get_one_by(self, attribute, value):
        for user in self.USERS:
            if attribute in user[attribute]:
                return True
        return False

    def get_user_by_email(self, email):
        return self.get_one_by('email', email)

Class UserRepository mengimplementasi Class IRepository dimana memiliki dua method utama yaitu create dan get_one_by. Karena method get_one_by merupakan method umtuk untuk mengambil data berdasarkan attribute dimana attribute nya sendiri tidak diketahui. Tetapi di Class UserRepository kita membuat method get_user_by_email untuk memudahkan code client menggunakannya. Dengan membuat Class UserRepository jika susatu saat kita ingin mengubah tempat penyimpanannya maka kita hanya perlu mengubah di class ini saja. Code client seperti code UserRegister tidak akan peduli bagimana disimpan dia tau kalau untuk menyimpan data user dia hanya cukup memanggil method create.

class Email:
    def send_message(self, message):
        print(message)
        # implement email service

Class Email khusus untuk melakukan service pengiriman email disini kita bisa mengimplementasikan Email Service seperti mailgun atau sendgrid dan disini juga code client tidak tahu service apa yang digunakan tetapi cukup panggil method send_message.

class UserRegister:

    def __init__(self, user_repo: IRepository, mail_service: Email):
        self.user_repo = user_repo
        self.mail_service = mail_service

    def validate(self, email):
        is_exist_user = self.user_repo.get_user_by_email(email)
        
        if is_exist_user:
            raise Exception("User sudah terdaftar")

        if password != repassword:
            raise Exception("Password tidak sama")

    
    def register(self, email, password, repassword):
        self.validate(email)
        
        password = hashlib.md5(b'{password}').hexdigest()
        
        self.user_repo.create(email=email, password=password)
        self.mail_service.send_message("Mengirim email: Terima Kasih sudah mendaftar")

Seperti yang saya sebutkan bahwa refactor code di atas belum lah ideal masih ada prinsip – prinsip lain yang perlu kita ketahui dan pelajari dan membuat code kita menjadi lebih baik, mudah dibaca, dan well-maintained.

References

  • https://towardsdatascience.com/solid-programming-part-1-single-responsibility-principle-efca5e7c2a87
  • https://en.wikipedia.org/wiki/Single-responsibility_principle.
  • https://stackabuse.com/reading-and-writing-json-to-a-file-in-python/
  • https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html