項目背景
涉及業務細節,均已做脫敏處理。
在一個移動端項目中,需要做城市定位功能。這個城市定位一開始沒什么,就是項目初始化的時候調用一下高德的城市定位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接口。大家的功能都是一樣的,只是一些小細節有所不同,這時候就可以通過一個適配器來兼容它們。簡單來說就是:消除細節差異。