重复代码(Duplicated Code)
方式:
- Extract Method
- Pull Up Method
- Form Template Method
- Extract Class
总体感觉就是提取公用函数、共用类做提炼,根据个人经验,提取重复代码主要需要注意以下几点:
- 先局部,再整体
- 提取代码的时候不要做代码重写,先提取再改造
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;
}
- 如果功能相近,那么要更小心,可以关注他们之间的关系:包含、组合、分支判断等关系,选择更合适的方案
过长函数(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 哲学中的:让每个程序做好一件事。要做一件新的工作,就构建新程序,而不是通过增加新“特性”使旧程序复杂化。
-
把程序实现变得更有组合性,产生新的程序
-
合理管理参数和临时变量,可以通过:
- Replace Temp with Query
- Introducer Parameter Object
- Preserve Whole Object
- Replace Method with Method Object
等模式来改善代码
- 条件表达式、循环,是可以提炼的地方,Decompose Conditional,可以处理条件表达式,主要处理方式是抽象过程策略,提取函数进行逻辑表达
过大的类(Large Class)
主要来自于类做了太多事情,这样内部就会有过多实例变量。提取原则:
-
命名可以用一个前缀的,可以认为是一个处理组件,可以提取
-
抽象 Extract Interface,用于梳理 Class 和 Subclass 的结构,清晰类的结构后进行重构
-
可以通过 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));