
C#项目维护到一定阶段重构是绕不开的话题。只要你还在写业务代码、还在接手别人的上位机项目就一定遇到过那种看三遍还不敢改的方法体变量名叫a1、b2一个方法两百行里面还嵌套三层if。这正是那篇被转了很多次的《C#重构代码的8种基本方法》想解决的问题——不是让你去背一堆理论而是给你一套能直接落地的操作清单。我做C#开发这些年大大小小重构过几十个项目从工控上位机到Web服务都碰过今天就把这8种基本方法结合真实场景重新讲一遍告诉你每种方法在什么时机用、怎么用、有哪些坑。这篇文章适合刚开始接触重构的新人也适合那些已经在重构但经常把代码越改越乱的开发者我会尽量把“为什么这么做”也讲透。1. 重构不是炫技先搞清楚要解决什么问题1.1 什么是重构什么不是重构很多人一提重构脑子里浮现的是“推翻重写”“换框架”“升级语法”。这不是重构这是重写。重构的定义很朴素在不改变代码外部行为的前提下改善内部结构。说人话就是——功能还是那个功能输入输出还是那个结果但代码变得更容易读、更容易改、更容易测。我在实际项目里见过太多把重构和重写搞混的情况。有一回同事觉得一个报表模块太乱花了两周“重构”结果把数据源从DataSet换成了EntityFramework顺带改了数据库表结构最后整条业务链路崩了大半。那不是重构那是重新发明了一套系统。真正的基本方法应该像给房子做内部改造承重墙不能动水管电线走向尽量不变改的是格局和收纳。C#里也一样接口的签名尽量不动方法的行为尽量保持一致你改的是方法内部的组织方式、类与类之间的协作关系、重复逻辑的收敛方式。1.2 重构的前置条件测试保护网没有测试就重构等于没有安全网就走钢丝。C#项目里当然有那种历史包袱特别重、压根没有单元测试的代码但这不代表你不需要保护网至少你要先把“手工验证清单”列出来。我的习惯是在重构之前先做两件事把核心流程跑一遍记录关键输入和输出。给最危险的方法补几个最小的单元测试不要求覆盖全只要求能抓住行为变化。比如你面对一个计算电费的方法输入用电量和峰谷时段输出电费。你至少要用三个数据点把正常路径、边界路径、异常路径固定住。如果项目里连测试框架都没建用控制台写个临时验证脚本也行关键在于重构前后跑出来的结果必须一致。保护网的意义在于你改完代码敢点“生成”失败了能立刻知道是哪一步改坏了而不是对着满屏报错发懵。1.3 识别代码坏味道的清单要使用8种基本方法你得先知道该用哪一种而判断依据就是代码里的“坏味道”。我总结了几个最常见的信号方法太长超过30行或者你需要在滚动条里找结尾。重复代码同一段逻辑复制粘贴了三处以上。过长参数列表一个方法有超过4个参数调用的人记不住顺序。过度使用switch或if-else看到switch(type)里每个case都调不同的方法就该考虑多态了。类太大一个类做太多事比如既管数据访问又管界面展示还管日志记录。霰弹式修改改一个需求需要动五六个不相关地方的代码。这些坏味道就是8种方法的触发条件。你不需要把一本书读完才动手只要闻到味找到对应的方法去做就行。2. 8种基本方法逐个拆解2.1 提取方法Extract Method——最常用、最安全的重构提取方法的意思是把一段独立的逻辑从一个大方法里搬出去成为一个新的、有名字的方法。这是所有重构方法里回报率最高的一种也最适合新手练习。为什么要提取因为人脑的工作记忆是有限的。一个方法里同时处理数据校验、格式转换、计算和日志输出读代码的人需要同时记住四件事。提取之后每个方法只做一件事方法名就是注释调用处读起来像在朗读业务步骤。比如你有一段判断设备是否允许启动的代码if (device.Status DeviceStatus.Ready device.LastHeartbeat.AddMinutes(5) DateTime.Now _authService.CheckPermission(currentUser, device.Id)) { StartDevice(device); }这段逻辑有业务含义但被一堆技术细节盖住了。提取出一个方法后if (CanStartDevice(currentUser, device)) { StartDevice(device); } private bool CanStartDevice(User user, Device device) { return device.Status DeviceStatus.Ready device.LastHeartbeat.AddMinutes(5) DateTime.Now _authService.CheckPermission(user, device.Id); }注意提取出来的方法名要能回答问题“这个方法到底在判断什么”。CanStartDevice远比CheckDeviceAndUser清晰。这是重构里最容易上手的一步但也是最容易被忽略的一步因为很多人习惯了“直接往下写”不愿意停下来给代码起名字。实操时有两个技巧一是提取出的方法体内不应使用临时变量来传值传递上下文尽量用参数或返回值二是提取后要立刻编译运行确认行为没变。如果提取过程中发现方法内部用了外部变量要么把变量作为参数传进去要么让它成为返回值的一部分绝不能直接引用一个“碰巧在作用域里的变量”否则你会造出隐式耦合。2.2 引入解释变量Introduce Explaining Variable——别再让读的人猜含义当你看到一个复杂的布尔表达式比如if (order.Total 1000 order.Customer.Level 3 order.CreatedDate DateTime.Today.AddDays(-30)) { // 给予VIP折扣 }这段表达式的每一个子句可能都有业务含义但它们全挤在一起读代码的人必须先猜order.Total 1000是什么意思再看Customer.Level 3又是什么。不如拆开bool isLargeOrder order.Total 1000; bool isHighLevelCustomer order.Customer.Level 3; bool isRecentOrder order.CreatedDate DateTime.Today.AddDays(-30); if (isLargeOrder isHighLevelCustomer isRecentOrder) { // 给予VIP折扣 }这就是引入解释变量。它的价值在于给一段“没有名字的计算结果”起一个业务名字。很多人在第一次重构时觉得这步多余但当你三个月后回来看代码这三个变量名能直接告诉你当时的业务判断依据。有一种情况你需要小心如果这个表达式会被多次使用比如在循环里或者多个if中被重复计算那么引入解释变量不仅提高可读性还避免重复求值。如果只在单个if块里用一次我更推荐直接提取成方法因为方法可以被复用变量做不到。2.3 用多态替换条件表达式Replace Conditional with Polymorphism——消灭switch魔鬼这是8种方法里“面向对象”味道最重的一个。当你看到switch或if-else根据某个类型做不同分支处理时意味着这个行为分散在了多个地方每增加一种新类型你就要打开这个开关再补一个case改着改着就漏了。比如你有一个计算不同设备数据解析的方法public object ParseDeviceData(string deviceType, byte[] rawData) { switch (deviceType) { case PLc: return ParsePlcData(rawData); case Dcs: return ParseDcsData(rawData); case Sensor: return ParseSensorData(rawData); default: throw new NotSupportedException(); } }假设设备类型的数量还会增长这段代码就会不断膨胀。换成多态的思路就是让每种设备自己负责自己的解析逻辑public interface IDeviceParser { string DeviceType { get; } object Parse(byte[] rawData); } public class PlcParser : IDeviceParser { public string DeviceType PLc; public object Parse(byte[] rawData) /* PLC解析逻辑 */; } public class DcsParser : IDeviceParser { public string DeviceType Dcs; public object Parse(byte[] rawData) /* DCS解析逻辑 */; }然后你可以用一个工厂来收集所有IDeviceParser调用处直接parser.Parse(rawData)业务逻辑不再关心设备类型分支。这个重构的收益在于“开闭原则”——新增设备类型时你只需要新增一个类不用回头改判断逻辑。代价是类数量变多结构变复杂。所以我在实际项目中有一条自己的原则只有当分支超过两个、且未来大概率会继续扩展类型时才用多态。如果只有两种类型且几年都不变switch反而更直接。过度设计往往是重构最容易踩的坑之一8种基本方法教的不是“凡是switch都要干掉”而是“在合适的时机用合适的工具”。2.4 提取类与引入参数对象Extract Class Introduce Parameter Object——给臃肿类瘦身当一个类里包含了太多不相关的职责或者一个方法需要传5个以上参数你需要考虑这两个基本方法。先看提取类。比如你的DeviceService里既有设备通信逻辑、又有数据解析逻辑、还有配置文件读写逻辑。每次改通信要动这个类改解析也要动这个类两边并行开发时还会冲突不断。这时候应该拆成DeviceCommunication、DeviceDataParser、DeviceConfig三个类让每个类的职责单一。拆类不是简单的把代码搬个家你要注意类与类之间的依赖关系。比如数据解析类需要通信类提供原始字节流那就让解析器依赖通信接口而不是直接依赖通信类。一旦你发现拆完后出现了大量跨类私有成员的互访说明拆分的边界没选对——两个类仍然在共享内部状态。再看引入参数对象。如果一个方法有六个参数public void SaveDeviceRecord(string deviceId, string deviceName, DeviceType type, string location, bool enabled, int timeoutSeconds)调用处的可读性和可维护性都很差。你可以定义一个DeviceRecord类把这些参数包起来public class DeviceRecord { public string DeviceId { get; set; } public string DeviceName { get; set; } public DeviceType Type { get; set; } public string Location { get; set; } public bool Enabled { get; set; } public int TimeoutSeconds { get; set; } } public void SaveDeviceRecord(DeviceRecord record)这个方法重构还附带一个好处当后面需要增加新字段比如增加“安装日期”你不需要改动方法签名只需要扩展DeviceRecord。调用方也不需要重新记参数顺序更加不容易出错。注意引入参数对象不是让你无脑把所有参数都塞进一个类。如果某几个参数在语义上根本没有关联强行包装会让代码更别扭。我的经验是只有那些“经常一起出现且共同描述一个概念”的参数才值得包装比如设备信息、用户信息、查询条件都属于这种概念聚合体。2.5 用委托和事件解耦Delegate Event——让类与类不再死死绑住C#里的委托delegate和事件event是重构中非常强大的工具可惜很多人只用来写按钮点击。它们的核心用途是把“通知别人”的逻辑从“执行自己”的逻辑中解耦出去。举个典型的例子上位机里有一个数据采集服务采集到新数据后要同时更新界面、写入数据库、可能还要转发给别的模块。最容易写出来的代码是这样的public void OnDataReceived(byte[] data) { _uiPanel.Update(data); _dbService.Save(data); _forwardService.Forward(data); }这样写的问题是DataAcquisitionService直接依赖了UiPanel、DbService、ForwardService。以后新增了一个“数据分析模块”你不得不回来改DataAcquisitionService。改多了数据采集服务就变成了一个所有模块的大杂烩中心。用事件重构public class DataAcquisitionService { public event EventHandlerDataReceivedEventArgs DataReceived; public void OnDataReceived(byte[] data) { DataReceived?.Invoke(this, new DataReceivedEventArgs(data)); } }UI、数据库、转发服务各自注册自己的事件处理器。数据采集服务完全不知道外面有谁在听新增模块时只需要在启动配置里多一行订阅代码。这就是依赖倒置在重构中的落地高层模块不再依赖低层模块而是双方都依赖抽象事件。实际操作中要注意三点第一事件处理器抛出的异常会打断后续订阅者所以你的事件调用方法里要包一层try-catch别让一个订阅者的崩溃影响其它订阅者第二如果反复订阅同一个事件会造成事件处理器的重复调用尤其是使用匿名方法时你一定要在合适的位置取消订阅第三事件不要随便暴露给外部类操作尽量使用event关键字包装委托这样外部只能和-不能随便触发维护起来安全得多。2.6 泛型化消除重复集合逻辑Generic Refactoring——把“拷贝代码”变成“复用逻辑”C#的泛型不是只有ListT和DictionaryTKey, TValue才叫泛型。你自己写的数据处理逻辑如果只是类型不同、逻辑完全相同就应该用泛型收敛。这是8种基本方法里很关键的一条尤其在后端开发、数据处理项目中价值巨大。假设你有两段几乎一样的代码一个处理Listint一个处理Liststringpublic int SumAll(Listint numbers) { int sum 0; foreach (var n in numbers) sum n; return sum; } public string ConcatAll(Liststring strings) { string result ; foreach (var s in strings) result s; return result; }虽然返回类型不同但“遍历集合并逐一累加”这个骨架是重复的。用泛型加委托你可以抽取公共逻辑public T AggregateT(IEnumerableT source, T seed, FuncT, T, T func) { T result seed; foreach (var item in source) { result func(result, item); } return result; }调用时传入具体累加函数就行。注意这个例子只是为了演示思路真正在C#里你直接用LINQ的Sum()、Aggregate()更省事但泛型化思维的本质是一样的把“类型无关的结构”和“类型相关的逻辑”分离。泛型化重构有一个隐性成本泛型约束一旦滥用或者使用不当会让代码变得非常抽象新人看不懂。我有一条经验至少有三处重复时才值得泛型化如果只有两处重复且这两处的逻辑差异并不只是类型那复制代码反而更稳。泛型化是为了消除“真正的重复”而不是消除“看起来相似”的重复。2.7 用异步重构阻塞调用Async/Await Refactoring——把卡顿变成流畅在C#里做重构绝对绕不开async/await。很多老代码里用的是Thread.Sleep、.Result、.Wait()这些在UI线程里直接卡界面在服务端会浪费线程资源。异步重构的基本方法就是把这些阻塞调用换成真正的异步调用。举一个常见的例子。一个C#上位机程序从PLC读数据老代码可能写成public bool ReadPlcData(string address, out int value) { Thread.Sleep(100); // 模拟IO等待 value 123; return true; }重构后public async Task(bool Success, int Value) ReadPlcDataAsync(string address, CancellationToken ct default) { await Task.Delay(100, ct); // IO等待让出线程 return (true, 123); }调用处也要跟着改从ReadPlcData(DB1, out var val)变成var result await ReadPlcDataAsync(DB1)。这次重构不仅仅是把Sleep换成Delay而是改变了线程模型异步等待期间线程可以回去处理其它事情界面不再卡死服务端的并发能力也会提高。异步重构有四个容易踩坑的地方避免async void除了事件处理器其它地方一律用async Task否则异常无法捕获。不要阻塞异步不要用.Result或.Wait()去等异步方法否则可能死锁。注意上下文在UI项目里await后会尝试回到UI线程如果被.Result阻塞就会互相等待。取消支持长耗时操作要接受CancellationToken方便用户中断或程序退出。我见过太多“看起来改成异步、实际上还是卡死”的代码往往就是把Thread.Sleep换成Task.Delay但外层调用用了.Result。重构完一定要用并发压力测一下别只看界面不卡就以为成功。2.8 简化方法调用链Remove Middle Man Reorganize——去掉多余的中间人第8种基本方法针对的是另一种坏味道过度的委托转调。面向对象设计里讲究封装但封装过头就会变成“中间人”——A调用B去调用C去调用D最后真的干活的是DB和C只是传话的。这种代码在加了多层架构的企业级项目里非常常见。举个例子public class UiController { private readonly BusinessService _service; public UiController(BusinessService service) _service service; public DeviceInfo GetDeviceInfo() _service.GetDeviceInfo(); } public class BusinessService { private readonly Repository _repository; public BusinessService(Repository repository) _repository repository; public DeviceInfo GetDeviceInfo() _repository.GetDeviceInfo(); }如果你UiController里的GetDeviceInfo()只做了一件事把调用转发给BusinessService并且BusinessService的GetDeviceInfo()又只是转发给Repository那这个中间层就没有价值。重构时要么直接让UiController调用Repository要么在BusinessService里添加真正的业务逻辑否则就去掉它。实际操作中要分清楚“中间人”和“必要抽象”。我说一个判断标准如果你删掉这个中间层让调用方直接和更底层协作你发现调用方需要知道太多底层细节那这个中间层就是必要抽象。反过来如果删掉后调用方依旧很舒服那这个中间层就是纯粹的中间人删掉能降低理解成本。3. 实操从一个真实的C#上位机示例开始重构说了这么多方法来看个完整的案例。我简化一个从PLC采集数据并更新画面显示的上位机模块这里有明显的坏味道方法过长、switch分支、参数过长、阻塞调用、类职责混乱。我会演示如何用上面几种方法逐步重构并说明每一步的意图。3.1 重构前一段满是坏味道的代码public class DataService { private string _plcIp; private int _plcPort; public string UpdateAndGetData(string deviceType, string ip, int port, string address, int timeout) { _plcIp ip; _plcPort port; object rawData null; switch (deviceType) { case plc: rawData ReadFromPlc(address, timeout); break; case dcs: rawData ReadFromDcs(address, timeout); break; default: rawData null; break; } if (rawData ! null) { string value ParseValue(rawData, deviceType); Thread.Sleep(500); return value; } return N/A; } private object ReadFromPlc(string address, int timeout) { /* 省略 */ } private object ReadFromDcs(string address, int timeout) { /* 省略 */ } private string ParseValue(object rawData, string deviceType) { /* 省略 */ } }这块代码的问题是UpdateAndGetData方法混合了连接配置、设备路由、数据获取、数据解析和显示值格式化。方法名UpdateAndGetData读起来也含混不清switch分支以后要扩展设备类型很麻烦Thread.Sleep(500)会卡住界面字段_plcIp和_plcPort被直接赋值这个类隐含了状态多个方法调用时会互相干扰。3.2 第一步区分职责提取类先把“设备通信”和“业务处理”分开。我建立一个DeviceConnector类负责不同设备的读取再让DataService只负责编排。public interface IDeviceConnector { object Read(string address, int timeoutMs); } public class PlcConnector : IDeviceConnector { public object Read(string address, int timeoutMs) { /* PLC协议读取 */ return new byte[] { 1, 2, 3 }; } } public class DcsConnector : IDeviceConnector { public object Read(string address, int timeoutMs) { /* DCS协议读取 */ return new byte[] { 4, 5, 6 }; } }这样switch就没必要存在了用字典或者依赖注入来路由即可。3.3 第二步消除阻塞引入异步把读取方法改成异步public interface IDeviceConnector { Taskobject ReadAsync(string address, int timeoutMs, CancellationToken ct default); } public class PlcConnector : IDeviceConnector { public async Taskobject ReadAsync(string address, int timeoutMs, CancellationToken ct default) { await Task.Delay(timeoutMs, ct); return new byte[] { 1, 2, 3 }; } }DataService里的Thread.Sleep(500)也一并移除改成在解析之后异步等待刷新或者干脆去掉等待直接返回结果。这里的思路是如果等待只是为了“让数据稳定”应该用循环重试读取来替代固定Sleep。3.4 第三步引入参数对象与解释变量原来UpdateAndGetData的四个参数deviceType, ip, port, address其实描述的是“一次设备点读取请求”完全可以包装成DeviceReadRequestpublic class DeviceReadRequest { public string DeviceType { get; set; } public string Ip { get; set; } public int Port { get; set; } public string Address { get; set; } public int TimeoutMs { get; set; } }方法签名变成public async Taskstring GetDisplayValueAsync(DeviceReadRequest request)在方法内部把isDataAvailable、canResolveValue这样的中间判断用解释变量命名读起来就像在阅读一条业务规则。经过这三步重构后的代码结构大致是这样public async Taskstring GetDisplayValueAsync(DeviceReadRequest request) { IDeviceConnector connector _connectorFactory.Create(request.DeviceType); object rawData await connector.ReadAsync(request.Address, request.TimeoutMs); bool hasData rawData ! null; if (!hasData) return N/A; string value _valueFormatter.Format(rawData, request.DeviceType); return value; }每个方法都只做一件事扩展新设备只需新增连接器类调用界面不再卡顿方法参数也变清晰了。这就是8种基本方法组合在一起的效果。4. 重构中的常见问题与排查技巧实录4.1 行为不保持多个问题逐一排查重构不改变行为但实践中经常出了诡异的问题。我遇到最多的场景是修改了字段的赋值时序老代码在方法开头临时给_plcIp赋值重构后你把它改成了局部变量但方法的后面某处还在用那个字段行为就会变。所以凡是看到“只在方法内使用却赋值给字段”的情况先检查有没有隐式依赖。switch顺序变化影响默认分支重构多态时路由工厂的创建顺序变了可能导致某些设备类型落到了错误的分支上。排查方法是把工厂里的映射字典打出来核对一遍看看是否有重复Type。异步上下文变化从同步改异步后UI代码的调用线程变了有些控件不能在非UI线程访问。这时你要在重构完成后启动程序跑一遍所有界面刷新路径发现问题后在await后调用Invoke或使用调度器别嫌麻烦。排查行为不一致时不要靠猜先对比重构前后的输入输出。上线代码前留一份手工测试用例脚本把所有关键业务路径过一遍比事后加班定位快得多。4.2 性能倒退重构后变慢怎么办有些重构会引入性能损耗比如滥用多态导致每一次调用都查字典或者异步操作频繁创建任务对象。遇到性能问题先区分是“变慢在可接受范围”还是“慢到无法容忍”。我认为绝大多数业务场景下可读性优先于极小性能损耗。一百次虚方法调用才损失几微秒但代码混乱带来的维护成本是按小时计的。当然如果你在循环里反复解析大量数据那确实要考虑优化。我的习惯是先用Stopwatch写个微基准测试确认瓶颈再对热点路径做针对性优化绝不为了“性能”放弃结构。如果确实需要高性能可以考虑把运行时多态改为策略缓存用ConcurrentDictionary缓存设备连接器实例用ValueTask减少热路径上异步分配用SpanT减少字节数组复制。这些优化手段应该在重构结构稳定后再做不要在结构大调的同时叠优化否则出问题很难定位。4.3 重构到一半发现依赖太多如何处理很多时候你拆着拆着发现这个类依赖了十几个其它类拆出来的类还得依赖它们。这说明初始设计就不合理或者原先的类承担了不该承担的职责。这时不要硬拆先把强依赖关系理清。我的做法是先用接口把所有外部依赖抽象出来再通过构造函数注入。一旦依赖变成接口你可以为拆出来的新类提供“最小接口”只把需要的方法暴露出去。比如原来DeviceService同时依赖ILogger、IDatabase、IConfiguration、IMqttPublisher拆分成DeviceReader后它只需要IDeviceConnectorFactory和ILogger两个依赖这样你就能在新类构造函数里只注入这两个而不是一股脑全传进去。依赖太多时的另一个常用手段是“分解接口”把一个大接口按语义拆成多个小接口然后各自实现。虽然拆完类数量变多但每个类的依赖面会变窄测试时mock对象也容易写。4.4 关于利用工具与本地AI模型辅助重构现在很多C#开发者提到用IDE自带的重构功能Visual Studio里就有“重命名”“提取方法”“提取接口”这些辅助操作。快捷键我经常用CtrlR, CtrlM提取方法CtrlR, CtrlR重命名。这些工具能大幅减少手改造成的低级错误但它们只负责“机械重构”不会替你做“如何拆类、如何选设计模式”的决策。最近热词里经常出现“用本地AI模型重构C#项目代码”我实测下来AI模型可以帮你做的是分析一段代码里有哪些坏味道、给出重构建议、生成第一版重构后的代码草稿。尤其是处理超长方法时先让AI帮你拆分再人工评审比自己硬啃要快很多。但你一定要保持警惕AI模型不理解你的业务上下文它生成的多态方案有时会把系统搞得更复杂。我的建议是把它当成一个随时可调用的结对伙伴而不是决策者。使用本地模型时注意不要上传敏感代码尽量用私有化部署。C#项目代码通常包含业务逻辑轻则违反保密要求重则导致安全风险。只把剥离过敏感信息的片段或者简化后的示例发给模型既能得到建议又守住安全线。4.5 常见错误与正确做法对照常见错误问题后果正确做法重构和重写混淆风险不可控周期拉长坚持行为不改变小步提交没有测试保护就开始重构出现行为差异难以定位先补最小测试用例或手工基线提取方法时引入隐式依赖方法间的隐式耦合加深参数显式传入避免直接访问外部变量滥用多态替换简单switch结构过度设计分支少于三处时保持switch异步重构外层用.Result死锁、线程池耗尽全链路使用async/await添加中间层来“隐藏依赖”调用链变长维护困难适度抽象区分必要中间层与纯转发这张表实际上是我每次Code Review时都会扫一遍的清单你可以在自己的项目里直接抄过去。5. 重构后的一件事提交与回顾如果非要说一个重构收尾技巧那就是小步提交。一次重构别改太多东西最好一次重构只对应一个方法或一个类跑通测试就提交一次。这样即使某个提交破坏了项目你也只需要回退这一小步而不是面对一个大仓库无处下手。我在实际项目里有一个习惯重构完成后会把当前分支的提交记录按“行为保持型重构”和“行为优化型重构”分类标记。前者如果出问题多半是我无意中改了行为后者则可能是预期中的性能或体验变化。分类清晰之后后续回溯会轻松很多。另外提交信息一定要写清楚“重构了什么、为什么这个顺序”。比如“提取DeviceConnector接口替代ReadFromPlc和ReadFromDcs的switch分支”下一周你再回来看能快速想起来当时决策的上下文。这比你写着“重构代码”四个字要强百倍。8种基本方法里没有哪一种能包治百病它们组合起来才能解决真实项目里错综复杂的坏味道。我在C#开发这些年最深刻的体会是重构拼的不是谁用了更高深的技术而是谁能在改代码之前想清楚“我要动哪一小块、动了之后怎么知道没改坏”。如果你能守着这两条原则剩下的方法细节都会逐渐变成你的本能。