封装
-
封装记录
用数据类取代记录型结构
const organization = { name:'Acme', country: "GB" }
class Organization { constructor(data){ this._name = data.name } get name(){ return this._name } set name(name){ this._name = name } } const organization = new Organization({...})
做法
- 对持有记录的变量使用封装变量,将其封装在一个函数中
- 创建一个类,将记录包装起来,并将记录变量的只替换成该类的一个实例,然后在类上定义访问和修改的函数
- 新建一个函数,让它返回该类的对象,而非原始记录
- 替换项目中的使用点
const organization = { name:'Acme', country: "GB" } let result = '' result += `<h1>${organization.name} : ${organization.country}</h1>\n` organization.name = '121' organization.country = '121' result += `<h1>${organization.name} : ${organization.country}</h1>` console.log(result)
class Organization { constructor(data){ this._name = data.name this._country = data.country } get name(){ return this._name } get country(){ return this._country } set name(name){ this._name = name } set country(country){ this._country = country } } const organization = new Organization({ name:'Acme', country: "GB" }) let result = '' result += `<h1>${organization.name} : ${organization.country}</h1>\n` organization.name = '121' organization.country = '121' result += `<h1>${organization.name} : ${organization.country}</h1>` console.log(result)
-
封装集合
对可变数据进行封装,不返回源数据
class Person { get courses() { return this._courses } set courses(courses) { this._courses = courses } } class Course { ... }
class Person { get courses() { return this._courses.slice() } set courses(courses) { this._courses = courses.slice() } addCourse(aCourse) { ... } removeCourse(aCourse) { ... } } class Course { ... }
做法
- 如果集合对引用尚未封装,则先用封装变量封装
- 在类上增加用于增删对函数
- 查找集合的引用点,修改后测试
- 每次只返回一份只读的副本
class Person { constructor(name){ this._name = name this._courses = [] } get name(){return this._name } get courses() { return this._courses } set courses(courses) { this._courses = courses } } class Course { constructor(name, isAdvanced){ this._name = name this._isAdvanced = isAdvanced } get name(){return this._name } get isAdvanced(){return this._isAdvanced } } const CoursesName = [{ name:'Math', isAdvanced: true }, { name:'Chinese', isAdvanced: true }, { name:'English', isAdvanced: false }] const p = new Person('xy') p.courses = CoursesName.map(({ name, isAdvanced }) => new Course(name, isAdvanced)) p.courses.push(new Course('history', true)) // 无法监测的行为!
class Person { constructor(name){ this._name = name this._courses = [] } get name(){return this._name } get courses() { return this._courses.slice() } set courses(courses) { this._courses = courses.slice() } addCourse(aCourse) { this._courses.push(aCourse) } removeCourse(aCourse){ const index = this._courses.indexOf(aCourse) if(index === -1) throw new RangeError("not in") this._courses.splice(index, 1) } } class Course { constructor(name, isAdvanced){ this._name = name this._isAdvanced = isAdvanced } get name(){return this._name } get isAdvanced(){return this._isAdvanced } } const CoursesName = [{ name:'Math', isAdvanced: true }, { name:'Chinese', isAdvanced: true }, { name:'English', isAdvanced: false }] const history = new Course('history', true) const p = new Person('xy') p.courses = CoursesName.map(({ name, isAdvanced }) => new Course(name, isAdvanced)) p.addCourse(history) p.removeCourse(history)
-
以对象取代基本类型
当基本数据类型不满足业务需求时,将其封装成类方便管理
class Order{ get priority(){return this._priority } } const priorities = ['low', 'normal', 'high', 'rush']
class Order{ constructor(data){ this._priority = new Priority() } } class Priority{ toString(){return this._value} static legalValues(){return ['low', 'normal', 'high', 'rush']} } const priorities = Priority.legalValues()
做法
- 如果变量未被封装起来,则先封装变量
- 为数据值创建一个简单的类,保存数据并提供取值函数
- 修改第一步得到的设值函数,令其创建一个新类的对象将其存入字段
- 修改取值函数,令其调用新类的取值函数
class Order{ constructor(data){ this._priority = data.priority } get priority(){return this._priority } } const priorities = ['low', 'normal', 'high', 'rush'] let orders = new Array(10).fill(0).map((item, index) => new Order({ priority: priorities[index % 4] })) console.log(orders.filter(item => item.priority === 'high' || item.priority === 'rush'))
class Order{ constructor(data){ this._priority = new Priority(data.priority) } get priorityString(){return this._priority.toString() } get priority(){return this._priority} set priority(aString){this._priority = new Priority(aString)} } class Priority{ constructor(value){ if(value instanceof Priority) return value if(Priority.legalValues().includes(value)){ this._value = value }else{ throw new Error('invalid for Priority') } } toString(){return this._value} static legalValues(){return ['low', 'normal', 'high', 'rush']} get _index(){ return Priority.legalValues().findIndex(item => item === this._value) } equals(order) { return this._index === order._index } higherThan(order) { return this._index > order._index } lessThan(order) { return this._index < order._index } } const priorities = Priority.legalValues() const normal = new Priority('normal') let orders = new Array(10).fill(0).map((item, index) => new Order({ priority: priorities[index % 4] })) console.log(orders.filter(item => item.priority.higherThan(normal)))
-
以查询取代临时变量
将临时变量中的计算逻辑移动到类/函数中,通过查询获取
class Order { get price() { let basePrice = ... let discountFactor = ... return basePrice * discountFactor } }
class Order { get price() {...} get basePrice() { ... } get discountFactor() {...} }
做法
- 检查变量在是否完全计算完毕,是否存在副作用
- 如果变量不是只读的,可以先改造成只读变量
- 将变量赋值代码提炼成设值函数
class Order { constructor(quantity, item){ this._quantity = quantity this._item = item } get price() { let basePrice = this._quantity * this._item.price let discountFactor = 0.98 if(basePrice > 1000) discountFactor -= 0.03 return basePrice * discountFactor } }
class Order { constructor(quantity, item){ this._quantity = quantity this._item = item } get price() { return this.basePrice * this.discountFactor } get basePrice() { return this._quantity * this._item.price } get discountFactor() { let discountFactor = 0.98 if(this.basePrice > 1000) discountFactor -= 0.03 return discountFactor } }
-
提炼类(内联类)
将较大的类中的模块分离出去成为独立的类
class Person { ... }
class Person { constructor(name){ this._telephoneNumber = new TelephoneNumber() } } class TelephoneNumber{ ... }
做法
- 决定如何分解类所负的责任
- 创建一个新的类,用以表现从旧类中分离出来的责任
- 构造旧类时创建一个新类的实例,建立联系
- 搬移字段,函数,去掉不再需要的函数接口
class Person { constructor(name){ this._name = name } get name(){return this._name } get telephoneNumber(){return `(${this.officeAreaCode}) ${this.officeNumber}` } get officeAreaCode(){return this._officeAreaCode } set officeAreaCode(arg){return this._officeAreaCode = arg } get officeNumber(){return this._officeNumber } set officeNumber(arg){return this._officeNumber = arg } }
class Person { constructor(name){ this._name = name this._telephoneNumber = new TelephoneNumber() } get name(){return this._name } get telephoneNumber(){return this._telephoneNumber.toString() } get officeAreaCode(){return this._telephoneNumber.areaCode } set officeAreaCode(arg){return this._telephoneNumber.areaCode = arg } get officeNumber(){return this._telephoneNumber.number } set officeNumber(arg){return this._telephoneNumber.number = arg } } class TelephoneNumber{ get number(){return this._number} set number(arg){return this._number = arg} get areaCode(){return this._areaCode } set areaCode(arg){return this._areaCode = arg} toString(){return `(${this._areaCode}) ${this._number}`} }
-
内联类(提炼类)
当一个类不再承担足够的责任时,将其直接内联到引用其的类中
class Shipment { constructor(){ this._trackingInformation = new TrackingInformation(company, number) } } class TrackingInformation { ... }
class Shipment { constructor(){ ... } ... //TrackingInformation }
做法
- 对于内联类中的所有函数,在目标类中创建对应的函数
- 修改源类函数所有的引用点,令其调用目标类对应的委托方法
- 将所有的函数数据移动到目标类中
class TrackingInformation { constructor(company, number){ this._shippingCompany = company this._trackingNumber = number } get shippingCompany(){return this._shippingCompany} set shippingCompany(arg) {this._shippingCompany = arg} get trackingNumber(){return this._trackingNumber} set trackingNumber(arg) {this._trackingNumber = arg} get display(){ return `${this.shippingCompany} ${this.trackingNumber}` } } class Shipment { constructor(name, company, number){ this._name = name this._trackingInformation = new TrackingInformation(company, number) } get trackingInfo(){return this._trackingInformation.display} get trackingInformation(){return this._trackingInformation} }
class Shipment { constructor(name, company, number){ this._name = name this._shippingCompany = company this._trackingNumber = number } get shippingCompany(){return this._shippingCompany} set shippingCompany(arg) {this._shippingCompany = arg} get trackingNumber(){return this._trackingNumber} set trackingNumber(arg) {this._trackingNumber = arg} get trackingInfo(){ return `${this.shippingCompany} ${this.trackingNumber}` } get trackingInformation(){return { _shippingCompany: this.shippingCompany, _trackingNumber: this._trackingNumber}} }
-
隐藏委托关系(移除中间人)
隐藏跨级引用的情况,增加快速访问的接口
class Person { get department(){return this._department } } class Department { get chargeCode(){return this._chargeCode } }
class Person { get manager(){return this._department.manager } } class Department { get manager(){return this._manager} }
做法
- 对于每个委托关系的函数,在服务对象端建立一个简单的委托函数
- 调整客户端,令其调用服务对象提供的函数
class Person { constructor(name){ this._name = name; } get name(){return this._name } get department(){return this._department } set department(arg){this._department = arg} } class Department { get chargeCode(){return this._chargeCode} set chargeCode(arg){this._chargeCode = arg} get manager(){return this._manager} set manager(arg){this._manager = arg} }
class Person { constructor(name){ this._name = name; } get name(){return this._name } set department(arg){this._department = arg} get manager(){return this._department.manager } } class Department { get chargeCode(){return this._chargeCode} set chargeCode(arg){this._chargeCode = arg} get manager(){return this._manager} set manager(arg){this._manager = arg} }
-
移除中间人(隐藏委托关系)
类中存在大量的委托函数,服务类变成类一个中间人
class Person { get manager(){return this._department.manager } } class Department { get manager(){return this._manager} }
class Person { get department(){return this._department } } class Department { get chargeCode(){return this._chargeCode } }
做法
- 为受托对象创建一个取值函数
- 对于每个委托函数,是客户端转为连续的调用访问
class Person { constructor(name){ this._name = name; } get name(){return this._name } set department(arg){this._department = arg} get manager(){return this._department.manager } get chargeCode(){return this._department.chargeCode} } class Department { get chargeCode(){return this._chargeCode} set chargeCode(arg){this._chargeCode = arg} get manager(){return this._manager} set manager(arg){this._manager = arg} }
class Person { constructor(name){ this._name = name; } get name(){return this._name } get department(){return this._department } set department(arg){this._department = arg} } class Department { get chargeCode(){return this._chargeCode} set chargeCode(arg){this._chargeCode = arg} get manager(){return this._manager} set manager(arg){this._manager = arg} }
-
替换算法
使用更清晰的方式替换复杂的方式
function findPerson(people){ for(let i = 0; i < people.length; i++){ if(people[i] === 'Don') return 'Don' } return '' }
function findPerson(people) { return people.find(p => ['Don'].includes(p)) || '' }
做法
- 整理代替换的算法,确保它以及被抽取到一个独立的函数中
- 为函数准备测试,固定其行为
- 准备好另一个算法进行替换
function findPerson(people){ for(let i = 0; i < people.length; i++){ if(people[i] === 'Don') return 'Don' if(people[i] === 'Join') return 'Join' if(people[i] === 'Kent') return 'Kent' } return '' }
function findPerson(people) { const candidates = ['Don', 'Join', 'Kent'] return people.find(p => candidates.includes(p)) || '' }
搬移特性
-
搬移函数
合理的把函数放置在它应该出现的位置
function trackSummary(points){ ... function distance(p1, p2){...} }
function trackSummary(points){ ... } function distance(p1, p2){...}
做法
- 检查函数在当前上下文引用的元素,考虑是否将他们一起搬移
- 检查待搬移函数是否具有多态性
- 将函数复制一份到目标的上下文中,调整函数
function trackSummary(points){ const totalTime = calculateTime() const totalDistance = calculateDistance() const pace = totalTime / 60 / totalDistance return { time: totalTime, distance: totalDistance, pace } function calculateDistance(){ let result = 0 for(let i = 1; i < points.length; i++){ result += distance(points[i - 1], points[i]) } return result } function distance(p1, p2){ const EARTH_RADIUS = 3959 const dLat = radians(p2.lat) - radians(p1.lat) const dLon = radians(p2.lon) - radians(p1.lon) const a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(radians(p2.lat)) * Math.cos(radians(p1.lat)) * Math.pow(Math.sin(dLon / 2), 2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) return EARTH_RADIUS * c } function radians(degrees){ return degrees * Math.PI / 180 } function calculateTime(){ return 6 } }
function trackSummary(points){ const totalTime = calculateTime() const distance = totalDistance(points) const pace = totalTime / 60 / distance return { time: totalTime, distance, pace } function calculateTime(){ return 6 } } function totalDistance(points){ let result = 0 for(let i = 1; i < points.length; i++){ result += distance(points[i - 1], points[i]) } return result } function distance(p1, p2){ const EARTH_RADIUS = 3959 const dLat = radians(p2.lat) - radians(p1.lat) const dLon = radians(p2.lon) - radians(p1.lon) const a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(radians(p2.lat)) * Math.cos(radians(p1.lat)) * Math.pow(Math.sin(dLon / 2), 2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) return EARTH_RADIUS * c } function radians(degrees){ return degrees * Math.PI / 180 }
-
搬移字段
如果更新一个字段,需要同时在几个结构中进行修改,则表明该字段需要被搬移到一个集中的地点
class Customer { constructor(name, discountRate){ this._discountRate = discountRate this._contact = new Contact() } } class Contact { ... }
class Customer { constructor(name, discountRate){ this._contact = new Contact(discountRate) } } class Contact { constructor(discountRate){ this._discountRate = discountRate } }
做法
- 确保源字段已经得到良好封装
- 在目标对象上创建一个字段
- 确保源对象能正常引用目标对象
- 调整源对象的访问函数,令其使用目标对象的字段
class Customer { constructor(name, discountRate){ this._name = name this._discountRate = discountRate this._contact = new Contact() } get discountRate(){return this._discountRate} becomePreferred(){ return this._discountRate += 0.03 } applyDiscount(amount){ return amount - amount * this.discountRate } } class Contact { constructor(){ this._startDate = new Date() } }
class Customer { constructor(name, discountRate){ this._name = name this._contact = new Contact(discountRate) } get discountRate(){return this._contact.discountRate} _setDiscountRate(aNumber){return this._contact.discountRate = aNumber} becomePreferred(){ return this._setDiscountRate(this._contact.discountRate += 0.03) } applyDiscount(amount){ return amount - amount * this.discountRate } } class Contact { constructor(discountRate){ this._startDate = new Date() this._discountRate = discountRate } get discountRate(){return this._discountRate} set discountRate(arg){this._discountRate = arg} }
-
搬移语句到函数(搬移语句到调用者)
某些语句与一个函数放在一起更加像一个整体的时候,就可以将其搬移到函数中
function renderPerson(person){ [`<p>title: ${aPhoto.title}</p>`, ...emitPhotoData(person.photo)] } function emitPhotoData(aPhoto){ return [`<p>date: ${aPhoto.date}</p>`] } function renderPhoto(aPhoto){ return [`<p>title: ${aPhoto.title}</p>`, `<p>date: ${aPhoto.date}</p>`].join('/n') }
function renderPerson(person){ [...emitPhotoData(person.photo)] } function emitPhotoData(aPhoto){ return [`<p>title: ${aPhoto.title}</p>`, `<p>date: ${aPhoto.date}</p>`] } function renderPhoto(aPhoto){ return [...emitPhotoData(person.photo)].join('/n') }
做法
- 如果重复的代码距离目标函数有些距离,先用移动语句
- 如果目标函数仅被唯一一个源函数调用,则只需要将源函数中重复的代码段剪切复制到函数中
- 如果由多个调用点,则先选择对一个调用点应用提炼函数,将待搬移语句与目标函数提炼成一个新函数
function renderPerson(person){ const result = [] result.push(`<p>${person.name}</p>`) result.push(renderPhoto(person.photo)) result.push(`<p>title: ${person.photo.title}</p>`) result.push(emitPhotoData(person.photo)) return result.join('\n') } function emitPhotoData(aPhoto){ const result = [] result.push(`<p>location: ${aPhoto.location}</p>`) result.push(`<p>date: ${aPhoto.date.toDateString()}</p>`) return result.join('\n') } function renderPhoto(aPhoto){ const result = [] result.push(`<p>name: ${aPhoto.name}</p>`) result.push(`<p>color: ${aPhoto.color}</p>`) return result.join('\n') } function renderDiv(person){ return [ "<div>", `<p>title: ${person.photo.title}</p>`, emitPhotoData(person.photo), "</div>" ].join('\n') }
function renderPerson(person){ const result = [] result.push(`<p>${person.name}</p>`) result.push(renderPhoto(person.photo)) result.push(emitPhotoData(person.photo)) return result.join('\n') } function renderPhoto(aPhoto){ const result = [] result.push(`<p>name: ${aPhoto.name}</p>`) result.push(`<p>color: ${aPhoto.color}</p>`) return result.join('\n') } function renderDiv(person){ const result = ["<div>", emitPhotoData(person.photo), "</div>"] return result.join('\n') } function emitPhotoData(aPhoto) { return [ `<p>title: ${aPhoto.title}</p>`, `<p>location: ${aPhoto.location}</p>`, `<p>date: ${aPhoto.date.toDateString()}</p>` ].join('\n') }
-
搬移语句到调用者(搬移语句到函数)
当函数边界发生偏移,在某些调用点表现出不同的行为
function renderPerson(person){ [...emitPhotoData(person.photo)] } function emitPhotoData(aPhoto){ return [`<p>title: ${aPhoto.title}</p>`, `<p>date: ${aPhoto.date}</p>`] } function renderPhoto(aPhoto){ return [...emitPhotoData(person.photo)].slice(1).join('/n') }
function renderPerson(person){ [`<p>title: ${aPhoto.title}</p>`, ...emitPhotoData(person.photo)] } function emitPhotoData(aPhoto){ return [`<p>date: ${aPhoto.date}</p>`] } function renderPhoto(aPhoto){ return [...emitPhotoData(aPhoto)].join('/n') }
做法
- 如果源函数很简单,且调用者也不多,则只需把要搬移的代码复制回去
- 若调用点不止一个,则先用提炼函数把不想搬移的代码提炼成一个新函数
- 对源函数应用内联函数,对提炼函数进行修改
function renderPerson(person){ const result = [] result.push(`<p>${person.name}</p>`) result.push(renderPhoto(person.photo)) result.push(emitPhotoData(person.photo)) return result.join('\n') } function renderPhoto(aPhoto){ const result = [] result.push(`<p>name: ${aPhoto.name}</p>`) result.push(`<p>color: ${aPhoto.color}</p>`) return result.join('\n') } function renderDiv(person){ const result = ["<div>", emitPhotoData(person.photo), "</div>"] return result.join('\n') } function emitPhotoData(aPhoto) { return [ `<p>title: ${aPhoto.title}</p>`, `<p>location: ${aPhoto.location}</p>`, `<p>date: ${aPhoto.date.toDateString()}</p>` ].join('\n') }
function renderPerson(person){ const result = [] result.push(`<p>${person.name}</p>`) result.push(renderPhoto(person.photo)) result.push(emitPhotoData(person.photo)) result.push(`<p>location: ${person.photo.location}</p>`) return result.join('\n') } function renderPhoto(aPhoto){ const result = [] result.push(`<p>name: ${aPhoto.name}</p>`) result.push(`<p>color: ${aPhoto.color}</p>`) return result.join('\n') } function renderDiv(person){ const result = ["<div>", emitPhotoData(person.photo), `<p>location: ${person.photo.location}</p>`, "</div>"] return result.join('\n') } function emitPhotoData(aPhoto) { return [ `<p>title: ${aPhoto.title}</p>`, `<p>date: ${aPhoto.date.toDateString()}</p>` ].join('\n') }
-
函数调用取代内联代码
通过函数的方式去消除重复的逻辑
let appliesToMass = false; let states = ['MA'] for(let s of states) { if(s === 'MA') appliesToMass = true; }
let states = ['MA'] let appliesToMass = states.includes('MA');
做法
- 内联代码替代为对一个既有函数的调用
let appliesToMass = false; let states = ['MA'] for(let s of states) { if(s === 'MA') appliesToMass = true; }
let states = ['MA'] let appliesToMass = states.includes('MA');
-
移动语句 让存在关联的代码一起出现,使代码更加容易理解
if(stack.length === 0){ ... stack.push(i) }else{ ... stack.push(i) }
if(stack.length === 0){ ... }else{ ... } stack.push(i)
做法
- 确定待移动的代码片段应该搬完何处,搬移后是否会影响正常工作
- 搬移代码
let result, stack = [] for(let i = 0; i < 5; i++){ if(stack.length === 0){ result = 0 stack.push(i) }else{ result = stack[stack.length - 1] stack.push(i) } }
let result, stack = [] for(let i = 0; i < 5; i++){ if(stack.length === 0){ result = 0 }else{ result = stack[stack.length - 1] } stack.push(i) }
-
拆分循环
拆分身兼多职的循环,让代码更加易读
for (const p of people) { if(p.age < youngest) youngest = p.age totalSalary += p.salary }
let youngest = Math.min(...people.map(p => p.age)) let totalSalary = people.reduce((prev, item) => prev + item.salary, 0)
做法
- 复制一遍循环代码
- 识别移除循环中的重复代码,使得每个循环只做一件事
- 拆分后使用提炼函数
const people = [ { age: 20, salary: 10000 }, { age: 30, salary: 10000 }, { age: 25, salary: 20000 }, { age: 22, salary: 50000 }, { age: 26, salary: 60000 }, ] let youngest = people[0] ? people[0].age : Infinity; let totalSalary = 0 for (const p of people) { if(p.age < youngest) youngest = p.age totalSalary += p.salary }
let youngest = Math.min(...people.map(p => p.age)) let totalSalary = people.reduce((prev, item) => prev + item.salary, 0)
-
管道取代循环
使用管道优化迭代结构,可读性更强
function acquireData(input) { for (const line of lines) { ... } }
function acquireData(input) { lines.slice(1).filter(line => line.trim() !== '')... }
做法
- 创建一个新变量,用于存放和参与循环过程的集合
- 从顶部开始,将循环每一块行为都搬移出来,在上一步创建的集合变量用一种管道运算替代
const input = `office, country, telephone Chicago, USA, +1 312 373 1000 Beijing, China, +86 4000 900 505 Chicago, USA, +1 312 373 1000 Beijing, China, +86 4000 900 505 Chicago, USA, +1 312 373 1000 Beijing, China, +86 4000 900 505`; function acquireData(input) { const lines = input.split('\n') let firstLine = true const result = [] for (const line of lines) { if(firstLine) { firstLine = false continue } if(line.trim() === '') continue const record = line.split(',') if(record[1].trim() === 'China'){ result.push({ city: record[0].trim(), phone: record[2].trim(), }) } } return result }
function acquireData(input) { const lines = input.split('\n') return lines .slice(1) .filter(line => line.trim() !== '') .map(line => line.split(',')) .filter(record => record[1].trim() === 'China') .map(record => ({ city: record[0].trim(), phone: record[2].trim(), })) }
-
移除死代码
没用的东西大胆删除了他,不然越来越多