Mặc dù đây có thể là một câu hỏi bất khả tri về ngôn ngữ lập trình, tôi quan tâm đến câu trả lời nhắm vào hệ sinh thái .NET.
Đây là kịch bản: giả sử chúng ta cần phát triển một ứng dụng bảng điều khiển đơn giản cho quản trị công cộng. Ứng dụng này là về thuế xe. Họ (chỉ) có các quy tắc kinh doanh sau:
1.a) Nếu chiếc xe là một chiếc xe hơi và lần cuối cùng chủ sở hữu của nó đã nộp thuế là 30 ngày trước thì chủ sở hữu phải trả lại.
1.b) Nếu phương tiện là xe máy và lần cuối cùng chủ sở hữu đã nộp thuế là 60 ngày trước thì chủ sở hữu phải trả lại.
Nói cách khác, nếu bạn có một chiếc ô tô bạn phải trả tiền cứ sau 30 ngày hoặc nếu bạn có một chiếc xe máy, bạn phải trả tiền sau mỗi 60 ngày.
Đối với mỗi phương tiện trong hệ thống, ứng dụng nên kiểm tra các quy tắc đó và in các phương tiện đó (số biển và thông tin chủ sở hữu) không thỏa mãn chúng.
Điều tôi muốn là:
2.a) Tuân thủ các nguyên tắc RẮN (đặc biệt là nguyên tắc Mở / Đóng).
Điều tôi không muốn là (tôi nghĩ):
2.b) Một miền thiếu máu, do đó logic kinh doanh nên đi vào bên trong các thực thể kinh doanh.
Tôi bắt đầu với điều này:
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: Nguyên tắc mở / đóng được đảm bảo; nếu họ muốn thuế xe đạp bạn chỉ cần thừa kế từ Xe, thì bạn ghi đè phương thức HasToPay và bạn đã hoàn thành. Nguyên tắc mở / đóng thỏa mãn thông qua thừa kế đơn giản.
Các "vấn đề" là:
3.a) Tại sao một chiếc xe phải biết nếu nó phải trả tiền? Không phải là một mối quan tâm công khai? Nếu quy tắc hành chính công thay đổi thuế xe hơi, tại sao Xe phải thay đổi? Tôi nghĩ bạn nên hỏi: "vậy thì tại sao bạn lại đặt phương thức HasToPay bên trong Xe?", Và câu trả lời là: bởi vì tôi không muốn kiểm tra loại phương tiện (typeof) trong PublicAd dùng. Bạn có thấy một sự thay thế tốt hơn?
3 Một giải pháp có thể là: chuyển phương thức HasToPay sang một lớp khác, chúng ta có thể gọi nó là TaxPayment. Sau đó, chúng tôi tạo ra hai lớp dẫn xuất TaxPayment; CarTaxPayment và MotorbikeTaxPayment. Sau đó, chúng tôi thêm một thuộc tính Thanh toán trừu tượng (thuộc loại TaxPayment) vào lớp Xe và chúng tôi trả lại ví dụ TaxPayment đúng từ các lớp Xe hơi và Xe máy:
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; }
}
}
Và chúng tôi gọi quy trình cũ theo cách này:
public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
{
return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
}
Nhưng bây giờ mã này sẽ không được biên dịch vì không có thành viên LastPaidTime trong CarTaxPayment / MotorbikeTaxPayment / TaxPayment. Tôi bắt đầu thấy CarTaxPayment và MotorbikeTaxPayment giống như "triển khai thuật toán" hơn là các thực thể kinh doanh. Bằng cách nào đó bây giờ những "thuật toán" đó cần giá trị LastPaidTime. Chắc chắn, chúng ta có thể chuyển giá trị cho người xây dựng TaxPayment, nhưng điều đó sẽ vi phạm đóng gói phương tiện, hoặc trách nhiệm, hoặc bất cứ điều gì mà những người truyền giáo OOP gọi nó, phải không?
3.c) Giả sử chúng ta đã có ObjectContext của Entity Framework (sử dụng các đối tượng miền của chúng ta; Person, Xe, Xe hơi, Xe máy, Công khai). Bạn sẽ làm thế nào để có được một tham chiếu ObjectContext từ phương thức PublicAd dùng.GetAllVehicles và do đó thực hiện chức năng này?
3.d) Nếu chúng ta cũng cần cùng một tham chiếu ObjectContext cho CarTaxPayment.HasToPay nhưng một tài liệu khác cho MotorbikeTaxPayment.HasToPay, người chịu trách nhiệm "tiêm" hoặc bạn sẽ chuyển các tham chiếu đó đến các lớp thanh toán như thế nào? Trong trường hợp này, tránh một miền thiếu máu (và do đó không có đối tượng dịch vụ), không dẫn chúng ta đến thảm họa?
Lựa chọn thiết kế của bạn cho kịch bản đơn giản này là gì? Rõ ràng các ví dụ tôi đã trình bày là "quá phức tạp" cho một nhiệm vụ dễ dàng, nhưng đồng thời, ý tưởng là tuân thủ các nguyên tắc RẮN và ngăn chặn các miền thiếu máu (trong đó đã được ủy nhiệm để phá hủy).