前言
本文是学习《重构:改善既有代码的设计》
后的一些心得,希望能用趣味的方式结合一些实例带领大家一起学习,提升自身代码质量。
想必最近的互联网裁员消息大家也有所耳闻,那么我们怎么才能够在这样的大环境下苟住自身呢?经过我的总结,我认为大家都不具备不可替换性
。
什么叫不可替换性
呢,通俗点来说就是,这活除了你别人都干不了。达到这种境界无异于两种情况,一种是自身过于优秀,优秀到没人能取代(该情况过少,希望大家能正视己身)。
另一种方法则是,制作出专属于你的代码!!下面我们来一起学习,怎样写出专属于你,不可被替代的代码!
以下不可替换写法皆为反面教材!!!
一、神秘命名(Mysterious Name)
命名让人猜不透,摸不准!
不可替换写法
const getPNum = (number) => {
......
}
无论是函数命名
还是入参命名
,相信都很难有人能参透你的深意,在别人接手你的代码时,必定会来向你请教,这在老板眼里你的价值将更为突出。
正常写法
const getPhoneCode = (phoneNumber) => {
......
}
从函数的驼峰命名我们可以很轻易猜出是获取手机验证码
,入参也能猜出是手机号码
的意思,这样的代码太通俗易懂了,显然达不到我们的效果。
二、重复代码(Duplicated Code)&& 过长函数(Long Function)
重复编写大量相同代码,内容过多的函数,使代码变得臃肿难以维护
不可替换写法
const showUserInfo = () => {
let totalAmount = 0;
const userInfo = request.get('/userInfo', 'admin')
const userAmountList = request.get('/userAmountList', 'admin')
console.log('name', userInfo.name);
console.log('age', userInfo.age);
console.log('sex', userInfo.sex);
console.log('address', userInfo.address);
for(let i of userAmountList) {
totalAmount += i.amount
}
console.log('总金额', totalAmount);
}
大量重复的代码让人瞬间产生疲劳感,完全不搭边的代码顺序混淆人的双眼,如果再加上一些神秘命名
,必将让代码更上一个台阶。
正常写法
const showUserInfo = () => {
const printUserDetail = (userInfo) => {
const { name, age, sex, address } = userInfo;
console.log('name', name);
console.log('age', age);
console.log('sex', sex);
console.log('address', address);
const printTotalAmount = (userAmountList) => {
const totalAmount = userList.reduce((pre, cur) => pre + cur.amount, 0)
console.log('总金额', totalAmount);
}
// 获取用户信息
const userInfo = request.get('/userInfo', 'admin')
printUserDetail(userInfo)
// 获取用户金额列表
const userAmountList = request.get('/userAmountList', 'admin')
printTotalAmount(userAmountList)
}
重复代码
都被提炼到单独的函数模块
中,用reduce
免去了重复的代码相加,并且代码顺序
也被移动至有关联的地方,这样的代码换做刚学前端的小白恐怕也能看懂,这样明显不能凸显自身的独特。
三、过长参数列表(Long Parameter List)
函数或组件的参数过多,影响代码可读性,某些环境甚至会对性能造成影响
不可替换写法
const getList = (id, name, age, address, sex) => {
...
}
将所有参数全部写进入参,当入参达到十几甚至上百时,你的函数定会震慑住你所有的同事。
正常写法
const getList = (data) => {
const { id, name, age, address, sex } = data;
...
}
将入参放置到一个对象中,再到函数里通过解构
的方式进行调用。这样的方式太过简洁,过少的入参凸显不出你这个函数的重要性。
四、全局数据(Global Data)
将数据全部挂载到全局,导致内存不及时被释放以及全局污染
不可替代写法
const id = 1;
const data1 = request.get('/userInfo', id)
const data2 = request.get('/userState', id)
const getUserInfo = () => {
...
}
const getUserState = () => {
...
}
所有变量放入全局,把后续开发者的路变窄,不敢随意去更改变量,此刻再加上神秘命名
,相信没有人能够取代你的位置。
正常写法
const id = 1;
const getUserInfo = () => {
const data = request.get('/userInfo', id)
...
}
const getUserState = () => {
const data = request.get('/userState', id)
...
}
id
作为多处用到变量,写到全局,剩下的局部变量都写在各自函数中,即不会引起全局污染
,也不会担心命名重复
的问题。在各自的作用域中作用也清晰明了。
五、发散式变化(Divergent Change)
将需要做的事分散到各个地方,每次修改需要修改对应函数,修改不当会导致另一个依赖此函数的功能崩塌
不可替代写法
const getPrice = (list) => {
const printName = (item) => {
if(item.type === 'totalList') {
console.log('totalName', item.name);
}else if(item.type === 'frozenList'){
console.log('frozenName', item.name);
}
}
const calcPrice = (item) => {
if(item.type === 'totalList') {
// todo: 计算totalPrice
const price = ...;
return price;
}else if(item.type === 'frozenList'){
// todo: 计算frozenPrice
const price = ...;
return price;
}
}
printName(list.totalList);
printName(list.frozenList);
return calcPrice(list.totalList) - calcPrice(list.frozenList)
}
将方法写成公用方法
,在每次修改或者新增时候都需要去修改对应的方法。无法知道每个价格对应着哪些操作,当增加一个新的价格类型时,需要同时去多个函数
中添加对应的判断逻辑。一不注意就会忘加漏加
形成bug。测试绩效max!
正常写法
const getPrice = (list) => {
const totalPrice = (item) => {
// todo: 计算totalPrice
const price = ...
console.log('totalName', item.name);
console.log('price', price);
}
const frozenPrice = (item) => {
// todo: 计算frozenPrice
const price = ...
console.log('frozenName', item.name);
console.log('price', price);
}
return totalPrice(list.totalList) - frozenPrice(list.frozenList)
}
每个价格对应需要的操作都被提炼到单独的函数
,专注于负责自己的事,如果价格计算方式需要改变,可以更加直观的修改。若需要添加新的价格品种,也将会更好添加。
六、霰弹式修改(Shotgun Surgery)
多处共用一个属性,不设置全局变量管理,每次修改需要修改大量代码
不可替代写法
getList(globalModel.id)
getUserInfo(globalModel.id)
getUserAmount(globalModel.id)
当需求改变,需要在多处进行修改,这样的工作量倍增,会让你的工作力max!
正常写法
const id = globalModel.id;
getList(id)
getUserInfo(id)
getUserAmount(id)
同一个属性被多处使用,使用一个变量进行存储,当需求发生改变(例如globalModel.id
变为globalModel.userId
),只需要修改一处
便能完成,大大节省时间。
七、依恋情结(Feature Envy)
大量引入其他函数或模块方法,导致代码耦合度极高,动一处则牵扯全身
不可替代写法
class Price {
constructor() {}
add(...num) {
return num.reduce((pre, cur) => pre + cur, 0);
}
dataFilter(value) {
return parseInt(value.substring(0, value.length - 2)) * 100;
}
}
class Amount {
constructor() {}
getAmount(amountList) {
const _amountList = amountList.map(item => {
return new Price().dataFilter(item);
});
return new Price().add(..._amountList);
}
}
所有的计算函数全部使用其他类里的方法,形成大量依赖。你要出事我跟着一起死,我就是要用你的,我就是玩~
正常写法
class Amount {
constructor() {}
add(...num) {
return num.reduce((pre, cur) => pre + cur, 0);
}
dataFilter(value) {
return parseInt(value.substring(0, value.length - 2)) * 100;
}
getAmount(amountList) {
const _amountList = amountList.map(item => {
return this.dataFilter(item);
});
return this.add(..._amountList);
}
}
类里所有使用的方法都在本身完成,所有的问题都在自身解决,形成闭环
。
八、数据泥团(Data Clumps)
众多数据糅合在一起,当其中某一项数据失去意义时,其他项数据也失去意义。
不可替代写法
const lastName = "卢"
const firstName = "本伟"
const name = `${lastName}${firstName}`
发现当其中某个变量失去意义的时候,另一个变量也失去意义,一损俱损。
正常写法
const person = {
lastName: "卢",
firstName: "本伟"
}
const name = `${person.lastName}${person.firstName}`
有强联系的数据,应为它们产生一个新对象。
九、基本类型偏执(Primitive Obsession)
认为基本类型一定更加简单,偏执的使用大量的基本类型而不去定义应有的结构
不可替代写法
class Price {
constructor(name, money) {
this.name = name;
this.money = money;
}
get name() {
return name;
}
get count() {
return parseFloat(this.money.slice(1));
}
get current() {
return this.money.slice(0, 1);
}
get unit() {
switch (this.money.slice(0, 1)) {
case '¥':
return 'CNY';
case '$':
return 'USD';
case 'k':
return 'HKD';
}
}
calcPrice() {
// todo: 金额换算
}
}
const myPrice = new Price("罐头", "$30")
偏执地使用字符串基本类型定义money,表面是Price的类,但在里面充斥着大量的money的数据处理。
正常写法
class Money {
constructor(value) {
this.value = value;
}
get count() {
return parseFloat(this.value.slice(1));
}
get current() {
return this.value.slice(0, 1);
}
get unit() {
switch (this.value.slice(0, 1)) {
case '¥':
return 'CNY';
case '$':
return 'USD';
case 'k':
return 'HKD';
}
}
}
class Price {
constructor(name, money) {
this.name = name;
this.money = new Money(money);
}
get name() {
return name;
}
calcPrice() {
// todo: 金额换算
}
}
const myPrice = new Price("罐头", "$20")
money中存在着大量的数据处理,应为期单独建立个对象来作为它的类型。
十、重复的switch(Repeated Switches)
大量使用重复逻辑的switch,这样的代码是臃肿且脆弱的
不可替代写法
class Money {
constructor(value) {
this.value = value;
}
get count() {
return parseFloat(this.value.slice(1));
}
get current() {
return this.value.slice(0, 1);
}
get unit() {
switch (this.current) {
case '¥':
return 'CNY';
case '$':
return 'USD';
case 'k':
return 'HKD';
}
}
get suffix() {
switch (this.current) {
case '¥':
return '元';
case '$':
return '美元';
case 'k':
return '港币';
}
}
get currentCount() {
switch (this.current) {
case '¥':
return this.count;
case '$':
return this.count * 7;
case 'k':
return this.count * 0.8;
}
}
}
大量相同逻辑的switch,若想增加一个判断项,需找到所有switch项进行修改,一不注意则会遗漏,引发bug
正常写法
class Money {
constructor(value) {
this.value = value;
}
get count() {
return parseFloat(this.value.slice(1));
}
get current() {
return this.value.slice(0, 1);
}
}
class cnyMoney extends Money {
constructor(props) {
super(props);
}
get unit() {
return 'CNY';
}
get suffix() {
return '元';
}
get currentCount() {
return this.count;
}
}
class usdMoney extends Money {
constructor(props) {
super(props);
}
get unit() {
return 'USD';
}
get suffix() {
return '美元';
}
get currentCount() {
return this.count * 7;
}
}
class hkdMoney extends Money {
constructor(props) {
super(props);
}
get unit() {
return 'HKD';
}
get suffix() {
return '港币';
}
get currentCount() {
return this.count * 0.8;
}
}
每一个分支项专注于自身的变化,修改时不会担心某处遗漏。
十一、循环语句(Loops)
舍弃ES自带api,全面拥抱for、while循环语句
不可替代写法
const arr = [1, 2, 3, 4, 5, 6];
// 找到数组指定项
for (let i of arr) {
if (i === 3) {
console.log(i);
}
}
// 数组求和
let sum = 0;
for (let i of arr) {
sum += i;
}
console.log(sum);
什么ES6,算法我都不用,就for循环一把梭!
正常写法
const arr = [1, 2, 3, 4, 5, 6];
// 找到数组指定项
console.log(arr.find((item) => item === 3));
// 数组求和
let sum = arr.reduce((pre, cur) => pre + cur);
console.log(sum);
巧用各类api,提升代码整洁度。
十二、冗赘的元素(Lazy Element)
单一的功能也会提炼成相应函数,即使它的实现代码和函数名基本一样。
不可替代写法
const getUserInfo = (user) => {
const info = {};
const getName = (data) => {
const firstName = data.firstName;
const lastName = data.lastName;
return firstName + lastName;
}
info.name = getName(user)
// todo: 获取用户其他信息
...
return info;
}
获取姓名,获取地址,哪怕只是简单的字符串相加我也要提炼成函数!面向函数编程!
正常写法
const getUserInfo = (user) => {
const info = {};
info.name = user.firstName + user.lastName
// todo: 获取用户其他信息
...
return info;
}
区分方法和命令的区别,能用命令解决的事不要多此一举。
十三、夸夸其谈通用性(Speculative Generality)
当我们设计代码时总会提前设计好一些结构,想着未来有一天会有新功能添加,但事实上,这一天永远不会到来。
不可替代写法
const getCampaign = (campaign, type) => {
switch (type) {
case 1:
// todo: type为1的处理
return ...
default:
// todo: 正常处理
return Campaign;
}
}
认为以后可能会对该包进行一些特殊处理,设置一个type进行判断,增加"通用性"。
正常写法
const getCampaign = (campaign) => {
// todo: 处理方案包
return campaign;
}
只处理当前需求所需要做的事,后面的事后面再说。
十四、临时字段(Temporary Field)
一个类中为了某种特定情况设定一个特殊字段,但除了那种情况外这个字段永远不会被使用。
不可替代写法
class Person{
constructor(data) {
this.name = data.name;
this.age = data.age;
this.address = data.address;
// 下列两个变量只有在进行续费处理时才会使用
this.balance = data.balance;
this.isRenew = data.isRenew;
}
...
}
所有特殊情况需要用到的字段全往类里塞,当被使用时对这些字段产生疑惑,并给予强行赋值。
正常写法
class Person{
constructor(data) {
this.name = data.name;
this.age = data.age;
this.address = data.address;
}
...
}
class RenewData {
constructor() {
this.balance = data.balance;
this.isRenew = data.isRenew;
}
}
将特殊情况字段提取出来,使用时再进行调用。
十五、过长的消息链(Message Chains)
A函数访问B函数,B函数访问C函数或变量,这样的代码会形成长长的消息链。
不可替代写法
const data = getData(getName(getiId(cookie)));
多层函数进行嵌套,耦合度更高,且无法理解函数这样组合的目的。
正常写法
const getPackage = (cookie) => {
const id = getId(cookie);
const name = getName(id);
return getData(name);
}
const data = getPackage(cookie);
将方法提炼成函数,并将每一步解脱出来。
十六、中间人(Middle Man)
将所有操作委托给另一个类,但事实上这一层是没有必要的。
不可替代写法
class Person {
constructor(data) {
this.balance = data.balance;
this.info = data.info;
this.id = data.id;
}
get name() {
return this.info.name;
}
get address() {
return this.info.address;
}
get age() {
return this.info.age;
}
}
info里的每一项获取都通过Person类代理,过多代理会让这个类变得十分臃肿。
正常写法
class Person {
constructor(data) {
this.balance = data.balance;
this.info = data.info;
this.id = data.id;
}
get info() {
return this.info;
}
}
免去Person来代理info里的变量获取,更加清晰明了。
十七、内幕交易(Insider Trading)
在类的内部有着与其他类的数据交换,调用者在调用时需要去了解其他接口的细节,增加类的耦合
不可替代写法
class Person {
constructor(data) {
this.name = name;
}
get name() {
return name;
}
set package(arg) {
this.package = arg;
}
get package() {
return this.package;
}
}
class Package {
constructor(name) {
this.name = name;
}
set id(arg) {
this.id = arg;
}
get id() {
return this.id;
}
}
要访问person的packageId时需要与Package类进行数据交换,了解Package的内部细节。
正常写法
class Person {
constructor(data, package) {
this.name = name;
this.package = package;
}
get name() {
return name;
}
get packageId() {
return this.package.id;
}
}
class Package {
constructor(name) {
this.name = name;
}
set id(arg) {
this.id = arg;
}
get id() {
return this.id;
}
}
将内部的数据交换直接引入到Person类,搬到明面上进行处理
十八、过大的类(Insider Trading)
所有与该类相关的字段全部放入类中,显得类十分的庞大臃肿。
不可替代写法
class Product {
constructor(data) {
this.name = data.name;
this.price = data.price;
this.id = data.id;
this.user = data.user;
this.uv = data.uv;
this.pv = data.pv;
this.click = data.click;
}
get realPrice() {
return this.price * this.discount;
}
get ctr() {
return this.uv / this.click;
}
}
商品的所有相关字段全部放入,复杂的场景中甚至能达到几十上百个字段。
正常写法
class Product {
constructor(data) {
this.name = data.name;
this.id = data.id;
this.user = data.user;
}
}
class ProductPrice {
constructor(data) {
this.price = data.price;
this.discount = data.discount;
}
get realPrice() {
return this.price * this.discount;
}
}
class ProductData {
constructor(data) {
this.uv = data.uv;
this.pv = data.pv;
this.click = data.click;
}
get ctr() {
return this.uv / this.click;
}
}
将内部强相关的字段提炼成一个新的类,会让结构更加清晰。
十九、异曲同工的类(Alternative Classes with Different Interfaces)
类的好处之一就是可以替换,但要避免出现做相似事情的类。
不可替代写法
class AmericanCompany {
constructor(name, createTime, address, phone, isLegitimate) {
this.name = name;
this.createTime = createTime;
this.address = address;
this.phone = phone;
this.isLegitimate = legitimate;
}
get name() {
return this.name;
}
get address() {
return this.address;
}
get phone() {
switch (this.address) {
case '群島':
return `+1-340${phone}`;
default:
return `+1${phone}`;
}
}
}
class ChineseCompany {
constructor(name, createTime, address, phone, isOverTime) {
this.name = name;
this.createTime = createTime;
this.address = address;
this.phone = phone;
this.isOverTime = isOverTime;
}
get name() {
return this.name;
}
get address() {
return this.address;
}
get phone() {
switch (this.address) {
case '香港':
return `+852${phone}`;
case '台湾':
return `+886${phone}`;
default:
return `+86${phone}`;
}
}
}
美国公司和中国公司类的字段绝大部分和方法都是一样的,若字段更多...若还有其他国家...每一个都创建一个类,真是不可想象。
正常写法
class Company {
constructor(name, createTime, address, phone) {
this.name = name;
this.createTime = createTime;
this.address = address;
this.phone = phone;
}
get name() {
return this.name;
}
get address() {
return this.address;
}
}
class AmericanCompany extends Company {
constructor(name, createTime, address, phone, isLegitimate) {
super(name, createTime, address, phone);
this.isLegitimate = legitimate;
}
get phone() {
switch (this.address) {
case '群島':
return `+1-340${this.phone}`;
default:
return `+1${this.phone}`;
}
}
}
class ChineseCompany extends Company {
constructor(name, createTime, address, phone, isOverTime) {
super(name, createTime, address, phone);
this.isOverTime = isOverTime;
}
get phone() {
switch (this.address) {
case '香港':
return `+852${this.phone}`;
case '台湾':
return `+886${this.phone}`;
default:
return `+86${this.phone}`;
}
}
}
将重复部分提炼成一个类,再分别继承。
二十、纯数据类(Data Class)
一个类中只有单纯的存储数据的作用,只能进行简单读取功能。
不可替代写法
class Person {
constructor(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
}
get name() {
return this.name;
}
get age() {
return this.age;
}
get address() {
return this.address;
}
}
哪怕只是简单的存储,我也要制作一个类!面向类编程!
正常写法
const person = {
......
}
或者
class Person {
constructor(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
}
get name() {
return this.name;
}
get age() {
return this.age;
}
get address() {
return this.address;
}
// todo: 一些与该类有关的处理方法
}
单纯的数据类使用对象
去承载便可,如果有必要使用类,请将相关函数
方法搬至类的内部。
二十一、被拒绝的馈赠(Refused Bequest)
父类中某个字段只被一个或极少数的继承类继承,其他的类并不需要这个字段。
不可替代写法
class Company {
constructor(name, createTime, address, phone, isOverTime) {
this.name = name;
this.createTime = createTime;
this.address = address;
this.phone = phone;
this.isOverTime = isOverTime;
}
// todo: 相关方法
...
}
class AmericanCompany extends Company {
constructor(name, createTime, address, phone, isLegitimate) {
super(name, createTime, address, phone);
this.isLegitimate = legitimate;
}
...
}
class ChineseCompany extends Company {
constructor(name, createTime, address, phone, isOverTime) {
super(name, createTime, address, phone, isOverTime);
}
// todo: 相关方法
...
}
class UKCompany extends Company {
constructor(name, createTime, address, phone) {
super(name, createTime, address, phone);
}
// todo: 相关方法
...
}
即使父类Company
中的isOverTime
只被ChineseCompany
使用,我也要给它加上!
正常写法
class Company {
constructor(name, createTime, address, phone) {
this.name = name;
this.createTime = createTime;
this.address = address;
this.phone = phone;
}
// todo: 相关方法
...
}
class AmericanCompany extends Company {
constructor(name, createTime, address, phone, isLegitimate) {
super(name, createTime, address, phone);
this.isLegitimate = legitimate;
}
...
}
class ChineseCompany extends Company {
constructor(name, createTime, address, phone, isOverTime) {
super(name, createTime, address, phone);
this.isOverTime = isOverTime;
}
// todo: 相关方法
...
}
class UKCompany extends Company {
constructor(name, createTime, address, phone) {
super(name, createTime, address, phone);
}
// todo: 相关方法
...
}
将专属字段isOverTime
下沉至ChineseCompany
中,自己的事自己做!
二十二、注释(Comments)
注释是一个双刃剑,好的注释能更好地帮助理解代码,但过于依赖注释会让人放弃对代码质量的要求。
不可替代写法
// 获取手机号码
const getPNum = () => {
// todo: 相关操作
}
什么代码质量我全都不管,反正我有注释,别人看得懂。
正常写法
const getPhoneNumber = () => {
// todo: 相关操作
}
注释不是我们的挡箭牌,而是我们最后一道防线,当你觉得需要用注释的时候,请再思考代码是否还有可以优化的地方。
总结
你见过的最不可替代
的代码是什么样的呢?