Artikel ini tersedia dalam Bahasa Inggris

🇬🇧 Read in English

22 Mei 2026

🇮🇩 Bahasa Indonesia

Fleet Management Part 6: Prinsip SOLID & Design Pattern dalam Praktik

Menerapkan prinsip SOLID dan design pattern dalam kode nyata. Refactor kode buruk menjadi clean code dengan contoh praktis di NestJS dan Laravel.

4 min read

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.

HurufPrinsipAnalogi Sederhana
SSingle ResponsibilityChef memasak. Pelayan melayani. Jangan suruh chef melayani meja.
OOpen/ClosedColokan listrik menerima steker baru tanpa perlu diubah kabelnya.
LLiskov SubstitutionJika Anda pesan "kendaraan", kendaraan apapun (mobil, truk, van) harusnya berfungsi.
IInterface SegregationRemote TV tidak seharusnya punya tombol DVD player.
DDependency InversionLampu 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

PatternDimana Kita GunakanMengapa
RepositoryLapisan akses databaseMemisahkan logika bisnis dari database
StrategyChannel notifikasiAlgoritma berbeda, interface sama
ObserverSistem event/listenerBereaksi terhadap event tanpa tight coupling
FactoryPembuatan laporanMembuat tipe laporan berbeda secara dinamis

Checklist Code Review (Untuk Mentoring Developer Junior)

Ketika saya me-review kode dari developer junior/medior, saya periksa:

  1. Apakah class ini melakukan lebih dari satu hal? → Pelanggaran SRP
  2. Apakah saya perlu modifikasi kode existing untuk menambah fitur ini? → Pelanggaran OCP
  3. Apakah ada tipe any di TypeScript? → Masalah type safety
  4. Apakah ada magic string/number? → Gunakan constants atau enum
  5. Apakah fungsinya lebih dari 20 baris? → Mungkin perlu dipecah
  6. 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.

Lanjut Membaca

Previous article thumbnail

← Artikel Sebelumnya

Fleet Management Part 5: Database Design — PostgreSQL, MySQL & Redis

Artikel Selanjutnya →

Fleet Management Part 7: From Monolith to Microservices

Next article thumbnail