评论

用<slot>标签实现建造者模式

使用<slot>标签的特性实现建造者模式,灵活应对业务的变化。

背景

在平常的开发过程中,我们经常会遇到一种情况:有一个组件(或页面)被用于多个场景,不同的场景下面,组件的部分内容发生变化,其他则不变。
某一天,前端开发小雷正在码代码,淳朴的产品经理(以下称为淳朴哥)跑过来说:“我们要做一个商品组件,根据后台返回的数据显示出来。这个简单吧!”


小雷一想,这个简单啊,框框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)拓展方便。易于测试。某个活动的有所改变,只需要修改对应的业务组件,影响范围被控制起来,测试妹妹也只需要测试某个组件就行。

点赞 0
收藏
评论

2 个评论

  • 三号稻草人😯
    三号稻草人😯
    2020-03-18

    商品组件有5种商城模式,5种价格,3种用户身份,2种显示排列方式,每个看似雷同,但是又略又不同。除了if只能if,因为你永远不知道明天会加进来一种什么

    2020-03-18
    赞同
    回复
  • 🚗🚕🚙🚌🚎🚒🚐🚚
    🚗🚕🚙🚌🚎🚒🚐🚚
    2019-10-28

    将变化部分单独封装,再以slot选择加载,确实减少重复,便于维护,但就怕那些看起来不会变化的突然变化,最后全是slot,还有可能这个元素往左一点,那个往右一点,这个字大点,那个小点,一个好好的组件搞得面目全非

    2019-10-28
    赞同
    回复 3
    • 玉衡
      玉衡
      2019-10-29
      你提的变化情况,属于在开发的过程中业务还没有确定的时候。设计模式,适合在业务确定且有重复的情况下使用。如果业务或者ui还没确定,此时不需要封装;如果组件没有出现第二次被使用,也不适合封装。封装的最佳时机就是:1.业务已经大致确定;2.组件的使用存在两个或者以上的使用场景;这个时候就适合封装起来了
      2019-10-29
      回复
    • 🚗🚕🚙🚌🚎🚒🚐🚚
      🚗🚕🚙🚌🚎🚒🚐🚚
      2019-10-29回复玉衡
      你不懂产品的心思
      2019-10-29
      回复
    • 玉衡
      玉衡
      2019-10-29回复🚗🚕🚙🚌🚎🚒🚐🚚
      这不是技术能解决的,考虑换个产品经理。。
      2019-10-29
      回复
登录 后发表内容