一键导入
solid-principles
Use when evaluating code against SOLID principles or deciding when to apply or skip them.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when evaluating code against SOLID principles or deciding when to apply or skip them.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when writing async code, propagating CancellationTokens, or fixing async/await pitfalls.
Use when applying or enforcing C# coding style — namespaces, sealed classes, var usage, XML docs.
Use when applying modern C# idioms — records, pattern matching, primary constructors, collection expressions.
Use when registering services, choosing lifetimes, or implementing DI patterns like decorator or keyed services.
Use when selecting or implementing design patterns in C# — factory, builder, strategy, decorator, or mediator.
| name | solid-principles |
| description | Use when evaluating code against SOLID principles or deciding when to apply or skip them. |
| when_to_use | Use when evaluating code against SOLID principles or deciding when to apply or skip them. |
| metadata | {"category":"core","agent":"dotnet-architect"} |
SOLID is a refactoring guide, not a starting requirement. Write simply first. Extract abstractions when complexity demands it.
| Principle | Apply When |
|---|---|
| SRP | A class handles both business logic and infrastructure (e.g., validation + database calls) |
| OCP | You keep modifying the same switch/if-else chain to add new behavior |
| LSP | You have a class hierarchy and consumers use the base type polymorphically |
| ISP | Implementors are forced to throw NotImplementedException for methods they don't need |
| DIP | You need to test business logic without hitting databases, APIs, or file systems |
IOrderService if OrderService is the only implementation and you have no testing seam requirement// GOOD: Each handler has exactly one responsibility
public sealed record CreateOrderCommand(
string CustomerName,
List<OrderItemDto> Items) : IRequest<Result<Guid>>;
public sealed class CreateOrderHandler(
IOrderRepository repository,
IUnitOfWork unitOfWork,
TimeProvider timeProvider) : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
CreateOrderCommand request,
CancellationToken ct)
{
var order = Order.Create(
request.CustomerName,
request.Items,
timeProvider.GetUtcNow());
repository.Add(order);
await unitOfWork.SaveChangesAsync(ct);
return Result.Success(order.Id);
}
}
// BAD: One service doing everything — creating, updating, deleting, emailing, reporting
public class OrderService
{
public Task CreateAsync(...) { /* ... */ }
public Task UpdateAsync(...) { /* ... */ }
public Task DeleteAsync(...) { /* ... */ }
public Task SendConfirmationEmailAsync(...) { /* ... */ }
public Task GenerateReportAsync(...) { /* ... */ }
}
// Define the contract
public interface IDiscountStrategy
{
decimal Calculate(Order order);
}
// Add new strategies without modifying existing ones
public sealed class LoyaltyDiscount : IDiscountStrategy
{
public decimal Calculate(Order order) =>
order.Customer.LoyaltyYears >= 3 ? order.Total * 0.10m : 0m;
}
public sealed class BulkDiscount : IDiscountStrategy
{
public decimal Calculate(Order order) =>
order.Items.Count >= 10 ? order.Total * 0.05m : 0m;
}
public sealed class SeasonalDiscount : IDiscountStrategy
{
public decimal Calculate(Order order) =>
order.CreatedAt.Month is 11 or 12 ? order.Total * 0.15m : 0m;
}
// Consumer is closed for modification — new discounts need zero changes here
public sealed class DiscountCalculator(IEnumerable<IDiscountStrategy> strategies)
{
public decimal CalculateTotal(Order order) =>
strategies.Sum(s => s.Calculate(order));
}
// Registration — add new strategies by registering, not by editing
services.AddSingleton<IDiscountStrategy, LoyaltyDiscount>();
services.AddSingleton<IDiscountStrategy, BulkDiscount>();
services.AddSingleton<IDiscountStrategy, SeasonalDiscount>();
// Base contract
public interface INotificationSender
{
Task SendAsync(Notification notification, CancellationToken ct);
}
// Both implementations are fully substitutable
public sealed class EmailNotificationSender(
IEmailClient emailClient) : INotificationSender
{
public async Task SendAsync(Notification notification, CancellationToken ct)
{
await emailClient.SendAsync(notification.Recipient, notification.Body, ct);
}
}
public sealed class SmsNotificationSender(
ISmsGateway gateway) : INotificationSender
{
public async Task SendAsync(Notification notification, CancellationToken ct)
{
await gateway.SendTextAsync(notification.Recipient, notification.Body, ct);
}
}
// BAD — violates LSP: subtype changes behavior the caller doesn't expect
public sealed class LogOnlyNotificationSender : INotificationSender
{
public Task SendAsync(Notification notification, CancellationToken ct)
{
// Silently does nothing — caller expects delivery
Console.WriteLine($"Would send: {notification.Body}");
return Task.CompletedTask;
}
}
// GOOD: Segregated interfaces — clients depend only on what they use
public interface IOrderReader
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
Task<IReadOnlyList<Order>> GetByCustomerAsync(Guid customerId, CancellationToken ct);
}
public interface IOrderWriter
{
void Add(Order order);
void Remove(Order order);
}
// Query handlers only need IOrderReader
public sealed class GetOrderHandler(
IOrderReader reader) : IRequestHandler<GetOrderQuery, OrderDto?>
{
public async Task<OrderDto?> Handle(GetOrderQuery query, CancellationToken ct)
{
var order = await reader.GetByIdAsync(query.Id, ct);
return order?.ToDto();
}
}
// Command handlers get IOrderWriter (and IOrderReader if needed)
public sealed class DeleteOrderHandler(
IOrderReader reader,
IOrderWriter writer,
IUnitOfWork unitOfWork) : IRequestHandler<DeleteOrderCommand, Result>
{
public async Task<Result> Handle(DeleteOrderCommand command, CancellationToken ct)
{
var order = await reader.GetByIdAsync(command.Id, ct);
if (order is null) return Result.NotFound();
writer.Remove(order);
await unitOfWork.SaveChangesAsync(ct);
return Result.Success();
}
}
// BAD: One bloated interface forces implementors to support everything
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct);
Task<IReadOnlyList<Order>> GetByCustomerAsync(Guid customerId, CancellationToken ct);
Task<IReadOnlyList<Order>> GetPendingAsync(CancellationToken ct);
Task<IReadOnlyList<Order>> SearchAsync(string query, CancellationToken ct);
void Add(Order order);
void Update(Order order);
void Remove(Order order);
Task BulkInsertAsync(IEnumerable<Order> orders, CancellationToken ct);
Task ArchiveOlderThanAsync(DateTimeOffset cutoff, CancellationToken ct);
Task<int> CountAsync(CancellationToken ct);
// ... 10 more methods most consumers never call
}
// High-level module defines the contract
public interface IPaymentProcessor
{
Task<PaymentResult> ChargeAsync(decimal amount, string currency, CancellationToken ct);
}
// Low-level module implements it
public sealed class StripePaymentProcessor(
IOptions<StripeOptions> options,
HttpClient httpClient) : IPaymentProcessor
{
public async Task<PaymentResult> ChargeAsync(
decimal amount, string currency, CancellationToken ct)
{
// Stripe-specific implementation
var response = await httpClient.PostAsJsonAsync(
"/v1/charges",
new { amount, currency },
ct);
return response.IsSuccessStatusCode
? PaymentResult.Success()
: PaymentResult.Failed("Charge declined");
}
}
// Business logic depends on abstraction — testable, swappable
public sealed class CheckoutHandler(
IPaymentProcessor paymentProcessor,
IOrderWriter orderWriter,
IUnitOfWork unitOfWork) : IRequestHandler<CheckoutCommand, Result>
{
public async Task<Result> Handle(CheckoutCommand command, CancellationToken ct)
{
var payment = await paymentProcessor.ChargeAsync(
command.Total, "usd", ct);
if (!payment.Succeeded)
return Result.Failure(payment.Error);
orderWriter.Add(Order.CreatePaid(command));
await unitOfWork.SaveChangesAsync(ct);
return Result.Success();
}
}
Don't create an interface for a class that will only ever have one implementation.
// BAD: Interface exists only because "SOLID says so"
public interface IOrderValidator { ... }
public sealed class OrderValidator : IOrderValidator { ... }
// No other implementation. No mock needed (FluentValidation is testable directly).
// GOOD: Just use the concrete class
services.AddScoped<OrderValidator>();
Too many tiny files for simple operations add navigation overhead without benefit.
// BAD: 6 files for one simple operation
// CreateOrderCommand.cs, CreateOrderCommandValidator.cs,
// CreateOrderCommandHandler.cs, CreateOrderCommandResponse.cs,
// ICreateOrderService.cs, CreateOrderService.cs
// GOOD: Co-locate command + handler in one file when the handler is short
// CreateOrder.cs — contains the record, validator, and handler
Don't extract an interface before you have a genuine second use case or a testing seam that demands it.
// BAD: Abstracting on day one with one implementation
public interface IDateTimeProvider { DateTimeOffset UtcNow { get; } }
public sealed class DateTimeProvider : IDateTimeProvider { ... }
// .NET 8+ has TimeProvider built in — no custom abstraction needed
// GOOD: Use the built-in TimeProvider abstract class
services.AddSingleton(TimeProvider.System);
| Scenario | Principle | Apply? | Why |
|---|---|---|---|
| Service handles HTTP + business logic + DB | SRP | Yes | Split into handler + repository |
Growing switch on OrderType for pricing | OCP | Yes | Strategy pattern eliminates the switch |
| Internal helper class, 30 lines | SRP | No | It already has one job |
| Repository used only by commands | ISP | Maybe | Split if query side exists separately |
| Class with one implementation, no tests | DIP | No | Add interface when you need a seam |
| Base class where subtypes skip methods | LSP | Yes | Subtypes must honor the full contract |
| 3+ implementations of same behavior | OCP | Yes | Add via registration, not modification |
| Simple CRUD API, no domain logic | All | No | SOLID adds overhead with no payoff |
| Shared library consumed by many teams | ISP | Yes | Consumers should not depend on unused methods |
| Unit testing a class with external deps | DIP | Yes | Interface enables mocking |
| Problem | Why It Hurts | Correct Approach |
|---|---|---|
| One interface per class, always | Doubles file count, adds indirection with no benefit | Create interfaces when you have 2+ implementations or need a test seam |
| God class with 500+ lines | Impossible to test, understand, or modify safely | Split by responsibility into focused classes |
| Deep inheritance hierarchies | Fragile base class problem, tight coupling | Prefer composition and interface implementation |
Empty interface methods (NotImplementedException) | Violates LSP, callers cannot trust the contract | Segregate interfaces so each implementor uses all methods |
| Abstracting everything on day one | Premature abstraction slows development and obscures intent | Start concrete, extract when the second use case appears |
| Injecting 8+ dependencies in constructor | Signals SRP violation — class does too much | Split the class or introduce a facade/mediator |
Creating IMapper, ILogger wrappers | Wrapping well-tested libraries adds no value | Use AutoMapper/Mapster and ILogger<T> directly |
| Refactoring stable, working code "for SOLID" | Risk of introducing bugs in code that works | Apply SOLID when you need to change the code, not before |
switch or long if-else chains that grow with new types (OCP candidate)NotImplementedException — signals ISP or LSP violation