[實踐] 為原型系統設計簡單的純文字資料交換協議 之二
[參考書目]
TAOUP 5:1-3
[目的]
我們新組建了一個軟體公司,LZSoft,針對微型客戶需求快速開發小軟體。為了儘快驗證功能需求可行性、實現複雜度與滿足程度,需要不停地建立流程模型和原型程式、在各種實現上迭代微調。自然在該過程中使用著大量動態程式語言如Perl、Python、Ruby等,同時配合以標準管道化工具如Shell、sed、awk、grep等作為輔助開發手段。由於上述語言和工具間的微妙異構性,必須設計一套合理輕便的資料交換協議,以便在各子程式之間可靠通訊。
[需求]
1. 易讀:人眼可讀、可理解,不需要額外直譯器/格式化工具;
2. 易寫:可用各種標準Linux實用工具生成輸出,也可用標準Linux機上編輯工具(vi/emacs)手寫;
3. 邊界清晰:不同記錄間有明確的邊界,可輕易感知;
4. 無固定結構:不同記錄可以有不同結構,以描述不同應用資料;
5. 語法規則簡潔:任何人、任何程式均能快速理解記憶;
6. 相容能力強:不同語言編寫的程式可簡單地解析/合成記錄;
7. 層級簡單:可以在某種可接受的複雜程度上描述層級結構。
[設計]
考慮到原型系統中開發人員參與度非常高,資訊交換過程最關鍵之處是必須隨時可讀可寫,因此擯棄二進位制資料交換協議,初步定位為使用純文字資料交換協議。
開始時嘗試從現有資料交換協議中選取合適物件,有如下選擇:JSON,XML,CSV。三者均為純文字資訊交換協議,各有利弊:
JSON優點有:
a)非常輕量與緊湊,協議本身使用一頁A4紙即可描述清楚;
b)天然具備層級和集合結構;
c)天然面向Web開發;
d)可自描述語義;
e)格式化後人眼讀寫都很方便;
f)相關外部工具眾多;
g)有效資訊負載比大。
缺點:
a)多條記錄混排輸出時邊界不清晰,難以定位;
b)緊湊輸出時人眼讀寫困難;
c)程式設計時需要載入第三方解析/生成庫。XML優點有:
a)天然具備層級和集合結構;
b)可自描述語義;
c)相容性與支援度最好;
d)外部工具眾多,成熟、功能良好。
缺點有:
a)多條記錄混排輸出時邊界不清晰,人眼難以定位;
b)手動編寫麻煩甚至困難;
c)程式設計時需要載入第三方解析/生成庫;
d)對文字資料限制過於嚴格;
e)有效資訊負載比小。CSV優點有:
a)以行作為記錄邊界,多條記錄混排輸出時便於人眼定位;
b)可匯入Excel進一步處理,更利於多人協同辦公;
c)手工編輯方便。
缺點有:
a)相容性較差;
b)外部工具較少;
c)語法結構稍微複雜;
d)轉義序列複雜易錯;
e)程式設計時需要載入第三方解析/生成庫;
f)缺少自描述語義能力。
由於主要該協議主要使用在原型系統中,強調快速辨識、修改以應對需求和功能的變化,而XML並不合適快速編輯與分析,故放棄;在有限前提下JSON可以使用,但不經過適當格式化,閱讀編輯都相當困難,出於開發效率考慮,同樣放棄;而CSV最大問題有兩個,一是不能自描述語義,二是轉義規則複雜,同樣只適合在有限前提下使用,也不得不放棄;最後,影響三個協議均不能入選的最終理由是,程式語言相容性都不夠高,某些語言使用時必須載入第三方庫,從而產生較強的功能限制或類庫依賴性,導致使用風險。
[結論]
設計一個用於原型系統的純文字資料交換協議,應當具備以下特點:
1. 邊界清晰,任何記錄混排輸出時均可快速識別,不論接收方是人眼還是機器;
2. 結構儘可能簡單,轉義規則能少則少,層級儘可能壓縮在兩到三層以內;
3. 能夠自描述各部分結構的語義,至少要可以聯想;
4. 可以在記錄間隨意插入空行和註釋,幫助定位、補充描述;
5. 程式設計時不必刻意載入第三方庫,使用語言提供的原生函式即可處理;
6. 最好能便利地對映為物件/雜湊表/陣列。
[成果]
最終選擇以下描述的簡單純文字資料交換協議。
1. 以換行(\n,LF)作為記錄邊界,如資料中存在換行,可以用C轉義序列\n替換之,也可以完全禁止使用帶換行的資料;
2. 以管道符(|,BAR)作為欄位邊界,如資料中存在管道符,則以轉義序列\|替換之;
3. 如果有需要,也可以使用製表符(\t,TAB)代替管道符(甚至定製化);
4. 每個欄位由鍵名和值組成,之間以等號(=,EQUAL)分隔,鍵名兩邊可填入可選空白符,必要時值兩邊亦可填入可選空白符;
5. 鍵名中可以包括句點符(.,PERIOD)表示層級結構;
6. 對於陣列元素可以用索引號作為對應層級結構的鍵名部分(每個元素作為一個欄位),也可以簡單地以分號(;,COMMA)分隔後作為單獨一個欄位的值。
[例項]
cmd=test_output|name=Dota|age=4|desc=Cannot say anything.|lang=CN;EN
arr.0=ABCD|arr.1=TEST
[缺點]
1. 如果從物件/雜湊表/陣列中生成記錄,則欄位順序可能是亂序的,擁有相同欄位的物件生成的記錄排在一起時比較好看,反之則相當難看;
2. 資料超過一屏字元數時會折成多個邏輯行,記錄邊界定位難度上升;
3. 有效字元數與總字元數之比偏小,有效資訊負載低。
[典型應用場景]
1. 稠密資料輸出;
2. 配置檔案中的規則描述;
3. 臨時過濾用資料格式。
[Ruby實現]
#!/usr/bin/env ruby
# use encoding : utf-8
################################################################################
# module : LZSoft::PSV
# A class parses or generates one line flattened data with fields delimited by bar
# signs.
#
# sample :
# name=Tom|age=30|gender=male|aka.cn=LiangTao|fond=programming;kidding
#
# author :
# Liang Tao
#
# created :
# 2012-01-02
#
################################################################################
module LZSoft
module PSV
FLD_DELI = %q{|}
NM_DELI = %q{.}
ARR_DELI = %q{;}
KV_DELI = %q{=}
NO_ORDER = lambda { |arr| arr }
DICT_ORDER = lambda { |arr| arr.sort{ |a,b| a[0] <=> b[0] } }
def to_psv (opts = {})
obj = case self
when Hash, Array
self
when String
{ self.class.name => self }
else
vars = instance_variables
if vars.empty? then
[ self.class.name, self.to_s ]
else
Hash[ vars.map do |v|
[ v.to_s.sub(/^@/, %q{}),
instance_variable_get(v) ]
end ]
end
end
PSV.generate(obj, opts)
end # to_psv
def from_psv (str, opts = {})
obj = PSV.parse(str, opts)
case self
when Hash, Array
self.replace(obj)
when String
self.replace(obj[self.class.name])
else
obj.each_pair{ |k, v| instance_variable_set("@#{k}", v) }
end
end # from_psv
def self.generate (obj, opts = {})
if not (obj.is_a?(Hash) or obj.is_a?(Array)) then
raise TypeError, 'Expect a Hash or Array object.'
end
_gen_opts(opts)
_serialize(obj, opts)
end # self.generate
def self.parse (str, opts = {})
raise 'Expect a String object.' if !str.is_a?(String)
_gen_opts(opts)
_unserialize(str, opts)
end # self.parse
private
def self._gen_opts (opts)
opts[:fld_deli] = opts[:fld_deli] || opts['fld_deli'] || FLD_DELI
opts[:nm_deli ] = opts[:nm_deli] || opts['nm_deli'] || NM_DELI
opts[:arr_deli] = opts[:arr_deli] || opts['arr_deli'] || ARR_DELI
opts[:kv_deli ] = opts[:kv_deli] || opts['kv_deli'] || KV_DELI
opts[:fld_deli_re] = _gen_deli_re(opts[:fld_deli])
opts[:nm_deli_re] = _gen_deli_re(opts[:nm_deli])
opts[:arr_deli_re] = _gen_deli_re(opts[:arr_deli])
opts[:kv_deli_re] = _gen_deli_re(opts[:kv_deli])
opts[:fld_deli_tr] = _gen_deli_tr(opts[:fld_deli])
opts[:nm_deli_tr] = _gen_deli_tr(opts[:nm_deli])
opts[:arr_deli_tr] = _gen_deli_tr(opts[:arr_deli])
opts[:kv_deli_tr] = _gen_deli_tr(opts[:kv_deli])
ord = opts[:order]
opts[:order] = \
case ord
when Proc then ord
when Array then lambda { |arr|
hash = Hash[arr]
rest = hash.keys - ord
[ord, rest].flatten.map{|k| [k, hash[k]]}
}
else
NO_ORDER
end
end # self.gen_opts
def self._escape (s, deli, tr); s.gsub(deli, tr); end
def self._unescape (s, tr, deli); s.gsub(tr, deli); end
def self._escape_val (v, opts)
t = _escape(v.to_s, opts[:fld_deli], opts[:fld_deli_tr])
t = _escape(t, opts[:arr_deli], opts[:arr_deli_tr])
t = t.gsub("\n", "\\n").gsub("\r", "\\r").gsub("\t", "\\t")
end # self._escape_val
def self._unescape_val (v, opts)
t = v.gsub("\\n", "\n").gsub("\\r", "\r").gsub("\\t", "\t")
t = _unescape(t, opts[:arr_deli_tr], opts[:arr_deli])
t = _unescape(t, opts[:fld_deli_tr], opts[:fld_deli])
end # _unescape_val
def self._gen_deli_re (deli);
str = "(?<![\\\\])[#{deli}]"
%r{#{str}}
end # self._gen_deli_re
def self._gen_deli_tr (deli); %Q{\\#{deli}}; end
def self._create_obj (obj, k)
(not obj.nil?) ? obj :
(k =~ /^\d+$/) ? [] : {}
end # self._create_obj
def self._unserialize (str, opts)
obj = nil
str.split(opts[:fld_deli_re]).each do |v|
key, val = v.split(opts[:kv_deli_re], 2)
val ||= %q{}
vals = val.split(opts[:arr_deli_re]).map{ |v| _unescape_val(v, opts) }
keys = key.split(opts[:nm_deli_re]).map{ |k|
t = _unescape(k, opts[:nm_deli_tr], opts[:nm_deli])
t[-1] == %q{:} ? t[0..-2].to_sym : t # process symbol
}
obj = _create_obj(obj, keys[0])
tmp, prev = obj, keys.shift
keys.each do |k|
prev = prev.to_i if tmp.is_a?(Array)
tmp = tmp[prev] = _create_obj(tmp[prev], k)
prev = k
end
prev = prev.to_i if tmp.is_a?(Array)
tmp[prev] = case vals.size
when 0 then %q{}
when 1 then vals[0]
else vals
end
end
obj
end # self._unserialize
def self._do_serialize (obj, opts, segments, prefix = %q{})
old_prefix = prefix
prefix += opts[:nm_deli] if not prefix.empty?
case obj
when Hash
obj.each do |k,v|
k = k.is_a?(Symbol) ? k.to_s + ':' : k
k = _escape(k.to_s, opts[:nm_deli], opts[:nm_deli_tr])
_do_serialize(v, opts, segments, "#{prefix}#{k}")
end
when Array
need_concat = true
tmp = []
obj.each_index do |i|
v = obj[i]
_do_serialize(v, opts, tmp, "#{prefix}#{i.to_s}")
need_concat = false if v.is_a?(Hash) || v.is_a?(Array)
end
if need_concat and not old_prefix.empty? then
segments.push([ old_prefix,
tmp.map{ |v| v[1] }.join(opts[:arr_deli])
])
else
segments.concat(tmp)
end
else
segments.push([ old_prefix, _escape_val(obj.to_s, opts) ])
end
end # self._do_serialize
def self._serialize (obj, opts)
segments = []
_do_serialize(obj, opts, segments)
opts[:order].(segments).map{ |v|
v.join(opts[:kv_deli])
}.join(opts[:fld_deli])
end # self._serialize
end # module PSV
end # module LZSoft
相關文章
- [實踐] 為原型系統設計簡單的純文字資料交換協議原型協議
- 系統設計實踐(02)- 文字儲存服務
- 簡單智慧手機原型設計原型
- 簡單的資料表統計
- MQTT協議實踐MQQT協議
- 實時計算,流資料處理系統簡介與簡單分析
- RUBY實踐—資料庫簡單操作資料庫
- 資料共享交換平臺的實踐分享
- 簡單的RPC程式設計實踐——HelloWorld的實現RPCC程式程式設計
- 原型設計工具比較及實踐原型
- 系統架構設計筆記(95)—— TCP 協議架構筆記TCP協議
- 簡單而重要的協議:ICMP協議
- 從Exchager資料交換到基於trade-off的系統設計
- SPI通訊協議 的移位暫存器資料交換過程協議
- 邏輯資料庫設計 - 單純的樹(遞迴關係資料)資料庫遞迴
- 簡單實用的客戶關係管理系統(CRM),在設計上力求簡單、實用。
- 史上最簡單的推薦系統設計
- 設計一個簡單的devops系統dev
- CISCO交換機STP實驗(生成樹協議)協議
- 簡單談談DNS協議DNS協議
- 《圖解HTTP》——簡單的HTTP協議圖解HTTP協議
- 《圖解HTTP》—簡單的HTTP協議圖解HTTP協議
- 02 前端HTTP協議(圖解HTTP) 之 簡單的HTTP協議前端HTTP協議圖解
- TCP對應的協議和UDP對應的協議(簡單概述)TCP協議UDP
- 資料管理系統設計和實現
- 裝飾/原型/外觀設計模式簡單理解原型設計模式
- 實用TCP協議(1):TCP 協議簡介TCP協議
- QUIC 協議初探 - iOS 實踐UI協議iOS
- Javaweb的例項--訂單管理系統--設計資料庫JavaWeb資料庫
- 資料庫設計簡單入門資料庫
- ModbusTCP協議簡介與程式設計流程圖TCP協議程式設計流程圖
- 使用go net實現簡單的redis通訊協議YWSVGoRedis協議
- 簡單的BBS論壇 資料庫設計資料庫
- 簡單的實現一個原型鏈原型
- http 協議集合,超級簡單HTTP協議
- ios 面向協議程式設計資源iOS協議程式設計
- 實驗一原型設計--背單詞APP原型APP
- 58同城敏捷BI系統的設計與實踐敏捷