天天看點

面向資料程式設計:ECS設計模式在數倉中應用的思考前言關于遊戲開發中的ECS簡單介紹對于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邏輯。

面向資料程式設計:ECS設計模式在數倉中應用的思考前言關于遊戲開發中的ECS簡單介紹對于ECS在數倉建設應用中的一些思考總結

對于一張表來說,又若幹個Schema構成。對于SQL代碼來說,它關心的隻是要用到的Schema,而不是表的業務邏輯。一張表可以由多個不同的SQL共同産出。是以依賴關系可以是這樣的:

面向資料程式設計:ECS設計模式在數倉中應用的思考前言關于遊戲開發中的ECS簡單介紹對于ECS在數倉建設應用中的一些思考總結

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

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

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

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

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

那麼,實際操作應該是:

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

insert into tmp_table2

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

from tmp_table1 add colum tmp_table2.b1+1 as c

from tmp_table1 add colum tmp_table2.b1+2 as c

(以上都是僞代碼)

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

總結

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

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