《重构》- 代码的坏味道 V1

148 阅读4分钟

重复代码(Duplicated Code)

方式:

  • Extract Method
  • Pull Up Method
  • Form Template Method
  • Extract Class

总体感觉就是提取公用函数、共用类做提炼,根据个人经验,提取重复代码主要需要注意以下几点:

  1. 先局部,再整体
  2. 提取代码的时候不要做代码重写,先提取再改造
function add1(x, y) {
  return x + y;
}

function add2(x, y) {
  return x + y;
}

// 重构第一步
function add(x, y) {
  return x + y;
}

// 如果不能完全替换,保留老的引用,注意不要改变调用 this
const add1 = add;

const add2 = add;

// 重构第二步,才可能重构代码
function add(x, y) {
  if (!valid(x, y)) {
    throw new Error(`x, y is must number type`);
  }

  return x + y;
}
  1. 如果功能相近,那么要更小心,可以关注他们之间的关系:包含、组合、分支判断等关系,选择更合适的方案

过长函数(Long Method)

整体原则:拆解函数粒度,短函数对象会活的比较好、比较长。

如果程序中有很多短小职责清晰且单一的函数,在添加软件分层的时候会更加灵活。拿前端举例,适配服务的数据层:

function applicationBiz() {
  const vm = this;
  
  if (vm.name) {
    vm.displayName = `你好,${name}`;
    vm.button.disabled = false;
  } else {
    vm.displayName = '请登录';
    vm.button.disabled = true;
  }
  
  if (vm.age) {...}
}
  
  // 可灵活组合
class Adaptor {
  convertPerson() {
    const newName = convertName();
    const newAge = convertAge();
    return { displayName: `你好,${newName}`, age: newAge };
  }
}
  
function convertName() {}
  
function convertAge() {}

这样的例子同样适用 Unix 哲学中的:让每个程序做好一件事。要做一件新的工作,就构建新程序,而不是通过增加新“特性”使旧程序复杂化。

  1. 把程序实现变得更有组合性,产生新的程序

  2. 合理管理参数和临时变量,可以通过:

  • Replace Temp with Query
  • Introducer Parameter Object
  • Preserve Whole Object
  • Replace Method with Method Object

等模式来改善代码

  1. 条件表达式、循环,是可以提炼的地方,Decompose Conditional,可以处理条件表达式,主要处理方式是抽象过程策略,提取函数进行逻辑表达

过大的类(Large Class)

主要来自于类做了太多事情,这样内部就会有过多实例变量。提取原则:

  1. 命名可以用一个前缀的,可以认为是一个处理组件,可以提取

  2. 抽象 Extract Interface,用于梳理 Class 和 Subclass 的结构,清晰类的结构后进行重构

  3. 可以通过 Duplicate Observed Data 模式来处理 GUI 数据和行为的领域对象

参见 8.6,这里提前结合梳理。

@binding(DomainData)
class UIComponent {
  constructor(private domainData: DomainData) {}

  render() {
    return <div>{this.domainData.sum}</div>;
  }
}

interface Observable {
  cal(x: number, y: number): number;
}

class DomainData implements Observable {
  constructor() {
    // 数据

    this.sum = 0;
  }

  // 行为:计算

  cal(x, y) {
    this.sum = x + y;
  }
}

第一步要区分:用户界面和业务逻辑分开。

个人经验:主要是用于数据、行为、视图的各自维护和降低需求变更互相影响。行为更多是拆解为函数,数据更多是 clone | mapping 等行为。所以 UI 的模式里面可以更好的采用 Behavior 的抽象类。这个可以参考 Adobe 的开源库(react-spectrum架构介绍文档

书中的例子主要是 observer 模式基础使用,没有一个统一上下文调度,在现代 UI 的开发有了 Hooks 封装,框架层去约定调度模式,逻辑抽象的实现复杂度就低了很多。

class IntervalWindow extends Frame implements Observer {
  constructor() {
    const subject = new Interval();

    subject.addObserver(this);

    update(subject, null);
  }

  update(observable, args) {}
}

class Interval {
  addObserver(instance) {
    this.subject.add(instance);
  }

  setEnd() {
    // ... 逻辑处理

    this.subject.notify();
  }
}

这个比较麻烦的处理领域数据和展示数据同步问题,更多的可以采用以下结构(但是个人觉得这种方式不好),我们的数据大体是遵循:ViewData = DomainData * UIData

const viewData = {
  // 存一个冗余的领域数据,但是尽量不使用,因为这份数据有时候在加入 Cache 的时候不会有双向绑定
  $domainData: new DomainData(),

  viewData1: 1,

  uiData1: 1,
};

我们换个 React 类似的模式,举一个实际的场景:我们要做一个 Iframe 父子 Message 通信 Height 的技术需求。

const GLOBAL_EVENT_TYPES = {
  BRIDGE: "BRIDGE",
};

// hooks 行为

function useIframeHeight() {
  const [height, setHeight] = useState();

  useEffect(() => {
    bindMessageEvent();
  }, []);

  function bindMessageEvent() {
    $bridge.on(GLOBAL_EVENT_TYPES.BRIDGE, (payload) => {
      const { type, data } = payload ?? {};

      if ("height" === type) {
        setHeight(data);
      }
    });
  }

  return height;
}

// 数据

class IframeBridge extends Bridge {
  constructor() {
    super();

    this.$bridge.emit(GLOBAL_EVENT_TYPES.BRIDGE, {
      type: "height",

      data: "400px",
    });
  }
}

// ui

function render() {
  const height = useIframeHeight();

  return <div style={{ height }} />;
}

过长参数列(Long Parameter List)

如果参数列太长,就需要重新梳理依赖结构是否合理。

这里说到 2 种技巧:

  • 以函数取代参数 Replace Parameter with Method

  • 引入参数对象 Introduce Parameter Object

这里我们参考:10.8 和 10.9 章节内容。

Replace Parameter with Method 主要做法就是能够通过其他方式获取值的情况下,就尽量不传递参数,来增加参数个数。这种模式特别适合逻辑计算过程,把值变成一个原子计算函数

Introduce Parameter Object 主要做法参数提取为对象的模式,用来减少参数,另外的作用就是通过提取识别更抽象的类出来,还能附带一些行为函数的处理抽象。

function submit(startDate, endDate) {
  if (this.date.includes([startDate, endDate])) {
    // 处理逻辑
  }
}

// 提取 startDate, endDate

class DateRange {
  constructor(startDate, endDate) {
    this.startDate = startDate;

    this.endDate = endDate;
  }

  includes(range): boolean {
    return true;
  }
}

// 替换

function submit(range: DateRange) {
  if (range.includes([range.startDate, range.endDate])) {
  }
}

submit(new DateRange("2022-11-11", null));