Building Better Code with SOLID Principles

Ivan Phanderson
13 min readMar 7, 2023

--

Sumber

Object-Oriented Programming

Object-Oriented Programming (OOP) merupakan paradigma pemrograman yang berfokus pada objek, yang merupakan instance dari suatu class yang memiliki perilaku tertentu. Objek dapat juga didefinisikan sebagai data yang memiliki atribut dan perilaku unik.

OOP berfokus pada objek yang ingin dimanipulasi oleh developers daripada logika yang diperlukan untuk memanipulasinya. Pendekatan OOP sangat cocok untuk program yang berskala besar, kompleks, dan terus berkembang. Metode OOP juga sangat relevan untuk pengembangan yang bersifat kolaboratif karena dapat dibagi menjadi beberapa kelompok yang terpisah.

Struktur OOP

Classes

Classes adalah sebuah blueprint untuk membuat objek. Classes mendefinisikan data dan perilaku dari suatu objek atau dapat dianggap juga sebagai template untuk membuat objek. Contoh:

class Account:
# Attribute
username = ""
email = ""
role = ""

# Method
def __init__(self, username, email, role):
self.username = role
self.email = email
self.role = role

def get_info(self):
return f"{self.username} with email {self.email} has role {self.role}"

Attributes

Attributes adalah template dari suatu class yang mewakili keadaan objek yang akan didefinisikan. Objek dapat memiliki data yang disimpan pada atribut. Contoh di bawah adalah sebuah class dengan atribut username, email, dan role.

class Account:
# Attribute
username = ""
email = ""
role = ""

def __init__(self, username, email, role):
self.username = role
self.email = email
self.role = role

Methods

Methods adalah fungsi yang didefinisikan dalam suatu class yang menggambarkan perilaku suatu objek. Setiap objek dari class yang didefinisikan dapat memanggil method yang terdapat dalam class tersebut. Penggunaan method ini juga dapat meningkatkan reusability kode kita. Contoh di bawah adalah method get_info yang dapat berfungsi untuk memberi informasi terkait atribut dari objek yang dibuat

class Account:
# Attribute
username = ""
email = ""
role = ""

# Method
def __init__(self, username, email, role):
self.username = role
self.email = email
self.role = role

def get_info(self):
return f"{self.username} with email {self.email} has {self.role} role"

Objects

Objects adalah turunan dari class. Objek berisi data dan perilaku yang ditentukan oleh class dan dapat berinteraksi satu sama lain melalui atribut dan method yang didefinisikan. Contoh:

# Instansiasi objek
account = Account("ivanp", "ivanp@gmail.com", "user")

# Mengakses atribut dari objek
print(account.username) # Output: ivanp
print(account.role) # Output: user

# Mengakses method dari objek
print(account.get_info()) # Output: ivanp with email ivanp@gmail.com has user role

Prinsip OOP

Encapsulation

Encapsulation merupakan prinsip menyembunyikan informasi penting yang terkandung dalam suatu objek dan hanya informasi tertentu yang dapat diakses secara publik. Prinsip ini memungkinkan pengorganisasian kode yang lebih baik dan mencegah objek lain mengakses atau mengubah data dan method secara langsung. Contoh:

class Account:
def __init__(self, username, age):
self.__username = username
self.__age = age

def get_username(self):
return self.__username

def set_username(self, username):
self.__name = username

def get_age(self):
return self.__age

def set_age(self, age):
if age >= 0:
self.__age = age
else:
raise ValueError("Age cannot be negative.")

Pada contoh di atas, kita memiliki atribut username dan age. Kita juga menggunakan __ pada awal atribut tersebut untuk mendefinisikan atribut yang private. Private artinya atribut tersebut tidak dapat diakses diluar class tersebut. Untuk mengakses variabel tersebut, kita dapat mendefinisikan getter dan setter. Getter dari class di atas adalah get_username dan get_age, sedangkan setter dari class di atas adalahset_username dan set_age . Pada method set_age kita juga mendefinisikan constraint berupa nilai yang harus tidak negatif.

Contoh penggunaan:

account = Account("Ivan", 21)

print(account.get_username()) # Output: Ivan
print(account.get_age()) # Output: 21

person1.set_name("Ivan P")
person1.set_age(15)

print(person1.get_username()) # Output: Ivan P
print(person1.get_age()) # Output: 15

Contoh di atas merupakan contoh enkapsulasi dimana implementasi dari class tersebut tersembunyi dari luar classnya dan pengaksesan ke atribut dari class-nya hanya dapat dilakukan melalui method publik yang disediakan.

Abstraction

Abstraksi merupakan proses penyederhanaan sistem yang kompleks dengan memecah kodenya menjadi bagian-bagian yang lebih kecil. Kita dapat menggunakan interface atau abstract classes untuk implementasi prinsip ini. Contoh:

from abc import ABC, abstractmethod

class Account(ABC):
def __init__(self, name):
self.name = name

@abstractmethod
def get_type(self):
pass

class SSOAccount(Account):
def get_type(self):
return "SSO"

class NonSSOAccount(Account):
def get_type(self):
return "Non SSO"

Contoh class Account di atas memiliki 1 abstract method yaitu get_type . Terdapat juga 2 class yang merupakan turunan dari class Account yaitu SSOAccount dan NonSSOAccount. Kedua class ini mengimplementasi method get_type dari class Account. Class Account di sini merupakan abstraksi karena menyediakan konsep account secara umum tanpa mengimplementasi method get_type. Hal ini memungkinkan developers untuk membuat tipe akun yang berbeda dengan menggunakan interface yang sama.

Inheritance

Inheritance merupakan prinsip OOP yang memungkinkan suatu class untuk mewariskan properti dan method dari parent class-nya. Prinsip ini juga mendukung reusability dari kode kita karena kita dapat menggunakan method-method yang sudah didefinisikan di parent class. Contoh:

class Account:
def __init__(self, username, email, role):
self.username = role
self.email = email
self.role = role

def get_info(self):
return f"{self.username} with email {self.email} has '{self.role}' role"

class SSOAccount(Account):
def __init__(self, username, email, role, npm):
super().__init__(username, email, role)
self.npm = npm

def get_npm(self):
return f"NPM {self.username}: {self.npm}"

Pada contoh di atas, kita memiliki class account dengan atribut username, email, dan role dan sebuah method get_info untuk memperoleh informasi terkait username, email, dan role dari account tersebut. Selain class account, terdapat class SSOAccount yang merupakan turunan dari class SSOAccount dan menambahkan npm sebagai atributnya dan sebuah method get_npm.

Class SSOAccount mewariskan seluruh atribut dan method dari class Account dan dapat menambahkan atribut baru atau meng-override method yang sudah ada. Pada contoh ini, class SSOAccount meng-override method __init__ yang sudah ada.

Contoh penggunaan:

account = Account("ivanp", "ivanp@gmail.com", "user")
print(account.get_info()) # Output: ivanp with email ivanp@gmail.com has 'user' role

student = SSOAccount("ivanp", "ivanp@gmail.com", "user", "2006463004")
student.get_info() # Output: ivanp with email ivanp@gmail.com has 'user' role
student.get_npm() # Output: NPM ivanp: 2006463004

Terlihat dari contoh di atas bahwa class SSOAccount mewariskan method get_info class Account sehingga kita dapat memanggil method tersebut ketika memiliki objek SSOAccount. Hal ini juga yang dikenal sebagai prinsip inheritance pada OOP

Polymorphism

Polymorphism adalah prinsip OOP yang memungkinkan objek untuk digunakan dalam bentuk dan perilaku yang berbeda sesuai dengan konteksnya. Hal ini dapat dicapai dengan meng-override suatu method atau meng-overload suatu method. Contoh:

class Account:
def __init__(self, username, email, role):
self.username = role
self.email = email
self.role = role

def get_info(self):
return f"{self.username} with email {self.email} has '{self.role}' role"

def get_type(self):
pass

class SSOAccount(Account):
def __init__(self, username, email, role, npm):
super().__init__(username, email, role)
self.npm = npm

def get_npm(self):
return f"NPM {self.username}: {self.npm}"

def get_type(self):
return "SSO UI"

class NonSSOAccount(Account):
def get_type(self):
return "Non SSO"

Pada contoh di atas, class Account memiliki method get_type yang mengembalikan tipe dari suatu objek account. Kita juga memiliki 2 subclass yaitu SSOAccount dan NonSSOAccount yang merupakan subclass dari Account dan meng-override method get_type dari class Account. Contoh penggunaan:

account1 = SSOAccount("ivanp", "ivanp@gmail.com", "user", "2006463004")
account2 = NonSSOAccount("ivanp", "ivanp@gmail.com", "user")

print(account1.get_type()) # Output: SSO UI
print(account2.get_type()) # Output: Non SSO

Terlihat dari contoh di atas bahwa pemanggilan get_type menghasilkan output yang berbeda tergantung dari tipe objek yang dipanggil. Hal inilah yang disebut dengan implementasi polymorphism pada OOP.

Manfaat OOP

  1. Modularity. OOP mengenkapsulasi objek-objek yang ada dan membagi menjadi bagian-bagian yang lebih kecil. Hal ini membuat kode kita lebih mudah untuk dipahami oleh developers lain.
  2. Reusability. Kode dapat digunakan kembali oleh subclass. Dengan demikian, developers tidak perlu menulis ulang kode yang sama.
  3. Productivity. Menggunakan OOP membuat kode kita lebih mudah dipahami dan meningkatkan reusability sesuai statement sebelumnya. Hal ini tentu saja dapat mempercepat kinerja developers sehingga meningkatkan produktivitas.
  4. Scalability. Dengan program yang independen (loosely coupled), developers dapat dengan mudah memodifikasi kode kita dan membangun software yang lebih kompleks.
  5. Security. Encapsulation dan abstraction menyembunyikan detail implementasi dari kode kita sehingga dapat mencegah terjadinya perubahan yang tidak diinginkan. Hal ini juga meningkatkan sisi keamanan dari kode kita karena adanya method dan atribut private yang tidak bisa diakses sembarangan.
  6. Flexibility. Polymorphism memungkinkan sebuah method untuk beradaptasi ke suatu class. Developers memiliki fleksibilitas untuk meng-override atau meng-overload method dari parent class-nya
SOLID Principles. Sumber

SOLID

SOLID merupakan kumpulan prinsip desain perangkat lunak yang dipakai dalam mengembangkan perangkat lunak yang object-oriented. SOLID merupakan singkatan dari Single responsibility principle, Open-closed principle, Liskov substitution principle, Interface segregation principle, dan Dependency inversion principle. Kelima prinsip ini sangat sering digunakan oleh developers karena memberikan banyak keuntungan bagi mereka.

Sejarah SOLID

Prinsip SOLID pertama kali diperkenalkan pada tahun 2000 oleh Robert C. Martin melalui essaynya yang berjudul “Design Principles and Design Patterns”. Sejak saat itu, prinsip SOLID menjadi populer dan dikenal dengan best practices dalam menulis kode yang object-oriented.

Single Responsibility Principle (SRP)

Pada prinsip ini, Robert C. Martin menekankan bahwa setiap class atau modul harus memiliki satu dan hanya satu tugas saja. Artinya, class atau modul tersebut hanya boleh mengalami perubahan jika terdapat perubahan pada tanggung jawabnya. Dengan mengikuti prinsip ini, maka kode kita akan lebih low coupling dan lebih mudah untuk di-maintain. Berikut adalah contoh kode yang melanggar SRP:

from django.shortcuts import render
from django.http import HttpResponse
from myapp.models import User

def manage_user(request, user_id):
user = User.objects.get(id=user_id)

if request.method == 'POST':
user.name = request.POST.get('name')
user.email = request.POST.get('email')
user.save()
return HttpResponse('User updated successfully')

return render(request, 'user.html', {'user': user, 'editable': True})

Kode di atas melanggar SRP karena fungsi manage_user memiliki dua tanggung jawab, yaitu menampilkan data dalam bentuk read-only atau dalam bentuk yang dapat di-update. Perbaikan dari kode di atas agar memenuhi SRP adalah sebagai berikut:

from django.shortcuts import render
from django.http import HttpResponse
from myapp.models import User

def get_user(request, user_id):
user = User.objects.get(id=user_id)
return render(request, 'user.html', {'user': user})

def update_user(request, user_id):
user = User.objects.get(id=user_id)
if request.method == 'POST':
user.name = request.POST.get('name')
user.email = request.POST.get('email')
user.save()
return HttpResponse('User updated successfully')
return render(request, 'update_user.html', {'user': user})

Pada contoh di atas, terdapat 2 fungsi yang masing-masing memiliki 1 tanggung jawab. Fungsi get_user berfungsi untuk mendapatkan user berdasarkan id, sedangkan fungsi update_user berfungsi untuk mengupdate data user tersebut berdasarkan id. Dengan demikian, contoh di atas sudah memenuhi SRP.

Open-Closed Principle (OCP)

Prinsip ini menekankan bahwa sebuah class harus terbuka terhadap extension tanpa melakukan modifikasi terhadap kodenya. Dengan menerapkan prinsip ini, class lain dapat memanfaatkan inheritance atau composition dari class tersebut. Akibatnya, reusability dari kode kita juga tinggi. Berikut adalah contoh dari kode yang melanggar Open-closed principle:

from django.db import models

class PaymentMethod:
def process_payment(self, amount):
raise NotImplementedError()

class CreditCard(PaymentMethod):
def process_payment(self, amount):
# kode untuk pembayaran menggunakan kartu kredit
pass

class BankTransfer(PaymentMethod):
def process_payment(self, amount):
# kode untuk pembayaran menggunakan transfer bank
pass

class PayPal(PaymentMethod):
def process_payment(self, amount):
# kode untuk pembayaran menggunakan PayPal
pass

class Payment(models.Model):
amount = models.DecimalField(max_digits=10, decimal_places=2)
payment_method = models.CharField(max_length=50)

def process_payment(self):
if self.payment_method == 'creditcard':
payment_method = CreditCard()
elif self.payment_method == 'banktransfer':
payment_method = BankTransfer()
elif self.payment_method == 'paypal':
payment_method = PayPal()
else:
raise ValueError("Invalid payment method")
payment_method.process_payment(self.amount)

Kode di atas melanggar OCP karena setiap penambahan class PaymentMethod, kita harus menambahkan conditional statement pada fungsi process_payment. Perbaikan dari kode tersebut agar memenuhi OCP adalah:

from django.db import models

class PaymentMethod:
def process_payment(self, amount):
raise NotImplementedError()

class CreditCard(PaymentMethod):
def process_payment(self, amount):
# kode untuk pembayaran menggunakan kartu kredit
pass

class BankTransfer(PaymentMethod):
def process_payment(self, amount):
# kode untuk pembayaran menggunakan transfer bank
pass

class Payment(models.Model):
amount = models.DecimalField(max_digits=10, decimal_places=2)
payment_method = models.CharField(max_length=50)

def process_payment(self):
payment_method_class = getattr(
__import__('payment.models', fromlist=[self.payment_method]),
self.payment_method
)
payment_method = payment_method_class()
payment_method.process_payment(self.amount)

Pada contoh di atas, class PaymentMethod merupakan kelas abstract. Selanjutnya terdapat class CreditCard dan BankTransfer yang meng-extend class PaymentMethod dan mengimplementasikan fungsi process_payment. Dengan menggunakan desain seperti di atas, kita dapat dengan mudah menambahkan class lainnya yang merupakan turunan dari PaymentMethod tanpa memodifikasi kode yang sudah ada.

Liskov Substitution Principle (LSP)

Prinsip ini menekankan bahwa object dari subclass harus dapat menggantikan object superclass-nya tanpa menghasilkan error atau perilaku yang tidak diinginkan. Berikut adalah contoh kode yang melanggar Liskov substitution Principle:

class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email

def get_name(self):
return self.name

def get_email(self):
return self.email

class AdminUser(User):
def __init__(self, name: str, email: str, is_admin: bool):
super().__init__(name, email)
self.is_admin = is_admin

def get_name(self):
return f"Admin {self.name}"

def get_email(self):
return self.email

def get_role(self):
return "Admin"

Kode di atas melanggar LSP karena terdapat fungsi get_name pada class AdminUser yang menambahkan tulisan Admin pada prefiksnya. Penambahan ini dapat mengakibatkan hal-hal yang tidak diinginkan karena yang diekspektasikan adalah nama dari penggunanya. Perbaikan dari kode di atas agar memenuhi LSP adalah:

class User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email

def get_name(self):
return self.name

def get_email(self):
return self.email

class AdminUser(User):
def __init__(self, name: str, email: str, is_admin: bool):
super().__init__(name, email)
self.is_admin = is_admin

def get_admin_name(self):
return f"Admin {self.name}"

def get_email(self):
return self.email

def get_role(self):
return "Admin"

Pada contoh di atas, AdminUser menggunakan fungsi baru untuk mendapatkan namanya dalam versi admin, yaitu get_admin_name. Dengan demikian, AdminUser dapat menggantikan class User kapan saja tanpa menghasilkan bug atau perilaku yang tidak diinginkan.

Interface segregation principle (ISP)

Prinsip ini menyatakan bahwa pengguna tidak boleh dipaksa untuk bergantung pada interface yang tidak mereka gunakan. Artinya, daripada membuat sebuah interface yang besar yang mencakup seluruhnya, lebih baik kita membuat beberapa interface kecil yang berfokus pada perilaku tertentu. Contoh kode yang melanggar ISP:

from abc import ABC, abstractmethod

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

class UserValidator(ABC):
@abstractmethod
def validate_username(self, username: str):
pass

@abstractmethod
def validate_email(self, email: str):
pass

@abstractmethod
def validate_phone(self, phone: str):
pass

class UserRegistrationService:
def __init__(self, validator: UserValidator):
self.validator = validator

def register_user(self, user: User):
if (self.validator.validate_username(user.username) and
self.validator.validate_email(user.email) and
self.validator.validate_phone(user.phone)):
# kode untuk registrasi pengguna baru
pass

class DefaultUserValidator(UserValidator):
def validate_username(self, username: str):
# kode untuk validasi username
pass

def validate_email(self, email: str):
# kode untuk validasi email
pass

def validate_phone(self, phone: str):
# kode untuk validasi nomor telepon
pass

Kode di atas melanggar ISP karena adanya validate_phone yang belum tentu perlu dilakukan oleh semua orang. Bisa saja service kita tidak memerlukan validasi nomor telepon. Penambahan method ini dapat membuat kode kita menjadi lebih sulit di-maintain. Perbaikan dari kode di atas agar memenuhi ISP adalah:

from abc import ABC, abstractmethod

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

class UserValidator(ABC):
@abstractmethod
def validate_username(self, username: str):
pass

@abstractmethod
def validate_email(self, email: str):
pass

class UserRegistrationService:
def __init__(self, validator: UserValidator):
self.validator = validator

def register_user(self, user: User):
if self.validator.validate_username(user.username) and self.validator.validate_email(user.email):
# kode untuk registrasi pengguna baru
pass

class DefaultUserValidator(UserValidator):
def validate_username(self, username: str):
# kode untuk validasi username
pass

def validate_email(self, email: str):
# kode untuk validasi email
pass

Kita memiliki interface UserValidator yang mencakup fungsi validate_username dan validate_email. Class DefaultUserValidator mengimplementasi interface tersebut. Terlihat bahwa interface yang ada sudah cukup kecil dan hanya berfokus pada bagian validasi user. Pada saat validasi user, kita tidak memerlukan perilaku lainnya yang tidak berhubungan. Dengan demikian, kode di atas sudah menerapkan ISP.

Dependency Inversion Principle (DIP)

Prinsip ini menyatakan bahwa modul level-tinggi (high-level) tidak boleh bergantung pada modul level-rendah (low-level), tetapi harus bergantung pada abstraksi. Sementara itu, abstraksi tidak boleh bergantung pada detail, tetapi detail harus bergantung pada abstraksi. Dengan kata lain, modul yang kompleks harus dibangun di atas modul yang sederhana tetapi tidak boleh bergantung kepadanya. Kedua modul ini harus bergantung pada abstraksi atau interface. Dengan demikian, perubahan pada modul low-level tidak akan memberi efek kepada modul high-level. Penerapan prinsip ini juga akan membantu tercapai prinsip OCP. Contoh dari kode yang melanggar DIP adalah:

from abc import ABC, abstractmethod

class NotificationService(ABC):
@abstractmethod
def send_notification(self, user_id: int, message: str):
pass

class EmailService(NotificationService):
def send_notification(self, user_id: int, message: str):
# kode untuk mengirimkan email ke user
pass

class SMSService(NotificationService):
def send_notification(self, user_id: int, message: str):
# kode untuk mengirimkan SMS ke user
pass

class Post:
def __init__(self, title, content):
self.title = title
self.content = content

def save(self):
# kode untuk menyimpan Post ke database
pass

def notify_users(self):
email_service = EmailService()
users = User.objects.all()
for user in users:
email_service.send_notification(user.email, f"A new post '{self.title}' has been created")

class User(models.Model):
name = models.CharField(max_length=255)
email = models.EmailField()
phone_number = models.CharField(max_length=20)

Contoh di atas melanggar DIP karena class Post bergantung pada implementasi konkrit dari EmailService, bukan abstraksinya. Akibatnya, kita akan kesulitan untuk mengubah fitur notifikasi ini di masa depan karena coupling yang tinggi dengan EmailService. Perbaikan dari kode di atas agar memenuhi DIP adalah:

from abc import ABC, abstractmethod

class NotificationService(ABC):
@abstractmethod
def send_notification(self, user_id: int, message: str):
pass

class EmailService(NotificationService):
def send_notification(self, user_id: int, message: str):
# kode untuk mengirimkan email ke user
pass

class SMSService(NotificationService):
def send_notification(self, user_id: int, message: str):
# kode untuk mengirimkan SMS ke user
pass

class Post:
def __init__(self, title, content):
self.title = title
self.content = content

def save(self):
# kode untuk menyimpan Post ke database
pass

def notify_users(self, notification_service: NotificationService):
users = User.objects.all()
for user in users:
notification_service.send_notification(user.id, f"A new post '{self.title}' has been created")

class User(models.Model):
name = models.CharField(max_length=255)
email = models.EmailField()
phone_number = models.CharField(max_length=20)

Pada kode di atas, terdapat class NotificationService yang merupakan interface untuk hal-hal yang berkaitan dengan notifikasi dan diimplementasikan oleh class konkrit EmailService dan SMSService. Pada class Post, terdapat method notify_users yang akan mengirim notifikasi ke user dengan memanggil fungsi send_notification dari instance NotificationService.

Kode tersebut memenuhi DIP karena class Post hanya bergantung pada class abstract NotificationService, bukan pada implementasi konkritnya. Dengan demikian, kita bisa mengganti menjadi tipe-tipe notifikasi lain tanpa harus mengubah class Post. Selain itu, kita juga dapat menambahkan implementasi lain pada class NotificationService tanpa mengganggu jalannya class Post. Terlihat juga bahwa hal ini memunculkan low-coupling antara class Post dengan class NotificationService.

Implementasi pada PPL

Implementasi Single Responsibility Principle (SRP). Berikut adalah contoh sebelum menerapkan SRP:

@login_required(login_url='/login')
@require_http_methods(['POST'])
def input_employee_to_event(request):
form_data = request.POST

total_honor = 0
for idx in range(num_fields):
if form_data[f'honor_field_{idx}'] != '':
total_honor += abs(int(form_data[f'honor_field_{idx}']))

# Beberapa kode untuk input employee

return get_events(request, action)

Setelah menerapkan SRP:

@login_required(login_url='/login')
@require_http_methods(['POST'])
def input_employee_to_event(request):
form_data = request.POST
total_honor = calculate_total_honor(form_data, num_fields)

# Beberapa kode untuk input employee

return get_events(request, action)

def calculate_total_honor(form_data, num_fields):
total_honor = 0
for idx in range(num_fields):
honor_field = form_data.get(f'honor_field_{idx}')
if honor_field:
total_honor += abs(int(honor_field))
return total_honor

Terlihat bahwa fungsi input_employee_to_event tidak lagi menghitung total honor sehingga hanya memiliki 1 tanggung jawab yaitu untuk menginput data employee. Selain itu, fungsi calculate_total_honor juga dapat digunakan pada fungsi lainnya.

Kesimpulan

Prinsip SOLID yang terdiri atas single responsibility, open/closed, Liskov substitution, interface segregation, dan dependency inversion memberikan pedoman yang dapat membantu developers menghasilkan kode yang lebih mudah di-maintain, extensible, dan mudah untuk di test. Walaupun prinsip-prinsip ini memerlukan beberapa waktu untuk dipelajari dan diterapkan secara efektif, prinsip-prinsip tersebut pada akhirnya akan membantu kita menghasilkan software yang lebih berkualitas sehingga dapat meningkatkan kepuasan pelanggan.

Sumber

--

--