7.10 - Decorator, module đăng ký và Scrutor
Tóm tắt: Ba kỹ thuật giúp phần đăng ký DI không phình to khi dự án lớn dần. Decorator thêm cache, log, thử lại vào một service mà không sửa dòng nào của service đó; bài 7.5 đã viết nó bằng tay, bài này dùng thư viện Scrutor để rút còn một dòng và chỉ ra cái giá phải trả. Module đăng ký theo tính năng gom các
Add...của một tính năng vào một extension đặt cạnh code của nó, đểProgram.csđọc như mục lục. Quét assembly đăng ký hàng loạt theo quy ước, đổi lại là khó lần ra ai đăng ký cái gì. Cuối bài là tiêu chí chọn giữa keyed services và factory tự viết, và lý do container bên thứ ba như Autofac hiếm khi còn cần.
Mục tiêu bài học
Sau bài này bạn có thể:
- Thêm decorator bằng Scrutor và giải thích thứ tự các lớp bọc.
- Tổ chức đăng ký theo tính năng, tách hạ tầng dùng chung ra một module riêng.
- Viết test kiểm tra một module tự đủ phụ thuộc.
- Chọn giữa keyed services, factory tự viết và
IEnumerable<T>. - Quyết định có cần container bên thứ ba không.
Cần biết trước: decorator viết tay và factory registration (bài 7.5), tách đăng ký theo layer (bài 7.6), keyed services (bài 7.8), extension method (bài 5.5).
Nội dung bài học
7.10.1 — Decorator với Scrutor
Scrutor là thư viện mã nguồn mở (giấy phép MIT) bổ sung hai thứ cho container .NET: Decorate và quét assembly. Nó không thay container, chỉ thêm extension method vào IServiceCollection. Cài bằng dotnet add package Scrutor.
Ví dụ dưới chạy được trong project console đã cài Scrutor, Microsoft.Extensions.DependencyInjection và Microsoft.Extensions.Caching.Memory. Repository thật được thay bằng một Dictionary để không cần database.
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddMemoryCache();
services.AddScoped<ILeadRepository, InMemoryLeadRepository>();
services.Decorate<ILeadRepository, CachedLeadRepository>(); // bọc lần 1
services.Decorate<ILeadRepository, TimedLeadRepository>(); // bọc lần 2: nằm NGOÀI CÙNG
using var provider = services.BuildServiceProvider(
new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true });
using var scope = provider.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<ILeadRepository>();
Console.WriteLine($"Kiểu nhận được: {repo.GetType().Name}");
await repo.GetByIdAsync(1);
await repo.GetByIdAsync(1);
public record Lead(int Id, string Name);
public interface ILeadRepository
{
Task<Lead?> GetByIdAsync(int id, CancellationToken ct = default);
}
// Lớp gốc: KHÔNG sửa gì khi thêm decorator
public sealed class InMemoryLeadRepository : ILeadRepository
{
private readonly Dictionary<int, Lead> _leads = new() { [1] = new(1, "Công ty An Phát") };
public async Task<Lead?> GetByIdAsync(int id, CancellationToken ct = default)
{
await Task.Delay(50, ct); // giả lập truy vấn database
Console.WriteLine($" [repo] đọc lead {id} từ nguồn dữ liệu");
return _leads.GetValueOrDefault(id);
}
}
// Decorator 1: cache
public sealed class CachedLeadRepository(ILeadRepository inner, IMemoryCache cache) : ILeadRepository
{
public async Task<Lead?> GetByIdAsync(int id, CancellationToken ct = default)
{
if (cache.TryGetValue(id, out Lead? cached))
{
Console.WriteLine($" [cache] trúng cache lead {id}");
return cached;
}
var lead = await inner.GetByIdAsync(id, ct);
if (lead is not null) cache.Set(id, lead, TimeSpan.FromMinutes(5));
return lead;
}
}
// Decorator 2: đo thời gian
public sealed class TimedLeadRepository(ILeadRepository inner) : ILeadRepository
{
public async Task<Lead?> GetByIdAsync(int id, CancellationToken ct = default)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var lead = await inner.GetByIdAsync(id, ct);
Console.WriteLine($"[timed] GetByIdAsync({id}) mất {sw.ElapsedMilliseconds} ms");
return lead;
}
}Kết quả chạy trên .NET 10 với Scrutor 7.0 (số mili-giây thay đổi mỗi lần chạy):
Kiểu nhận được: TimedLeadRepository
[repo] đọc lead 1 từ nguồn dữ liệu
[timed] GetByIdAsync(1) mất 48 ms
[cache] trúng cache lead 1
[timed] GetByIdAsync(1) mất 0 msBa điều đọc ra từ output:
Decorategọi sau nằm ngoài cùng. Chuỗi thật làTimed -> Cached -> InMemory. Ai xinILeadRepositorycũng nhận lớp ngoài cùng mà không biết, và không cần biết, có lớp nào bên trong.- Thứ tự bọc là quyết định có ý nghĩa.
Timednằm ngoàiCachednên đo được cả lần trúng cache (0 ms). Đảo lại thìTimedchỉ chạy khi cache trượt, và bạn mất thông tin "endpoint nhanh nhờ cache". - Lớp gốc không đổi. Bỏ cache là xoá một dòng
Decorate.
Viết cùng chuỗi đó bằng container thuần thì như bài 7.5: đăng ký lớp gốc theo kiểu cụ thể, rồi tự lồng các lớp trong một factory.
services.AddScoped<InMemoryLeadRepository>();
services.AddScoped<ILeadRepository>(sp =>
new TimedLeadRepository(
new CachedLeadRepository(
sp.GetRequiredService<InMemoryLeadRepository>(),
sp.GetRequiredService<IMemoryCache>())));Cái giá của Decorate: lỗi thiếu đăng ký lộ ra muộn hơn. Scrutor cài Decorate bằng factory, mà ValidateOnBuild không nhìn được vào trong factory (bài 7.5). Chạy thử trên .NET 10: bỏ dòng AddMemoryCache() rồi build với ValidateOnBuild = true, container không báo thiếu IMemoryCache; lỗi chỉ nổ lúc resolve ILeadRepository lần đầu. Vì vậy mỗi decorator nên có một test resolve thật (Bài 1).
7.10.2 — Module đăng ký theo tính năng
Bài 7.6 tách đăng ký theo layer (AddInfrastructure, AddApplication). Khi số tính năng tăng, mỗi extension đó lại dài ra. Cách tiếp theo là tách theo tính năng, mỗi tính năng một extension đặt cạnh code của nó:
// src/Crm.Api/Features/Leads/LeadsModule.cs — đặt cạnh code của tính năng Lead
public static class LeadsModule
{
public static IServiceCollection AddLeadsFeature(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<ILeadRepository, EfLeadRepository>();
services.Decorate<ILeadRepository, CachedLeadRepository>();
services.AddScoped<ILeadService, LeadService>();
services.AddOptions<SalesOptions>()
.Bind(config.GetSection("Sales"))
.ValidateOnStart();
return services;
}
}// Program.cs — đọc như một mục lục
builder.Services
.AddInfrastructure(builder.Configuration)
.AddLeadsFeature(builder.Configuration)
.AddCustomersFeature()
.AddInvoicesFeature();Lợi ích, ngoài việc dễ đọc:
- Ít xung đột merge. Ba người thêm service cho ba tính năng sửa ba file khác nhau, không cùng sửa
Program.cs. - Xoá tính năng gọn. Xoá một thư mục và một dòng, không phải đi tìm 8 dòng rải rác giữa 90 dòng.
- Ranh giới rõ. Mỗi module tự chứa đăng ký và cấu hình của nó, nên sau này tách module thành service riêng dễ hơn (bài 18.1, phần nâng cao).
Quy ước quan trọng: module tính năng không tự đăng ký hạ tầng dùng chung. DbContext, cache, TimeProvider nằm ở một module hạ tầng, gọi đúng một lần:
public static class InfrastructureModule
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<CrmDbContext>(o => o.UseNpgsql(config.GetConnectionString("Crm")));
services.AddMemoryCache();
services.AddSingleton(TimeProvider.System);
return services;
}
}SQL Server
services.AddDbContext<CrmDbContext>(o => o.UseSqlServer(config.GetConnectionString("Crm")));Nếu ba module cùng tự gọi AddDbContext, bạn có ba lần đăng ký trùng; nếu một module "mượn" service do module khác đăng ký mà không nói ra, xoá module kia là module này hỏng. Bài 2 viết test bắt đúng trường hợp đó.
Chỗ này cũng dễ làm quá tay: khi số module vượt xa số tính năng thật, bạn chỉ đang di chuyển sự lộn xộn sang chỗ khác.
7.10.3 — Đăng ký theo quy ước bằng quét assembly
Khi có hàng chục lớp cùng một khuôn (30 repository, 40 handler), Scrutor quét assembly và đăng ký hàng loạt:
services.Scan(scan => scan
.FromAssemblyOf<LeadService>() // quét assembly chứa LeadService
.AddClasses(c => c.AssignableTo(typeof(IRepository<>))) // mọi lớp cài IRepository<T>
.AsImplementedInterfaces() // đăng ký theo interface của chúng
.WithScopedLifetime());Một khối thay 30 dòng AddScoped. Đánh đổi thật: bạn không còn thấy service nào được đăng ký khi đọc code, grep "AddScoped<ILeadRepository" không ra kết quả, và người mới vào đội khó lần ra vì sao một lớp có mặt trong container.
Cách cân bằng: quét cho nhóm lớp đồng nhất và nhiều, đăng ký tường minh cho service có cấu hình đặc biệt hoặc lifetime khác thường.
7.10.4 — Keyed services, factory hay IEnumerable<T>
Bài 7.8 đã dạy keyed services. Câu hỏi còn lại là khi nào nó không đủ:
| Nhu cầu | Dùng |
|---|---|
| Chọn một cài đặt theo một khoá biết lúc biên dịch | [FromKeyedServices(Channel.Email)] |
| Chọn theo khoá chỉ biết lúc chạy (cấu hình tenant, tham số request) | Keyed services, gói sau một interface hẹp (bài 7.8) |
| Chọn theo logic trên dữ liệu (nhiều điều kiện) | Factory tự viết |
| Tạo đối tượng với tham số động (host, cổng do người dùng nhập) | Factory tự viết |
| Chạy tất cả cài đặt | IEnumerable<T> không khoá |
Khoá của keyed services là object, nên enum dùng được và đổi tên an toàn. Khác biệt thật giữa hai cách nằm ở chỗ khác: factory với switch expression trên enum được trình biên dịch nhắc khi thiếu nhánh (cảnh báo CS8509, nếu không có nhánh _), còn keyed services thiếu đăng ký cho một khoá thì chỉ lộ ra lúc chạy.
7.10.5 — Container bên thứ ba: thường không cần
Autofac, Lamar từng cần thiết vì container .NET thiếu nhiều tính năng. Điều đó không còn đúng:
| Tính năng | Container .NET | Cần bên thứ ba? |
|---|---|---|
Ba lifetime, scope, Dispose | Có | Không |
| Keyed services | Có (từ .NET 8) | Không |
| Nhiều cài đặt cho một interface | Có (IEnumerable<T>) | Không |
| Open generic | Có | Không |
| Decorator, quét assembly | Qua Scrutor | Không |
| Property injection | Không | Có — nhưng đó là anti-pattern (bài 7.3) |
| Interception động (proxy chèn hành vi vào mọi lời gọi) | Không | Có — nhưng decorator tường minh thường rõ hơn |
Hai hàng cuối là lý do duy nhất còn lại, và cả hai đều đáng cân nhắc lại trước khi thêm một container mà cả đội phải học.
7.10.6 — Lỗi thường gặp
| Lỗi | Hậu quả | Cách tránh |
|---|---|---|
Thêm AddScoped<ILeadRepository, X>() sau các dòng Decorate | Đăng ký sau thắng (bài 7.2), mọi decorator biến mất âm thầm | Test Assert.IsType<LớpNgoàiCùng> (Bài 1) |
| Quên đăng ký phụ thuộc của decorator | ValidateOnBuild không bắt, lỗi lúc resolve đầu tiên | Test resolve thật từng service có decorator |
Module tính năng tự AddDbContext | Đăng ký trùng, mỗi module một cấu hình | Hạ tầng dùng chung ở một module riêng |
| Module dùng service do module khác đăng ký | Xoá module kia là module này hỏng | Test dựng riêng từng module cộng hạ tầng (Bài 2) |
| Quét assembly cho mọi thứ | Không lần được ai đăng ký gì | Chỉ quét nhóm lớn và đồng nhất |
Danh sách rà soát
- Cache, log, thử lại được thêm bằng decorator, không sửa service gốc.
- Mỗi service có decorator có một test resolve và kiểm tra lớp ngoài cùng.
- Đăng ký gom theo tính năng, mỗi module đặt cạnh code của nó.
- Hạ tầng dùng chung chỉ đăng ký ở một module.
- Quét theo quy ước chỉ dùng cho nhóm lớp đồng nhất.
- Đã cân nhắc kỹ trước khi thêm container bên thứ ba.
Bài tập áp dụng
Bài 1 — Thêm decorator và khoá thứ tự bằng test
Chạy ví dụ ở mục 7.10.1. Thêm một decorator thứ ba RetryLeadRepository thử lại tối đa 3 lần khi lớp bên trong ném TimeoutException, đặt nó giữa Timed và Cached. Viết một test xUnit khẳng định lớp ngoài cùng là TimedLeadRepository.
Tiêu chí hoàn thành: InMemoryLeadRepository không đổi dòng nào; output cho thấy thứ tự Timed -> Retry -> Cached -> InMemory; test xanh, và đỏ khi bạn đảo thứ tự hai dòng Decorate cuối.
Gợi ý và lời giải — Bài 1
Gợi ý. Lớp đăng ký Decorate sau nằm ngoài. Muốn Retry ở giữa thì nó phải được Decorate sau Cached và trước Timed.
Lời giải — decorator thử lại:
public sealed class RetryLeadRepository(ILeadRepository inner) : ILeadRepository
{
public async Task<Lead?> GetByIdAsync(int id, CancellationToken ct = default)
{
for (var attempt = 1; ; attempt++)
{
try
{
return await inner.GetByIdAsync(id, ct);
}
catch (TimeoutException) when (attempt < 3)
{
Console.WriteLine($" [retry] lần {attempt} hết thời gian, thử lại");
}
}
}
}catch ... when (attempt < 3) chỉ bắt lỗi ở lần 1 và 2; lần thứ 3 lỗi thì ngoại lệ bay ra ngoài như bình thường.
Đăng ký:
services.AddScoped<ILeadRepository, InMemoryLeadRepository>();
services.Decorate<ILeadRepository, CachedLeadRepository>();
services.Decorate<ILeadRepository, RetryLeadRepository>();
services.Decorate<ILeadRepository, TimedLeadRepository>(); // ngoài cùngTest (project xUnit tham chiếu project chứa các lớp trên, cài thêm Scrutor và Microsoft.Extensions.Caching.Memory):
[Fact]
public void LeadRepository_OutermostDecorator_IsTimed()
{
var services = new ServiceCollection();
services.AddMemoryCache();
services.AddScoped<ILeadRepository, InMemoryLeadRepository>();
services.Decorate<ILeadRepository, CachedLeadRepository>();
services.Decorate<ILeadRepository, RetryLeadRepository>();
services.Decorate<ILeadRepository, TimedLeadRepository>();
using var sp = services.BuildServiceProvider();
using var scope = sp.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<ILeadRepository>(); // resolve thật
Assert.IsType<TimedLeadRepository>(repo);
}Test này làm hai việc cùng lúc: khẳng định thứ tự, và resolve thật cả chuỗi, nên bắt được lỗi quên AddMemoryCache() mà ValidateOnBuild bỏ sót (mục 7.10.1). Trong dự án thật, phần đăng ký nằm trong một extension như AddLeadsFeature, và test gọi đúng extension đó thay vì chép lại các dòng Decorate.
Khi nào không dùng decorator:
| Tình huống | Vì sao |
|---|---|
| Interface có 15 phương thức | Mỗi decorator phải cài lại cả 15, kể cả khi chỉ quan tâm 1 |
| Việc cần thêm phụ thuộc vào dữ liệu nội bộ của lớp gốc | Decorator chỉ thấy tham số và kết quả |
| Chỉ có một chỗ cần | Viết thẳng vào lớp gốc đơn giản hơn |
Với interface lớn, tách interface nhỏ hơn trước (nguyên tắc phân tách interface, bài 5.1).
Bài 2 — Gom module và kiểm tra module tự đủ
Tách đăng ký của một dự án nhỏ thành AddInfrastructure và AddLeadsFeature như mục 7.10.2 (dùng InMemoryLeadRepository thay EF Core). Cho LeadService nhận thêm TimeProvider. Viết hai test: module Lead cộng hạ tầng dựng được; module Lead đứng một mình thì không.
Tiêu chí hoàn thành: Program.cs chỉ còn các dòng gọi module; test thứ hai in ra đúng service bị thiếu.
Gợi ý và lời giải — Bài 2
Gợi ý. ValidateOnBuild = true biến "thiếu đăng ký" thành ngoại lệ ngay khi gọi BuildServiceProvider, không cần resolve gì.
Lời giải — hai module:
public static class InfrastructureModule
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration config)
{
services.AddMemoryCache();
services.AddSingleton(TimeProvider.System);
return services;
}
}
public static class LeadsModule
{
public static IServiceCollection AddLeadsFeature(this IServiceCollection services, IConfiguration config)
{
services.AddScoped<ILeadRepository, InMemoryLeadRepository>();
services.Decorate<ILeadRepository, CachedLeadRepository>();
services.AddScoped<ILeadService, LeadService>();
services.AddOptions<SalesOptions>().Bind(config.GetSection("Sales")).ValidateOnStart();
return services;
}
}
public sealed class LeadService(ILeadRepository repo, TimeProvider time) : ILeadService
{
public Task<Lead?> FindAsync(int id, CancellationToken ct = default) => repo.GetByIdAsync(id, ct);
}Hai test:
[Fact]
public void LeadsModule_WithInfrastructure_ResolvesEverything()
{
var config = new ConfigurationBuilder().Build(); // cấu hình rỗng là đủ cho test này
var services = new ServiceCollection();
services.AddInfrastructure(config);
services.AddLeadsFeature(config);
using var sp = services.BuildServiceProvider(
new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true });
using var scope = sp.CreateScope();
Assert.NotNull(scope.ServiceProvider.GetRequiredService<ILeadService>());
}
[Fact]
public void LeadsModule_Alone_FailsBecauseInfrastructureMissing()
{
var config = new ConfigurationBuilder().Build();
var services = new ServiceCollection();
services.AddLeadsFeature(config); // KHÔNG có hạ tầng
var ex = Assert.Throws<AggregateException>(() =>
services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }));
Console.WriteLine(ex.InnerExceptions[0].Message);
}Cả hai test xanh trên .NET 10. Thông điệp của test thứ hai:
Error while validating the service descriptor 'ServiceType: ILeadService Lifetime: Scoped ImplementationType: LeadService': Unable to resolve service for type 'System.TimeProvider' while attempting to activate 'LeadService'.Để ý thứ không có trong thông điệp: CachedLeadRepository cũng thiếu IMemoryCache, nhưng ValidateOnBuild không thấy vì Decorate dùng factory. Test thứ nhất resolve thật ILeadService (kéo theo cả chuỗi repository) nên mới bắt được lỗi đó.
Test thứ hai là tài liệu sống: nó ghi lại rằng module Lead cần hạ tầng. Nếu một ngày ai đó cho LeadService dùng thêm service của module Invoice, một test tương tự với AddInfrastructure + AddLeadsFeature (không có Invoice) sẽ đỏ, và phụ thuộc ẩn giữa hai module lộ ra.
Bài 3 — Chọn giữa keyed services và factory
Hệ thống gửi thông báo qua Email, Sms, Zalo. Quy tắc: tin khẩn gửi SMS nếu khách có số điện thoại; còn lại gửi email nếu khách đồng ý nhận email, không thì gửi Zalo nếu có, không thì không gửi. Viết phần chọn kênh một lần bằng keyed services, một lần bằng factory, và nêu cách nào hợp hơn.
Tiêu chí hoàn thành: cả hai bản biên dịch được; bạn chỉ ra được phần nào của quy tắc keyed services không diễn đạt được.
Gợi ý và lời giải — Bài 3
Gợi ý. Keyed services trả lời câu hỏi "khoá X ứng với cài đặt nào". Quy tắc ở đây không có sẵn khoá: khoá phải tính ra từ dữ liệu khách hàng.
Lời giải — keyed services lo phần tra cứu:
public enum Channel { Email, Sms, Zalo }
builder.Services.AddKeyedScoped<INotificationSender, EmailSender>(Channel.Email);
builder.Services.AddKeyedScoped<INotificationSender, SmsSender>(Channel.Sms);
builder.Services.AddKeyedScoped<INotificationSender, ZaloSender>(Channel.Zalo);Nhưng ai đó vẫn phải tính ra Channel từ quy tắc. Keyed services không làm được phần đó, nên vẫn cần một lớp chọn:
public sealed class NotificationSenderSelector(IServiceProvider sp)
{
public INotificationSender? Select(Customer c, bool urgent)
{
Channel? channel =
urgent && c.HasPhone ? Channel.Sms :
c.AcceptsEmail ? Channel.Email :
c.HasZalo ? Channel.Zalo :
null;
return channel is { } key ? sp.GetRequiredKeyedService<INotificationSender>(key) : null;
}
}Factory — nhận thẳng các cài đặt, không cần khoá:
public sealed class NotificationSenderFactory(EmailSender email, SmsSender sms, ZaloSender zalo)
{
public INotificationSender? Select(Customer c, bool urgent)
{
if (urgent && c.HasPhone) return sms;
if (c.AcceptsEmail) return email;
if (c.HasZalo) return zalo;
return null; // không gửi
}
}
builder.Services.AddScoped<EmailSender>();
builder.Services.AddScoped<SmsSender>();
builder.Services.AddScoped<ZaloSender>();
builder.Services.AddScoped<NotificationSenderFactory>();So sánh:
| Keyed services + lớp chọn | Factory | |
|---|---|---|
| Phần quy tắc nằm ở | Lớp chọn | Factory |
| Phụ thuộc hiện trong constructor | Không — chỉ thấy IServiceProvider | Có — thấy đủ ba sender |
ValidateOnBuild kiểm tra được | Không | Có |
| Thêm kênh thứ tư | Thêm giá trị enum, một dòng đăng ký, sửa quy tắc | Thêm tham số constructor, một dòng đăng ký, sửa quy tắc |
Ở bài này factory hợp hơn: quy tắc là logic trên dữ liệu, không phải tra một khoá có sẵn, nên keyed services không bớt được việc gì mà còn giấu phụ thuộc sau IServiceProvider. Keyed services hợp khi khoá đến từ dữ liệu sẵn có (cột PreferredChannel trong bảng khách hàng, cấu hình của tenant) và chỉ cần tra.
Tự kiểm tra
Decorate gọi hai lần thì lớp nào nằm ngoài cùng?
Lớp được Decorate sau cùng nằm ngoài cùng. Với Decorate Cached rồi Decorate Timed, chuỗi là Timed bọc Cached bọc lớp gốc, nên Timed đo được cả những lần trúng cache.
Vì sao ValidateOnBuild không bắt được phụ thuộc thiếu của decorator Scrutor?
Vì Scrutor cài Decorate bằng factory registration, và ValidateOnBuild không nhìn được vào bên trong lambda của factory. Lỗi chỉ lộ ra khi service được resolve lần đầu, nên mỗi service có decorator nên có một test resolve thật.
Thêm AddScoped cho cùng interface sau các dòng Decorate thì sao?
Đăng ký sau thắng khi resolve một instance, nên lớp mới thay hẳn chuỗi decorator và mọi decorator biến mất mà không có lỗi nào. Test Assert.IsType với lớp ngoài cùng bắt được trường hợp này.
Module đăng ký theo tính năng khác tách theo layer ở bài 7.6 thế nào?
Tách theo layer gom theo loại (hạ tầng, ứng dụng), nên mỗi extension vẫn chứa đăng ký của mọi tính năng. Tách theo tính năng gom mọi đăng ký của một tính năng vào một extension đặt cạnh code của nó, nên ít xung đột merge và xoá tính năng gọn hơn. Hạ tầng dùng chung vẫn nằm ở một module riêng.
Đánh đổi của quét assembly theo quy ước là gì?
Gọn hơn nhiều khi có hàng chục lớp cùng khuôn, nhưng bạn không còn thấy service nào được đăng ký khi đọc code và không grep ra được. Nên chỉ quét nhóm lớp lớn và đồng nhất, đăng ký tường minh phần còn lại.
Khi nào factory tự viết hợp hơn keyed services?
Khi việc chọn là logic trên dữ liệu chứ không phải tra một khoá có sẵn, hoặc khi cần tạo đối tượng với tham số động. Factory nhận thẳng các cài đặt qua constructor nên phụ thuộc hiện rõ và ValidateOnBuild kiểm tra được.
Kết luận
Ba điều đáng nhớ nhất:
- Decorator thêm hành vi mà không sửa code gốc.
Decorategọi sau nằm ngoài cùng, và vì nó dùng factory nên cần test resolve thật. - Gom đăng ký theo tính năng, hạ tầng dùng chung ở một chỗ, và viết test dựng từng module để lộ phụ thuộc ẩn.
- Container tích hợp cộng Scrutor đủ cho hầu hết dự án .NET.
Tham khảo
Điều hướng
- Bài trước: 7.9 — Anti-patterns
- Bài tiếp theo: 7.11 — Captive dependency làm lộ dữ liệu giữa tenant
- Về module: Trang mục lục