Bondan Prakoso - Microsoft Visual Basic [design]
1200, 1440 6720 x 4800
Portfolio - modBlogPost (Code)

Menerapkan SOLID pada Kalkulasi Corrosion Risk Assessment

12 Agustus 2026  ▪  Kategori: Arsitektur  ▪  Perkiraan baca: 10 menit

Catatan

Tulisan ini membahas struktur dan pola desain modul kalkulasi risiko korosi. Nama kelas dan antarmuka ditulis apa adanya karena itulah inti pembahasannya, tetapi seluruh angka ambang, bobot faktor, dan tabel penilaian sengaja diganti nilai ilustratif.

Modul Corrosion Risk Assessment pada aplikasi AIMS (Asset Integration Management System) bertugas menghitung Probability of Failure (PoF) untuk aset dan jalur pipa. Kedengarannya seperti satu rumus. Kenyataannya, modul ini berisi hampir dua ratus kelas perhitungan.

Angka itu bukan tanda kode yang membengkak. Ia konsekuensi langsung dari domainnya: penilaian risiko korosi tidak pernah satu rumus, melainkan matriks.

Kenapa jumlahnya jadi sebanyak itu

Penilaian dibelah dulu menjadi empat wilayah, sesuai letak dan sisi aset yang dinilai:

Calculator▼ Struktur Folder▼
Calculator/
├── Interfaces/ICalculationFactor.cs      // kontrak tunggal seluruh faktor
├── IncompleteDataException.cs
├── NotSusceptibleException.cs
│
├── PipelineInternal/    // 6 mekanisme: CO2, MIC, O2, H2S, SSC, HIC
├── PipelineExternal/    // 3 penempatan: Onshore, Riser, Subsea
├── TopsideInternal/     // 4 mekanisme: CO2, MIC, O2, H2S
└── TopsideExternal/     // 4 mekanisme: ExtCS, CS-CUI, ExtSS, SS-CUI

    masing-masing berisi:
      <Mekanisme>/       // kelas-kelas faktor penilaian
      Mechanisms/        // pembungkus + daftar registrasi
      PoF/               // perhitungan Probability of Failure

Setiap mekanisme punya daftar faktor penilaiannya sendiri: keberadaan air bebas, kandungan gas, pH, suhu operasi, efektivitas pigging, program inhibitor, sumber data laju korosi, riwayat kebocoran, dan seterusnya. Setiap faktor punya tabel skor dan bobotnya masing-masing.

Kalikan tujuh belas mekanisme dengan sepuluh sampai dua belas faktor, dan angkanya ketemu.

Pertanyaan desainnya

Dengan volume seperti itu, pertanyaannya bukan “bagaimana menghitungnya?” melainkan “bagaimana supaya menambah satu faktor tidak berarti menyentuh kode yang sudah berjalan?” Standar penilaian direvisi berkala, dan tiap revisi berarti ada tabel skor yang bergeser.

Satu kontrak yang sangat kecil

Seluruh bangunan bertumpu pada satu antarmuka dengan tiga anggota. Tidak lebih:

public interface ICalculationFactor
{
    string GetDescription();
    double GetScore();
    double GetWeight();
}

Inilah Interface Segregation Principle dalam bentuk paling sederhana. Sebuah faktor tidak perlu tahu cara menyimpan dirinya, cara menampilkan dirinya di layar, atau siapa yang memanggilnya. Ia hanya menjawab tiga pertanyaan: berapa skornya, berapa bobotnya, dan bagaimana menjelaskannya ke penilai.

Anggota ketiga — GetDescription() — kelihatan seperti pelengkap, padahal justru yang paling sering dipakai. Tim inspeksi tidak cukup melihat angka; mereka perlu tahu kenapa sebuah faktor bernilai tinggi. Deskripsi itulah yang muncul di laporan.

Kelas dasar sebagai cetakan

Semua faktor mewarisi satu kelas dasar yang mengatur urutan pengerjaannya:

public abstract class CalculationFactorBase
{
    protected string _description;
    protected double _score;
    private bool _isMissing;

    public bool IsMissing => _isMissing;

    protected void InitializeCalculation()
    {
        _description = ComputeDescription();
        _score       = ComputeScore();
    }

    protected void SetMissing(string label)
    {
        _score       = 0;
        _description = "Missing: " + label;
        _isMissing   = true;
    }

    protected abstract string ComputeDescription();
    protected abstract double ComputeScore();

    public string GetDescription() => _description;
    public double GetScore()       => _score;
}

Kelas turunannya cuma mengisi dua metode. Selebihnya sudah diurus. Sebuah faktor pH, misalnya, isinya hanya validasi masukan lalu dua tabel keputusan:

public class pHCalculation : BaseCO2CorrosionCalculation, ICalculationFactor
{
    private const double WEIGHT = 0.10;   // nilai ilustratif
    private readonly double _pH;

    public pHCalculation(PipelineInternalAssessmentContext context)
        : base(context)
    {
        if (context.PHValue == null)
        { SetMissing("pH Value"); return; }

        if (context.PHValue.Value < 0 || context.PHValue.Value > 14)
        { SetMissing("pH Value (out of range 0-14)"); return; }

        _pH = context.PHValue.Value;
        InitializeCalculation();
    }

    protected override string ComputeDescription() { /* teks per rentang */ }
    protected override double ComputeScore()       { /* skor per rentang */ }

    public double GetWeight() => WEIGHT;
}

Ketika standar direvisi dan rentang pH bergeser, yang dibuka satu berkas. Pengujian ulang cukup pada satu kelas. Tidak ada mekanisme lain yang ikut terseret.

Data yang hilang bukan nol

Ini pelajaran yang datang dari lapangan, bukan dari buku. Pada sistem penilaian berbasis skor, nilai 0 punya arti bisnis: risiko paling rendah. Sementara data yang belum diisi juga secara alami terbaca nol.

Kalau keduanya tidak dibedakan, aset yang datanya belum lengkap akan tampil sebagai aset paling aman di seluruh laporan. Persis kebalikan dari kenyataannya.

/// Dilempar saat masukan numerik wajib belum tersedia (NULL di sumber data).
/// Berbeda dari nilai nol yang sah: hanya data yang benar-benar hilang yang
/// memicu ini, sehingga mekanismenya bisa ditandai "Incomplete data"
/// alih-alih diam-diam diberi skor 0 yang menyesatkan.
public class IncompleteDataException : Exception
{
    public string ParameterName { get; }
}

/// Dilempar saat jenis material memang tidak rentan terhadap mekanisme
/// korosi/keretakan yang sedang dinilai.
public class NotSusceptibleException : Exception
{
    public string FactorCode  { get; }   // "CO2", "MIC", "O2", "H2S", "SSC", "HIC"
    public bool   IsSusceptible => false;
}

Dua pengecualian ini memisahkan tiga keadaan yang sekilas mirip tapi maknanya jauh berbeda:

  • Not applicable — mekanisme ini tidak relevan untuk konfigurasi aset tersebut.
  • Not susceptible — relevan, tapi materialnya memang tidak rentan.
  • Incomplete data — relevan dan rentan, tapi datanya belum ada. Perlu tindakan.

Hanya keadaan ketiga yang menuntut orang bergerak. Membedakannya di tingkat kode berarti laporan bisa membedakannya juga.

Menghilangkan percabangan besar

Bagian yang paling terasa manfaatnya ada di orkestrator. Setiap mekanisme dibungkus di balik satu antarmuka kecil:

public interface IPipelineInternalMechanism
{
    string Code { get; }
    bool IsEnabled(CRA2_PIPELINE_INTERNAL entity);
    void Apply(CRA2_PIPELINE_INTERNAL entity, PipelineInternalAssessmentContext ctx);
}

Lalu didaftarkan di satu tempat:

public static class PipelineInternalMechanisms
{
    public static readonly IReadOnlyList<IPipelineInternalMechanism> All =
        new IPipelineInternalMechanism[]
        {
            new Co2Mechanism(),
            new MicMechanism(),
            new O2Mechanism(),
            new H2sMechanism(),
            new SscMechanism(),
            new HicMechanism()
        };
}

Dengan begitu, pemanggilnya tidak lagi berisi rantai if sepanjang layar. Ia hanya menjalankan seluruh mekanisme yang terdaftar:

foreach (var mechanism in PipelineInternalMechanisms.All)
{
    mechanism.Apply(entity, context);
}

Inilah Open/Closed Principle yang benar-benar terpakai, bukan sekadar disebut di dokumen desain. Menambah mekanisme korosi baru berarti membuat satu kelas dan menambah satu baris di daftar. Berkas orkestrator tidak pernah dibuka lagi.

Skor akhir yang seragam

Perhitungan PoF-nya sendiri ternyata sama untuk semua mekanisme, jadi ia naik ke kelas dasar:

public abstract class BasePoFCalculation
{
    protected readonly IList<ICalculationFactor> _factors;

    /// PoF Score = Σ (skor faktor × bobot faktor)
    public virtual double GetParameterScore()
        => _factors.Sum(f => f.GetScore() * f.GetWeight());

    public virtual int GetFinalPoF() => MapScoreToPoF(GetParameterScore());

    public static string MapPoFToStatus(int pof)
    {
        switch (pof)
        {
            case 2:  return "Unlikely";
            case 3:  return "Moderate";
            case 4:  return "Likely";
            case 5:  return "Almost Certain";
            default: return "Rare";
        }
    }
}

Sebagian mekanisme membedakan PoF awal dari PoF akhir — PoF awal menggambarkan risiko lingkungan mentah, PoF akhir sudah memperhitungkan mitigasi. Perbedaan itu ditampung satu kelas turunan (BaseInitialPoFCalculation), sementara mekanisme yang tidak membedakannya cukup memakai kelas dasar apa adanya. Keduanya tetap bisa saling menggantikan di mata pemanggil — Liskov Substitution Principle.

Bonus yang tidak direncanakan

Ada satu manfaat yang baru terasa setelah struktur ini berjalan beberapa bulan.

Bobot tiap faktor semula juga tersimpan di kolom basis data, supaya bisa ditampilkan di halaman pengaturan. Masalahnya klasik: kolom itu dirawat manual, dan lama-lama isinya melenceng dari bobot yang benar-benar dipakai kalkulator. Halaman pengaturan menunjukkan satu angka, mesin menghitung dengan angka lain.

Karena GetWeight() pada setiap faktor adalah konstanta murni, bobotnya bisa dibaca langsung dari kelas kalkulasinya lewat refleksi — tanpa perlu konteks penilaian yang sungguhan:

private sealed class FactorEntry
{
    public string FactorCode;
    public string SectionCode;   // CRITICAL_FACTOR | MITIGATION | MONITORING
    public Type   CalculationType;
}

Sumber kebenaran bobot kembali cuma satu: kode kalkulatornya. Kolom basis data yang bisa melenceng tidak dipakai lagi. Ini konsekuensi yang tidak saya rencanakan sejak awal — ia muncul begitu saja karena tiap faktor sudah punya kontrak seragam yang bisa ditanyai.

Yang saya bawa dari modul ini

  • Kontrak yang kecil menskalakan jauh lebih baik. Antarmuka tiga anggota menopang hampir dua ratus kelas tanpa pernah perlu diubah.
  • Kelas dasar bukan untuk berbagi kode, tapi untuk mengunci urutan. CalculationFactorBase memastikan tidak ada faktor yang lupa menangani data hilang.
  • Bedakan keadaan yang maknanya berbeda. Tiga keadaan yang sama-sama terlihat “nol” menuntut tiga tindakan yang berbeda di lapangan.
  • Daftar registrasi mengalahkan percabangan. Satu foreach atas daftar mekanisme menghapus rantai percabangan yang tak pernah selesai bertambah.
  • Nama kelas ikut nama di dokumen standar. Ketika tim inspeksi menyebut satu mekanisme, saya langsung tahu berkas mana yang dibuka.

Poin terakhir yang paling sering diremehkan. Kode yang penamaannya selaras dengan istilah orang lapangan menghemat lebih banyak waktu daripada optimasi apa pun, karena ia memangkas jarak antara ruang rapat dan editor kode.

Catatan penutup

SOLID bukan tujuan, dan memaksakannya pada kode kecil justru membuat semuanya terasa berat. Pertanyaan pemandunya selalu sama: bagian mana yang paling sering berubah, dan bagaimana memagarinya supaya perubahan itu tidak merembet?

Di modul ini jawabannya jelas sejak awal — yang berubah adalah tabel skor tiap faktor. Maka faktor itulah yang dijadikan unit terkecil, diberi kontrak seragam, dan dibiarkan berkembang biak sampai dua ratus kelas tanpa satu pun di antaranya perlu tahu keberadaan yang lain.

Pertanyaan lanjutannya — seberapa besar sebenarnya pengaruh cara ini terhadap kemudahan pemeliharaan, dalam angka — saya ukur secara terpisah dan tulis di tulisan berikutnya.

◄ Tulisan Sebelumnya Daftar Tulisan Tulisan Berikutnya ►
Selamat datang di portofolio Bondan Prakoso  ▪  System Analyst & Senior IT Programmer  ▪  VB.NET · C# .NET · ASP.NET · SQL Server · React.js  ▪  Situs ini dibangun murni dengan HTML + CSS, tanpa satu baris JavaScript pun. Ln 1, Col 1 INS Ready