Meskipun ini bisa menjadi pertanyaan agnostik bahasa pemrograman, saya tertarik pada jawaban yang menargetkan ekosistem .NET.
Ini adalah skenario: misalkan kita perlu mengembangkan aplikasi konsol sederhana untuk administrasi publik. Aplikasi ini tentang pajak kendaraan. Mereka (hanya) memiliki aturan bisnis berikut:
1.a) Jika kendaraan adalah mobil dan terakhir kali pemiliknya membayar pajak adalah 30 hari yang lalu maka pemilik harus membayar lagi.
1.b) Jika kendaraan itu adalah sepeda motor dan terakhir kali pemiliknya membayar pajak adalah 60 hari yang lalu maka pemilik harus membayar lagi.
Dengan kata lain, jika Anda memiliki mobil, Anda harus membayar setiap 30 hari atau jika Anda memiliki sepeda motor, Anda harus membayar setiap 60 hari.
Untuk setiap kendaraan dalam sistem, aplikasi harus menguji aturan tersebut dan mencetak kendaraan tersebut (nomor plat dan informasi pemilik) yang tidak memuaskan mereka.
Yang saya inginkan adalah:
2.a) Sesuai dengan prinsip SOLID (terutama prinsip Terbuka / tertutup).
Yang tidak saya inginkan adalah (saya pikir):
2.b) Domain anemia, oleh karena itu logika bisnis harus masuk ke dalam entitas bisnis.
Saya mulai dengan ini:
public class Person
// You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
{
public string Name { get; set; }
public string Surname { get; set; }
}
public abstract class Vehicle
{
public string PlateNumber { get; set; }
public Person Owner { get; set; }
public DateTime LastPaidTime { get; set; }
public abstract bool HasToPay();
}
public class Car : Vehicle
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
}
}
public class Motorbike : Vehicle
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
}
}
public class PublicAdministration
{
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.HasToPay());
}
private IEnumerable<Vehicle> GetAllVehicles()
{
throw new NotImplementedException();
}
}
class Program
{
static void Main(string[] args)
{
PublicAdministration administration = new PublicAdministration();
foreach (var vehicle in administration.GetVehiclesThatHaveToPay())
{
Console.WriteLine("Plate number: {0}\tOwner: {1}, {2}", vehicle.PlateNumber, vehicle.Owner.Surname, vehicle.Owner.Name);
}
}
}
2.a: Prinsip Terbuka / tertutup dijamin; jika mereka menginginkan pajak sepeda yang baru saja Anda warisi dari Kendaraan, maka Anda mengganti metode HasToPay dan Anda selesai. Prinsip terbuka / tertutup dipenuhi melalui warisan sederhana.
"Masalahnya" adalah:
3.a) Mengapa kendaraan harus tahu jika harus membayar? Bukan masalah Administrasi Publik? Jika administrasi publik mengatur perubahan pajak mobil, mengapa Mobil harus berubah? Saya pikir Anda harus bertanya: "lalu mengapa Anda memasukkan metode HasToPay di dalam Vehicle?", Dan jawabannya adalah: karena saya tidak ingin menguji untuk jenis kendaraan (typeof) di dalam Administrasi Publik. Apakah Anda melihat alternatif yang lebih baik?
3.b) Mungkin pembayaran pajak adalah masalah kendaraan, atau mungkin desain awal Person-Vehicle-Car-Motorbike-Publicadalah salah, atau saya butuh filsuf dan analis bisnis yang lebih baik. Solusi dapat berupa: pindahkan metode HasToPay ke kelas lain, kita dapat menyebutnya Pembayaran Pajak. Lalu, kami membuat dua kelas turunan Pembayaran Pajak; Pembayaran CarTax dan Pembayaran Motor. Kemudian kami menambahkan properti Pembayaran abstrak (dari jenis Pembayaran Pajak) ke kelas Kendaraan dan kami mengembalikan contoh Pembayaran Pajak yang benar dari kelas Mobil dan Sepeda Motor:
public abstract class TaxPayment
{
public abstract bool HasToPay();
}
public class CarTaxPayment : TaxPayment
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
}
}
public class MotorbikeTaxPayment : TaxPayment
{
public override bool HasToPay()
{
return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
}
}
public abstract class Vehicle
{
public string PlateNumber { get; set; }
public Person Owner { get; set; }
public DateTime LastPaidTime { get; set; }
public abstract TaxPayment Payment { get; }
}
public class Car : Vehicle
{
private CarTaxPayment payment = new CarTaxPayment();
public override TaxPayment Payment
{
get { return this.payment; }
}
}
public class Motorbike : Vehicle
{
private MotorbikeTaxPayment payment = new MotorbikeTaxPayment();
public override TaxPayment Payment
{
get { return this.payment; }
}
}
Dan kami menerapkan proses lama dengan cara ini:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Tetapi sekarang kode ini tidak dapat dikompilasi karena tidak ada anggota LastPaidTime di dalam CarTaxPayment / MotorbikeTaxPayment / TaxPayment. Saya mulai melihat CarTaxPayment dan MotorbikeTaxPayment lebih seperti "implementasi algoritma" daripada entitas bisnis. Entah bagaimana sekarang "algoritma" tersebut membutuhkan nilai LastPaidTime. Tentu, kita dapat memberikan nilainya kepada konstruktor Pembayaran Pajak, tetapi itu akan melanggar enkapsulasi kendaraan, atau tanggung jawab, atau apa pun yang disebut oleh para penginjil OOP itu, bukan?
3.c) Misalkan kita sudah memiliki Entitas Kerangka ObjectContext (yang menggunakan objek domain kita; Orang, Kendaraan, Mobil, Sepeda Motor, Administrasi Publik). Bagaimana yang Anda lakukan untuk mendapatkan referensi ObjectContext dari metode PublicAdministration.GetAllVehicles dan karenanya mengimplementasikan funcionality?
3.d) Jika kita juga memerlukan referensi ObjectContext yang sama untuk CarTaxPayment.HasToPay tetapi berbeda untuk MotorbikeTaxPayment.HasToPay, siapa yang bertanggung jawab atas "menyuntikkan" atau bagaimana Anda meneruskan referensi tersebut ke kelas pembayaran? Dalam hal ini, menghindari domain yang anemia (dan karenanya tidak ada objek layanan), tidak membawa kita ke bencana?
Apa pilihan desain Anda untuk skenario sederhana ini? Jelas contoh yang saya tunjukkan "terlalu rumit" untuk tugas yang mudah, tetapi pada saat yang sama idenya adalah untuk mematuhi prinsip-prinsip SOLID dan mencegah domain anemia (yang telah ditugaskan untuk menghancurkan).
sumber
Saya pikir dalam situasi seperti ini, di mana Anda ingin aturan bisnis ada di luar objek target, pengujian untuk tipe lebih dari dapat diterima.
Tentu saja, dalam banyak situasi Anda harus menggunakan pola Strategi dan membiarkan objek menangani hal-hal itu sendiri, tetapi itu tidak selalu diinginkan.
Di dunia nyata, petugas akan melihat jenis kendaraan dan mencari data pajak berdasarkan itu. Mengapa perangkat lunak Anda tidak harus mengikuti aturan bisnis yang sama?
sumber
Anda memiliki ini mundur. Domain anemia adalah domain yang logika berada di luar entitas bisnis. Ada banyak alasan mengapa menggunakan kelas tambahan untuk mengimplementasikan logika adalah ide yang buruk; dan hanya pasangan untuk mengapa Anda harus melakukannya.
Karenanya alasan mengapa disebut anemik: kelas tidak memiliki apa pun di dalamnya ..
Apa yang akan saya lakukan:
sumber
Tidak. Seperti yang saya mengerti, seluruh aplikasi Anda adalah tentang perpajakan. Jadi kekhawatiran tentang apakah kendaraan harus dikenakan pajak tidak lagi menjadi perhatian Administrasi Publik maka hal lain di seluruh program. Tidak ada alasan untuk meletakkannya di tempat lain selain di sini.
Kedua cuplikan kode ini hanya berbeda dengan konstanta. Alih-alih menimpa seluruh fungsi HasToPay () menimpa
int DaysBetweenPayments()
fungsi. Sebut itu alih-alih menggunakan konstanta. Jika ini yang mereka benar-benar berbeda, saya akan meninggalkan kelas.Saya juga menyarankan bahwa Sepeda Motor / Mobil harus menjadi properti kategori kendaraan daripada subclass. Itu memberi Anda lebih banyak fleksibilitas. Misalnya, membuatnya mudah untuk mengubah kategori sepeda motor atau mobil. Tentu saja sepeda tidak mungkin diubah menjadi mobil di kehidupan nyata. Tapi Anda tahu bahwa kendaraan akan dimasukkan salah dan perlu diubah nanti.
Tidak juga. Objek yang dengan sengaja mengirimkan datanya ke objek lain sebagai parameter bukanlah masalah enkapsulasi yang besar. Perhatian sebenarnya adalah objek lain yang mencapai dalam objek ini untuk menarik data keluar atau memanipulasi data di dalam objek.
sumber
Anda mungkin berada di jalur yang benar. Masalahnya adalah, seperti yang Anda perhatikan, bahwa tidak ada anggota LastPaidTime di dalam Pembayaran Pajak . Itu persis di mana tempatnya, meskipun tidak perlu diungkapkan kepada publik sama sekali:
Setiap kali Anda menerima pembayaran, Anda membuat instance baru
ITaxPayment
sesuai dengan kebijakan saat ini, yang dapat memiliki aturan yang sama sekali berbeda dari yang sebelumnya.sumber
Saya setuju dengan jawaban @sebastiangeiger bahwa masalahnya sebenarnya tentang kepemilikan kendaraan dan bukan kendaraan. Saya akan menyarankan menggunakan jawabannya tetapi dengan beberapa perubahan. Saya akan membuat
Ownership
kelas menjadi kelas generik:Dan gunakan warisan untuk menentukan interval pembayaran untuk setiap jenis kendaraan. Saya juga menyarankan menggunakan kelas yang sangat diketik
TimeSpan
daripada bilangan bulat biasa:Sekarang kode aktual Anda hanya perlu berurusan dengan satu kelas -
Ownership<Vehicle>
.sumber
Saya benci untuk membocorkannya kepada Anda, tetapi Anda memiliki domain anemia , yaitu logika bisnis di luar objek. Jika ini yang Anda maksud, tidak apa-apa, tetapi Anda harus membaca alasan untuk menghindarinya.
Cobalah untuk tidak memodelkan domain masalah, tetapi memodelkan solusinya. Itulah sebabnya Anda berakhir dengan hierarki warisan paralel dan kompleksitas lebih dari yang Anda butuhkan.
Sesuatu seperti ini adalah yang Anda butuhkan untuk contoh itu:
Jika Anda ingin mengikuti SOLID itu bagus, tetapi seperti yang dikatakan Paman Bob sendiri, itu hanya prinsip rekayasa . Mereka tidak dimaksudkan untuk diterapkan secara ketat, tetapi merupakan pedoman yang harus dipertimbangkan.
sumber
isMotorBike
metode bukanlah pilihan desain OOP yang baik. Ini seperti mengganti polimorfisme dengan kode tipe (walaupun boolean).enum Vehicles
Saya pikir Anda benar bahwa periode pembayaran bukan properti dari Mobil / Motor yang sebenarnya. Tapi itu adalah atribut dari kelas kendaraan.
Meskipun Anda bisa pergi jalan memiliki if / select pada Jenis objek, ini tidak terlalu scalable. Jika Anda kemudian menambahkan truk dan Segway dan mendorong sepeda dan bus dan pesawat terbang (mungkin tampak tidak mungkin, tetapi Anda tidak pernah tahu dengan layanan publik) maka kondisinya menjadi lebih besar. Dan jika Anda ingin mengubah aturan untuk truk, Anda harus menemukannya di daftar itu.
Jadi mengapa tidak membuatnya, secara harfiah, atribut dan menggunakan refleksi untuk menariknya? Sesuatu seperti ini:
Anda juga bisa menggunakan variabel statis, jika itu lebih nyaman.
Kerugiannya adalah
bahwa itu akan sedikit lebih lambat, dan saya berasumsi Anda harus memproses banyak kendaraan. Jika itu adalah masalah maka saya mungkin mengambil pandangan yang lebih pragmatis dan tetap dengan properti abstrak atau pendekatan yang terhubung. (Meskipun, saya akan mempertimbangkan caching berdasarkan jenis juga.)
Anda harus memvalidasi saat startup bahwa setiap subkelas Kendaraan memiliki atribut IPaymentRequiredCalculator (atau mengizinkan default), karena Anda tidak ingin memproses 1.000 mobil, hanya macet karena Sepeda Motor tidak memiliki PaymentPeriod.
Sisi baiknya adalah jika Anda menemukan bulan kalender atau pembayaran tahunan, Anda bisa menulis kelas atribut baru. Ini adalah definisi utama dari OCP.
sumber