Menerapkan SOLID pada Kalkulasi Corrosion Risk Assessment
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/
├── 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.
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.
CalculationFactorBasememastikan 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
foreachatas 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.