再谈如何优雅修改代码

news/2024/12/23 18:13:43/文章来源:https://www.cnblogs.com/OceanEyes/p/18450797

书接上回(https://t.zsxq.com/xo0M0)再做下扩展

上文谈到:“基于抽象接口编程确实是最佳实践:把易于变动的功能点通过定义抽象接口的形式暴露出来,不同的实现做到隔离和扩展,这体现了开闭原则”
public class Foo {
    private Bar bar ;
    @Inject
    public Foo(Bar bar) {
        this.bar = bar;
    }
    public String doSomething(int key) {
        //Bar#getResult 体验了代码的复杂性,通过注入不同的 Bar 实现对象,做到功能点的隔离和扩展
        return bar.getResult(key);
    }
}

但在真实项目里,往往是多人协作一起开发,一些历史原因导致某些代码片段的实现往往“千奇百怪”,既不能很好的单侧覆盖,同时也充斥着违反了开闭原则的“代码坏味道”;

而此时的你,作为“被选中的人”,需要对其功能迭代;

或许经过你的评估后,可以去大刀阔斧的架构演进,这是点赞的;

但有时也要全局 ROI 去评估大刀阔斧重构收益是否足够大,有时候我们只能妥协(trade-off)。即:如何在紧张的交付周期内做到比较好的重构,不让代码继续腐化;

所以这次继续介绍两种修改代码的艺术:方法新增方法覆盖

策略 1:方法新增

通过新增方法来隔离旧逻辑,即:在旧方法里横切“缝隙”,注入新的业务逻辑被调用;

拿之前的 Case 举例,一个历史老方法,需要对返回的数据集合过滤掉空对象

public class Foo {
  private Bar bar;
  public Foo() {
    bar = new Bar();
  }
  public List<Data> doSomething(int key) {
    //依赖三方服务,RPC 调用结果集
    List<Data> result = bar.getResult(key);
    //过滤掉空对象
    return result.stream().filter(Objects::nonNull).collect(Collectors.toList());
  }
}

此处逻辑很简单,使用了Java Lambda 表达式做了过滤,但这样的写法无疑雪上加霜:确实原先方法已经很 Low了,也无法单侧。本次只是在最后加了一段简单的逻辑。已经驾轻就熟了,可能不少人都会这样搞;

但作为好的程序员,眼前现状确实我们只能妥协,但后续的每一行代码,需要做到保质保量,努力做到不影响原有业务逻辑下做到可测试;

“方法新增”:通过新增方法 getDataIfNotNull 来隔离旧逻辑:

public List<Data> doSomething(int key) {
  //依赖三方服务,RPC 调用结果集
  List<Data> result = bar.getResult(key);
  return getDataIfNotNull(result);
}

如下 getDataIfNotNull 作为新增方法,很容易对其进行独立测试,同时原有的方法 doSomething 也没有继续腐化

public List<Data> getDataIfNotNull(List<Data> result) {
  return result.stream().filter(Objects::nonNull).collect(Collectors.toList());
}

可以看到优点很明显:新老代码清晰隔离;当然为了更加职责分明,使用新增类隔离会更好;

策略 2:方法覆盖

将待修改的方法重命名,并创建一个新方法和原方法名和签名一致,同时在新方法中调用重命名后的原方法;

假设有新需求:针对 doSomething 方法做一个消息通知操作,那么“方法覆盖”即:

将原方法 doSomething 重命名为 doSomethingAndFilterData,再创建一个与原方法同名的新方法 doSomething,最后在新方法中调用更名后的原方法:

//将原方法 doSomething 重命名为 doSomethingAndFilterData
public List<Data> doSomethingAndFilterData(int key) {
//依赖三方服务,RPC 调用结果集
  List<Data> result = bar.getResult(key);
  return getDataIfNotNull(result);
}
//创建一个与原方法同名的新方法 doSomething
public List<Data> doSomething(int key) {
  //调用旧方法
  List<Data> data = this.doSomethingAndFilterData(key);
  //调用新方法
  doNotifyMsg(data);
  return data;
}
//新的扩展方法符合隔离扩展,不影响旧方法,也支持单侧覆盖
public void doNotifyMsg(List<Data> data){
  //
}
方法覆盖的另一种写法:通常是再定义一个新的方法,然后在新的方法依次调用新老业务逻辑;

一般在架构演进的时候,用于切流新老逻辑;例如:基于客户端版本,大于 3.10.x 的客户端切流使用新的逻辑——我们创建一个新的方法调用新旧两个方法。

//老的历史代码,不做改造
public List<Data> doSomething(int key) {
  //依赖三方服务,RPC 调用结果集
  List<Data> result = bar.getResult(key);
  List<Data> data = getDataIfNotNull(result);
  return data;
}
//新创建一个方法,聚合调用新老逻辑
public List<Data> doSomethingWithNotifyMsg(int key) {
  List<Data> data = this.doSomething(key);
  //调用新方法
  doNotifyMsg(data);
  return data;
}
//新的扩展方法符合隔离扩展,不影响旧方法,也支持单侧覆盖
public void doNotifyMsg(List<Data> data){
  //
}

这样的好处是显然易见的,不针对旧方法做修改,在更高维度的“上层”切流:保证新功能正常迭代演进,老功能维持不变

boolean enableFunc=getClientVersion()>DEFAULT_CLIENT_VERSION;
if (enableFunc){
  return doSomethingWithNotifyMsg();
}else{
  return doSomething();
}

可以看到“方法覆盖”不管用何总方式实现,它不会在当前旧方法里增加逻辑,而是通过使用新方法作为入口,这样避免新老逻辑耦合在一起;

“方法覆盖”可以再进阶一步,使用独立的类来隔离,也就是装饰者模式。通常情况下原有的类已经非常复杂了,已经不想在它上做功能迭代了,考虑使用装饰者来解耦:

class DecoratedFoo extends Foo{

private Foo foo;
  public DecoratedFoo(Foo foo){
}
 
@Override
public List<Data> doSomething(int key) {
  List<Data> data = super.doSomething(key);
  notifyMsg();
  return data;
}
private void notifyMsg(){}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/809673.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

为什么需要依赖注入

为什么需要“依赖注入” Case 1 public class Foo { private Bar bar; public Foo() { bar = new Bar(); } public void doSomething(int key) { String result = bar.getResult(key); //swithch result } } 反模式 ,在构造函数中,初始化了合作类,导致: • 外部使用者不知道…

嵌入式驱动开发学习路线整理

1、学习路线 1.1 知乎:菜鸟分享嵌入式Linux驱动开发学习路线与心得(一)嵌入式方向:嵌入式Linux系统中,往上有应用程序功能开发和界面设计,往下有内核编程相关的驱动开发; 学习嵌入式Linux需要的基础知识:C语言基础知识; 基础知识:操作系统相关知识、Linux系统基本使用…

你可能没听过的手机输入法技巧

其实手机输入法真的很强其实手机输入法真的很强 ‍ 手机输入法的自带功能 比起电脑上的输入法,手机上的输入法有一些特有的功能。这里就以 iPhone 为例进行讲解,而安卓的话大部分情况也有类似的功能,这里就不一一进行评测了。 ‍ 快速移动光标 当内容很多,想要移动光标到内…

Webpack 初始化

1、初始化npm项目 npm init -y 得到 package.json 2、安装 webpack,webpack-cli相关 npm install webpack webpack-cli --dev node_modules .bin目录有webpack相关 即可通过npm 运行 webpack命令 如: npm webpack --version 可直接执行 webpack命令进行打包也可这么着: webpac…

iPhone开机一直白苹果闪烁,3个解决卡在开机画面的方法!

很多苹果用户在网上反馈,某天iPhone开机一直卡在开机Apple标志画面,而且白苹果一直闪烁。长按电源键也无法正常启动设备,黑屏后还是会出现苹果logo,一直闪烁。遇到iPhone开机一直白苹果闪烁的情况,多半是因为iOS系统故障。更专业的说,是系统组件调用执行失败而导致无法出…

因果推断与间接效应

img { display: block; margin-left: auto; margin-right: auto } table { margin-left: auto; margin-right: auto } 因果推断(Causal Inference)是统计学和数据科学中的重要分支,用于理解事件之间的因果关系,而不仅仅是相关性。与相关性分析不同,因果推断追求揭示因变量…

#HACKTHEBOX——Driver

靶机详情靶机地址:10.10.11.106 kali地址:10.10.16.4 初始侦察与渗透确认kali与靶机之间可以ping通使用nmap进行侦查扫描 nmap -sT --min-rate 1000 -p- 10.10.11.106 #以TCP协议,基于当前网络情况,以最低1000的速率进行该IP的全部端口nmap -sT -sC -sV -p 80,135,445,5985…

HACKTHEBOX——Crafty

靶机详情靶机地址:10.10.11.249 kali地址:10.10.16.3 端口服务扫描确认kali与靶机可以ping通使用nmap进行扫描 nmap -sT --min-rate 1000 -p- 10.10.11.249 #以TCP协议,基于当前网络情况,以最低1000的速率进行该IP的全部端口nmap -sT -sC -sV -p 80,25565 10.10.11.249 #详…

字符编码发展史5 — UTF-16和UTF-32

上一篇《字符编码发展史4 — Unicode与UTF-8》我们讲解了Unicode字符集与UTF-8编码。本篇我们将继续讲解字符编码的第三个发展阶段中的UTF-16和UTF-32。 2.3. 第三个阶段 国际化 2.3.2. Unicode的编码方式 2.3.2.2. UTF-16 UTF-16也是一种变长编码,对于一个Unicode字符被编码成…

24.10.07

A 质朴的想法,每一行都放 ryxyryx...,然后因为有 \(40\) 列所以最后一列完全没用,所以把那一行竖着放进去,算一下有 \(2223\) 个。 然后枚举填多少行,如果数量多了就在最后一行选一个位置插入字符(这样后面就没有斜着的贡献了)。还多就从后往前覆盖 y。然后总可以构造出…

ubuntu无法进入系统

ubutun重启卡在initramfs,无法进入系统_initramfs进不了界面-CSDN博客

matplotlib 斜体

matplotlib 斜体在 Matplotlib 中,斜体(Italic)字体可以用于改善图表的可读性或美观度。要设置斜体字体,你可以使用 Matplotlib 的字体属性。这可以通过几种方式实现,比如直接在文本字符串中使用 LaTeX 风格的斜体命令,或者使用字体属性字典来指定斜体。使用 LaTeX 风格的…