0%

pocket-random 開發紀錄 - 5. 我的資料結構有點巢

說明

下方是這次應用程式的資料,是主要的設定,他存在資料庫裏面。

說明一下它的用途:

mission 是一串設定資料,有不重複的ID,protos 屬性存了篩選器資料。

weekday 存了每天的設定,根據星期一到日,每天不同。weekday 儲存當天要執行的 mission 們的 id,有需要的時候,透過 mid 屬性去存取 mission 中的設定,使用他的篩選器 protos。

為了方便 mission 反查有哪個 weekday 使用了他,所以每個 mission 中還有個 weekday 陣列儲存這個訊息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const config = {
viewer : {
accomplishTag: "String",
weekday : {
"1" : {
targetNum_total: 5,
groups: [ // Loop 2
{ mid: "missionID", targetNum: 2 },
{ mid: "missionID2", targetNum: 3 },
]
},
"2" : {
targetNum_total: 3,
groups: [ // Loop 2
{ mid: "missionID3", targetNum: 1 },
{ mid: "missionID4", targetNum: 2 },
]
}
// 以下直到 "7".....
}
},
mission : {
"missionID": {
mid: "missionID",
name: "sample",
protos: [
["動畫", "學校"],
["工作"]
],
status: "active",
weekday : ["1"] // loop
},
},
}

情況

寫這個應用程式的過程中,我遇到一些狀況,然後有些想法。

  1. 巢狀資料結構
    這裡指的是 config.viewer 的部分。在這篇文章中我會先把這個部分說成「用 weekday 群組化」的資料。

    如果我要取出某一天中的 groups 陣列:

    1
    2
    3
    var day = "1";
    var groups = config.viewer.weekday["1"].groups
    // 好長啊...................................

    因為巢狀太多層了,導致在取值的時候都要點好幾層。所以往往想多一行變數指派,讓他看起來不要那麼長。

    可以看底下第2項範例的第一行。

    我在初期的時候,很直覺想到這樣的資料結構:

    「這是 viewer 的設定,viewer 底下有每天(weekday)的設定,每天的設定稍有不同,分成1234567,底下有他們各自的設定」

    可是太多層的確會挺困擾的,所以如果沒有要管理大量設定,或是即使不歸在 viewer 底下也無所謂的資料,其實可以拿出來。
    感覺上是這種巢狀盡量不要超過三層比較好理解。我自己在超過三層的時候腦袋想起來會很累。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //....................
    accomplishTag: "String",
    weekday : {
    "1" : {
    targetNum_total: 5,
    groups: [ // Loop 2
    { mid: "missionID", targetNum: 2 },
    { mid: "missionID2", targetNum: 3 },
    ]
    },
    "2" : {
    targetNum_total: 3,
    groups: [ // Loop 2
    { mid: "missionID3", targetNum: 1 },
    { mid: "missionID4", targetNum: 2 },
    ]
    }
    // 以下直到 "7".....
    },
    mission : {
    //................

    var groups = configs.weekday["1"].groups;
    // 這邊仍然是 4 層,.............................
  2. 運算對象是巢狀資料中的所有陣列

    題目:在所有 weekday 中尋找某個 missionID 是否存在。在的話,從該 weekday 的 groups 中移除那個 mission。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var weekdays = config.viewer.weekday;
    var targetID = 'someID';
    for (const day in weekdays) {
    const daySetting = weekdays[day];
    const groups = daySetting.groups;

    // 尋找某個 mission 是否存在,在的話,移除
    var index = groups.findIndex(mission => mission.mid === targetID);
    if (index){
    groups.splice(index,1)
    }
    }

    這邊有兩個想法:

    1. for in迴圈比較慢(其實通常沒有甚麼差別)

    2. 雙重迴圈

      迴圈速度比較

      這個連結比較了 for in 迭代物件,以及 forEach 迭代一般陣列的速度差。轉到 100000 圈的時候差大概 10 毫秒。除非是真的很有潔癖,否則這邊用不用 for in 其實不算是問題。但剛好我有點在意orz。

      但就算不管 for in 的速度,這邊還是有個雙重迴圈,寫起來有點討厭。其實這個雙重迴圈的演算過程,在那個資料結構設定好的時候就應該可以預料到了。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      weekday : {
      "1" : { // Loop 1 for in 第一層迴圈
      targetNum_total: 5,
      groups: [ // Loop 2 forEach 第二層迴圈
      { mid: "missionID", targetNum: 2 },
      { mid: "missionID2", targetNum: 3 },
      ]
      },
      "2" : { // Loop 1 for in
      targetNum_total: 3,
      groups: [ // Loop 2 forEach
      { mid: "missionID3", targetNum: 1 },
      { mid: "missionID4", targetNum: 2 },
      ]
      }
      // 以下直到 "7".....
      },

      為了避免這種要轉兩層的很痛苦的感覺,我試著把結構改成這樣:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      weekday : [
      { day:"1", mid: "missionID", targetNum: 2 },
      { day:"1", mid: "missionID2", targetNum: 3 },
      { day:"2", mid: "missionID3", targetNum: 1 },
      { day:"2", mid: "missionID4", targetNum: 2 },
      ]

      // 用這個資料結構做同一個題目

      var targetID = "someID";
      var index = weekday.findIndex(mission => mission.mid === targetID);
      while (index){
      groups.splice(index,1)
      index = groups.findIndex(mission => mission.mid === targetID);
      }
      // 結果還是轉了兩次迴圈啊哈哈哈哈哈

      結果還是轉了兩次迴圈啊哈哈哈哈哈~~。
      再試著用兩種結構去算各種可能遇到的問題:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      // 1. 合計某一天的 targetNum_total

      // 群組結構
      weekday["1"].groups
      .reduce((pre,now)=> pre.targetNum + now.targetNum)
      // 陣列結構
      weekday.filter(itm => itm.day === "1")
      .reduce((pre,now)=> pre.targetNum + now.targetNum)

      // 2. 找到某個 mission 在哪些 weekday 中
      // 群組結構
      var targetID = "missionID";

      var result = [];
      for (var day in weekday){
      var index = weekday[day].groups.findIndex(group => group.mid === targetID);
      if (index !== -1){
      result.push(day)
      }
      }

      // 陣列結構
      weekday.map(itm => {
      if (itm.mid === targetID){
      return itm.day
      }
      })

      如果我要針對某一天算的時候,群組結構比較省事;陣列結構反而要先 filter。

      可是要跨群組的時候,群組結構必須要跑for in,而且每圈都要 findIndex。相對的陣列結構很省事。

  1. 使用在 Vue 模板
    欸,這個我就比較支持使用群組化的資料,至少就這次來說,以下是在表單中出現的狀況:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 使用巢狀結構  -->
    <!-- 分群渲染 -->
    <div v-for="(daySetting, day) in configs.weekday" :key="day">
    <h3>{{ day }}</h3>
    <div v-for="(group,i) in daySetting.missions">
    <p>名稱:<input type="text" v-model="group.mid"> </p>
    <p>目標數量: <input type="number" v-model="group.targetNum"> </p>
    </div>
    </div>

    因為我需要讓使用者清楚看到某一天底下設定了什麼 mission,所以這種很符合直覺的巢狀結構反而適得其所。

    相對的,陣列結構就比較麻煩了,需要外掛資料,寫個 computed 群組化將他群組化

結論

基本上巢狀結構能夠精簡就精簡比較好。但真的要拿來演算的時候,兩種結構各有優缺(廢話)。

整理一下:

- 陣列結構
    -  優點:
        1. 適合多屬性(props)交叉演算,因為所有資料都是同等的
        2. 彈性,可以自由根據屬性(props)群組化
    -  缺點:
        1. 針對特定領域(field)的演算需要先做一層filter

- 巢狀結構:
    -  優點:
        1. 特定情況下(群組內)查詢跟演算比較快 (包括用.運算子也是比較快的)
        2. 群組化,貼近直覺,方便理解

    -  缺點:
        1. 跨群組運算要多轉forin迴圈
        2. 難以用其他領域 (props) 組成資料
        3. 太多層會 ........... 到死
    -  建議:
        1. 適時精簡巢狀

最大的問題是,在初期規劃階段,到底要怎麼決定(廢話)。
總之,要考量一下他會在那些場景使用,雖然曾經在開發紀錄3 思考篩選文章用的資料格式
稍微考慮過類似的問題,但沒想到其他地方也會發生這種算的有點不舒服的狀況。orz