Apa itu SOLID?
SOLID adalah singkatan dari lima prinsip desain yang membantu Anda menulis kode yang mudah dipelihara, mudah ditest, dan mudah diperluas. Mari saya jelaskan dengan analogi sehari-hari, lalu tunjukkan kode nyata dari sistem fleet kita.
| Huruf | Prinsip | Analogi Sederhana |
|---|---|---|
| S | Single Responsibility | Chef memasak. Pelayan melayani. Jangan suruh chef melayani meja. |
| O | Open/Closed | Colokan listrik menerima steker baru tanpa perlu diubah kabelnya. |
| L | Liskov Substitution | Jika Anda pesan "kendaraan", kendaraan apapun (mobil, truk, van) harusnya berfungsi. |
| I | Interface Segregation | Remote TV tidak seharusnya punya tombol DVD player. |
| D | Dependency Inversion | Lampu bergantung pada "listrik", bukan pada "PLTU di jalan ke-5". |
S — Single Responsibility Principle
"Sebuah class seharusnya hanya punya satu alasan untuk berubah."
Masalah: God Controller
// ❌ BURUK — Controller ini melakukan SEGALANYA
@Post()
async createDelivery(@Body() dto: CreateDeliveryDto) {
// Validasi aturan bisnis
const driver = await this.driverRepo.findOne(dto.driverId);
if (driver.status !== 'active') throw new BadRequestException();
// Buat delivery
const delivery = await this.deliveryRepo.save({ ...dto });
// Kirim notifikasi
await this.emailService.send(driver.email, 'Pengiriman baru');
await this.smsService.send(driver.phone, 'Cek aplikasi');
// Log audit
await this.auditRepo.save({ action: 'delivery_created' });
return delivery;
}
Versi yang Di-refactor
// ✅ BAIK — Setiap class punya SATU tanggung jawab
// 1. Logika validasi
@Injectable()
export class DeliveryValidator {
async validate(dto: CreateDeliveryDto): Promise<void> {
const driver = await this.driverRepo.findOneOrFail(dto.driverId);
if (driver.status !== 'active') throw new BadRequestException('Driver tidak aktif');
}
}
// 2. Logika bisnis
@Injectable()
export class DeliveryService {
async create(dto: CreateDeliveryDto, userId: string): Promise<Delivery> {
await this.validator.validate(dto);
const delivery = await this.deliveryRepo.save({ ...dto, status: 'scheduled' });
this.eventEmitter.emit('delivery.created', { delivery, userId });
return delivery;
}
}
// 3. Controller — tipis, hanya menangani HTTP
@Controller('api/deliveries')
export class DeliveryController {
@Post()
create(@Body() dto: CreateDeliveryDto, @Req() req: Request) {
return this.deliveryService.create(dto, req.user.id);
}
}
O — Open/Closed Principle
"Terbuka untuk ekstensi, tertutup untuk modifikasi."
// ❌ BURUK — Harus modifikasi class setiap kali tambah channel
class NotificationSender
{
public function send(string $channel, string $to, string $message): void
{
if ($channel === 'email') { /* ... */ }
elseif ($channel === 'sms') { /* ... */ }
elseif ($channel === 'whatsapp') { /* ditambah kemudian */ }
}
}
// ✅ BAIK — Terbuka untuk ekstensi
interface NotificationChannel
{
public function send(string $to, string $message): void;
public function supports(string $channel): bool;
}
class EmailChannel implements NotificationChannel { /* ... */ }
class SmsChannel implements NotificationChannel { /* ... */ }
// Tambah WhatsApp? Buat class baru. TIDAK ADA kode existing yang dimodifikasi.
class WhatsAppChannel implements NotificationChannel { /* ... */ }
D — Dependency Inversion Principle
"Bergantung pada abstraksi, bukan implementasi konkret."
// ❌ BURUK — Service bergantung pada implementasi konkret
@Injectable()
export class FleetService {
constructor(private readonly mysqlRepo: MysqlVehicleRepository) {} // Konkret!
}
// ✅ BAIK — Service bergantung pada interface (abstraksi)
export interface VehicleRepository {
findAll(): Promise<Vehicle[]>;
findById(id: string): Promise<Vehicle | null>;
save(vehicle: Vehicle): Promise<Vehicle>;
}
@Injectable()
export class FleetService {
constructor(
@Inject('VehicleRepository')
private readonly vehicleRepo: VehicleRepository, // Abstraksi!
) {}
}
Mengapa? Besok, jika Anda ganti dari TypeORM ke Prisma, Anda hanya mengubah implementasi repository — service bahkan tidak tahu database berubah.
Design Pattern dalam Sistem Fleet
| Pattern | Dimana Kita Gunakan | Mengapa |
|---|---|---|
| Repository | Lapisan akses database | Memisahkan logika bisnis dari database |
| Strategy | Channel notifikasi | Algoritma berbeda, interface sama |
| Observer | Sistem event/listener | Bereaksi terhadap event tanpa tight coupling |
| Factory | Pembuatan laporan | Membuat tipe laporan berbeda secara dinamis |
Checklist Code Review (Untuk Mentoring Developer Junior)
Ketika saya me-review kode dari developer junior/medior, saya periksa:
- Apakah class ini melakukan lebih dari satu hal? → Pelanggaran SRP
- Apakah saya perlu modifikasi kode existing untuk menambah fitur ini? → Pelanggaran OCP
- Apakah ada tipe
anydi TypeScript? → Masalah type safety - Apakah ada magic string/number? → Gunakan constants atau enum
- Apakah fungsinya lebih dari 20 baris? → Mungkin perlu dipecah
- Apakah test butuh database asli? → Masalah dependency inversion
Checklist ini bukan tentang ketat — ini tentang membangun pemahaman bersama tentang kualitas.
Selanjutnya
Di Part 7, kita akan mentransformasi aplikasi dari monolith menjadi microservices. Kita akan bahas service boundaries, komunikasi antar-service, saga pattern, dan — yang terpenting — kapan TIDAK menggunakan microservices.
Ini adalah Part 6 dari seri Fleet Management System. SOLID bukan teori akademis — ini fondasi dari setiap codebase yang bisa dipelihara.
