论设计模式如何解决项目迭代造成的代码堆积问题

338 阅读6分钟

1. if...else真的好用吗? - 策略模式重构

1.1 场景介绍

在项目起步阶段,功能还比较少且不完善,开发以及产品童鞋都处于摸索阶段。由于项目涉及的业务具有专业性,为了讲述方便以及读者理解方便,我将用代称进行通俗易懂地描述。

整个项目是围绕着资源进行拓展的,资源是:父、子 。 项目中遇到判断资源的时候,需要判断,这还不简单? 直接 if...else 啊!于是,十几个页面里,都写了这种用起来非常轻松愉悦的代码。

忽然有一天,产品童鞋调研摸索发现“不行啊,父、子 不够用,我们还得加个爷爷”。啥?加个爷爷?那就if...else if ... else 嘛。简单,立马给你加!十几个页面一起加,也不是...特别难吧?

忽然又有一天,产品童鞋提出“不是所有页面都是爷、父、子,有些页面是只有爷、父”。啥? 那也好像不是特别难... 就把某些页面的if... else if ... else 改成 if... else if... 就好了吧。

if (type === 'grandfathers') {
 // do something
} else if (type === 'fathers') {
 // do something
} else {
 // do something
}

1.2 代码改写

项目慢慢变得越来越庞大,开始弄不清哪些页面是爷、父,哪些页面是爷、父、子。于是我决定用策略模式开始重写部分代码。策略模式,核心就是封装一个策略对象。我们可以简化成下列的写法:

const strategy = {
    grandfathers: function() {
     	// do something
    },
    fathers: function() {
      // do something
    },
    sons: function() {
      // do something
    }
}


// type表示当前的资源是grandfathers/fathers/sons:
strategy[type]()

用策略模式重构完上面的代码后,项目变得更加清晰,再也不用担心产品童鞋改需求了。并且由此我深刻地体会到了一件事:只要不是确定是对立面的东西,尽量避免用if...else,因为你永远没办法确定你将来会加多少个else if(捂脸)。

2. 发布-订阅模式

2.1 场景介绍

不知不觉,项目已经到达20几个页面了,三个页面用来分别管理“爷、父、孙”列表,其他页面依赖这些列表可以做选择,页面图大致如下:

举两个栗子:

场景A. 当内容部分展示的是管理列表页面时,会有正常的【增删改查】。
场景B. 进入页面1时会展示所有的,用来展示。

场景A我们不说,增删改查太简单了。当增加父的入口只有管理列表时,实现场景B也完全无压力,就正常地在进入页面时获取一个列表就可以了。

忽然某一天,产品童鞋说“我们要在侧边栏加一个新增爷父孙的入口”。哦?新增,很简单嘛,新增的弹框调出来就行了吧。

写着写着开始觉得一些页面的交互不对,新增之后,如果页面不刷新,是不会在场景A里刷新列表的,场景B也不会出现新增的

开始我是这样写的: 在vuex里用文件名为resource.js维护一份这个新增的状态,我用grandfathersRefreshTs、fathersRefreshTs、sonsRefreshTs来标记新增的时间戳。如果是从侧边栏调的新增弹框,则更新这个时间戳。在页面上读取时间戳,watch它的变化来做一些刷新操作。

对于场景A来说,当内容页面时管理页面,且在侧边栏点击了【新增】,监听fathersRefreshTs的更新,判断到新增后重新获取管理列表就行。 对于场景B也是如此。

export default {
 // 省略一些代码...
 computed: {
    ...mapState("resource", ["fathersRefreshTs"])
 },
 watch: {
    fathersRefreshTs: {
       immediate: true,
       handler(val) {
         // 在这里做刷新后的列表重新获取资源操作
         // 其他操作
        }
    }
 }
}

场景A、B的刷新问题得到了解决,同时在其他二十几个页面也逐一排查了相关问题,并一一修复。

在产品童鞋的辛勤探索之下,业务又有了进一步的发展。项目需要做这些事情:

业务1. 增加一个的时候,后端童鞋得通过自动生成一个
业务2. 某些页面需要保持用户离开时的样子,也就是前端对某些页面使用keep-alive(上面提到的场景A不需要keep-alive,场景B需要keep-alive)

实现业务1时,对于上面提到的场景A,因为场景A不需要keep-alive,所以新增入口只有一个侧边栏和本页面。当前内容展示管理页面时,监听的fatherRefreshTs依赖三个场景:① 侧边栏点击增加的时候,更新列表。 ② 侧边栏点击增加的时候,更新列表。③当前页面内容里进行对的增删改的时候,更新列表。

实现业务2时,对于上面提到的使用了keep-alive的场景B,改变fatherRefreshTs的地方就变得更多:① 侧边栏的新增 ② 侧边栏的新增管理页面增删改列表 ④ 管理页面增删改列表

在这里公用一份fatherRefreshTs,将所有改动列表的地方都通过vuex去改变fatherRefreshTs的值也可以,毕竟一些页面不在当前在没有keep-alive的时候,资源只和一进入页面时的获取的列表还有侧边栏的有关,而当跳转到其他页面时这个页面就destroyed了。就算这个值变化了,也不会触发。

在这种情况下,页面开始变得很难维护且耦合。作为code reviewer,增删改一个爷/父/子我都得去看看这个页面是否有keep-alive,grandfathersRefreshTs、fathersRefreshTs、sonsRefreshTs,而查看它们的值改变的地方在侧边栏、爷管理页面、父管理页面、子管理页面、 就得查看大量的代码。

2.1 代码重构

我参考了发布-订阅模式重写了部分的逻辑,在全局挂载了一个eventBuseventBus直接封装vue实现的发布订阅,代码如下:

// bus.js
function install(Vue) {
  const Bus = new Vue({
    methods: {
      on(event, callback) {
        this.$on(event, callback);
      },
      emit(event, ...params) {
        this.$emit(event, ...params);
      },
      off(event, callback) {
        this.$off(event, callback);
      }
    }
  });
  Vue.prototype.$bus = Bus;
}

export default install;

挂在到全局

// main.js
import Vue from "vue";
import App from "./App.vue";
import eventBus from "./utils/observer";


eventBus(Vue);

new Vue({
  render: (h) => h(App)
}).$mount("#app");

在侧边栏点击创建时,发出一个事件:

 this.$bus.emit("aside-create-father");
// this.$bus.emit("aside-create-grandfather"); // 如果是新增爷就用这一行

在场景A的管理列表可以监听事件

// FatherManagement.vue
<script>
export default {
  name: "FatherManagement",
  created() {
    this.$bus.on("aside-create-father", this.handler);
    this.$bus.on("aside-create-grandfather", this.handler);
  },
  methods: {
    handler(...params) {
      console.log(params);
    },
  },
};
</script>

场景B也同理。在对应逻辑里将对应事件抛出,然后在用到的页面监听。

在重构完一小部分页面后,体会到的是,不用维护多份状态使bug出现的几率大大降低。同时,因为大大降低了代码耦合度,减轻了code reviewer的负担,对于新进项目的童鞋来说,也更容易上手项目,不再需要看大量的业务代码才能弄明白哪些地方触发了资源的变动。