C#分层架构下层级继承DTO模型无侵入扩展实现方法
我使用C#进行开发(编程语言不影响问题本质),核心类库中现有如下服务层公用DTO定义:
public abstract class SuspensionRequestCreatedBaseDto { public int RequestId { get; set; } public string DeviceIMEI { get; set; } public string DeviceName { get; set; } } public class SuspensionRequestCreatedExpirationTimeDto : SuspensionRequestCreatedBaseDto { public DateTime? LastActive { get; set; } public DateTime ApprovePlannedDateTime { get; set; } } public class SuspensionRequestCreatedReachDataLimitDto : SuspensionRequestCreatedBaseDto { public int DataUsedMb { get; set; } }
以上DTO归属于核心类库,禁止耦合任何UI层专属逻辑。但在UI项目中,需要为不同类型的暂停申请DTO实现差异化的邮件内容生成逻辑,理想状态下可以直接调用request.PrepareEmailPart(...),无需显式判断DTO具体类型,期望的逻辑结构如下:
public abstract class SuspensionRequestCreatedBaseDto { public int RequestId { get; set; } public string DeviceIMEI { get; set; } public string DeviceName { get; set; } public virtual string PrepareEmailPart(string body) { // 逻辑1(基础逻辑) } } public class SuspensionRequestCreatedExpirationTimeDto : SuspensionRequestCreatedBaseDto { public DateTime? LastActive { get; set; } public DateTime ApprovePlannedDateTime { get; set; } public override string PrepareEmailPart(string body) { // 逻辑2 } } public class SuspensionRequestCreatedReachDataLimitDto : SuspensionRequestCreatedBaseDto { public int DataUsedMb { get; set; } public override string PrepareEmailPart(string body) { // 逻辑3 } }
由于PrepareEmailPart属于UI专属逻辑,不能直接添加到核心类库的DTO定义中。最初尝试通过外层包装Model的方式实现,代码如下:
public class SuspensionRequestCreatedBaseModel { public virtual SuspensionRequestCreatedBaseDto SuspensionRequest { get; set; } public virtual string PrepareEmailPart(string body) { // 基础逻辑 } } public class SuspensionRequestCreatedExpirationTimeModel : SuspensionRequestCreatedBaseModel { public override SuspensionRequestCreatedExpirationTimeDto SuspensionRequest {get; set;} public override string PrepareEmailPart(string body) { // 逻辑2 } }
编译时出现如下报错:
'SuspensionRequestCreatedExpirationTimeModel.SuspensionRequest':
type must be 'SuspensionRequestCreatedBaseDto' to match overridden
member 'SuspensionRequestCreatedBaseModel.SuspensionRequest'
需要实现的目标:
- 避免使用
case/类型判断的反模式 - 不将UI层专属逻辑混入核心类库
- 调用时可以直接执行对应类型的差异化逻辑,无需手动判断类型
方案1:泛型包装类(改动最小,适配当前思路)
之前的编译报错是因为C#不支持普通属性重写时修改返回类型(不支持属性协变重写),通过泛型加类型约束可以完美解决这个问题,完全不需要修改核心类库代码:
// 先抽非泛型基类,用于统一处理所有类型的包装实例 public abstract class SuspensionRequestCreatedBaseModel { public abstract string PrepareEmailPart(string body); } // 泛型基类,约束泛型参数必须是核心层的基础DTO类型 public abstract class SuspensionRequestCreatedBaseModel<T> : SuspensionRequestCreatedBaseModel where T : SuspensionRequestCreatedBaseDto { public T SuspensionRequest { get; set; } public override string PrepareEmailPart(string body) { // 基础通用逻辑,可直接访问基类公共属性 return $"申请ID:{SuspensionRequest.RequestId},设备:{SuspensionRequest.DeviceName}\n{body}"; } } // 到期类型的包装实现 public class SuspensionRequestCreatedExpirationTimeModel : SuspensionRequestCreatedBaseModel<SuspensionRequestCreatedExpirationTimeDto> { public override string PrepareEmailPart(string body) { // 差异化逻辑,可直接访问子类专属属性LastActive、ApprovePlannedDateTime var extraInfo = $"计划审批时间:{SuspensionRequest.ApprovePlannedDateTime:yyyy-MM-dd HH:mm}"; return base.PrepareEmailPart($"{extraInfo}\n{body}"); } } // 流量超限类型的包装实现 public class SuspensionRequestCreatedReachDataLimitModel : SuspensionRequestCreatedBaseModel<SuspensionRequestCreatedReachDataLimitDto> { public override string PrepareEmailPart(string body) { // 差异化逻辑,可直接访问子类专属属性DataUsedMb var extraInfo = $"已使用流量:{SuspensionRequest.DataUsedMb}MB"; return base.PrepareEmailPart($"{extraInfo}\n{body}"); } }
使用时只需要把核心层返回的DTO包装成对应的Model类型,后续统一以SuspensionRequestCreatedBaseModel类型传递,直接调用PrepareEmailPart就会自动执行对应子类的逻辑,完全不需要类型判断。
方案2:访问者模式(适合多逻辑扩展场景)
如果后续除了生成邮件,还有大量针对不同DTO类型的差异化操作(比如生成弹窗内容、导出数据、推送差异化通知),可以用访问者模式,核心类库只需要依赖抽象访问者接口,完全不耦合具体UI逻辑:
首先在核心类库添加抽象访问者定义和基类Accept方法,核心层不引用任何UI层代码:
// 核心层定义抽象访问者接口 public interface ISuspensionRequestVisitor { void Visit(SuspensionRequestCreatedExpirationTimeDto dto); void Visit(SuspensionRequestCreatedReachDataLimitDto dto); } public abstract class SuspensionRequestCreatedBaseDto { public int RequestId { get; set; } public string DeviceIMEI { get; set; } public string DeviceName { get; set; } // 仅依赖抽象接口,不耦合具体实现 public abstract void Accept(ISuspensionRequestVisitor visitor); } // 两个子类实现Accept方法 public class SuspensionRequestCreatedExpirationTimeDto : SuspensionRequestCreatedBaseDto { public DateTime? LastActive { get; set; } public DateTime ApprovePlannedDateTime { get; set; } public override void Accept(ISuspensionRequestVisitor visitor) => visitor.Visit(this); } public class SuspensionRequestCreatedReachDataLimitDto : SuspensionRequestCreatedBaseDto { public int DataUsedMb { get; set; } public override void Accept(ISuspensionRequestVisitor visitor) => visitor.Visit(this); }
然后在UI层实现具体的邮件生成访问者:
public class EmailPartGenerateVisitor : ISuspensionRequestVisitor { public string EmailContent { get; private set; } private readonly string _rawBody; public EmailPartGenerateVisitor(string rawBody) { _rawBody = rawBody; } public void Visit(SuspensionRequestCreatedExpirationTimeDto dto) { // 到期类型的邮件生成逻辑 var extraInfo = $"计划审批时间:{dto.ApprovePlannedDateTime:yyyy-MM-dd HH:mm}"; EmailContent = $"申请ID:{dto.RequestId},设备:{dto.DeviceName}\n{extraInfo}\n{_rawBody}"; } public void Visit(SuspensionRequestCreatedReachDataLimitDto dto) { // 流量超限类型的邮件生成逻辑 var extraInfo = $"已使用流量:{dto.DataUsedMb}MB"; EmailContent = $"申请ID:{dto.RequestId},设备:{dto.DeviceName}\n{extraInfo}\n{_rawBody}"; } }
调用时不需要判断类型,直接执行Accept即可:
var visitor = new EmailPartGenerateVisitor(body); dto.Accept(visitor); var emailPart = visitor.EmailContent;
这个方案完全符合开闭原则,后续新增差异化逻辑只需要添加新的访问者实现,不需要修改DTO和现有逻辑代码。
方案选择建议
- 如果只有少量UI侧差异化逻辑,优先选泛型包装类,实现简单、改动量小
- 如果后续会持续新增针对不同DTO的差异化操作,选访问者模式,长期可维护性更高
两种方案都不需要写switch/case做类型判断,也不会把UI层逻辑泄露到核心类库。
内容的提问来源于stack exchange,提问作者Oleg Sh

