函数上移
动机:如果某个函数在各个子类中的函数体都相同,则将函数上移
- 检查待上移的函数,确定完全一致
- 检查函数体内引用的所有函数调用和字段都能从超类中调用到
- 如果待上移的函数声明不同,则修改为将要在超类中使用的声明
- 超类中创建一个函数,将待上移函数代码复制其中
- 执行静态检查
- 移除一个待上移子类函数
- 测试
- 移除其余待上移子类函数
Before:
1 2 3 4 5 6 7 8 9 10
| class Employee extends Party { get annualCost() { return this.monthlyCost * 12; } } class Department extends Party { get annualCost() { return this.monthlyCost * 12; } }
|
After:
1 2 3 4 5 6 7 8 9
| class Party { get annualCost() { return this.monthlyCost * 12; }
get monthlyCost() { throw new SubclassResponsibilityError(); } }
|
字段上移
动机:观察函数如何使用字段来判断它们是否重复,如果它们被使用方式很相似,则可以将它们上移到超类中去
- 检查待上移的字段使用方式一致
- 如果在在类中名字不一致,则取相同的名字
- 超类中创建一个字段
- 移除子类字段
- 测试
Before:
1 2 3 4 5 6 7
| class Employee {} class Salesman extends Employee { name; } class Engineer extends Employee { name; }
|
After:
1 2 3 4 5
| class Employee { name; } class Salesman extends Employee {} class Engineer extends Employee {}
|
构造函数本体上移
动机: 各个子类中构造函数有共同的行为
- 超类不存在构造函数,则创建一个,并确保子类调用
- 将子类构造函数中的公共语句移动到超类构造函数中
- 删除子类构造函数公共代码
- 测试
- 如存在无法简单上移至超类的公共代码,利用函数上移提升
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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; } }
|
函数下移
动机:如果超类中的某个函数只与一个子类有关,最好将其从超类中移除,放到正在关心它的子类中去
- 将超类中的函数本体复制到需要此函数的子类中
- 删除超类中的函数
- 测试
- 将该函数从所有不需要的子类中删除
- 测试
字段下移
动机:如果属性字段只被一个子类用到,则下移至该子类中
- 在子类中声明该字段
- 从超类中移除
- 测试
- 将该字段从不需要它的子类中删除
- 测试
以子类取代类型码
动机:继承可以用多态来处理条件逻辑,更能明确地表达数据和类型之间的关系
- 自封装类型码字段
- 任选一个类型码取值,为其创建一个子类
- 创建一个选择器逻辑,把类型码参数映射到新的子类
- 测试
- 针对每个类型码取值,重复 2
- 去除类型码字段
- 测试
- 处理原本访问类型码的函数
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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}`); } }
|
移除子类
动机:如果子类的用处太少,最好移除子类,将其替换为超类的一个字段
- 把子类的构造函数包装到超类的工厂函数中
- 将类型检查逻辑包装起来搬移到超类
- 新建一个字段,代表子类类型
- 将判断子类类型的函数改为新建字段
- 删除子类
- 测试
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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; } }
|
提炼超类
动机:如果两个类在做相似的事,可以利用继承把相似之处提炼到超类。
- 为原本类新建一个空的超类
- 测试
- 逐一将子类共同元素上移至超类
- 检查子类中的函数,看是否还有共同的成分,有则提炼并上移
- 检查所有原本的类,将其调整为使用超类接口
折叠继承体系
动机:如果一个子类和超类已经没多大差别,则将子类和超类合并起来
- 选择移除超类还是子类?
- 将所有元素移动到同一个类中
- 修改将被移除类的所有引用点,改为合并后留下的类
- 移除类
- 测试
以委托取代子类
动机:与继承相比使用委托关系时接口更清晰、耦合更少,对象组合常常优于类继承
- 如果构造函数有多个调用者,首先工厂函数把构造函数包装起来
- 创建一个空的委托类
- 在超类中添加一个字段,用于安放委托对象
- 修改子类创建逻辑,使其初始化委托字段,放入一个委托对象的实例中
- 选择一个子类的函数,将其移入委托类
- 搬移上述函数,不要删除类中的委托代码
- 如果原函数在子类之外被调用,则把委托代码上移至超类,如果子类外没有调用,则移除委托代码
- 测试
- 重复,直到子类中所有函数都搬到委托类
- 找到所有子类构造函数的地方,逐一改为使用超类的构造函数
- 测试
- 移除子类
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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; } }
|
以委托取代超类
动机:如果超类的一些函数对子类并不适用,则以委托取代超类
- 在子类中新建一个字段,使其引用超类的一个对象,并将委托引用初始化为超类的新实例
- 针对超类的每个函数,在子类中创建一个转发函数,将调用请求转发委托引用
- 当所有超类函数都被转发函数覆写后,去掉继承关系
Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| 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); } }
|