养成良好的编程习惯

2020-04-09  本文已影响0人  美食家儿菜

认识

代码首先重要的是应该是可读,其次是可执行。
糟糕的代码可能需要花费很大的成本来维护,所以一开始写的时候就要认真。

变量命名

变量命名非常重要,是代码可读的基础,一个变量名称,不仅是一个名称,是要表达出它在这个环境中的一些作用的,承担的角色的,是需要传递一些信息出来的。

1.命名对象(名词)

bananaList 不仅要表达是什么东西,可能还要表达出集合的概念, List ? Map 的形式, 可以通过加后缀的形式来凸显。

2.命名函数(动词)

fetchUserInfoAsync 这个函数是要 (fetch)干什么(返回值)

3.命名的上下文

每个上下文关注的点不同,表达同一个意思,但在不同的环境中,有可能会有不同的表达。

4.命名一致

不管什么时候,这个东西都是这样命名的 ~

错误处理

函数

一个函数应该只做一件事
函数中合理地处理异常
async function getUserDetail(id) {
  const user = await fetchSingleUser(id);
  user.favoriteBooks = (await fetchUserFavorits(id)).books;
  // 上面这一行报错了:Can not read property 'books' of undefined.
  // ...
}

试图通过下面的方式去修改

const favorites = await fetchUserFavorits(id);
user.favoriteBooks = favorites && favorites.books;
// 这下不会报错了

这种处理方法并不合理,原因如下:

  1. 函数 fetchUserFavorits 原本有它合理的返回,但是如果返回了不合理的东西 如,undefined 就已经失控了,程序就不应该再往下执行了
    2.通过 这种方式修改 之后,favoriteBooks如果被赋值为 undefined ,如果后续有对 favoriteBooks 数组做一些操作,如 执行数组的某些方法,就会又出现问题,这是非常糟糕了。

如果 fetchUserFavorits 属于当前的项目,那这里 getUserDetail 并没有什么责任,应该 在 fetchUserFavorits 函数中去处理,因为 fetchUserFavorits 就不应该返回 undefined,我们应该去修复 fetchUserFavorits,任务失败时显式地告知出来,或者直接抛出异常。同时,getUserDetail 稍作修改:

// 情况1:显式告知,此时应认为获取不到收藏数据不算致命的错误
const result = await fetchUserFavorits(id);
if(result.success) {
  user.favoriteBooks = result.data.books;
} else {
  user.favoriteBooks = []
}

// 情况2:直接抛出异常
user.favoriteBooks = (await fetchUserFavorits(id)).books;
// 这时 `getUserDetail` 不需要改动,任由异常沿着调用栈向上冒泡

如果 fetchUserFavorits 不在当前项目中,而是依赖的外部模块呢?我认为,这时你就该为选择了这样一个不可靠的模块负责,在 getUserDetail 中增加一些「擦屁股」代码,来避免你的项目的其他部分受到侵害。

const favorites = await fetchUserFavorits(id);
if(favorites) {
  user.favoriteBooks = favorites.books;
} else {
  throw new Error('获取用户收藏失败');
}
控制函数的副作用

无副作用的函数 指不依赖上下文,也不改变上下文的函数

async function getUserDetail(id) {
  const user = await fetchSingleUserInfo(id);
  await addFavoritesToUser(user);
  ...
}
async function addFavoritesToUser(user) {
  const result = await fetchUserFavorits(user.id);
  user.favoriteBooks = result.books;
  user.favoriteSongs = result.songs;
  user.isMusicFan = result.songs.length > 100;
}

addFavoritesToUser 就是个有副作用的函数,因为它改变了 user ,无副作用的形式

async function getUserDetail(id) {
  const user = await fetchSingleUserInfo(id);
  const {books, songs, isMusicFan} = await getUserFavorites(id);
  return Object.assign(user, {books, songs, isMusicFan})
}
async function getUserFavorites(id) {
  const {books, songs} = await fetchUserFavorits(user.id);
  return {
    books, songs, isMusicFan: result.songs.length > 100
  }
}
非入侵性地改造函数

函数是一段独立和内聚的逻辑。在产品迭代的过程中,我们有时候不得不去修改函数的逻辑,为其添加一些新特性。之前我们也说过,一个函数只应做一件事,如果我们需要添加的新特性,与原先函数中的逻辑没有什么联系,那么决定是否通过改造这个函数来添加新功能,应当格外谨慎。

const fetchUserInfo = (userId, callback) => {
  const param = {
    url: '/api/user',
    method: 'post',
    payload: {id: userId}
  };
  request(param, callback);
}

现在有了一个新需求:为 fetchUserInfo 函数增加一道本地缓存,如果第二次请求同一个 userId 的用户信息,就不再重新向服务器发起请求,而直接以第一次请求得到的数据返回。

const userInfoMap = {};
const fetchUserInfo = (userId, callback) => {
  if (userInfoMap[userId]) {            // 新增代码
    callback(userInfoMap[userId]);    // 新增代码
  } else {                              // 新增代码
    const param = {
      // ... 参数
    };
    request(param, (result) => {
      userInfoMap[userId] = result;   // 新增代码
      callback(result);
    });
  }
}

实际上,「缓存」和「获取用户数据」完全是独立的两件事。我提出的方案是,编写一个通用的缓存包装函数(类似装饰器)memorizeThunk,对 fetchUserInfo 进行包装,产出一个新的具有缓存功能的 fetchUserInfoCache,在不破坏原有函数可读性的基础上,提供缓存功能.

const memorizeThunk = (func, reducer) => {
  const cache = {};
  return (...args, callback) => {
    const key = reducer(...args);
    if (cache[key]) {
      callback(...cache[key]);
    } else {
      func(...args, (...result) => {
        cache[key] = result;
        callback(...result);
      });
    }
  }
}
const fetchUserInfo = (userInfo, callback) => {
  // 原来的逻辑
}
const fetchUserInfoCache = memorize(fetchUserInfo, (userId) => userId);

避免滥用成员函数

JavaScript 中的类,是 ES6 才有的概念,此前是通过函数和原型链来模拟的。在编写类的时候,我们常常忍不住地写很多没必要的成员函数:当类的某个成员函数的内部逻辑有点复杂了,行数有点多了之后,我们往往会将其中一部分「独立」逻辑拆分出来,实现为类的另一个成员函数。比如,假设我们编写某个 React 组件来显示用户列表,用户列表的形式是每两个用户为一行

class UserList extends React.Component{
  // ...
  chunk = (users) => {
    // 将 ['张三', '李四', '王二', '麻子'] 转化为 [['张三', '李四'], ['王二', '麻子']]
  }
  render(){
    const chunks = this.chunk(this.props.users);
    // 每两个用户为一行
    return (
      <div>
        {chunks.map(users=>
          <row>
            {users.map(user => 
              <col><UserItem user={user}></col>
            )}
          </row>
        )}
      </div>
    )
  }
}

如上述代码所示,UserList 组件按照「两个一行」的方式来显示用户列表,所以需要先将用户列表进行组合。进行组合的工作这件事情看上去是比较独立的,所以我们往往会将 chunk 实现成 UserList 的一个成员函数,在 render 中调用它。

我认为这样做并不可取,因为 chunk 只会被 render 所调用,仅仅服务于 render。阅读这个类源码的时候,读者其实只需要在 render 中去了解 chunk 函数就够了。然而 chunk 以成员函数的形式出现,扩大了它的可用范围,提前把自己曝光给了读者,反而会造成干扰。读者阅读源码,首先就是将代码折叠起来,然后他看到的是这样的景象:

class UserList extends React.Component {
  componentDidMount() {...}
  componentWillUnmount() {...}
  chunk() {...}    // 读者的内心独白:这是什么鬼?
  render() {...}
}

熟悉 React 的同学对组件中出现一个不熟悉的方法多半会感到困惑。不管怎么说,读者肯定会首先去浏览一遍这些成员函数,但是阅读 chunk 函数带给读者的信息基本是零,反而还会干扰读者的思路,因为读者现在还不知道用户列表需要以「每两个一行」的方式呈现。所以我认为,chunk 函数绝对应该定义在 render 中,如下所示:

render(){
  const chunk = (users) => ...
  const chunks = this.chunk(this.props.users);
  return (
    <div>
  ...
}

这样虽然函数的行数可能会比较多,但将代码折叠起来后,函数的逻辑则会非常清楚。而且,chunk 函数曝光在读者眼中的时机是非常正确的,那就是,在它即将被调用的地方。实际上,在「计算函数的代码行数」这个问题上,我会把内部定义的函数视为一行,因为函数对读者可以是黑盒,它的负担只有一行。

总结

伟大的文学作品都是建立在废纸堆上的,不断删改作品的过程有助于写作者培养良好的「语感」。当然,代码毕竟不是艺术品,程序员没有精力也不一定有必要像作家一样反复打磨自己的代码/作品。但是,如果我们能够在编写代码时稍稍多考虑一下实现的合理性,或者在添加新功能的时候稍稍回顾一下之前的实现,我们就能够培养出一些「代码语感」。这种「代码语感」会非常有助于我们写出高质量的可读的代码。

参考文章:
https://fed.taobao.org/blog/taofed/do71ct/writing-readable-code/?spm=taofed.blogs.blog-list.6.40315ac8VXtPHE

上一篇下一篇

猜你喜欢

热点阅读