背景
在平常的开发过程中,我们经常会遇到一种情况:有一个组件(或页面)被用于多个场景,不同的场景下面,组件的部分内容发生变化,其他则不变。
某一天,前端开发小雷正在码代码,淳朴的产品经理(以下称为淳朴哥)跑过来说:“我们要做一个商品组件,根据后台返回的数据显示出来。这个简单吧!”
小雷一想,这个简单啊,框框10分钟把代码敲了出来。
开发一个商品组件
<!--sku-panel.wxml-->
<view style="border: 1rpx solid red; padding: 10rpx;">
<image src="{{goods.url}}"></image>
<view>名称:"{{goods.name}}"</view>
<view>规格:"{{goods.sku}}"</view>
<view>价格:"{{goods.price}}"元</view>
</view>
// sku-panel.js
Component({
/**
* 组件的属性列表
*/
properties: {
goods: Object
},
/**
* 组件的初始数据
*/
data: {
},
/**
* 组件的方法列表
*/
methods: {
}
})
过了两天,淳朴哥又跑过来说:“小雷啊,我们现在要搞国庆活动,在活动页面的商品显示不太一样,增加在括号里面显示活动价。很简单的!”。
小雷一想,这个确实也简单嘛:给商品组件加个类型,如果是普通页面,就按照以前的方式显示,如果是活动界面,就按照新的条件显示。
添加各种活动
改造js文件,添加type入参
Component({
/**
* 组件的属性列表
*/
properties: {
goods: Object,
type: String
}
})
改造wxml文件
<view style="border: 1rpx solid red; padding: 10rpx;">
<image src="{{goods.url}}"></image>
<view>名称:"{{goods.name}}"</view>
<view>规格:"{{goods.sku}}"</view>
<view wx:if="{{type === 'activity'}}">价格:{{goods.price}}元(活动价8折:{{goods.price * 0.8}}元)</view>
<view wx:else>价格:{{goods.price}}元</view>
</view>
过了几天,淳朴哥又跑过来说:“小雷啊!活动商品搞得很好,反响很不错,老板很高兴,我们决定趁热打铁,如果是会员,直接打七折。这个也简单吧!”
小雷一想:“还好我机智,这次增加个‘会员’类型嘛”。于是修改xml文件实现了需求。
改造wxml文件
<view style="border: 1rpx solid red; padding: 10rpx;">
<image src="{{goods.url}}"></image>
<view>名称:"{{goods.name}}"</view>
<view>规格:"{{goods.sku}}"</view>
<view wx:if="{{type === 'member'}}">价格:{{goods.price}}元(会员价7折:{{goods.price * 0.7}})元</view>
<view wx:elif="{{type === 'activity'}}">价格:{{goods.price}}元(活动价8折:{{goods.price * 0.8}}元)</view>
<view wx:else>价格:{{goods.price}}元</view>
</view>
从表面上看,这样的处理解决了需求,但是实际上却存在问题。那么存在哪些问题呢?我们不妨发散开来想想.
随着活动的种类不断增加,假设增加到a,b,c,d,e,f,g…等活动,那么wxml代码里面是不是会充斥十几个if条件呢?如果后续给商品组件添加一个onClick()事件,点击的后续动作通过这个onClick()函数处理,根据不同的种类,进行不同的处理,js中是不是也要增加十几条if else 呢?
Component({
/**
* 组件的属性列表
*/
properties: {
goods: Object,
type: String
},
onClick() {
if(type === a) {
log(a)
}
if(type === b) {
log(b)
}
if(type === c) {
log(c)
}
....
if(type === z) {
log(z)
}
}
})
这样的代码违反了两个设计原则
单一责任原则
指的是一个类或者一个方法只做一件事。如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化就可能抑制或者削弱这个类完成其他职责的能力。
开放封闭原则
对扩展开放,对修改关闭。意为一个类独立之后就不应该去修改它,而是以扩展的方式适应新需求。
比如说过了几天,淳朴哥又又跑过来说:“小雷啊!我们的会员价现在不打7折了,改成打6折。这个很简单哈!”。
小雷开始感觉不对了,但是又不知道出了什么问题,(我怀疑你有问题,但是我没有证据)心里开始嘀咕了:“这需求怎么老是改来改去?这么一大堆代码,我都要翻几屏呢。”于是小雷打开编辑器,找到type===member的代码块,修改完成,然后交给测试妹妹(以下简称可爱妹)。
可爱妹:“小雷,这次修改了哪些地方啊?”
小雷:“所有使用到商品组件的地方都改动了,麻烦你把所有相关界面过一遍。”(因为所有页面都是用的商品组件,改动一个地方会影响所有使用该组件的页面)
可爱妹:“什么?我尼玛。。。你想累死我吗?”
小雷:“跟我也没有关系啊,我想改嘛?还不是淳朴哥老在改需求!”
淳朴哥:“我尼玛,这老板要求改的,我找谁说理去啊?”
于是一场撕逼大戏展开。。。。
存在的问题:
1)代码量爆炸,阅读起来困难
2)每次需求变更都要改动原代码,不利于维护和测试
相关的改动在产品的开发过程中不可避免,解决这类问题小雷需要将代码抽象出来,而不是简单的if else。
过程分析
淳朴哥在修改需求的过程中,有一些特点,他所改变的是商品的价格显示。因此,存在着变化(价格的显示,点击事件的响应)和不变(商品组件的基本布局:图片/名称/sku/价格)。
根据业务发展的特点,从设计模式中寻找对应方案
建造者(生成器)模式: 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。
介绍: 将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
主要解决:主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
何时使用:一些基本部件不会变,而其组合经常变化的时候。
如何解决:将变与不变分离开。
优点: 1、建造者独立,易扩展。 2、便于控制细节风险。
缺点: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。
解决过程
指导思想:封装变化,灵活替换。
实现思路:将变化的部分独立封装出来,使用的时候进行灵活替换。
过程:
1)将原有的商品组件重新封装,变化的部分用slot代替
<!--sku-panel.wxml-->
<view style="border: 1rpx solid red; padding: 10rpx;">
<image src="{{goods.url}}"></image>
<view>名称:{{goods.name}}</view>
<view>规格:{{goods.sku}}</view>
<view>价格:{{goods.price}}元<slot /></view>
</view>
2)将变化的部分封装出来
活动商品组件
<!--components/activity-builder/index.wxml-->
<sku-panel goods="{{goods}}">
<text>(活动价8折:{{goods.price * 0.8}}元)</text>
</sku-panel>
会员商品组件
<!--components/member-builder/index.wxml-->
<sku-panel goods="{{goods}}">
<text>(会员价7折:{{goods.price * 0.7}}元)</text>
</sku-panel>
3)在活动页面调用活动组件,在会员页面调用会员组件
活动页面
<!--pages/activty/index.wxml-->
<view class="container">
<view>活动</view>
<sku-panel-activity goods='{{goods}}' />//活动组件
</view>
会员页面
<!--pages/member/index.wxml-->
<view class="container">
<view>会员</view>
<sku-panel-member goods='{{goods}}' />//会员组件
</view>
改造之后的结果
1)代码量减少,阅读轻松。不同活动之间的判断逻辑被分散到各个业务组件中了
2)结构清晰,职责分明。每个业务组件中只有与自己相关业务的代码
3)拓展方便。易于测试。某个活动的有所改变,只需要修改对应的业务组件,影响范围被控制起来,测试妹妹也只需要测试某个组件就行。
商品组件有5种商城模式,5种价格,3种用户身份,2种显示排列方式,每个看似雷同,但是又略又不同。除了if只能if,因为你永远不知道明天会加进来一种什么
将变化部分单独封装,再以slot选择加载,确实减少重复,便于维护,但就怕那些看起来不会变化的突然变化,最后全是slot,还有可能这个元素往左一点,那个往右一点,这个字大点,那个小点,一个好好的组件搞得面目全非