重构JS书籍知识点总结

函数上移

动机:如果某个函数在各个子类中的函数体都相同,则将函数上移

  1. 检查待上移的函数,确定完全一致
  2. 检查函数体内引用的所有函数调用和字段都能从超类中调用到
  3. 如果待上移的函数声明不同,则修改为将要在超类中使用的声明
  4. 超类中创建一个函数,将待上移函数代码复制其中
  5. 执行静态检查
  6. 移除一个待上移子类函数
  7. 测试
  8. 移除其余待上移子类函数

Before:

class Employee extends Party {
  get annualCost() {
    return this.monthlyCost * 12;
  }
}
class Department extends Party {
  get annualCost() {
    return this.monthlyCost * 12;
  }
}

After:

class Party {
  get annualCost() {
    return this.monthlyCost * 12;
  }

  get monthlyCost() {
    throw new SubclassResponsibilityError();
  }
}

字段上移

动机:观察函数如何使用字段来判断它们是否重复,如果它们被使用方式很相似,则可以将它们上移到超类中去

  1. 检查待上移的字段使用方式一致
  2. 如果在在类中名字不一致,则取相同的名字
  3. 超类中创建一个字段
  4. 移除子类字段
  5. 测试

Before:

class Employee {}
class Salesman extends Employee {
  name;
}
class Engineer extends Employee {
  name;
}

After:

class Employee {
  name;
}
class Salesman extends Employee {}
class Engineer extends Employee {}

构造函数本体上移

动机: 各个子类中构造函数有共同的行为

  1. 超类不存在构造函数,则创建一个,并确保子类调用
  2. 将子类构造函数中的公共语句移动到超类构造函数中
  3. 删除子类构造函数公共代码
  4. 测试
  5. 如存在无法简单上移至超类的公共代码,利用函数上移提升

Before:

class Party {}
class Employee extends Party {
  constructor(name, id, monthlyCost) {
    super();
    this._id = id;
    this._name = name;
    this._monthlyCost = monthlyCost;
  }
}
class Department extends Party {
  constructor(name, staff) {
    super();
    this._name = name;
    this._staff = staff;
  }
}

After:

class Party {
  constructor(name) {
    this._name = name;
  }
}
class Employee extends Party {
  constructor(name, id, monthlyCost) {
    super(name);
    this._id = id;
    this._monthlyCost = monthlyCost;
  }
}
class Department extends Party {
  constructor(name, staff) {
    super(name);
    this._staff = staff;
  }
}

函数下移

动机:如果超类中的某个函数只与一个子类有关,最好将其从超类中移除,放到正在关心它的子类中去

  1. 将超类中的函数本体复制到需要此函数的子类中
  2. 删除超类中的函数
  3. 测试
  4. 将该函数从所有不需要的子类中删除
  5. 测试

字段下移

动机:如果属性字段只被一个子类用到,则下移至该子类中

  1. 在子类中声明该字段
  2. 从超类中移除
  3. 测试
  4. 将该字段从不需要它的子类中删除
  5. 测试

以子类取代类型码

动机:继承可以用多态来处理条件逻辑,更能明确地表达数据和类型之间的关系

  1. 自封装类型码字段
  2. 任选一个类型码取值,为其创建一个子类
  3. 创建一个选择器逻辑,把类型码参数映射到新的子类
  4. 测试
  5. 针对每个类型码取值,重复 2
  6. 去除类型码字段
  7. 测试
  8. 处理原本访问类型码的函数

Before:

class Employee {
  constructor(name, type) {
    this.validateType(type);
    this._name = name;
    this._type = type;
  }
  validateType(arg) {
    if (!["engineer", "manager", "salesman"].includes(arg))
      throw new Error(`Employee cannot be of type ${arg}`);
  }
  toString() {
    return `${this._name} (${this._type})`;
  }
}

After:

class Employee {
  constructor(name, type) {
    this._name = name;
  }
}
class Engineer extends Employee {
  get type() {
    return "engineer";
  }
}
class Salesman extends Employee {
  get type() {
    return "salesman";
  }
}
class Manager extends Employee {
  get type() {
    return "manager";
  }
}
function createEmployee(name, type) {
  switch (type) {
    case "engineer":
      return new Engineer(name);
    case "salesman":
      return new Salesman(name);
    case "manager":
      return new Manager(name);
    default:
      throw new Error(`Employee cannot be of type ${type}`);
  }
}

移除子类

动机:如果子类的用处太少,最好移除子类,将其替换为超类的一个字段

  1. 把子类的构造函数包装到超类的工厂函数中
  2. 将类型检查逻辑包装起来搬移到超类
  3. 新建一个字段,代表子类类型
  4. 将判断子类类型的函数改为新建字段
  5. 删除子类
  6. 测试

Before:

class Person {
  constructor(name) {
    this._name = name;
  }
  get name() {
    return this._name;
  }
  get genderCode() {
    return "X";
  }
}
class Male extends Person {
  get genderCode() {
    return "M";
  }
}
class Female extends Person {
  get genderCode() {
    return "F";
  }
}

After:

function createPerson(aRecord) {
  switch (aRecord.gender) {
    case "M":
      return new Person(aRecord.name, "M");
    case "F":
      return new Person(aRecord.name, "F");
    default:
      return new Person(aRecord.name);
  }
}
class Person {
  constructor(name, genderCode) {
    this._name = name;
    this._genderCode = genderCode;
  }

  get genderCode() {
    return this._genderCode;
  }
}

提炼超类

动机:如果两个类在做相似的事,可以利用继承把相似之处提炼到超类。

  1. 为原本类新建一个空的超类
  2. 测试
  3. 逐一将子类共同元素上移至超类
  4. 检查子类中的函数,看是否还有共同的成分,有则提炼并上移
  5. 检查所有原本的类,将其调整为使用超类接口

折叠继承体系

动机:如果一个子类和超类已经没多大差别,则将子类和超类合并起来

  1. 选择移除超类还是子类?
  2. 将所有元素移动到同一个类中
  3. 修改将被移除类的所有引用点,改为合并后留下的类
  4. 移除类
  5. 测试

以委托取代子类

动机:与继承相比使用委托关系时接口更清晰、耦合更少,对象组合常常优于类继承

  1. 如果构造函数有多个调用者,首先工厂函数把构造函数包装起来
  2. 创建一个空的委托类
  3. 在超类中添加一个字段,用于安放委托对象
  4. 修改子类创建逻辑,使其初始化委托字段,放入一个委托对象的实例中
  5. 选择一个子类的函数,将其移入委托类
  6. 搬移上述函数,不要删除类中的委托代码
  7. 如果原函数在子类之外被调用,则把委托代码上移至超类,如果子类外没有调用,则移除委托代码
  8. 测试
  9. 重复,直到子类中所有函数都搬到委托类
  10. 找到所有子类构造函数的地方,逐一改为使用超类的构造函数
  11. 测试
  12. 移除子类

Before:

class Booking {
  constructor(show, date) {
    this._show = show;
    this._date = date;
  }
  get hasTalkback() {
    return this._show.hasOwnProperty("talkback") && !this.isPeakDay;
  }
  get basePrice() {
    let result = this._show.price;
    if (this.isPeakDay) result += Math.round(result * 0.15);
    return result;
  }
}

class PremiumBooking extends Booking {
  constructor(show, date, extras) {
    super(show, date);
    this._extras = extras;
  }
  get hasTalkback() {
    return this._show.hasOwnProperty("talkback");
  }
  get basePrice() {
    return Math.round(super.basePrice + this._extras.premiumFee);
  }
  get hasDinner() {
    return this._extras.hasOwnProperty("dinner") && !this.isPeakDay;
  }
}

After:

class Booking {
  constructor(show, date) {
    this._show = show;
    this._date = date;
  }
  _bePremium(extras) {
    this._premiumDelegate = new PremiumBookingDelegate(this, extras);
  }

  get hasTalkback() {
    return this._premiumDelegate
      ? this._premiumDelegate.hasTalkback
      : this._show.hasOwnProperty("talkback") && !this.isPeakDay;
  }
  get basePrice() {
    let result = this._show.price;
    if (this.isPeakDay) result += Math.round(result * 0.15);
    return this._premiumDelegate
      ? this._premiumDelegate.extendBasePrice(result)
      : result;
  }
  get hasDinner() {
    return this._premiumDelegate ? this._premiumDelegate.hasDinner : undefined;
  }
}

function createBooking(show, date) {
  return new Booking(show, date);
}
function createPremiumBooking(show, date, extras) {
  const result = new Booking(show, date);
  result._bePremium(extras);
  return result;
}

class PremiumBookingDelegate {
  constructor(hostBooking, extras) {
    this._host = hostBooking;
    this._extras = extras;
  }
  get hasTalkback() {
    return this._host._show.hasOwnProperty("talkback");
  }
  extendBasePrice(base) {
    return Math.round(base + this._extras.premiumFee);
  }
  get hasDinner() {
    return this._extras.hasOwnProperty("dinner") && !this._host.isPeakDay;
  }
}

以委托取代超类

动机:如果超类的一些函数对子类并不适用,则以委托取代超类

  1. 在子类中新建一个字段,使其引用超类的一个对象,并将委托引用初始化为超类的新实例
  2. 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发委托引用
  3. 当所有超类函数都被转发函数覆写后,去掉继承关系

Before:

class CatalogItem {
  constructor(id, title, tags) {
    this._id = id;
    this._title = title;
    this._tags = tags;
  }
  get id() {
    return this._id;
  }
  get title() {
    return this._title;
  }
  hasTag(arg) {
    return this._tags.includes(arg);
  }
}

class Scroll extends CatalogItem {
  constructor(id, title, tags, dateLastCleaned) {
    super(id, title, tags);
    this._lastCleaned = dateLastCleaned;
  }
  needsCleaning(targetDate) {
    const threshold = this.hasTag("revered") ? 700 : 1500;
    return this.daysSinceLastCleaning(targetDate) > threshold;
  }
  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }
}

After:

class CatalogItem {
  constructor(id, title, tags) {
    this._id = id;
    this._title = title;
    this._tags = tags;
  }
  get id() {
    return this._id;
  }
  get title() {
    return this._title;
  }
  hasTag(arg) {
    return this._tags.includes(arg);
  }
}

class Scroll {
  constructor(id, dateLastCleaned, catalogID, catalog) {
    this._id = id;
    this._catalogItem = catalog.get(catalogID);
    this._lastCleaned = dateLastCleaned;
  }
  get id() {
    return this._id;
  }
  get title() {
    return this._catalogItem.title;
  }
  hasTag(aString) {
    return this._catalogItem.hasTag(aString);
  }
  needsCleaning(targetDate) {
    const threshold = this.hasTag("revered") ? 700 : 1500;
    return this.daysSinceLastCleaning(targetDate) > threshold;
  }
  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }
}

将查询函数和修改函数分离

动机:任何有返回值的函数,都不应该有看得到的副作用。

一种常见的优化办法是:将查询所得记过缓存于某个字段中,后续重复查询可以大大加快速度。

  1. 复制函数,以查询来命名
  2. 移除有副作用的代码
  3. 执行静态检查
  4. 查找所有调用原函数的地方,替换为新函数,并在下面调用一次原函数
  5. 从原函数中去掉返回值
  6. 测试

Before:

// Miscreant - 恶棍
function alertForMiscreant(people) {
  for (const p of people) {
    if (p === "Don") {
      setOffAlarms();
      return "Don";
    }
    if (p === "John") {
      setOffAlarms();
      return "John";
    }
  }
  return "";
}

const found = alertForMiscreant(people);

After:

function findMiscreant(people) {
  for (const p of people) {
    if (p === "Don") {
      return "Don";
    }
    if (p === "John") {
      return "John";
    }
  }
  return "";
}

function alertForMiscreant(people) {
  if (findMiscreant(people) !== "") setOffAlarms(); // 替换算法
}

const found = findMiscreant(people);
alertForMiscreant(people);

函数参数化

动机:两个函数逻辑非常相似,可以将其合并为一个函数,以参数形式传入不同值,从而消除重复。

  1. 从一组相似函数选则一个
  2. 把需要作为参数传入的字面量添加到参数列表
  3. 修改该函数所有调用地方
  4. 测试
  5. 修改函数体,令其使用新传入的参数
  6. 替换与其相似的函数,并测试

Before:

function baseCharge(usage) {
  if (usage < 0) return usd(0);
  const amount =
    bottomBand(usage) * 0.03 + middleBand(usage) * 0.05 + topBand(usage) * 0.07;
  return usd(amount);
}
function bottomBand(usage) {
  return Math.min(usage, 100);
}
function middleBand(usage) {
  return usage > 100 ? Math.min(usage, 200) - 100 : 0;
}
function topBand(usage) {
  return usage > 200 ? usage - 200 : 0;
}

After:

function withinBand(usage, bottom, top) {
  return usage > bottom ? Math.min(usage, top) - bottom : 0;
}
function baseCharge(usage) {
  if (usage < 0) return usd(0);
  const amount =
    withinBand(usage, 0, 100) * 0.03 +
    withinBand(usage, 100, 200) * 0.05 +
    withinBand(usage, 200, Infinity) * 0.07;
  return usd(amount);
}

移除标记参数

动机:用标记参数来指示被调函数应该执行哪一部分逻辑,影响了函数内部控制流。移除标记参数是代码更加整洁。

  1. 针对参数的每一种可能值,新建一个明确函数
  2. 修改调用函数的地方为新建明确函数

Before:

function deliveryDate(anOrder, isRush) {
  if (isRush) {
    let deliveryTime;
    if (["MA", "CT"].includes(anOrder.deliveryState)) deliveryTime = 1;
    else if (["NY", "NH"].includes(anOrder.deliveryState)) deliveryTime = 2;
    else deliveryTime = 3;
    return anOrder.placedOn.plusDays(1 + deliveryTime);
  } else {
    let deliveryTime;
    if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) deliveryTime = 2;
    else if (["ME", "NH"].includes(anOrder.deliveryState)) deliveryTime = 3;
    else deliveryTime = 4;
    return anOrder.placedOn.plusDays(2 + deliveryTime);
  }
}

After:

function deliveryDate(anOrder, isRush) {
  if (isRush) return rushDeliveryDate(anOrder);
  else return regularDeliveryDate(anOrder);
}
function rushDeliveryDate(anOrder) {
  let deliveryTime;
  if (["MA", "CT"].includes(anOrder.deliveryState)) deliveryTime = 1;
  else if (["NY", "NH"].includes(anOrder.deliveryState)) deliveryTime = 2;
  else deliveryTime = 3;
  return anOrder.placedOn.plusDays(1 + deliveryTime);
}
function regularDeliveryDate(anOrder) {
  let deliveryTime;
  if (["MA", "CT", "NY"].includes(anOrder.deliveryState)) deliveryTime = 2;
  else if (["ME", "NH"].includes(anOrder.deliveryState)) deliveryTime = 3;
  else deliveryTime = 4;
  return anOrder.placedOn.plusDays(2 + deliveryTime);
}

保持对象完整

动机:如果一个函数需要传入一个对象的多个属性值,传递对象本身是更好地方式。

  1. 新建空函数,传入对象
  2. 新函数中调用旧函数,并把新参数映射到就的参数列表
  3. 执行静态检查
  4. 修改调用地方为新函数
  5. 把旧函数内联到新函数体内
  6. 修改函数名为旧函数名,并修改所有调用的地方

Before:

const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (!aPlan.withinRange(low, high))
  alerts.push("room temperature went outside range");

class HeatingPlan {
  withinRange(bottom, top) {
    return (
      bottom >= this._temperatureRange.low && top <= this._temperatureRange.high
    );
  }
}

After:

if (!aPlan.withinRange(aRoom.daysTemRange))
  alerts.push("room temperature went outside range");

class HeatingPlan {
  withinRange(aNumberRange) {
    return (
      aNumberRange.low >= this._temperatureRange.low &&
      aNumberRange.high <= this._temperatureRange.high
    );
  }
}

以查询取代参数

动机:如果函数的一个参数只需要向另一个参数查询就能得到,则参数列表应避免重复。

  1. 如有必要使用提炼函数将参数的查询过程提炼到一个独立函数中
  2. 将函数体内参数饮用的地方改为调用新建的函数,并测试
  3. 将参数去掉

Before:

class Order {
  get finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
    let discountLevel;
    if (this.quantity > 100) discountLevel = 2;
    else discountLevel = 1;
    return this.discountedPrice(basePrice, discountLevel);
  }
  discountedPrice(basePrice, discountLevel) {
    switch (discountLevel) {
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
    }
  }
}

After:

class Order {
  get finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
    return this.discountedPrice(basePrice);
  }
  get discountLevel() {
    return this.quantity > 100 ? 2 : 1;
  }
  discountedPrice(basePrice) {
    switch (discountLevel) {
      case 1:
        return basePrice * 0.95;
      case 2:
        return basePrice * 0.9;
    }
  }
}

以参数取代查询

动机:在负责逻辑处理的模块中只有纯函数,其外再包裹处理 I/O 和其他可变元素的逻辑代码,使其更容易测试及理解。JavaScript 的类模型无法强制要求类的不可变形——始终有办法修改对象的内部数据,以参数取代查询是达成让类保持不可变的利器。

  1. 对查询操作的代码提炼为变量,从函数体中分离出去
  2. 提炼函数体内代码为新函数
  3. 使用内联变量消除刚提炼出来的变量
  4. 对原函数使用内联函数
  5. 新函数该会原函数名字

Before:

class HeatingPlan {
  get targetTemperature() {
    if (thermostat.selectedTemperature > this._max) return this._max;
    else if (thermostat.selectedTemperature < this._min) return this._min;
    else return thermostat.selectedTemperature;
  }
}

if (thePlan.targetTemperature > thermostat.currentTemperature) setToH;
else if (thePlan.targetTemperature < thermostat.currentTemperature) setToC;
else setOff();

After:

class HeatingPlan {
  targetTemperature(selectedTemperature) {
    if (selectedTemperature > this._max) return this._max;
    else if (selectedTemperature < this._min) return this._min;
    else return selectedTemperature;
  }
}

if (
  thePlan.targetTemperature(thermostat.selectedTemperature) >
  thermostat.currentTemperature
)
  setToHeat();
else if (
  thePlan.targetTemperature(thermostat.selectedTemperature) <
  thermostat.currentTemperature
)
  setToCool();
else setOff();

移除设值函数

动机:如果不希望在对象创建之后某个属性还有机会被改变,就不要为它提供 set 函数。

  1. 在构造函数中调用设值函数,对字段设值
  2. 移除所有在构造函数之外对设值函数的调用,改为使用新的构造函数,并测试
  3. 使用内联函数消去设置函数
  4. 测试

Before:

class Person {
  get name() {
    return this._name;
  }
  set name(arg) {
    this._name = arg;
  }
  get id() {
    return this._id;
  }
  set id(arg) {
    this._id = arg;
  }
}

const martin = new Person();
martin.name = "martin";
martin.id = "1234";

After:

class Person {
  constructor(id) {
    this._id = id;
  }
  get name() {
    return this._name;
  }
  set name(arg) {
    this._name = arg;
  }
  get id() {
    return this._id;
  }
}
const martin = new Person("1234");
martin.name = "martin";

以工厂函数取代构造函数

动机:与一般函数相比,构造函数常有一些丑陋的局限性,只能返回当前所调用类的实例,构造函数名称是固定的类名,需要通过特殊操作符调用。工厂函数的实现内部可以调用构造函数,也可以换别的方式实现。

  1. 新建一个工厂函数,让它调用现有的构造函数
  2. 将调用构造函数的代码替换为工厂函数
  3. 每次修改,执行测试
  4. 尽量缩小构造函数可见范围

Before:

class Employee {
  constructor(name, typeCode) {
    this._name = name;
    this._typeCode = typeCode;
  }
  get name() {
    return this._name;
  }
  get type() {
    return Employee.legalTypeCodes[this._typeCode];
  }
  static get legalTypeCodes() {
    return { E: "Engineer", M: "Manager", S: "Salesman" };
  }
}

const candidate = new Employee(document.name, document.empType);
const leadEngineer = new Employee(document.leadEngineer, "E");

After:

class Employee {
  constructor(name, typeCode) {
    this._name = name;
    this._typeCode = typeCode;
  }
  get name() {
    return this._name;
  }
  get type() {
    return Employee.legalTypeCodes[this._typeCode];
  }
  static get legalTypeCodes() {
    return { E: "Engineer", M: "Manager", S: "Salesman" };
  }
}

function createEmployee(name, typeCode) {
  return new Employee(name, typeCode);
}
const candidate = createEmployee(document.name, document.empType);
function createEngineer(name) {
  return new Employee(name, "E");
}
const leadEngineer = createEngineer(document.leadEngineer);

以命令取代函数

动机:将函数封装成自己的对象,称为“命令对象”,简称“命令”,只服务于单一函数,获得对该函数的请求,执行函数。

  1. 为想要包装的函数创建一个空类,根据该函数名字命名
  2. 把函数移动到空类里
  3. 给每个参数创建一个字段,并在构造函数中添加对应的参数

Before:

function score(candidate, medicalExam, scoringGuide) {
  let result = 0;
  let healthLevel = 0;
  let highMedicalRiskFlag = false;
  if (medicalExam.isSmoker) {
    healthLevel += 10;
    highMedicalRiskFlag = true;
  }
  let certificationGrade = "regular";
  if (scoringGuide.stateWithLowCertification(candidate.originState)) {
    certificationGrade = "low";
    result -= 5;
  }
  // lots more code like this
  result -= Math.max(healthLevel - 5, 0);
  return result;
}

After:

function score(candidate, medicalExam, scoringGuide) {
  return new Scorer().execute(candidate, medicalExam, scoringGuide);
}
class Scorer {
  constructor(candidate, medicalExam, scoringGuide) {
    this._candidate = candidate;
    this._medicalExam = medicalExam;
    this._scoringGuide = scoringGuide;
  }

  execute() {
    this._result = 0;
    this._healthLevel = 0;
    this._highMedicalRiskFlag = false;
    this.this.scoreSmoking();
    this._certificationGrade = "regular";
    if (
      this._scoringGuide.stateWithLowCertification(this._candidate.originState)
    ) {
      this._certificationGrade = "low";
      this._result -= 5;
    }
    // lots more code like this
    this._result -= Math.max(healthLevel - 5, 0);
    return this._result;
  }

  scoreSmoking() {
    if (this._medicalExam.isSmoker) {
      this._healthLevel += 10;
      this._highMedicalRiskFlag = true;
    }
  }
}

以函数取代命令

动机:借助命令对象可以轻松地将原本复杂的函数拆解为多个方法,彼此间通过字段共享状态,拆解后的方法分别调用,开始调用前的数据状态也可以逐步构建。但如果这个函数不太复杂,可以考虑将其变回普通函数

  1. 把“创建并执行命令对象”的代码单独提炼到一个函数中
  2. 对命令对象在执行阶段调用到的函数,逐一使用内联函数
  3. 把构造函数的参数转移到执行函数声明中
  4. 执行函数中引用的所有字段改为使用参数,并测试
  5. 把“调用构造函数”和“调用执行函数”都内联到调用方
  6. 测试
  7. 把命令类删除

Before:

class ChargeCalculator {
  constructor(customer, usage, provider) {
    this._customer = customer;
    this._usage = usage;
    this._provider = provider;
  }
  get baseCharge() {
    return this._customer.baseRate * this._usage;
  }
  get charge() {
    return this.baseCharge + this._provider.connectionCharge;
  }
}

After:

function charge(customer, usage, provider) {
  const baseCharge = customer.baseRate * usage;
  return baseCharge + provider.connectionCharge;
}

只总结了一部分

发布了31 篇原创文章 · 获赞 13 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/weixin_42881744/article/details/104051846