记一次策略模式+适配器模式在实际项目中的真实使用

项目背景

涉及业务细节,均已做脱敏处理。

在一个移动端项目中,需要做城市定位功能。这个城市定位一开始没什么,就是项目初始化的时候调用一下高德的城市定位API,仅此而已。

经过一些业务调整和产品变动,城市定位的需求逐渐变得复杂了起来。比如:内嵌到一个原生APP提供的webview中,需要调用原生方法取APP外层的城市定位;作为url链接抛出给第三方使用时,希望锁定城市,不要去做自动定位。

成功取到city后,会存储在sessionStorage中,供全局取用。

而且比较麻烦的是:这些定位的字段还不太一样,比如城市名,有的叫cityName,有的叫chineseCityName,诸如此类。

难以维护的历史代码

换了几波开发团队后,这部分代码逐渐演化成了下面这样:

async function getCity() {
    let city = {};
    
    // 优先从sessionStorage里面取
    city = JSON.parse(sessionStorage.getItem('city'))
    
    // 原生方法获取城市
    if (!city.cityCode && window.$native.getCity) {
        // xxx
        return {
            cityName: nativeCity.cityName,
            cityCode: nativeCity.cityCode
        }
    }
    
    // 假如url有绑定城市
    if ($route.query.cityCode) {
        // xxx
        return {
            cityName: matchCity.city,
            cityCode: matchCity.areaCode
        }
    }
    
    // 调用高德api定位
    if (!city.cityCode) {
        // xxx
        return {
            cityName: res.chineseCityName,
            cityCode: res.adcode
        }
    }
    
    // 这里已经违反了函数单一职责原则
    // 函数名是getCity,却做了超越getCity职责的事情
    if (!city.cityCode) {
        $router.replace('/select-city');
    } else {
        sessionStorage.setItem(city)
    }
    
    return city
}

上面这个函数是原函数的简化版,实际上的原函数代码比这个要长得多,当这个函数里面有一点点修改,测试都不得不对全部环境回归:回归测试APP内打开定位是否正常、回归测试URL携带城市时是否定位正常、回归测试浏览器正常打开定位是否正常。

这就是我当时接锅这个这个项目的真实现状:当定位的需求发生更改时,没人愿意去碰这个破函数。然而我却真的被安排去接这个锅了orz。

改造前的分析

经过一番思索,我初步决定改造如下:

  • 不同环境context(比如在APP内或者在浏览器内),对应的获取城市策略strategy是不同的,这就是策略模式
  • 要保持函数单一职责原则,一个函数只做一件事
  • 对于不同的字段名,可以通过适配器模式来做到统一
  • 最后输出给全局用的城市格式应该无论在何种环境下都是一致的

改造后的代码

这里要说明一下,为了让结构更清晰,这里省略了很多实现细节,比如try catch的异常捕获处理,以及一些类型的判断等等等等
/*
* 城市字段适配器
* @params {Object} city 各种字段的城市对象
* @return {Object} 标准统一格式的城市对象
*/
const formatCityByAdapter = (city) => {
    return {
        cityName: city.cityName || city.chineseCityName || city.city,
        cityCode: city.cityCode || city.adcode || city.areaCode
    }
}

// 通过高德获取城市
const getCityFromAMap = async () => {
  // xxx
  return city
}

// 通过APP原生获取城市
const getCityFromNative = async () => {
    // xxx
    return city
}

// 通过sessionStorage获取城市
const getCityFromSessionStorage = () => {
    // xxx
    return city
}

// 通过url中获取城市
const getCityFromUrl = () => {
    // xxx
    return city
}

// 在APP的webview中获取城市的方法,经确认没有url固定城市的情况
// 而且如果已经在原生环境中,就不用加一些条件去判断有没有原生的方法
const getCityInAPP = async () => {
    const city = getCityFromSessionStorage() || await getCityFromNative()
    return formatCityByAdapter(res)
}

// 在浏览器中获取城市的方法,可能存在有url固定城市的情况
const getCityInBrowser = async () => {
    const city = getCityFromSessionStorage() || getCityFromUrl() || await getCityFromAMap()
    return formatCityByAdapter(res)
}

// 获取环境信息,先确定环境
const getEnv = () => {
    return window.$native.getEnv()
}

// 最终总的函数
const getCity = () => {
    const envMap = {
        app: getCityInAPP,
        browser: getCityInBrowser
    };
    const env = getEnv();
    
    return envMap[env]()
}

总结

  • 什么时候用策略模式?我认为是情况比较多的时候。比如:情况1要怎么怎么样,情况2则要怎么怎么样。简单来说就是:不同的情况对应不同的方案。
  • 什么时候用适配器模式?我认为非常适合那种功能相同,字段不同的情况。就比如生活中,充电线的功能都是给手机充电🔋,但是iPhone的是lighting接口,一些安卓的是typeC接口,一些老式安卓是microUSB接口。大家的功能都是一样的,只是一些小细节有所不同,这时候就可以通过一个适配器来兼容它们。简单来说就是:消除细节差异
posted @ 2020-07-06 20:31  陌上兮月  阅读(1339)  评论(0编辑  收藏  举报