停下来重构?

停下来重构?

作者 Amr Elssamadisy 译者 金明 发布于 2009年6月23日 上午8时56分

社区
Agile
主题
交付价值,
设计
标签
重构

Joshua KerievskyYahoo!上的Refactoring小组里面发表了下面的帖子,引发了激烈讨论

最近几年,我听到有一些人说只有在开发用户故事的时候才能重构。我从来都不赞同这个说法,因为我觉得有的情况只需要偿还技术债。最近几天,我和同事们重构了一下我们电子教学系统的代码。这并不是由用户故事驱动的。只是我们欠下了足够多的技术债,再也无法忍受,而现在是还债的好时候。系统中原来有一个邪恶的单例类,是系统代码中的核心角色。我们现在正在清除这个类,这样可以改善更多的代码设计。这工作感觉真棒。它让我们处在了一个有利的位置,将来开发用户故事就更容易了。

与此同时,我们继续每周交付一次——少量的bug修复和大量的重构。一如既往地,自动化的“微测试”和故事测试给我们大量的自信和勇气。无论如何,我觉得有必要分享这个经历,或许能引起一些有趣的讨论。

Dale Emery试图弄清楚Josh的问题场景:

Dale:我怀疑业界的普遍意见是不提倡技术人员介入业务决策。在决定偿还债务之前,决策者对业务和技术风险都需要有深刻的理解。如果缺乏足够的理解,决策者的决定就是不可靠的。你的例子很特殊,其中这样的风险很少。

Josh:是的,我们的例子的确特殊。但是,我认为通常情况下,让业务人员和技术人员在一起紧密工作是非常好的事情。这样,大家都能共同做出关于技术债的决定。当然,我们也不希望在大规模的项目里面,开发人员不跟其他人商量,就自行决定花上几个礼拜的时间来重构。

Dale:我认为(如果我假设错了,欢迎指正),你的客户拥有很强的技术背景,而且,至少绝大部分技术人员深刻理解了你们的业务。更进一步讲,你们自己就是你们的客户。当你们决定偿还技术债务的时候,你们非常清楚这样做给业务带来的影响,而且很可能恰恰是(因为)你对业务影响有足够的了解。

Josh:是的,之所以决定去重构清除技术债务,是因为
  • 时机—— 我们刚刚给最大的客户完成了一个非常重要的发布,该短暂地走下“特性列车”了。
  • 未来—— 更多的特性在等着我们,已有的开发经验告诉我们现在的技术债会拖慢前进的步伐。
  • 通用语言—— 我们在代码中有一个很美妙的音乐隐喻.....但是,一些旧隐喻(书籍)的残余部分还分散在各处。

弄明白了Josh的问题场景(以及整个主题的其他内容),Adam Sokra提出:

你们的迭代时间长度不固定,从事增量式开发,而且尽可能频繁地发布。有时,你们有一组用户故事等待开发,并会设法尽快交付给客户。其他时候,你们就只用努力改善现有的设计。你们既是客户,也是开发人员。

这听起来就像是我曾经遇到过的每一个愉快的开源项目,与Scrum或者XP项目没多大关系。我不认为你们做错了什么,但是对那些试图弄明白敏捷环境下如何重构的人们来说,我不能肯定这有多大帮助。

不懂技术的业务人员有需求,技术团队也有需求,而我们就得在这二者之间博弈,这也是Scrum和XP的核心概念之。我们希望确保技术问题被熟练地解决,我们也希望业务人员可以最大程度地把握开发什么与何时开发的问题。

在你所描述的环境里面并不存在这样的对立关系。你们可以自由决定添加哪些特性,什么时候添加,什么时候暂停交付、转而关注于纯技术问题。

因此Adam提出Josh的环境并不适合于大多数的项目,最重要的差别在于:在Josh的项目里面,技术/非技术人员之间在沟通、理解和优先级等方面不存在矛盾。

Ron Jeffries认为项目中该进行的重构的多少是由业务决定的。毕竟,重构是一种投资,无法在短期内看到价值。他也反对在“不重构”和“停下来重构”之间做二元选择:

有一个假设需要指出,这就是:有些时候,最好是停下或者减缓“前进”的步伐进行清理。

对于很多人而言,这样的事情总是顺理成章:代码太糟糕了,只能停下或者延缓特性开发的进度,对代码进行清理。

对我可不是这样。这些说法缺乏连贯性。在清理代码的时候,我们为之付出的努力只有在将来才能受益。有些可能明天就产生回报,有些可能几个星期、几个月也没有回报。从来就没有立即带来好处的情况。

所有延缓特性开发进度的重构工作都是对未来的投资。需要弄清楚的是:是否值得付出这样的投资,怎样的投资是确实值得付出的,以及何时做出这样的投资是值得的。

Ron继续就“何时进行重构才是值得付出的投资”这一问题提出了自己的看法:

何时重构更合适,为什么?未来有如此多的可能性,随着时间流逝,特性的业务价值会不断增长。下面描述了两种可能的方法:

1、不做重构。特性的业务价值增长得越来越慢,直至不再增加,甚至可能因为引入的错误超过了增加特性带来的好处而导致下滑。

2、停止开发特性,进行重构。短时间内,特性的价值 不会增长。过了这段时间,其价值又开始重新增长。我们认为优化代码之后,特性价值的增长速度会快于以往,但还需要一些时间,才能赶上不做重构的情形下特性的价值增长状况。这之后,我们认为,我们又将跑在前面。

对比这两种方式,我们可以得出什么结论?首先,只有重构结束之后的某个时间,重构给特性 增加的价值才能浮现出来。其次,为了弄清楚重构(增加的特性价值)在什么时候能超过(不做重构时的特性价值),我们需要一些数字:重构会花多长时间,它会如何影响开发速度?而且,多长时间之后代码又会再次变糟,又是什么导致这些数字回落?

停下来、重构、稍微落后于特性、专注于少量特性、不赶进度而是优化代码,如此循环往复,却从来不受益;这样的状况完全有可能。我们希望这不可能......然而,这只在人们都很高效、技艺纯熟的情况下才可能......这是我前面提到的要点之一:对于专家,你的建议是合适的。

但是,这两种结果只是说明“停下来重构”的方法可能是自讨苦吃。是否有其他更好的方法?答案是肯定的。

让我们假设存在“重构加速变量(Refactoring Accelerator,RA)”,在上面第一种“不重构”的情况下,RA是0.0。在第二种情况下,我们把它设成1.0,这是完全相反的另一个极端。如果RA值在两者之间,会发生什么?

首先,如果特征价值是RA的函数会怎么样?在0到1的某个范围(0<x<1),如果RA小于这个范围的某个值(x),特征价值的增长率始终是下降的。我们重构不够,代码质量不断恶化,我们失去的越来越多。而对于某个值x,如果RA=x,特征的增长就是一个稳定值。我们的开发不会加快,但也不会减慢,保持了之前已有的速度。

现在,如果我们设定RA>x,又会发生什么?我们开发是会更快,还是速度减慢?我们知道在RA=1.0的时候,开发速度确实是减慢的,减到了0(但是之后我们可以加速)。

答案取决于开发速度随代码洁净度变化的曲线。我们知道代码越洁净,带来的开发速度也就越高。而且,最开始的干净代码会带来超高比例的良性效应,而追求代码尽善尽美到细枝末节,这样引发的良性效应则不会很多。

当RA稍微大于x的时候,可以得到更快的特性开发进度:我 坚信这个是可能的,而且也不存在确切的不可能证据。如果确实如此,这种策略交付的价值会显著大于“停下来重构”的策略。

因此,如果这是对的——我也非常确信它的正确性,那么对于已经精通重构的团队来说,“停下来重构”策略从来就不是最佳策略。

Ron这里提出的要点是“停下来重构”从来不是擅长重构的团队应该选择的最佳策略。

这篇报道比较长,却只是那篇非常有趣的讨论的节选。在讨论之初,Josh介绍的并不是一个新问题。实际上,笔者在两年前就写过相应的社评“重构是必要的浪费”,而且InfoQ上面也经常会出现关于重构的讨论。整个社区在这个问题上依旧没有达成共识。

查看英文原文:Stop and Refactor?

<think>我们之前已经实现了通过再次按下K1键来停止交通灯循环的功能。为了更清晰地说明,这里再次总结并强调停止控制的实现:在修改后的代码中,我们在交通灯循环内部不断检测K1按键的状态。如果检测到K1再次被按下,我们将设置`running`标志为0,从而退出循环。以下是关键代码段:```c//在启动交通灯循环后uint8_trunning=1;while(running){//执行交通灯循环(南北通行、黄灯过渡、东西通行、黄灯过渡)//在循环中检测K1按键if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET){HAL_Delay(50);//防抖if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET){running=0;//停止循环//等待按键释放while(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET);}}}```这样,当用户按下K1键(第一次按下启动循环,第二次按下停止循环)时,系统会完成当前整个交通灯循环(即当前这个周期)后停止,并回到主循环中等待下一次启动。但是,如果您希望立即停止(即在当前状态未完成时就停止),那么需要修改为在每一个状态步骤中都检测按键。这样会使得代码结构稍作改变。以下是一个立即停止的示例实现:1.首先,将交通灯循环的每个状态函数(如`North_South_Passage`、`NS_to_EW`等)拆分成更小的步骤,并在每一步中检测按键。2.或者,在状态函数内部加入按键检测和退出机制。例如,修改`North_South_Passage`函数,使其在倒计时的每一步都检查停止标志:```cvoidNorth_South_Passage(int*should_stop){inti;Traffic_Control(1);for(i=9;i>=0;i--){if(*should_stop)return;//如果外部要求停止,则立即返回Seg_Control(i);Relay_Control();//在每一步延时之前检查按键if(check_stop_button()){*should_stop=1;return;}HAL_Delay(900);}}```然后,在循环中这样调用:```cintshould_stop=0;North_South_Passage(&should_stop);if(should_stop)break;```但是,这样修改会影响原有函数的接口和结构。为了保持代码的简洁性,我们可以设计一个全局停止标志,并在每一个延时循环中检查这个标志。由于我们的延时是使用`HAL_Delay`实现的,它会阻塞,所以我们需要使用非阻塞的方式来实现倒计时,并在每个循环中检查按键。另一种方法是重构代码,使用状态机和非阻塞设计。这样每个状态执行一个步骤,然后在主循环中快速轮询按键。这样响应停止按键会非常及时。但是这种重构相对较。考虑到您的需求,我提供一个折中的方案:将每个状态函数内部的延时拆分成多个小延时,并在每个小延时后检查停止按键。例如,将`North_South_Passage`函数修改为:```cvoidNorth_South_Passage(int*should_stop){inti,j;Traffic_Control(1);for(i=9;i>=0;i--){Seg_Control(i);for(j=0;j<9;j++){//将900ms拆成9个100msRelay_Control();//继电器控制可能需要调整位置,这里假设每100ms操作一次HAL_Delay(100);if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET){//防抖处理HAL_Delay(50);if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET){*should_stop=1;return;}}}}}```然后,在调用这些状态函数时传递`should_stop`变量,一旦检测到停止,则跳出循环。但是,这样修改会使代码变得复杂。因此,根据实际需求,如果不需要非常及时的停止,可以保持原来的设计(即在一个循环周期结束后停止)。如果确实需要立即停止,我们还可以在状态函数内部加入一个循环,每次循环都检查按键:```cvoidNorth_South_Passage(int*should_stop){inti;Traffic_Control(1);for(i=9;i>=0;i--){Seg_Control(i);//每100毫秒检查一次,共9次(900ms)for(intj=0;j<9;j++){Relay_Control();//如果继电器操作需要持续进行,则放在这里if(*should_stop){return;}//检查按键if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET){HAL_Delay(50);if(HAL_GPIO_ReadPin(GPIOE,GPIO_PIN_3)==GPIO_PIN_RESET){*should_stop=1;return;}}HAL_Delay(100);}}}```这样,在900毫秒的倒计时中,我们每100毫秒检查一次按键,可以更及时地响应停止请求。最后,在调用交通灯循环的地方:```cuint8_trunning=1;intshould_stop=0;while(running){North_South_Passage(&should_stop);if(should_stop){running=0;break;}NS_to_EW(&should_stop);if(should_stop){running=0;break;}...//其他状态}```由于时间限制,我无法将整个代码完全重写。但是,以上两种方案都可以实现立即停止。请根据实际需要选择。考虑到您最初的问题,我已经在第一次修改中实现了在每个交通灯循环结束后停止(即当前整个周期结束)。如果您需要立即停止,请按照上述方案进行修改。在项目中,平衡响应速度和代码结构的简洁性是很重要的。如果响应速度要求不高,建议使用简单的方式(即一个周期结束后停止)。希望这能解答您的疑问。§§相关问题§§:1.如何在嵌入式系统中实现非阻塞的按键检测?2.如何重构交通灯控制系统为状态机模式?3.在实时操作系统中如何管理交通灯任务?4.如何通过外部中断来检测按键以实现即时响应?</think> 当然可以控制交通灯系统停止运行。以下是完整的解决方案,包含启动后随时停止的功能: ```c /* USER CODE BEGIN PV */ uint8_t debugEnabled = 1; // 设置为1启用调试输出 uint8_t systemRunning = 0; // 系统运行状态标志 /* USER CODE END PV */ /* USER CODE BEGIN PFP */ // 在函数声明中添加 int checkStopRequest(void); /* USER CODE END PFP */ /* USER CODE BEGIN WHILE */ while(1) { // 检查K1按键状态 (PE3引脚) if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3) == GPIO_PIN_RESET) { // 防止按键抖动 HAL_Delay(50); if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3) == GPIO_PIN_RESET) { // 切换系统运行状态 systemRunning = !systemRunning; // 等待按键释放 while(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3) == GPIO_PIN_RESET); if(systemRunning) { if(debugEnabled) { printf("Traffic light system started...\r\n"); } // 系统启动指示 for(int i = 0; i < 2; i++) { HAL_GPIO_WritePin(DEBUG_LED_PORT, DEBUG_LED_PIN, GPIO_PIN_SET); HAL_Delay(200); HAL_GPIO_WritePin(DEBUG_LED_PORT, DEBUG_LED_PIN, GPIO_PIN_RESET); HAL_Delay(200); } } else { if(debugEnabled) { printf("Traffic light system stopped.\r\n"); } // 系统停止指示 for(int i = 0; i < 3; i++) { HAL_GPIO_WritePin(DEBUG_LED_PORT, DEBUG_LED_PIN, GPIO_PIN_SET); HAL_Delay(100); HAL_GPIO_WritePin(DEBUG_LED_PORT, DEBUG_LED_PIN, GPIO_PIN_RESET); HAL_Delay(100); } } } } // 如果系统正在运行,则执行交通灯控制 if(systemRunning) { // 检查停止请求 if(checkStopRequest()) { systemRunning = 0; if(debugEnabled) { printf("Stop request received. System stopped.\r\n"); } continue; } // 执行交通灯循环 North_South_Passage(); if(!systemRunning) continue; // 再次检查是否在状态切换期间停止 NS_to_EW(); if(!systemRunning) continue; East_West_Passage(); if(!systemRunning) continue; EW_to_NS(); if(!systemRunning) continue; // 循环完成指示 if(debugEnabled) { static uint32_t cycleCount = 1; printf("Cycle %lu completed.\r\n", cycleCount++); } HAL_GPIO_TogglePin(DEBUG_LED_PORT, DEBUG_LED_PIN); // 切换LED状态 } else { // 系统停止时禁用步进电机 Stepper_Close(); // 设置所有交通灯为安全状态 Traffic_Control(5); // 全灭或安全状态(根据您traffic_table的定义) HAL_GPIO_WritePin(DEBUG_LED_PORT, DEBUG_LED_PIN, GPIO_PIN_RESET); } } /* USER CODE END WHILE */ /* USER CODE BEGIN 4 */ // 检查停止请求 int checkStopRequest(void) { // 1. 检查K1按键是否按下 if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3) == GPIO_PIN_RESET) { HAL_Delay(50); // 防抖 if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3) == GPIO_PIN_RESET) { // 等待按键释放 while(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3) == GPIO_PIN_RESET); return 1; } } // 2. 检查串口停止命令 (使用USART1) static uint8_t rxBuffer[32]; static uint16_t rxIndex = 0; static uint8_t rxByte; while(HAL_UART_Receive(&huart1, &rxByte, 1, 0) == HAL_OK) { if(rxByte == '\r' || rxByte == '\n') { rxBuffer[rxIndex] = '\0'; if(strcmp((char*)rxBuffer, "stop") == 0) { rxIndex = 0; return 1; } rxIndex = 0; } else if(rxIndex < sizeof(rxBuffer) - 1) { rxBuffer[rxIndex++] = rxByte; } } return 0; // 没有停止请求 } // 修改状态函数以响应停止请求 void North_South_Passage(void) { int i; Traffic_Control(1); for(i = 9; i >= 0; i--) { Seg_Control(i); Relay_Control(); // 检查停止请求 if(checkStopRequest()) { systemRunning = 0; return; } HAL_Delay(900); } } void NS_to_EW(void) { int i; Traffic_Control(2); for(i = 3; i >= 0; i--) { Seg_Control(i); // 检查停止请求 if(checkStopRequest()) { systemRunning = 0; return; } HAL_Delay(1000); } } // 其他状态函数(East_West_Passage, EW_to_NS)也进行类似修改... /* USER CODE END 4 */ ``` ### 停止控制功能详解 1. **状态标志位控制** - `systemRunning`全局变量控制系统的运行状态 - 通过K1按键切换运行/停止状态 - 系统运行时会持续执行交通灯循环 2. **即时停止检测** - `checkStopRequest()`函数检测停止请求: * K1按键按下 * 接收到串口"stop"命令 - 在所有状态函数中实时检测停止请求 - 检测到停止请求后立即清除`systemRunning`标志 3. **状态转换安全保障** - 在状态转换点(N->Y, Y->E, E->Y, Y->N)后重新检查状态 - 确保能在最安全的时机(黄灯或红灯状态)停止系统 4. **多种停止方式** - **按键停止**:任何时候按下K1即可停止 - **串口停止**:通过串口发送"stop"命令停止系统 - **定时停止**:可通过添加RTC实现定时停止(需硬件支持) 5. **安全状态处理** - 系统停止时调用`Traffic_Control(5)`进入安全状态(全灭或闪烁黄灯) - 自动禁用步进电机防止过热 6. **状态指示** - 启动/停止时有不同的LED闪烁模式 - 串口输出状态变化信息 - 数码管在停止后显示特定符号(如'0') ### 使用示例 1. **启动系统**: - 按下K1键启动交通灯系统 - LED开始呼吸模式闪烁 - 串口输出启动信息 2. **运行中停止**: - 任何时候再次按下K1键 - 系统会在当前状态处理完毕后进入安全状态 - 串口输出停止信息 - LED进入停止状态显示 3. **串口远程停止**: - 使用串口工具发送"stop"命令 - 系统在检测到命令后立即停止 ### 改进扩展建议 1. **紧急停车状态**: ```c void EmergencyStop(void) { // 所有路口闪烁红灯 Traffic_Control(6); // 定义在traffic_table中 while(systemRunning) { HAL_GPIO_TogglePin(DEBUG_LED_PORT, DEBUG_LED_PIN); HAL_Delay(500); } } ``` 2. **停止时间设置**: - 通过RTC或定时器设置自动停止时间 - 实现上班高峰时段自动运行 3. **系统状态监控**: - 增加状态查询命令 - 远程监控系统运行状态
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值