如何設計投放系統系列—-靈活的欄位對映補全機制

澶淵發表於2018-07-10

引言

我們知道搭建系統跟投放系統是兩個緊密關聯的系統,搭建產出的是頁面的結構,投放產出的是頁面的資料。搭建產出的頁面包含各式各樣的模組,這些模組包含的欄位也沒有太多規律可言,那麼投放系統怎麼為這些模組補全資料呢?

在回答這個問題之前,我們先嚐試解決一些簡單的業務 case

幾個案例

案例一

有一個商品模組,欄位包括:商品的標題、商品圖片、購買連結、商品價格、商品描述,想要投放某個選品集的商品,請問投放系統應該怎麼設計以補全這些欄位資訊?

拿到這個需求,最直觀的解決方案就是,直接去商品庫取選品集對應的商品列表,把商品庫裡的欄位塞到對應的模組欄位上。

// 商品選品集
var goodSet = [1,2,3,4,5];

// 從商品庫獲取對應的商品實體資訊
var entityMap = goodService.fetch(goodSet);


var output = [];
for (var i = 0, l = goodSet.length; i < l; ++i) {
  var goodId = goodSet[i];
  var entity = entityMap[goodId];
  if (!entity) {
    continue;
  }
  // 欄位對映
  output.push({
    `商品名稱`: entity[`goodName`],
    `商品圖片`: entity[`goodImg`],
    `商品連結`: entity[`goodLink`],
    `商品價格`: entity[`goodPrice`]
    ...
  });
}
return output;

案例二

有一個店鋪模組,欄位包括:店鋪的名稱、店鋪的照片、店鋪連結,想要投放某個選品集的店鋪列表,請問投放系統應該怎麼設計以補全這些欄位資訊?

這個需求跟上面一個類似,依然是最直觀的解決方案,直接去店鋪庫取選品集對應的店鋪列表,把店鋪庫裡的欄位塞到對應的模組欄位上。

// 店鋪選品集
var shopSet = [1,2,3,4,5];

// 從店鋪庫獲取對應的店鋪實體資訊
var entityMap = shopService.fetch(shopSet);


var output = [];
for (var i = 0, l = shopSet.length; i < l; ++i) {
  var shopId = shopSet[i];
  var entity = entityMap[shopId];
  if (!entity) {
    continue;
  }
  // 欄位對映
  output.push({
    `店鋪名稱`: entity[`shopName`],
    `店鋪圖片`: entity[`shopImg`],
    `店鋪連結`: entity[`shopLink`]
  });
}
return output;

案例三

同樣一個商品模組,欄位包括:商品的標題、商品圖片、購買連結、商品價格、優惠價格,想要投放某個選品集的商品,請問投放系統應該怎麼設計以補全這些欄位資訊?

這個 case 我們發現單純的從商品庫取不到優惠價格資訊,必須去另外一個服務獲取商品的優惠價格。

對應的虛擬碼為:

// 商品選品集
var goodSet = [1,2,3,4,5];

// 從商品庫獲取對應的商品實體資訊
var entityMap = goodService.fetch(goodSet);


var output = [];
for (var i = 0, l = goodSet.length; i < l; ++i) {
  var goodId = goodSet[i];
  var entity = entityMap[goodId];
  if (!entity) {
    continue;
  }
  // 欄位對映
  output.push({
    `商品名稱`: entity[`goodName`],
    `商品圖片`: entity[`goodImg`],
    `商品連結`: entity[`goodLink`],
    `商品價格`: entity[`goodPrice`],
    // 去優惠券服務獲取商品的優惠價格
    `優惠價格`: couponService.fetch(goodId).price
  });
}
return output;

到了這裡,我們發現模組只要一變,程式碼就得跟著變,有沒有辦法能模組變化,程式碼不變呢?

終極解決方案

相信聰明的你已經可以看出,隨著需求的變化,我們的程式碼變化的都是欄位的補全來源以及欄位的對映關係,不變的是整個程式碼的流程。如果我們可以把這些變化的部分做成可配置的,似乎程式碼就不需要變動了。

我們試著寫了這樣一個介面

function get(id, field);

這個介面只需要傳實體的 id,以及需要返回的欄位名,就可以返回對應的值。

每個欄位的具體補全邏輯都是 get 的具體實現,我們把實現做成可配置的形式

商品資料來源配置
{
    `goodName`: {adapter: `goodService`, param: [`id`, `goodName`]},
    `goodPrice`: {adapter: `goodService`, param: [`id`, `goodPrice`]}
    `couponPrice`: {adapter: `couponService`, param: [`id`, `couponPrice`]}
}
店鋪資料來源配置
{
    `shopName`: {adapter: `shopService`, param: [`id`, `shopName`]},
    `shopImg`: {adapter: `shopService`, param: [`id`, `shopImg`]}
}

接著我們再新增一些配置,配置的是模組素材欄位跟資料來源中的欄位ID的對映關係

商品欄位對映
模組素材欄位編碼 => 資料來源欄位 ID
{
  `商品名稱`: `goodName`,
  `商品圖片`: `goodImg`,
  `商品連結`: `goodLink`,
  `商品價格`: `goodPrice`,
  `優惠價格`: `couponPrice`
}

店鋪欄位對映
{
  `店鋪名稱`: `shopName`,
  `店鋪圖片`: `shopImg`,
  `店鋪連結`: `shopLink`
}

最後我們再修改下虛擬碼:


function get(id, field) {
    var param = configService.getDatastoreConfig(field).param;
    return adapterFactory.get(field).apply(param);
}


// 選品集
var entitySet = [1,2,3,4,5];
// 實體型別
var entityType = model.entityType;
// 獲取欄位對映配置
var fieldMappingConfig = configService.getFieldMappingConfig(entityType);

var output = [];
for (var i = 0, l = entitySet.length; i < l; ++i) {
  var entityId = entitySet[i];

  var entity = {id: entityId};
  // 遍歷模組的欄位列表
  for (var j = 0; j < model.fields.length; ++j) {
    // 模組欄位編碼
    var field = model.fields[j];
    // 補全該欄位的值,get 會用資料來源配置的類和引數補全該欄位的值
    entity[field] = get(entityId, fieldMappingConfig[field]);
  }

  output.push(entity);
}
return output;

上面其實也是我們 UTCP 系統目前的設計思路。

get

get 介面對應的就是 AbstractEntity.get 方法。

欄位對映配置

image.png

資料來源配置

image.png


相關文章