继承含子类的超类时的代码设计重构方案求助
这个问题在C#这类单继承语言里简直是日常痛点——多继承的限制确实会把人卡得难受,复制代码绝对是下下策,给你几个靠谱的重构方向,都是行业里常用的解决方案:
核心思路是用“持有对象”代替“继承对象”,把SepaControl里的定制行为抽离成接口,然后让SepaTextField通过内部持有SepaControl的实例来复用这些逻辑,同时保留和TextField的继承关系。
举个具体的例子:
首先,把你需要在SepaControl里修改/重写的行为抽象成一个接口:
public interface ISepaControlBehavior { void ExecuteSepaCustomLogic(); void ApplySepaPaintStyle(PaintEventArgs e); // 把所有Sepa专属的行为都定义在这里 }
然后让SepaControl实现这个接口,同时把原本的protected逻辑暴露成公共方法(或者直接在接口方法里实现):
public class SepaControl : Control, ISepaControlBehavior { public void ExecuteSepaCustomLogic() { // 这里放你原本在SepaControl里定制的业务逻辑 } public void ApplySepaPaintStyle(PaintEventArgs e) { // 这里放你原本在OnPaint里重写的Sepa绘制逻辑 // 比如修改边框颜色、添加水印之类的 } // 保留原本的Control重写方法,供自身使用 protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); ApplySepaPaintStyle(e); } }
接下来,SepaTextField继承TextField,同时实现这个接口,内部委托给SepaControl的实例:
public class SepaTextField : TextField, ISepaControlBehavior { // 持有SepaControl的实例,用来复用逻辑 private readonly ISepaControlBehavior _sepaBehavior = new SepaControl(); public void ExecuteSepaCustomLogic() { _sepaBehavior.ExecuteSepaCustomLogic(); } protected override void OnPaint(PaintEventArgs e) { // 先执行TextField原本的绘制逻辑 base.OnPaint(e); // 再应用Sepa的定制样式 _sepaBehavior.ApplySepaPaintStyle(e); } // 其他需要重写的Control方法,都可以通过委托给_sepaBehavior来实现 }
这个方案的好处是完全不破坏原有继承链,而且SepaControl的逻辑可以被任何需要的控件复用,后续修改也只需要改一处,维护成本极低。唯一的小缺点是需要写一些委托代码,但比起复制代码来说简直不值一提。
如果项目A和B的依赖关系可以调整,最直接的办法是把TextField的基类从Control改成SepaControl,这样SepaTextField继承TextField后自然就拥有了SepaControl的所有行为。
比如:
// 修改项目A中的TextField public class TextField : SepaControl { // 保留原本TextField的所有逻辑 } // 你的SepaTextField就可以直接继承TextField,自动获得SepaControl的行为 public class SepaTextField : TextField { // 只需要添加SepaTextField专属的逻辑即可 }
这个方案最简洁,但前提是所有依赖TextField的项目都能接受它继承SepaControl——如果项目A还有其他项目依赖,而且那些项目不需要SepaControl的行为,那这个方案就不适用了。
如果不想修改现有类的继承关系,也可以用装饰器来动态给TextField添加SepaControl的行为。
比如创建一个装饰器类,包装Control并添加Sepa的定制逻辑:
public class SepaControlDecorator { private readonly Control _targetControl; public SepaControlDecorator(Control targetControl) { _targetControl = targetControl; // 可以在这里订阅目标控件的事件,比如Paint事件,添加Sepa的逻辑 _targetControl.Paint += OnTargetControlPaint; } private void OnTargetControlPaint(object sender, PaintEventArgs e) { // 这里添加Sepa的定制绘制逻辑 // 比如给文本框添加Sepa风格的边框 } // 其他需要定制的行为,比如处理输入验证,都可以通过订阅事件来实现 }
然后在SepaTextField里使用这个装饰器:
public class SepaTextField : TextField { private SepaControlDecorator _sepaDecorator; public SepaTextField() { _sepaDecorator = new SepaControlDecorator(this); } }
这个方案的优点是非常灵活,可以动态给任何Control添加Sepa行为,缺点是如果需要定制的方法很多,订阅事件的代码会比较零散,不如组合模式集中。
如果你的项目用的是C# 8及以上版本,可以用接口默认方法来实现类似Mixin的效果,把Sepa的行为定义在接口里,让SepaTextField直接实现接口获得默认逻辑。
比如:
public interface ISepaControl { void ApplySepaStyle(PaintEventArgs e) { // 这里写Sepa的默认绘制逻辑 e.Graphics.DrawRectangle(Pens.Blue, this.ClientRectangle); } void ValidateSepaInput() { // 默认的Sepa输入验证逻辑 } }
然后SepaTextField实现这个接口:
public class SepaTextField : TextField, ISepaControl { protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); // 调用接口的默认方法 this.ApplySepaStyle(e); } protected override void OnValidating(CancelEventArgs e) { base.OnValidating(e); this.ValidateSepaInput(); } }
这个方案的好处是代码简洁,但局限性也很明显:接口默认方法不能有自己的状态(比如不能定义字段),如果SepaControl需要维护一些内部状态(比如配置参数),这个方案就不适用了,还是得结合组合模式。
总的来说,组合+委托模式是最稳妥的选择,不管项目结构多么复杂,它都能在不破坏现有代码的前提下实现需求,而且后续维护和扩展都很方便。
内容的提问来源于stack exchange,提问作者Konzy262

