雲計算

面向數據編程:ECS設計模式在數倉中應用的思考

前言

作為一個從Java轉去做大數據的開發,尤其是基於Hiv採用SQL的開發來說,拋棄了使用了很久的OOP,面向對象編程的設計思想後,總覺得有點不習慣。傳統的web項目中,對SQL的使用更多還是在數據的增刪改查上,而在大數據領域,更多複雜的數據分析,數據交併差的處理,導致SQL代碼量急速增加,可維護性大幅降低。而SQL本身就是一個面向過程描述的語言,Java中常見的MVC,MVVP等設計模式也不適合套用在SQL身上。那麼,是不是應該存在一種設計模式,適用於面向過程的編程設計呢?

帶著這樣的疑問,我開始關注面向數據編程。面向數據編程,核心在於數據。我希望數據可以變得更加靈活,方便開發者對它進行加工。同時,加工過程可以做到高內聚,低耦合。帶著這樣的需求,查閱了很多資料。直到有一次,無意中看到遊戲引擎Unity3D採用的ECS設計模式,突發奇想,意識到這是不是可以滿足我的需求呢?

關於遊戲開發中的ECS簡單介紹

ECS是Entity-Component-System三個單詞的縮寫。最早是在2002年的Game Dungeon Siege上被提出來,是為了解決遊戲設計中,物體直接數據交互和性能的問題。

它在遊戲開發中的演變邏輯可以參考這篇文章:https://zhuanlan.zhihu.com/p/32787878

簡單的說,Entity、Component、System分別代表了三類模型。

實體(Entity):實體是一個普通的對象。通常,它只包含了一個獨一無二的ID值,用來標記它是一個獨立的對象。通常使用整型數字作為它的實現。

組件(Component):對象一個方面的數據,以及對象如何和世界進行交互。用來標記實體是否需要進行這一方面的處理,通常使用結構體,類或關聯數組實現。

系統(System):每個系統不間斷地運行(就像每個系統運行在自己的私有線程上),處理標記使用了該系統處理的組件的每個實體。

 

它跟傳統OOP編程有什麼不一樣呢?

最核心差異點在於:傳統OOP編程裡,我們會先對編程對象進行虛擬化抽象,將共同的一類數據歸到父類或者接口中,子類繼承或實現對應的接口。在遊戲開發中,父類往往是被鎖死的,而一旦需要對邏輯作出修改,要麼重寫實現,要麼繼承基類進行覆蓋。但遊戲策劃的創意是天天都可能會變化的。從而造成大量子類重複出現,大幅降低。此外,在對於C++語言中,使用對象池優化時就會造成災難性的後果——一種類型一個池。

其次,從計算機底層數據傳輸上來說,傳統OOP在傳遞數據時都是採用對象進行封裝。但通常需要用到的數據只是對象中一兩個屬性。對於大部分web應用上來說,多讀取的對象數據影響不大,但對數據密集型計算(例如遊戲圖像領域),則對性能會產生影響。

 

而ECS就是可以解決以上問題。ECS全寫即“實例-組件-系統”的設計模式。簡言之,實例就是一個遊戲對象實體,一個實體擁有眾多的組件,而遊戲系統則負責依據組件對實例做出更新。

舉個例子,如果對象A需要實現碰撞和渲染,那麼我們就給它加一個碰撞組件和一個渲染組件;如果對象B只需要渲染不需要碰撞,那麼我們就給它加一個渲染組件即可。而在遊戲循環中,每一個系統都會遍歷一次對象,當渲染系統發現對象持有一個渲染組件時,就會根據渲染組件的數據來執行相應的渲染過程。同樣的碰撞系統也是如此。

也就是說遊戲對象需要什麼就會給自己加一個組件。而系統會依據遊戲對象增加了哪些組件來做出行為。換言之實例只需要持有必要的數據,由系統負責邏輯就行了。由於只需要持有必要數據,因此對於緩存是非常友好的。這也就是ECS模式能和數據驅動很好結合的一個原因。

 

對於ECS在數倉建設應用中的一些思考

對於數倉建設,也是一個面向數據驅動的開發。因此我將ECS和數倉的代碼聯繫起來,思考如何將ECS的設計模式在數倉中應用。我給出了以下的一些想法:

一個基本假設:

在數倉中,如果可以拋棄pk依賴後,一張表就是一群Schema的合集。

這是我對數倉中數據構成的根本假設。如果一張表裡的其他Schema被PK約束,自然會導致Schema直接產生邏輯關係。如果沒有PK,那麼各個Schema互相之間是平等的,Schema之間可以互相組合。表只是由一個個的Schema填充而成的。這樣聽起來是不是很像Entity和Component之間的關係呢?

所以我大膽的列出一個映射關係。

與ECS的關係映射:

Entity對應於數倉中的Table,Component對應Schema,System對應數倉中SQL邏輯。

image.png

 

對於一張表來說,又若干個Schema構成。對於SQL代碼來說,它關心的只是要用到的Schema,而不是表的業務邏輯。一張表可以由多個不同的SQL共同產出。所以依賴關係可以是這樣的:

image.png

SQL只需關心它加工邏輯中需要用到什麼Schema,產出什麼Schema;Table只需要關心,它的業務邏輯是由哪幾個Schema組成;而Schema自己只需要關心,自己代表什麼原子含義。

ECS模式下的SQL偽代碼簡單實現

在SQL語言,我們一般代碼會寫成這樣:

Select A1

From Tbale1

Where Condition1

A1代表我們需要的Schema,Table1是表,Condition1是需要滿足的條件。

對於ECS架構來說,這樣寫違背了System不跟Entity交互的原則。理想的ECS實現是:

Select Table1.A1

Where  Condition1

如果不同表中的Schema都是平等的,那麼只需要指出使用的是哪個表裡的Schema,和對應的加工條件。無需再將表名列入其中。

當然,有人會說,不就是多個From Table嘛,多寫這一句話也不會怎麼樣。

是的,但大多數數倉開發中,並不是簡簡單單的一張表的處理。往往我們還會遇到很多表之間交併差的情況。這個時候,我們寫的最多的代碼是:

select t1.a,t2.b

from (

select *

from table1

where condition1

) as t1 left

join table2 as t2

on condition2

 

對於一個ECS架構,我們的實現是:

select table1.a,table2.b

where condition1 and condition2

這樣看起來,代碼是不是就簡潔明瞭多了呢?(當然,現階段SQL語法並不支持這種寫法)

 

另外,我們在處理表數據的時候,經常還會遇到這樣一種情況:

insert into tmp_table1

select a1,a2,a3……a31,a32,cast(a33 as bigint) as b1

from table

where condition1

 

inset into result_table1

select a1,a2,a3……a31,a32,b1+1 as c

from tmp_table1

 

inset into result_table2

select a1,a2,a3……a31,a32,b1+2 as c

from tmp_table1

從a1到a32 一共32個列名,其實是不需要做任何特殊處理的,只需要根據condition1條件篩選出來。之後我們又要帶著a1……a32在兩張結果表中進行插入。且不提這樣複製粘貼列名操作十分麻煩,容易出錯,就是我們是否有必要這麼做?

我們的訴求可能只是修改某一張表裡的某一列值,但不得不把這張表的其他字段反覆提取插入。

根據ECS的設計思想,所有列值都是互相平等的。每張表(Entity)只是由列(Component)填充,Sql(System)只是負責邏輯行為。

那麼,實際操作應該是:

insert into tmp_table1

select table.a1,table.a2,table.a3……table.a31,table.a32

where condition1

 

insert into tmp_table2

select ,cast(table.a33 as bigint) as b1

where condition1

 

inset into result_table1

from tmp_table1 add colum tmp_table2.b1+1 as c

 

inset into result_table2

from tmp_table1 add colum tmp_table2.b1+2 as c

 

(以上都是偽代碼)

這樣寫看上去代碼行數沒變化,但好處是,如果table中結構發生變更,只需修改上層tmp_table1的結構即可,對結果表無感知。這一點上反而有點像OOP中的繼承關係。

 

總結

思考將ECS設計模式引入數倉設計,本意是希望開發者可以更加關注於邏輯,關注數據如何處理,也就是S的部分。業務則由從列構建表的時候產生。將表結構和數據處理邏輯進行拆分,從而希望能提升SQL代碼的可讀性和結構性。

SQL本身是一個非常優秀的描述型語言,給數據處理帶來了極大的便利。但在表結構越發複雜的今天,我已經感覺到傳統的SQL的侷限性。希望通過ECS設計模式的思考,可以大家帶來更多的啟發,可以讓SQL代碼像其他工程語言一樣,簡潔優雅。

 

 

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *