SRP — Single Responsibility Principle #
Ada satu file di hampir setiap codebase yang semua engineer takut untuk diubah. Namanya sering berakhiran Service atau Manager, isinya ribuan baris, dan setiap kali ada bug baru, investigasinya selalu berakhir di sana. File itu bukan hasil keputusan yang disengaja — ia tumbuh secara organik karena setiap fitur baru ditambahkan ke tempat yang paling mudah dijangkau, dan tempat yang paling mudah dijangkau selalu adalah file yang sudah ada. Inilah yang terjadi ketika Single Responsibility Principle dilanggar secara konsisten. SRP adalah prinsip pertama dari SOLID yang paling langsung memengaruhi kualitas kode sehari-hari: sebuah modul hanya boleh memiliki satu alasan untuk berubah. Bukan satu method, bukan satu file — tapi satu alasan, satu aktor, satu domain perubahan. Artikel ini membahas apa yang benar-benar dimaksud definisi itu, bagaimana mengenali pelanggaran SRP sebelum ia menjadi masalah besar, cara refactor yang bertahap dan aman, dan kapan pemisahan tanggung jawab justru terlalu jauh.
Makna Sebenarnya “Satu Alasan untuk Berubah” #
Definisi klasik SRP dari Robert C. Martin:
A module should have one, and only one, reason to change.
Banyak yang menginterpretasikan ini sebagai “satu class harus kecil” atau “satu function hanya boleh melakukan satu hal”. Keduanya bukan definisi yang tepat. Yang dimaksud alasan untuk berubah adalah aktor atau stakeholder — pihak yang kebutuhannya bisa memaksa modul itu dimodifikasi.
Jika sebuah UserService bisa dipaksa berubah oleh:
- Tim produk yang mengubah aturan bisnis registrasi
- Tim infrastruktur yang mengganti database engine
- Tim marketing yang mengubah template email selamat datang
- Tim DevOps yang mengubah format logging
Maka UserService memiliki empat alasan untuk berubah — dan itu berarti empat tanggung jawab yang seharusnya dipisahkan.
Tes SRP yang paling praktis:
Tulis kalimat: "Modul X harus diubah jika..."
Contoh yang melanggar SRP:
"UserService harus diubah jika:
- aturan validasi email berubah (tim produk)
- schema database berubah (tim infrastruktur)
- template email selamat datang berubah (tim marketing)
- format log berubah (tim DevOps)"
→ Empat aktor = empat tanggung jawab = SRP dilanggar
Contoh yang mengikuti SRP:
"UserValidator harus diubah jika aturan validasi user berubah"
"UserRepository harus diubah jika schema database user berubah"
"WelcomeEmailSender harus diubah jika template email selamat datang berubah"
→ Satu aktor per modul = SRP terpenuhi
SRP juga berlaku di level yang lebih kecil dari struct — ia berlaku untuk fungsi, package, bahkan file. Sebuah fungsi yang melakukan validasi, transformasi, dan side effect sekaligus dalam satu blok linear adalah pelanggaran SRP di skala mikro.
Empat Dampak Nyata Pelanggaran SRP #
Pelanggaran SRP tidak hanya masalah estetika kode. Ia menghasilkan biaya nyata yang terasa semakin berat seiring waktu:
1. Perubahan yang tidak terduga. Ketika semua tanggung jawab ada di satu tempat, perubahan kecil di satu area bisa berdampak ke area lain secara tidak sengaja. Mengganti format log menyebabkan bug di business logic karena keduanya ada di file yang sama dan berbagi state. Unit test untuk validasi tiba-tiba gagal karena ada perubahan di fungsi penyimpanan database.
2. Test yang membutuhkan setup kompleks. Jika sebuah struct melakukan validasi, database access, dan email sending sekaligus, unit test untuk logika validasinya harus menyiapkan mock untuk database dan email server — meski keduanya tidak relevan untuk test yang sedang ditulis. Setiap test menjadi integration test secara tidak sengaja.
3. Merge conflict yang terus berulang. Ketika dua engineer mengerjakan fitur yang berbeda tetapi keduanya perlu memodifikasi file yang sama — misalnya satu mengubah validasi dan yang lain mengubah format email — merge conflict muncul bukan karena mereka mengerjakan hal yang sama, tapi karena tanggung jawab yang berbeda tersimpan di tempat yang sama.
4. Onboarding yang lambat. Engineer baru harus memahami seluruh kompleksitas sebuah GodService hanya untuk membuat perubahan kecil di salah satu tanggung jawabnya. Tidak ada batas yang jelas antara “ini bagian yang relevan” dan “ini detail yang tidak perlu dipahami sekarang”.
Mengenali Pelanggaran SRP #
Sebelum bisa memperbaiki, perlu bisa mengenali. Ini adalah sinyal yang paling sering muncul:
RED FLAG DI LEVEL STRUCT:
✗ Nama yang terlalu generik: UserService, DataManager, CoreProcessor
✗ Constructor dengan lebih dari 4–5 dependency
✗ Field yang tidak digunakan oleh semua method
✗ Method yang hanya relevan dalam konteks tertentu
RED FLAG DI LEVEL METHOD:
✗ Nama method mengandung "And": validateAndSave(), parseAndSend()
✗ Satu method lebih dari 30–40 baris dengan logika yang berbeda-beda
✗ Banyak komentar "// bagian validasi", "// bagian database",
"// bagian notifikasi" — tanda seharusnya dipisah jadi method berbeda
RED FLAG DI LEVEL FILE:
✗ Satu file yang diubah dalam commit untuk alasan yang berbeda-beda
✗ Satu file yang muncul di hampir setiap PR karena "semua perlu melaluinya"
✗ Import list yang panjang dan tidak kohesif: database, smtp, http, json, pdf
RED FLAG DI LEVEL PACKAGE:
✗ Package bernama "util", "helper", atau "common" yang menampung segalanya
✗ Package yang di-import oleh hampir semua package lain
(sinyal package itu terlalu banyak tahu)
Dari GodService ke Komponen Terfokus — Refactor Bertahap #
Contoh paling representatif: UserService yang melakukan terlalu banyak hal.
// ANTI-PATTERN: UserService dengan empat tanggung jawab berbeda
// Empat aktor berbeda bisa memaksanya berubah
package user
import (
"database/sql"
"fmt"
"net/smtp"
"regexp"
)
type UserService struct {
db *sql.DB
}
func (s *UserService) Register(name, email, password string) error {
// === TANGGUNG JAWAB 1: Validasi ===
// Jika aturan validasi berubah → UserService harus diubah
if name == "" {
return fmt.Errorf("name is required")
}
emailRegex := regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
if !emailRegex.MatchString(email) {
return fmt.Errorf("invalid email format")
}
if len(password) < 8 {
return fmt.Errorf("password must be at least 8 characters")
}
// === TANGGUNG JAWAB 2: Hash password ===
// Jika algoritma hashing berubah → UserService harus diubah
hashedPassword := fmt.Sprintf("hashed_%s", password) // simplified
// === TANGGUNG JAWAB 3: Akses database ===
// Jika schema atau database engine berubah → UserService harus diubah
var exists bool
s.db.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE email = ?)", email).Scan(&exists)
if exists {
return fmt.Errorf("email already registered")
}
_, err := s.db.Exec(
"INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)",
name, email, hashedPassword,
)
if err != nil {
return fmt.Errorf("failed to save user: %w", err)
}
// === TANGGUNG JAWAB 4: Email notification ===
// Jika template atau provider email berubah → UserService harus diubah
auth := smtp.PlainAuth("", "[email protected]", "password", "smtp.example.com")
msg := []byte("Subject: Welcome!\r\n\r\nWelcome to our platform, " + name + "!")
smtp.SendMail("smtp.example.com:587", auth, "[email protected]", []string{email}, msg)
// === TANGGUNG JAWAB 5: Logging ===
// Jika format atau sistem logging berubah → UserService harus diubah
fmt.Printf("[INFO] %s: user registered successfully: %s\n",
time.Now().Format(time.RFC3339), email)
return nil
}
Untuk memecahnya, lakukan secara bertahap — satu tanggung jawab per langkah:
Langkah 1: Ekstrak validasi ke UserValidator
// internal/user/validator.go
package user
import (
"errors"
"regexp"
)
var emailRegex = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
type UserValidator struct{}
func (v UserValidator) ValidateRegistration(name, email, password string) error {
if name == "" {
return errors.New("name is required")
}
if !emailRegex.MatchString(email) {
return errors.New("invalid email format")
}
if len(password) < 8 {
return errors.New("password must be at least 8 characters")
}
return nil
}
// Satu-satunya alasan untuk berubah: aturan validasi user berubah
Langkah 2: Ekstrak akses data ke UserRepository
// internal/user/repository.go
package user
import (
"context"
"database/sql"
"errors"
"fmt"
)
var ErrEmailExists = errors.New("email already registered")
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) ExistsByEmail(ctx context.Context, email string) (bool, error) {
var exists bool
err := r.db.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM users WHERE email = $1)", email,
).Scan(&exists)
return exists, err
}
func (r *UserRepository) Save(ctx context.Context, u NewUser) error {
_, err := r.db.ExecContext(ctx,
"INSERT INTO users (name, email, password_hash) VALUES ($1, $2, $3)",
u.Name, u.Email, u.PasswordHash,
)
if err != nil {
return fmt.Errorf("save user: %w", err)
}
return nil
}
// Satu-satunya alasan untuk berubah: schema database atau database engine berubah
Langkah 3: Ekstrak notifikasi ke WelcomeNotifier
// internal/notification/welcome.go
package notification
import "context"
type WelcomeNotifier interface {
SendWelcome(ctx context.Context, name, email string) error
}
// SMTPWelcomeNotifier: implementasi dengan SMTP
type SMTPWelcomeNotifier struct {
host string
port int
username string
password string
from string
}
func (n *SMTPWelcomeNotifier) SendWelcome(ctx context.Context, name, email string) error {
subject := "Selamat datang di platform kami!"
body := fmt.Sprintf("Halo %s, akun kamu sudah berhasil dibuat.", name)
return n.sendEmail(email, subject, body)
}
// Satu-satunya alasan untuk berubah: cara atau template email selamat datang berubah
Langkah 4: Gunakan slog standar untuk logging — tidak perlu wrapper custom
// Go 1.21+ sudah punya structured logging bawaan
// Tidak perlu Logger struct sendiri untuk kasus sederhana
import "log/slog"
slog.Info("user registered", "email", email, "name", name)
Langkah 5: UserService hanya sebagai orkestrator
// internal/user/service.go
package user
import (
"context"
"fmt"
"log/slog"
"github.com/example/app/internal/notification"
)
type PasswordHasher interface {
Hash(password string) (string, error)
}
type UserService struct {
validator UserValidator
repo *UserRepository
notifier notification.WelcomeNotifier
hasher PasswordHasher
}
func NewUserService(
repo *UserRepository,
notifier notification.WelcomeNotifier,
hasher PasswordHasher,
) *UserService {
return &UserService{
validator: UserValidator{},
repo: repo,
notifier: notifier,
hasher: hasher,
}
}
func (s *UserService) Register(ctx context.Context, name, email, password string) error {
// Validasi — delegasi ke UserValidator
if err := s.validator.ValidateRegistration(name, email, password); err != nil {
return fmt.Errorf("register: validation: %w", err)
}
// Cek duplikat — delegasi ke UserRepository
exists, err := s.repo.ExistsByEmail(ctx, email)
if err != nil {
return fmt.Errorf("register: check email: %w", err)
}
if exists {
return ErrEmailExists
}
// Hash password — delegasi ke PasswordHasher
hash, err := s.hasher.Hash(password)
if err != nil {
return fmt.Errorf("register: hash password: %w", err)
}
// Simpan — delegasi ke UserRepository
if err := s.repo.Save(ctx, NewUser{Name: name, Email: email, PasswordHash: hash}); err != nil {
return fmt.Errorf("register: save: %w", err)
}
// Notifikasi — delegasi ke WelcomeNotifier (async agar tidak memblok)
go func() {
if err := s.notifier.SendWelcome(context.Background(), name, email); err != nil {
slog.Error("failed to send welcome email", "email", email, "error", err)
}
}()
slog.Info("user registered", "email", email)
return nil
}
// Satu-satunya alasan untuk berubah: alur bisnis registrasi user berubah
flowchart TD
REQ["HTTP Handler\n(menerima request)"]
SVC["UserService\n(orkestrator alur bisnis)"]
VAL["UserValidator\n(aturan validasi)"]
REPO["UserRepository\n(akses database)"]
HASH["PasswordHasher\n(algoritma hashing)"]
NOTIF["WelcomeNotifier\n(template + provider email)"]
REQ -->|"Register(ctx, name, email, pass)"| SVC
SVC -->|"ValidateRegistration(...)"| VAL
SVC -->|"ExistsByEmail(...)"| REPO
SVC -->|"Hash(password)"| HASH
SVC -->|"Save(...)"| REPO
SVC -->|"SendWelcome(...) async"| NOTIF
style SVC fill:#4C9BE8,color:#fff
style VAL fill:#5CB85C,color:#fff
style REPO fill:#5CB85C,color:#fff
style HASH fill:#5CB85C,color:#fff
style NOTIF fill:#5CB85C,color:#fffHasilnya: perubahan template email hanya menyentuh SMTPWelcomeNotifier. Perubahan schema database hanya menyentuh UserRepository. Perubahan aturan validasi hanya menyentuh UserValidator. UserService hanya diubah jika alur bisnis registrasi itu sendiri berubah — itulah satu-satunya alasan perubahan yang sah untuknya.
SRP di Level Fungsi #
SRP tidak hanya berlaku untuk struct — ia juga berlaku untuk fungsi. Sebuah fungsi yang melakukan beberapa hal sekaligus adalah pelanggaran SRP di skala mikro, dan dampaknya sama: sulit ditest, sulit dibaca, mudah rusak.
// ANTI-PATTERN: satu fungsi melakukan parsing, validasi, dan transformasi sekaligus
func processOrderRequest(body []byte) (*Order, error) {
// Parsing
var req struct {
UserID string `json:"user_id"`
Items []OrderItem `json:"items"`
Note string `json:"note"`
}
if err := json.Unmarshal(body, &req); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
// Validasi
if req.UserID == "" {
return nil, errors.New("user_id is required")
}
if len(req.Items) == 0 {
return nil, errors.New("at least one item is required")
}
for _, item := range req.Items {
if item.Quantity <= 0 {
return nil, fmt.Errorf("invalid quantity for item %s", item.ProductID)
}
}
// Transformasi ke domain model
items := make([]DomainItem, len(req.Items))
for i, item := range req.Items {
items[i] = DomainItem{
ProductID: item.ProductID,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
}
}
return &Order{UserID: req.UserID, Items: items, Note: req.Note}, nil
}
// BENAR: setiap fungsi satu tanggung jawab, mudah ditest secara independen
type CreateOrderRequest struct {
UserID string `json:"user_id"`
Items []OrderItem `json:"items"`
Note string `json:"note"`
}
func parseCreateOrderRequest(body []byte) (CreateOrderRequest, error) {
var req CreateOrderRequest
if err := json.Unmarshal(body, &req); err != nil {
return req, fmt.Errorf("parse order request: %w", err)
}
return req, nil
}
func validateCreateOrderRequest(req CreateOrderRequest) error {
if req.UserID == "" {
return errors.New("user_id is required")
}
if len(req.Items) == 0 {
return errors.New("at least one item is required")
}
for _, item := range req.Items {
if item.Quantity <= 0 {
return fmt.Errorf("invalid quantity for item %s", item.ProductID)
}
}
return nil
}
func toOrderDomain(req CreateOrderRequest) Order {
items := make([]DomainItem, len(req.Items))
for i, item := range req.Items {
items[i] = DomainItem{
ProductID: item.ProductID,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
}
}
return Order{UserID: req.UserID, Items: items, Note: req.Note}
}
// Handler menggunakan ketiganya secara berurutan — alur jelas
func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
req, err := parseCreateOrderRequest(body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := validateCreateOrderRequest(req); err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
order := toOrderDomain(req)
// lanjut ke service...
}
Memisahkan ketiga fungsi ini memberikan keuntungan konkret di testing: test untuk validateCreateOrderRequest bisa dijalankan tanpa menyentuh JSON parsing sama sekali — dan sebaliknya.
SRP di Level Package #
Di Go, package adalah unit organisasi kode yang paling penting. SRP di level package berarti setiap package punya fokus yang jelas — dan bisa dideskripsikan dalam satu kalimat tanpa kata “dan”.
ANTI-PATTERN: package "utils" yang menampung segalanya
internal/utils/
├── email.go // kirim email
├── pdf.go // generate PDF
├── hash.go // bcrypt
├── jwt.go // token generation
├── pagination.go // pagination helper
├── validation.go // semua validasi
├── formatter.go // format currency, date
└── http.go // HTTP helpers
Masalah:
- Package ini di-import oleh hampir semua package lain
- Perubahan di email.go bisa memaksa recompile seluruh dependency tree
- Tidak ada boundary yang jelas — "utils" artinya apa saja
- Onboarding sulit: engineer baru tidak tahu harus mencari apa di mana
BENAR: setiap package punya tanggung jawab tunggal yang jelas
internal/
├── notification/
│ ├── email.go // ← "package yang mengirim email"
│ └── sms.go
├── document/
│ └── pdf.go // ← "package yang generate dokumen PDF"
├── auth/
│ ├── password.go // ← "package yang menangani auth"
│ └── token.go
├── pagination/
│ └── pagination.go // ← "package yang menangani paginasi"
└── money/
└── formatter.go // ← "package yang memformat nilai moneter"
Package yang namanya util, helper, atau common adalah sinyal kuat SRP dilanggar di level package. Package yang baik bisa dideskripsikan dengan kata benda domain: notification, auth, document, pricing, inventory.
SRP dan Testability #
Salah satu cara paling cepat untuk mengukur apakah SRP terpenuhi adalah melihat seberapa mudah komponen bisa ditest secara independen.
// Ketika SRP terpenuhi, setiap komponen mudah ditest sendiri:
// Test UserValidator — tidak butuh database, tidak butuh email server
func TestUserValidator_ValidateRegistration(t *testing.T) {
v := UserValidator{}
tests := []struct {
name string
input [3]string // name, email, password
wantErr bool
}{
{"valid input", [3]string{"Budi", "[email protected]", "password123"}, false},
{"empty name", [3]string{"", "[email protected]", "password123"}, true},
{"invalid email", [3]string{"Budi", "not-an-email", "password123"}, true},
{"short password", [3]string{"Budi", "[email protected]", "short"}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := v.ValidateRegistration(tt.input[0], tt.input[1], tt.input[2])
if (err != nil) != tt.wantErr {
t.Errorf("ValidateRegistration() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
// Test UserService — inject fake untuk semua dependency
func TestUserService_Register(t *testing.T) {
fakeRepo := &fakeUserRepository{}
fakeNotifier := &fakeWelcomeNotifier{}
fakeHasher := &fakePasswordHasher{hash: "hashed_password"}
svc := NewUserService(fakeRepo, fakeNotifier, fakeHasher)
err := svc.Register(context.Background(), "Budi", "[email protected]", "password123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fakeRepo.saved) != 1 {
t.Error("expected one user to be saved")
}
// Tidak butuh database nyata, tidak butuh SMTP server
}
Ketika menulis test dan kamu menemukan diri sedang menyiapkan mock untuk dependency yang tidak relevan dengan aspek yang sedang ditest — itu sinyal kuat SRP dilanggar. Test yang bersih adalah bukti bahwa tanggung jawab sudah terpisah dengan benar.
SRP dan Merge Conflict #
Salah satu dampak SRP yang jarang disebut tapi sangat nyata dirasakan tim adalah pengurangan merge conflict. Ketika tanggung jawab terpisah menjadi file yang berbeda, dua engineer yang mengerjakan hal yang berbeda hampir tidak pernah perlu menyentuh file yang sama.
flowchart LR
subgraph ANTI["Tanpa SRP — Satu File"]
US["user_service.go\n(validasi + DB + email + log)"]
E1["Engineer A\n(ubah template email)"] -->|"edit"| US
E2["Engineer B\n(ubah validasi password)"] -->|"edit"| US
US -->|"MERGE CONFLICT\nbukan karena\nkerjaan sama"| MC["💥 Conflict"]
end
subgraph SRP_GOOD["Dengan SRP — File Terpisah"]
VAL2["validator.go"]
NOTIF2["welcome_notifier.go"]
E3["Engineer A\n(ubah template email)"] -->|"edit"| NOTIF2
E4["Engineer B\n(ubah validasi password)"] -->|"edit"| VAL2
NOTIF2 & VAL2 -->|"No conflict —\nbeda file"| OK["✓ Clean merge"]
end
style MC fill:#D9534F,color:#fff
style OK fill:#5CB85C,color:#fffIni bukan kebetulan — ini adalah konsekuensi langsung dari SRP. Ketika setiap komponen punya satu alasan untuk berubah, dua perubahan dengan alasan yang berbeda tidak pernah perlu menyentuh file yang sama.
Kapan Pemisahan Justru Berlebihan #
SRP bukan berarti setiap fungsi harus jadi struct, setiap struct harus jadi package, atau setiap package harus jadi service terpisah. Ada titik di mana over-decomposition justru merusak keterbacaan.
PEMISAHAN YANG BERLEBIHAN:
// Tidak perlu struct untuk sesuatu yang cukup jadi fungsi
type AgeValidator struct{}
func (v AgeValidator) IsAdult(age int) bool { return age >= 18 }
// → Cukup jadi fungsi: func isAdult(age int) bool { return age >= 18 }
// Tidak perlu interface untuk sesuatu yang tidak akan di-swap atau di-mock
type FileReader interface { Read(path string) ([]byte, error) }
type DefaultFileReader struct{}
func (r DefaultFileReader) Read(path string) ([]byte, error) { return os.ReadFile(path) }
// → Cukup panggil os.ReadFile langsung
// Tidak perlu layer UseCase yang hanya meneruskan panggilan ke Repository
type GetUserUseCase struct{ repo UserRepository }
func (u *GetUserUseCase) Execute(id string) (*User, error) {
return u.repo.FindByID(id) // tidak ada logic tambahan
}
// → Ini pass-through layer, melanggar KISS bukan memenuhi SRP
PANDUAN PRAKTIS:
Pisahkan ketika ada alasan untuk berubah yang berbeda — bukan hanya karena
"seharusnya dipisah". Jika satu tanggung jawab diubah tidak pernah memaksa
yang lain berubah, dan keduanya sudah di satu tempat, tidak ada urgensi
untuk memisahnya.
Over-decomposition sama buruknya dengan GodObject. Ketika sebuah alur sederhana tersebar di 10 file kecil yang saling memanggil, pembaca harus melompat ke sana-sini hanya untuk memahami satu flow. SRP adalah tentang kohesi yang tepat — bukan jumlah file yang maksimal.
Anti-Pattern dalam Satu Pandangan #
// ✗ GodService — terlalu banyak tanggung jawab dalam satu struct
type UserService struct {
db *sql.DB
smtp *smtp.Client
s3 *s3.Client
pdf *pdf.Generator
// 8 dependency lagi yang tidak semua method butuhkan
}
// ✗ Method "And" — nama yang mengungkapkan lebih dari satu tanggung jawab
func (s *UserService) ValidateAndSaveAndNotify(user User) error { ... }
// ✗ Package "util" yang menampung semua yang tidak ada tempat lain
package util
// email, pdf, hash, pagination, validation, formatter — semua di sini
// ✗ Satu fungsi yang melakukan parsing + validasi + transformasi
func processRequest(body []byte) (*DomainModel, error) {
// 50 baris yang melakukan tiga hal sekaligus
}
// ✗ Logging, business logic, dan database access bercampur
func (s *Service) DoSomething() error {
log.Info("starting")
result := s.db.Query("SELECT ...")
if result.Error != nil {
log.Error("db error")
sendAlert() // side effect di tengah business logic
}
log.Info("done")
return nil
}
Checklist Review SRP #
STRUCT DAN SERVICE:
□ Nama struct mencerminkan satu domain atau tanggung jawab spesifik
□ Constructor tidak memiliki lebih dari 4–5 dependency
□ Semua field digunakan oleh lebih dari satu method
□ Bisa menjawab "satu-satunya alasan untuk mengubah struct ini adalah..."
tanpa kata "atau"
METHOD DAN FUNGSI:
□ Tidak ada nama method yang mengandung "And"
□ Fungsi tidak lebih dari ~30 baris kecuali ada justifikasi kuat
□ Tidak ada komentar blok "// bagian X" di tengah fungsi —
setiap bagian seharusnya jadi fungsi terpisah
PACKAGE:
□ Package bisa dideskripsikan dalam satu kalimat tanpa kata "dan"
□ Package tidak bernama "util", "helper", atau "common"
□ Package tidak di-import oleh hampir semua package lain
□ Setiap file dalam package relevan dengan satu domain
TESTABILITY:
□ Unit test tidak butuh mock untuk dependency yang tidak relevan
□ Test bisa ditulis tanpa setup database atau external service
kecuali yang memang sedang ditest (integration test)
□ Setiap komponen bisa ditest secara independen
Ringkasan #
- SRP bukan tentang ukuran — bukan “satu method satu baris” atau “file harus kecil”. SRP tentang alasan untuk berubah: sebuah modul hanya boleh dipaksa berubah oleh satu aktor atau satu domain perubahan.
- Tes SRP yang paling praktis: tulis “modul X harus diubah jika…” — jika kalimatnya butuh kata “atau”, SRP dilanggar.
- Empat dampak nyata: perubahan yang tidak terduga karena shared state, test yang butuh setup kompleks untuk dependency yang tidak relevan, merge conflict yang terjadi bukan karena kerjaan sama, dan onboarding yang lambat.
- Dari GodService ke komponen terfokus: pecah secara bertahap —
UserValidatoruntuk validasi,UserRepositoryuntuk akses data,WelcomeNotifieruntuk notifikasi,PasswordHasheruntuk keamanan.UserServicehanya sebagai orkestrator alur bisnis.- SRP di level fungsi: pisahkan parsing, validasi, dan transformasi menjadi fungsi terpisah. Nama yang mengandung “And” adalah tanda yang perlu dipisah.
- SRP di level package: hindari package
util,helper,common. Setiap package bisa dideskripsikan dengan kata benda domain yang jelas.- SRP dan testability: komponen yang memenuhi SRP bisa ditest secara independen tanpa menyiapkan infrastruktur yang tidak relevan.
- SRP dan merge conflict: ketika setiap komponen punya satu alasan untuk berubah, dua perubahan dengan alasan berbeda hampir tidak pernah menyentuh file yang sama.
- Hindari over-decomposition: pass-through layer tanpa logic adalah pelanggaran KISS, bukan penerapan SRP. Pisahkan ketika ada perbedaan nyata dalam alasan perubahan — bukan hanya karena “seharusnya dipisah”.