前端中间层策略模式的小小实践总结

2,764 阅读7分钟

业务场景介绍

目前有以下页面:

  1. 普通商品详情页面
  2. 新人专享商品详情页面
  3. 秒杀商品详情页面
  4. 团购商品详情页面
  5. 满减商品详情页面
  6. 积分商品详情页面
  7. 直播商品详情页面
  8. 内购商品详情页面

这些页面都有共同的业务逻辑,比如展示商品信息,添加购物车,立即购买,选择规格属性,分享,等等。

存在问题

这些页面都有共同的商品数据字段,但又会存在各自不同的数据字段,但又因为不同的接口分别由不同的后端同学实现,返回的字段也不一致,比如同一个商品名称字段,可能A后端返回的数据字段是prodName,B后端返回的数据字段是productName,C后端返回的数据字段是name。 又比如说团购商品返回的数据结构是这样的:

{
	startTime: '',// 团购开始时间
	endTime: '', // 团购结束时间
	prodName: '' // 商品名称
	...
}

满减商品:

{
	startTime: '',// 活动开始时间
	endTime: '', // 活动结束时间
	productVO: { // 商品详细数据
		prodName: ''
	}
	...
}

又比如说: 积分商品的数据则由两个接口分别返回 A接口返回积分商品的SKU的配置信息和积分活动信息 B接口返回商品的其他信息

传统解决方案

分别建立各自的页面模块,各自实现各自的业务逻辑

缺点:

产生大量的冗余代码,而且后期维护成本非常高

中间层策略模式解决方案

什么是中间层

传统的前端中间层指的是Node层,Node层可以向server层获取数据,再通过对数据的计算整合转换成符合前端UI要求的数据格式。 另外Node层还可以做代理转发、接口聚合、数据缓存、接口限流、日志操作等等

我们现在没有Node层,但可以实现一个Model层的service模块,用来专门处理数据,统一数据格式之后交给View层,让View层更加专注于UI,即高效关注点分离,提高维护性。

什么是策略模式

策略模式的核心思想和 if else 如出一辙,根据不同的key动态的找到不同的业务逻辑。

优点:

  • 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
  • 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法 。

具体实现

流程图

01.jpg

定义一个商品详细数据转换服务层类中间层

/**
 * 商品详细数据转换服务层
 */
class GoodsDetailService {
    constructor(goodsDetailPolicy, goodsType, priceKey) {
        this.goodsDetailPolicy = goodsDetailPolicy
        this.goodsType = goodsType
        this.priceKey = priceKey
    }
	
    /**
	 * 开始
	 * @param {Object} params api 请求参数
	 */
    start(params) {
        this.setNavigationBarTitle()
        return new Promise((resolve, reject) => {
            this.goodsDetailPolicy.detailApi(params).then(r => {
                this.goodsInfo = this.goodsDetailPolicy.initData ? this.goodsDetailPolicy.initData(r) : r.data
                this.transform(this.goodsInfo)
                resolve(this.goodsInfo)
            }).catch(err => reject(err))
        })
    }
	
    /**
	 * 设置标题
	 */
    setNavigationBarTitle() {
        uni.setNavigationBarTitle({
            title: this.goodsDetailPolicy.navigationBarTitle || '商品详情'
        })
    }
	
    /**
	 * 数据转换
	 * @param {Object} goodsInfo 产品详细
	 */
    transform(goodsInfo) {
        this.goodsDetailPolicy.transform(goodsInfo)
        // 如果不是单属性,就设置多属性的SKU数据
        if (!this.setIsSingleSpecification(goodsInfo)) {
            goodsInfo['productAttr'] = this.normalizeSkus(goodsInfo.skus)
        }
        // 获取最小价格的SKU商品
        let minHeap = util.minHeap(goodsInfo.skus, this.priceKey)
        minHeap['price'] = minHeap[this.priceKey]
        // 如果存在限量,那么库存就是限量
        minHeap.actualStocks = minHeap.limits ? minHeap.limits : minHeap.actualStocks
        goodsInfo['minHeap'] = minHeap
        goodsInfo['price'] = minHeap.price
        goodsInfo['actualStocks'] = minHeap.actualStocks
        goodsInfo['marketPrice'] = minHeap.marketPrice
    }

    /**
	 * 设置否单规格
	 * @param {Object} goodsInfo 产品详细
	 */
    setIsSingleSpecification(goodsInfo) {
        goodsInfo['isSingleSpecification'] = goodsInfo.skus.length === 1 && !goodsInfo.skus[0].properties
        return goodsInfo.isSingleSpecification
    }

    /**
	 * SKU数据结构初始化
	 * @param {Object} skus
	 */
    normalizeSkus(skus) {
        let temp = []
        // 先把所有的属性拼装成一个一维数组,再通过normalizeSkus进行去重拼装成一个二维数组 MVK独有,主要可以兼容秒杀和直播商品
        skus.forEach(o => {
            const properties = JSON.parse(o.properties)
            Object.keys(properties).forEach(v => {
                temp.push({
                    attrName: v,
                    attrVal: properties[v]
                })
            })
        })
        // SKU数据结构初始化
        return this.normalizeSkusToTwoDimensionalArray(temp)
    }

    /**
	 * 商品SKU数据结构构建
	 * @param {Array} data 
	 */
    normalizeSkusToTwoDimensionalArray(data) {
        let temp = []
        data.forEach(o => {
            const attrName = o.attrName
            const skuVal = o.attrVal
            if (temp.length > 0) {
                if (temp.some(val => val.attrName === attrName)) {
                    temp.forEach((v, j) => {
                        if (v.attrName === attrName && v.attrValues.every(sv => sv.val !== o
                            .attrVal)) {
                            temp[j].attrValues.push({
                                val: skuVal,
                                isSelect: false
                            })
                        }
                    })
                } else {
                    temp.push({
                        attrName,
                        attrId: attrName,
                        attrValues: [{
                            val: skuVal,
                            isSelect: false
                        }]
                    })
                }
            } else {
                temp.push({
                    attrName,
                    attrid: attrName,
                    attrValues: [{
                        val: skuVal,
                        isSelect: false
                    }]
                })
            }
        })
        return temp
    }
}

每个模块都需要进行共同的操作,比如设置标题,设置否单规格,SKU数据结构初始化,商品SKU数据结构构建,所以这些操作都放到共同的Service模块进行操作,另外各自不同的数据转换则放到各自的策略里进行转换操作。

定义转换策略

实现各自不同的策略转换操作,也可以说是实现不同的算法计算。

/**
 * 转换策略
 */
const goodsDetailPolicy = {
    [goodsTypes.GROUP_BUYING]: {
        detailApi: fetchGroupBuyingGoodsDetail,
        navigationBarTitle: '团购商品详情',
        transform: goodsInfo => {
            // 轮播图
            goodsInfo['sliderImage'] = JSON.parse(goodsInfo.imgs)
            goodsInfo['prodName'] = goodsInfo.productName
            goodsInfo['skus'] = goodsInfo.skuList.map(o => {
                o['prodName'] = goodsInfo.productName
                // 团购限制一个
                o['limits'] = 1
                return o
            })
        }
    },
    [goodsTypes.FLASH_SALE]: {
        detailApi: fetchFlashSaleGoodsDetail,
        navigationBarTitle: '秒杀商品详情',
        transform: goodsInfo => {
            // 轮播图
            goodsInfo['sliderImage'] = goodsInfo.imgs.split(',')
            goodsInfo['prodName'] = goodsInfo.title
            goodsInfo['skus'] = goodsInfo.seckillSkuVO
        }
    },
    ...
}

调用实现

const service = new GoodsDetailService(goodsDetailPolicy[this.goodsType], this.goodsType, this.priceKey)
			
service.start(params).then(r => {
    
    this.goodsInfo = r
    
}).catch(err => console.log(err))

通过goodsDetailPolicy[this.goodsType]实现调用不同的转换策略,this.goodsType可以为商品类型:普通,秒杀,直播,满减,积分等。

具体案例实现解析

下面以积分商品详情的实现进行解析,因为积分商品详情几乎包含了各种情况。 首先积分商品详情需要通过两个接口返回相关的数据 api-A返回积分配置的信息,比如活动开始时间,积分配置的SKU信息 api-B返回商品的所有信息 那么我们需要的信息应该是api-A中有的数据,就取api-A中的信息,api-A没有的就取api-B中的信息 清楚需求之后,我们开始实现

定义一个转换策略

{
    [goodsTypes.POINTS_MALLS]: {
        detailApi: fetchPointsMallsGoodsDetail,
        navigationBarTitle: '积分商品详情',
        initData: data => {

        },
        transform: goodsInfo => {

        }
    },
}

虽然积分商品详情数据需要两个api提供,但在转换策略里面我们还是使用一个api作为入口

然后再api层进行两个api接口并发的请求处理,注意api层不作数据处理,让api层只专注api请求的工作

/**
 * 获取积分商品详情
 * 
 */
export function fetchPointsMallsGoodsDetail(data) {
    const pointsGoodsDetailApi = request.get(`/smsPointsMalls/${data.pointsMallsId}`, {})
    const plainGoodsDetailApi = request.get('/smsFullMinus/product', { ids: data.ids })
    return Promise.all([
        pointsGoodsDetailApi,
        plainGoodsDetailApi
    ])
}

请求回来的数据先在策略里进行初始化处理

    [goodsTypes.POINTS_MALLS]: {
	    detailApi: fetchPointsMallsGoodsDetail,
	    navigationBarTitle: '积分商品详情',
		initData: data => {
			const pointsGoodsDetail = data[0].data
			const plainGoodsDetail = data[1].data[0]
			
			const pointSkus = JSON.parse(pointsGoodsDetail.sku)
			// 设置最小积分和最小价格
			let integral = ''
			let price = ''
			pointSkus.forEach(o => {
				if (o.integral > integral && o.integral) integral = o.integral
				if (o.price > price && o.price) price = o.price
			})
			
			const skus = pointSkus.map(o => {
				o['pic'] = o.img 
				// 每人可以兑换数量
				o['actualStocks'] = pointsGoodsDetail.personExchange
				// 限购
				o['limits'] = pointsGoodsDetail.personExchange
				o['prodName'] = plainGoodsDetail.prodName
				return o
			})
			// return data[0]
			return {
				...plainGoodsDetail,
				pointsGoodsDetail,
				skus,
				integral,
				price
			}
		},
	    transform: goodsInfo => {console.log('goodsInfo', goodsInfo)
	        // 轮播图
	        goodsInfo['sliderImage'] = JSON.parse(goodsInfo.imgs)
			// goodsInfo['skus'] = JSON.parse(goodsInfo.skus)
	    }
    }

初始化处理好数据之后,再进行各自的转换和公共层的数据转换。

总结:何时使用策略模式

根据阿里开发规约-编程规约-控制语句-第六条 :超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现。

相信大家都见过这种代码:

if (conditionA) {
    逻辑1
} else if (conditionB) {
    逻辑2
} else if (conditionC) {
    逻辑3
} else {
    逻辑4
}

这种代码虽然写起来简单,但违反了开闭原则(对扩展开放,对修改关闭):如果此时需要添加(删除)某个逻辑,那么不可避免的要修改原来的代码。

尤其是当 if-else 块中的代码量比较大时,后续代码的扩展和维护就会逐渐变得非常困难且容易出错,使用卫语句也同样避免不了以上两个问题。

根据阿里开发规约:

  • if-else 不超过 2 层,块中代码 1~5 行,直接写到块中,否则封装为方法
  • if-else 超过 2 层,但块中的代码不超过 3 行,尽量使用卫语句
  • if-else 超过 2 层,且块中代码超过 3 行,尽量使用策略模式

总结来自阿里巴巴淘系技术《设计模式最佳套路—— 愉快地使用策略模式》本文讲述的是后端策略模式的实现,但核心思想同样适用于前端。