5.11 — Mini case study
Một switch 8 nhánh trong hàm tính giảm giá, mỗi lần thêm chương trình khuyến mãi là sửa lại hàm đó — vi phạm rõ ràng nguyên tắc mở/đóng. Bài này áp dụng SOLID để biến nó thành hệ thống thêm mới không sửa cũ, và đo lại bằng số liệu. Nhưng phần có giá trị hơn nằm ở nửa sau: khi nào SOLID bị lạm dụng. Áp dụng máy móc, cùng bài toán này có thể sinh ra 12 interface và 15 lớp cho việc mà 20 dòng switch làm được — và đó không phải thiết kế tốt hơn. Bài đưa ra hai câu hỏi để phân biệt hai trường hợp.
Mục tiêu bài học
Sau bài này bạn có thể:
- Nhận ra vi phạm nguyên tắc mở/đóng trong code thật.
- Refactor một
switchlớn thành tập hợp chiến lược. - Đăng ký và chọn cài đặt qua DI.
- Nhận ra khi SOLID đang bị lạm dụng.
Nội dung bài học
5.11.1 — Code ban đầu
public decimal CalculateDiscount(Order order, PromotionType type)
{
switch (type)
{
case PromotionType.Percentage:
return order.Total * 0.1m;
case PromotionType.FixedAmount:
return 50_000m;
case PromotionType.BuyTwoGetOne:
return order.Lines.Max(l => l.UnitPrice);
case PromotionType.Loyalty:
return order.Customer.Tier switch
{
"Vàng" => order.Total * 0.15m,
"Bạc" => order.Total * 0.1m,
_ => 0m
};
case PromotionType.FreeShipping:
return order.ShippingFee;
// ... 3 case nữa
default:
return 0m;
}
}
Ba vấn đề đo được:
- Mỗi chương trình khuyến mãi mới: sửa hàm này (đã 8 lần trong 6 tháng)
- Độ dài: 187 dòng
- Độ phụ thuộc của lớp chứa nó: 7 dependency (vì mỗi loại cần thứ khác nhau)
- Số test cho hàm này: 1 test file, 340 dòng, 24 trường hợp
Vấn đề lớn nhất không phải độ dài mà là mỗi thay đổi đều chạm vào code đang chạy đúng. Thêm một khuyến mãi mới có thể làm hỏng bảy cái cũ.
5.11.2 — Áp dụng nguyên tắc mở/đóng
Mở cho mở rộng, đóng cho sửa đổi: thêm hành vi mới bằng cách thêm code, không phải sửa code cũ.
public interface IDiscountStrategy
{
PromotionType Type { get; }
bool AppliesTo(Order order);
Money CalculateDiscount(Order order);
}
public sealed class PercentageDiscount(IOptions<PromotionOptions> options) : IDiscountStrategy
{
public PromotionType Type => PromotionType.Percentage;
public bool AppliesTo(Order order)
=> order.Total.Amount >= options.Value.MinimumThreshold;
public Money CalculateDiscount(Order order)
=> order.Total * options.Value.Percentage;
}
public sealed class LoyaltyDiscount(ICustomerTierTable tierTable) : IDiscountStrategy
{
public PromotionType Type => PromotionType.Loyalty;
public bool AppliesTo(Order order) => order.Customer.Tier is not null;
public Money CalculateDiscount(Order order)
=> order.Total * tierTable.GetRate(order.Customer.Tier!);
}
Mỗi lớp chỉ có một lý do để thay đổi — đúng nguyên tắc trách nhiệm đơn nhất. PercentageDiscount chỉ đổi khi quy tắc phần trăm đổi, và nó chỉ cần đúng một dependency thay vì bảy.
5.11.3 — Chọn chiến lược
public sealed class DiscountService(IEnumerable<IDiscountStrategy> strategies)
{
public Money CalculateDiscount(Order order, PromotionType type)
{
var strategy = strategies.FirstOrDefault(s => s.Type == type);
if (strategy is null || !strategy.AppliesTo(order))
return Money.Zero;
return strategy.CalculateDiscount(order);
}
// Chọn chương trình có lợi nhất cho khách — TRƯỚC ĐÂY không làm được
public Money CalculateBestDiscount(Order order)
=> strategies
.Where(s => s.AppliesTo(order))
.Select(s => s.CalculateDiscount(order))
.DefaultIfEmpty(Money.Zero)
.Max();
}
// Đăng ký — thêm chiến lược mới chỉ cần thêm MỘT dòng
builder.Services.AddScoped<IDiscountStrategy, PercentageDiscount>();
builder.Services.AddScoped<IDiscountStrategy, FixedAmountDiscount>();
builder.Services.AddScoped<IDiscountStrategy, LoyaltyDiscount>();
.NET DI tự gom mọi đăng ký cùng interface thành IEnumerable<T> khi bạn yêu cầu (bài 7.6).
Phương thức CalculateBestDiscount đáng chú ý: với switch cũ, viết được nó nghĩa là phải lặp qua mọi giá trị enum và gọi lại hàm 8 lần. Với thiết kế mới, nó là ba dòng LINQ — một khả năng mới xuất hiện nhờ cấu trúc, không phải nhờ viết thêm logic.
5.11.4 — Kết quả
| Chỉ số | Trước | Sau |
|---|---|---|
| Dòng trong một file | 187 | 25–40 mỗi chiến lược |
| Dependency của lớp điều phối | 7 | 1 |
| Thêm khuyến mãi mới | Sửa file đang chạy đúng | Thêm file mới |
| Test cho một chiến lược | Phải dựng 7 dependency | 1–2 dependency |
| Chọn khuyến mãi tốt nhất | Không làm được | 3 dòng LINQ |
Hàng thứ tư là lợi ích lớn nhất trong thực tế: test của PercentageDiscount không còn quan tâm tới bảng hạng khách hàng hay phí vận chuyển.
5.11.5 — Khi SOLID bị lạm dụng
Cùng bài toán, áp dụng máy móc có thể ra thế này:
// LẠM DỤNG
public interface IDiscountStrategy { }
public interface IConditionalDiscountStrategy : IDiscountStrategy { }
public interface IDiscountStrategyFactory { }
public interface IDiscountCoordinator { }
public interface IDiscountValidator { }
public interface IDiscountFormatter { }
public interface IDiscountLogger { }
public abstract class DiscountStrategyBase { }
public class DefaultDiscountCoordinator : IDiscountCoordinator { }
// ... 15 lớp cho việc mà 20 dòng switch làm được
Đây không phải thiết kế tốt hơn — nó là chi phí thuần. Người đọc phải nhảy qua 6 file để hiểu một phép tính.
Hai câu hỏi phân biệt:
1. Phần này có thật sự thay đổi thường xuyên không?
Trong ca này: có — 8 lần trong 6 tháng. Đó là bằng chứng, không phải phỏng đoán.
Nếu một switch 3 nhánh chưa đổi lần nào trong hai năm, tách nó ra là giải quyết vấn đề không tồn tại.
2. Abstraction này có ít nhất hai cài đặt thật không?
Interface có một cài đặt duy nhất và không có kế hoạch thêm cái thứ hai là abstraction không cần thiết. Nó chỉ thêm một lớp gián tiếp.
// Không cần — chỉ có một cài đặt, và sẽ không có cái thứ hai
public interface IDateFormatter { string Format(DateTime d); }
public class DefaultDateFormatter : IDateFormatter { }
Ngoại lệ hợp lệ: bạn cần interface để test thay thế được, ví dụ IEmailSender. Đó là cài đặt thứ hai thật (fake dùng trong test).
Quy tắc thực dụng: viết code đơn giản nhất chạy được, và tách ra khi bạn chạm vào nó lần thứ ba (bài 16.15).
5.11.6 — Rà lại code c ủa bạn
Danh sách rà soát áp dụng SOLID
- •Có bằng chứng phần code này thay đổi thường xuyên trước khi tách.
- •Mỗi interface có ít nhất hai cài đặt thật, hoặc cần cho test.
- •Mỗi lớp chỉ có một lý do để thay đổi.
- •Thêm hành vi mới là thêm file, không sửa file đang chạy đúng.
- •Lớp điều phối không cần biết chi tiết của từng cài đặt.
- •Test của một cài đặt không phải dựng dependency của cài đặt khác.
- •Số file phải mở để hiểu một luồng không quá ba.
Bài tập áp dụng
Bài 1 — Thêm chiến lược
Thêm "giảm giá theo mã voucher" vào thiết kế mới và đếm số file phải sửa.
Tiêu chí hoàn thành: bạn đếm được số file cho cả hai thiết kế, và nhận ra con số 0 file hiện có phải sửa mới là điều nguyên tắc mở/đóng hứa hẹn.
Gợi ý và lời giải — Bài 1
Gợi ý. Đếm riêng "file thêm mới" và "file hiện có phải sửa". Chỉ con số thứ hai mới quan trọng.
Lời giải — với thiết kế chiến lược:
// File MỚI: VoucherDiscount.cs
public sealed class VoucherDiscount(IVoucherRepository kho, IDateTimeProvider thoiGian)
: IDiscountStrategy
{
public PromotionType Type => PromotionType.Voucher;
public bool AppliesTo(Order order)
=> order.MaVoucher is not null;
public Money CalculateDiscount(Order order)
{
var v = kho.Tim(order.MaVoucher!);
if (v is null || v.HetHan < thoiGian.Now || v.DaDung >= v.GioiHan)
return Money.Zero;
return v.LaPhanTram
? order.Total * v.GiaTri
: Money.Vnd(Math.Min(v.GiaTri, order.Total.Amount));
}
}
// Đăng ký — một dòng
builder.Services.AddScoped<IDiscountStrategy, VoucherDiscount>();
File thêm mới : 1 (cộng 1 file test)
File hiện có phải sửa: 1 (chỉ dòng đăng ký DI)
DiscountService : KHÔNG SỬA
7 chiến lược cũ : KHÔNG SỬA
Test của 7 cái cũ : KHÔNG SỬA, và vẫn xanh
Với thiết kế switch ban đầu:
1. Thêm giá trị vào enum PromotionType
2. Thêm một case vào hàm 187 dòng -> giờ thành 210 dòng
3. Thêm hai dependency (IVoucherRepository, IDateTimeProvider) vào lớp chứa
-> lớp đó từ 7 lên 9 dependency
4. Sửa constructor -> mọi chỗ tạo lớp đó phải sửa theo
5. File test 340 dòng phải sửa và chạy lại toàn bộ 24 trường hợp
File hiện có phải sửa: 4–5
Và mọi test của 7 khuyến mãi cũ phải chạy lại — vì code của chúng
nằm trong CÙNG một hàm vừa bị sửa.
Bảng so sánh:
| Chiến lược | switch | |
|---|---|---|
| File mới | 2 | 0 |
| File hiện có phải sửa | 1 | 4–5 |
| Code đang chạy đúng bị chạm tới | Không | Có |
| Dependency của lớp lớn nhất | không đổi | 7 → 9 |
| Phải chạy lại test của khuyến mãi cũ | Không | Có |
Và đây là điều nguyên tắc mở/đóng thật sự hứa hẹn:
Không phải "ít code hơn" — bản chiến lược có nhiều file hơn.
Mà là: thêm hành vi mới KHÔNG chạm vào hành vi cũ
-> rủi ro của một thay đổi không còn tỉ lệ với số tính năng đã có
Với switch: tính năng thứ 20 vẫn sửa cùng một hàm với tính năng thứ 1
-> mỗi lần thêm, xác suất làm hỏng cái cũ TĂNG
Với chiến lược: tính năng thứ 20 là một file độc lập
-> rủi ro giữ nguyên, không tích luỹ
Một chi tiết đáng chú ý trong cài đặt VoucherDiscount: nó cần 2 dependency mà không chiến lược nào khác cần.
Với switch: lớp chứa phải nhận CẢ 9 dependency, dù mỗi lần gọi
chỉ dùng một hai cái
-> và test phải giả lập cả 9, kể cả khi chỉ kiểm tra khuyến mãi phần trăm
Với chiến lược: mỗi lớp nhận đúng thứ nó cần
-> test VoucherDiscount giả lập 2 thứ, không phải 9
Đây là lý do bài 2 dưới đây đo số dependency: nó là chỉ số dễ đếm nh ất cho thấy một lớp đang gánh quá nhiều trách nhiệm.
Và một cảnh báo để cân bằng — đừng áp dụng mẫu này cho mọi switch:
// switch này KHÔNG cần chiến lược
public string TenTrangThai(TrangThaiLead t) => t switch
{
TrangThaiLead.Moi => "Mới",
TrangThaiLead.DangXuLy => "Đang xử lý",
TrangThaiLead.DaChot => "Đã chốt",
_ => "Không rõ",
};
Ba dấu hiệu cho thấy switch NÊN thành chiến lược:
1. Mỗi nhánh cần dependency KHÁC NHAU
2. Danh sách nhánh còn dài ra theo thời gian
3. Logic mỗi nhánh dài hơn vài dòng
Thiếu cả ba -> switch là lựa chọn đúng, và ngắn hơn nhiều.
Bài 2 — Đo dependency
Với một service lớn trong dự án, đếm số dependency và xem có thể tách theo trách nhiệm không.
Tiêu chí hoàn thành: bạn có số liệu cho vài lớp, và biết dùng một câu hỏi để quyết định tách hay không — thay vì dựa vào một ngưỡng cứng.
Gợi ý và lời giải — Bài 2
Gợi ý. Số dependency không tự nó là vấn đề. Vấn đề là mỗi phương thức dùng bao nhiêu trong số đó.
Lời giải — đếm bằng một lệnh:
for f in $(find src -name "*Service.cs" -not -path "*/Tests/*"); do
so=$(grep -ozP '(?s)public\s+\w+Service\s*\([^)]*\)' "$f" | tr -d '\0' | grep -oc ',' || echo 0)
dong=$(wc -l < "$f")
echo "$((so + 1)) dep | $dong dòng | $f"
done | sort -rn | head -8
9 dep | 412 dòng | src/Crm.Api/Services/DonHangService.cs
7 dep | 187 dòng | src/Crm.Api/Services/KhuyenMaiService.cs
6 dep | 298 dòng | src/Crm.Api/Services/LeadService.cs
5 dep | 156 dòng | src/Crm.Api/Services/BaoCaoService.cs
3 dep | 88 dòng | src/Crm.Api/Services/KhachHangService.cs
2 dep | 54 dòng | src/Crm.Api/Services/ThongBaoService.cs
Nhưng con số đó chưa đủ. Cần thêm một bước: xem mỗi phương thức dùng bao nhiêu.
DonHangService — 9 dependency, 6 phương thức công khai:
TaoDonAsync dùng _db, _khuyenMai, _tonKho (3/9)
ChotDonAsync dùng _db, _thanhToan, _email (3/9)
HuyDonAsync dùng _db, _thanhToan (2/9)
LayDonAsync dùng _db (1/9)
XuatHoaDonAsync dùng _db, _pdf, _luuTru (3/9)
GuiNhacNhoAsync dùng _db, _email, _sms (3/9)
Mỗi phương thức dùng 1–3 trong số 9.
Không phương thức nào dùng quá một phần ba.
Đây là dấu hiệu rõ nhất — và nó có tên: độ gắn kết thấp.
Một lớp gắn kết CAO: mọi phương thức dùng phần lớn các trường
-> chúng thuộc về nhau
Một lớp gắn kết THẤP: mỗi phương thức dùng một tập con rời nhau
-> đây là nhiều lớp đang nằm chung một file
Tách theo đúng các tập con đó:
// TaoDonAsync + ChotDonAsync + HuyDonAsync
public sealed class QuyTrinhDonHangService(
CrmDbContext db, IKhuyenMai khuyenMai, ITonKho tonKho, IThanhToan thanhToan); // 4 dep
// XuatHoaDonAsync
public sealed class XuatHoaDonService(CrmDbContext db, IPdf pdf, ILuuTru luuTru); // 3 dep
// GuiNhacNhoAsync
public sealed class NhacNhoDonHangService(CrmDbContext db, IEmail email, ISms sms); // 3 dep
// LayDonAsync -> chuyển thẳng vào handler truy vấn, không cần service riêng
Trước: 1 lớp, 9 dependency, 412 dòng
Sau : 3 lớp, 3–4 dependency mỗi lớp, mỗi lớp dưới 160 dòng
Và đây là câu hỏi quyết định, thay cho mọi ngưỡng cứng:
"Có thay đổi nào chỉ chạm vào MỘT phần của lớp này không?"
Có -> phần đó nên là một lớp riêng
Không -> lớp đang gắn kết tốt, kể cả khi nó có 8 dependency
Ví dụ cụ thể: đổi nhà cung cấp PDF
-> chỉ chạm XuatHoaDonAsync
-> nhưng phải mở một file 412 dòng có 9 dependency
-> và mọi test của 5 phương thức kia phải chạy lại
Vì sao ngưỡng cứng ("quá 5 dependency là xấu") không dùng được:
// 8 dependency, nhưng MỌI phương thức đều dùng gần hết -> gắn kết cao
public sealed class XuLyThanhToanService(
CrmDbContext db, ICongThanhToan cong, IChongGianLan gianLan,
ITyGia tyGia, IHoaDon hoaDon, IThongBao thongBao,
ILogger<XuLyThanhToanService> log, IDateTimeProvider thoiGian);
Một luồng thanh toán thật sự cần cả 8 thứ đó, ở gần như mọi bước.
Tách ra sẽ tạo ra các lớp phải truyền dữ liệu qua lại — tệ hơn.
Ba chỉ số nên đo cùng nhau, không đo riêng:
| Chỉ số | Cảnh báo khi | Nhưng kiểm tra thêm |
|---|---|---|
| Số dependency | > 6 | Mỗi phương thức dùng bao nhiêu? |
| Số dòng | > 300 | Có nhóm phương thức rời nhau không? |
| Số phương thức công khai | > 8 | Chúng có chung dữ liệu không? |
Cả ba cùng cao -> gần như chắc chắn nên tách
Chỉ một cái cao -> xem xét, đừng tự động tách
Và một cách đo gắn kết chính xác hơn, nếu muốn:
# LCOM (lack of cohesion): dotnet tool install -g dotnet-metrics-tool
# hoặc dùng tính năng Code Metrics của Visual Studio
Chỉ số này tính đúng tỉ lệ "phương thức dùng chung trường" mà bạn vừa đếm bằng tay ở trên — hữu ích khi cần rà nhiều lớp cùng lúc.
Bài 3 — Tìm abstraction thừa
Rà dự án tìm interface chỉ có một cài đặt và không dùng trong test.
Tiêu chí hoàn thành: bạn có danh sách kèm đánh giá, và phân biệt được interface thừa với interface có lý do dù chỉ một cài đặt.
Gợi ý và lời giải — Bài 3
Gợi ý. "Một cài đặt" không tự nó là bằng chứng thừa. Câu hỏi thật là: interface này đang giúp gì?
Lời giải — tìm bằng lệnh:
#!/usr/bin/env bash
for i in $(grep -rhoE "public interface (I[A-Za-z]+)" --include="*.cs" src/ \
| awk '{print $3}' | sort -u); do
so_cai_dat=$(grep -rE "class [A-Za-z]+ *: *(.*, *)?$i\b" --include="*.cs" src/ | wc -l)
dung_trong_test=$(grep -rE "(Substitute\.For<$i>|Mock<$i>|new Fake$i)" \
--include="*.cs" tests/ 2>/dev/null | wc -l)
[ "$so_cai_dat" -le 1 ] && [ "$dung_trong_test" -eq 0 ] \
&& echo "$i (cài đặt: $so_cai_dat, dùng trong test: $dung_trong_test)"
done
IKhachHangMapper (cài đặt: 1, dùng trong test: 0)
ILeadValidator (cài đặt: 1, dùng trong test: 0)
IDonHangCalculator (cài đặt: 1, dùng trong test: 0)
IEmailTemplateEngine (cài đặt: 1, dùng trong test: 0)
IBaoCaoExporter (cài đặt: 1, dùng trong test: 0)
Đánh giá từng cái bằng bốn câu hỏi:
1. Có ranh giới ra ngoài tiến trình không? (mạng, đĩa, đồng hồ, ngẫu nhiên)
2. Có kế hoạch cụ thể cho cài đặt thứ hai không?
3. Nó có làm test của lớp KHÁC dễ hơn không?
4. Nó có phải là hợp đồng công khai của một module không?
| Interface | Đánh giá | Lý do |
|---|---|---|
IKhachHangMapper | Thừa | Ánh xạ thuần, không I/O, test được trực tiếp |
ILeadValidator | Thừa | Hàm thuần, gọi thẳng là được |
IDonHangCalculator | Thừa | Tính toán thuần |
IEmailTemplateEngine | Giữ | Đọc template từ đĩa — có I/O |
IBaoCaoExporter | Giữ | Ghi ra lưu trữ ngoài, và có kế hoạch thêm định dạng |
Ba interface "thừa" có cái giá thật:
Mỗi interface thừa là:
- một file nữa phải mở khi lần theo code
- một bước nhảy nữa trong IDE (F12 vào interface, rồi tìm cài đặt)
- một dòng đăng ký DI nữa
- và một lời hứa sai: "chỗ này có thể thay thế được"
trong khi không ai có ý định thay
// Trước
public interface IDonHangCalculator { Money Tinh(DonHang d); }
public sealed class DonHangCalculator : IDonHangCalculator { ... }
builder.Services.AddScoped<IDonHangCalculator, DonHangCalculator>();
// Sau — một lớp, không interface
public static class DonHangCalculator
{
public static Money Tinh(DonHang d) => ...;
}
Test vẫn viết được bình thường — nó là hàm thuần, gọi thẳng và kiểm tra kết quả.
Không cần giả lập gì cả, nên không cần interface để giả lập.
Và hai interface "giữ" — vì sao một cài đặt vẫn đáng:
IEmailTemplateEngine đọc file từ đĩa.
-> test của LeadService muốn kiểm tra "có gửi email không"
mà KHÔNG muốn chạm đĩa
-> nên nó cần giả lập -> nên nó cần interface
Tiêu chí: interface tồn tại để TÁCH RA NGOÀI TIẾN TRÌNH, không phải để
"trừu tượng hoá cho đẹp".
Một trường hợp thứ ba, không thuộc hai nhóm trên:
public interface IThoiGian { DateTimeOffset BayGio { get; } }
Một cài đặt, và nó chỉ bọc DateTimeOffset.UtcNow.
Nhưng nó cho phép test kiểm soát thời gian — điều không làm được
với DateTimeOffset.UtcNow gọi trực tiếp.
-> GIỮ, dù nhỏ tới mức trông như thừa.
// .NET 8 trở lên có sẵn TimeProvider, không cần tự viết
public class LeadService(TimeProvider thoiGian)
{
public Lead Tao(string ten) => new(ten, thoiGian.GetUtcNow());
}
// Trong test
var gia = new FakeTimeProvider(new DateTimeOffset(2026, 9, 25, 10, 0, 0, TimeSpan.Zero));
Quy tắc thực dụng, gói gọn:
Tạo interface khi có LÝ DO CỤ THỂ:
- ranh giới ra ngoài tiến trình cần giả lập trong test
- đã có, hoặc chắc chắn sẽ có, cài đặt thứ hai
- là hợp đồng công khai giữa hai module
Không tạo interface vì:
- "sau này có thể cần"
- "để theo nguyên tắc đảo ngược phụ thuộc"
- "vì mọi service khác đều có"
Đây là điều mục 5.11.5 nói về việc lạm dụng SOLID: nguyên tắc đảo ngược phụ thu ộc nói phụ thuộc vào trừu tượng ở ranh giới, không nói mọi lớp phải có một interface. Ba interface bị xoá ở trên không vi phạm nguyên tắc nào — chúng chỉ đang trả chi phí cho một lợi ích không tồn tại.
Tự kiểm tra
Frequently asked questions
Vấn đề lớn nhất của switch 8 nhánh là gì?
Không phải độ dài, mà là mỗi thay đổi đều chạm vào code đang chạy đúng. Thêm một khuyến mãi mới có thể làm hỏng bảy cái cũ, và test phải dựng mọi dependency của mọi nhánh.
Nguyên tắc mở đóng nghĩa là gì trong ca này?
Thêm hành vi mới bằng cách thêm một lớp chiến lược và một dòng đăng ký DI, không sửa file chứa logic cũ. Code đang chạy đúng không bị động tới.
Vì sao phương thức chọn khuyến mãi tốt nhất dễ hơn sau khi refactor?
Vì mọi chiến lược đều cùng một interface nên duyệt qua chúng là ba dòng LINQ. Với switch cũ, phải lặp qua mọi giá trị enum và gọi lại hàm nhiều lần. Đây là khả năng mới xuất hiện nhờ cấu trúc chứ không nhờ viết thêm logic.
Hai câu hỏi phân biệt SOLID đúng chỗ và lạm dụng là gì?
Phần này có thật sự thay đổi thường xuyên không, và abstraction này có ít nhất hai cài đặt thật không. Cả hai đều cần bằng chứng chứ không phải phỏng đoán về tương lai.
Interface chỉ có một cài đặt có bao giờ hợp lệ không?
Có, khi bạn cần nó để test thay thế được, ví dụ IEmailSender với một fake dùng trong test. Đó là cài đặt thứ hai thật. Ngoài trường hợp đó, nó chỉ thêm một lớp gián tiếp.
Quy tắc thực dụng về thời điểm tách là gì?
Viết code đơn giản nhất chạy được, và tách ra khi bạn chạm vào nó lần thứ ba. Lúc đó bạn đã có bằng chứng về việc nó thay đổi thường xuyên.
Kết luận
Ba điều đáng nhớ nhất:
- Mỗi thay đổi chạm vào code đang chạy đúng là dấu hiệu rõ nhất của vi phạm mở/đóng.
- Cần bằng chứng trước khi tách, không phải phỏng đoán về tương lai.
- Interface một cài đặt và không dùng cho test là chi phí thuần.
Tham khảo
- Lập trình hướng đối tượng trong C#
- Dependency injection trong .NET
- Nguyên tắc kiến trúc
- Generics trong C#
Điều hướng
- Bài trước: 5.9 — Mở rộng và đào sâu
- Bài tiếp theo: 5.11 — Ví dụ thực tế nhanh
- Về module: Trang mục lục