談談 DSL 以及 DSL 的應用(以 CocoaPods 為例)

最近在公司做了一次有關 DSL 在 iOS 開發中的應用的分享,這篇文章會簡單介紹這次分享的內容。


因為 DSL 以及 DSL 的界定本身就是一個比較模糊的概念,所以難免有與他人觀點意見相左的地方,如果有不同的意見,我們可以具體討論。

這次文章的題目雖然是談談 DSL 以及 DSL 的應用,不過文章中主要側重點仍然是 DSL,會簡單介紹 DSL 在 iOS 開發中(CocoaPods)是如何應用的。


沒有銀彈?

1987 年,IBM 大型電腦之父 Fred Brooks 發表了一篇關于軟件工程中的論文 No Silver Bullet—Essence and Accidents of Software Engineering 文中主要圍繞這么一個觀點:沒有任何一種技術或者方法能使軟件工程的生產力在十年之內提高十倍。


There is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in productivity, in reliability, in simplicity.

時至今日,我們暫且不談銀彈在軟件工程中是否存在(這句話在老板或者項目經理要求加快項目進度時,還是十分好用的),作為一個開發者也不是很關心這種抽象的理論,我們更關心的是開發效率能否有實質的提升。



而今天要介紹的 DSL 就可以真正的提升生產力,減少不必要的工作,在一些領域幫助我們更快的實現需求。


DSL 是什么?

筆者是在兩年以前,在大一的一次分享上聽到 DSL 這個詞的,但是當時并沒有對這個名詞有多深的理解與認識,聽過也就忘記了,但是最近做的一些開源項目讓我重新想起了 DSL,也是這次分享題目的由來。

DSL 其實是 Domain Specific Language 的縮寫,中文翻譯為領域特定語言(下簡稱 DSL);而與 DSL 相對的就是 GPL,這里的 GPL 并不是我們知道的開源許可證,而是 General Purpose Language 的簡稱,即通用編程語言,也就是我們非常熟悉的 Objective-C、Java、Python 以及 C 語言等等。


Wikipedia 對于 DSL 的定義還是比較簡單的:


A specialized computer language designed for a specific task.


為了解決某一類任務而專門設計的計算機語言。

與 GPL 相對,DSL 與傳統意義上的通用編程語言 C、Python 以及 Haskell 完全不同。通用的計算機編程語言是可以用來編寫任意計算機程序的,并且能表達任何的可被計算的邏輯,同時也是 圖靈完備 的。


這一小節中的 DSL 指外部 DSL,下一節中會介紹 內部 DSL/嵌入式 DSL

但是在里所說的 DSL 并不是圖靈完備的,它們的表達能力有限,只是在特定領域解決特定任務的。


A computer programming language of limited expressiveness focused on a particular domain.

另一個世界級軟件開發大師 Martin Fowler 對于領域特定語言的定義在筆者看來就更加具體了,DSL 通過在表達能力上做的妥協換取在某一領域內的高效。


而有限的表達能力就成為了 GPL 和 DSL 之間的一條界限。


幾個栗子

最常見的 DSL 包括 Regex 以及 HTML & CSS,在這里會對這幾個例子進行簡單介紹


Regex

正則表達式僅僅指定了字符串的 pattern,其引擎就會根據 pattern 判斷當前字符串跟正則表達式是否匹配。

SQL

SQL 語句在使用時也并沒有真正的執行,我們輸入的 SQL 語句最終還要交給數據庫來進行處理,數據庫會從 SQL 語句中讀取有用的信息,然后從數據庫中返回使用者期望的結果。

HTML & CSS

HTML 和 CSS 只是對 Web 界面的結構語義和樣式進行描述,雖然它們在構建網站時非常重要,但是它們并非是一種編程語言,正相反,我們可以認為 HTML 和 CSS 是在 Web 中的領域特定語言。

Features

上面的幾個?明顯的縮小了通用編程語言的概念,但是它們確實在自己領域表現地非常出色,因為這些 DSL 就是根據某一個特定領域的特點塑造的;而通用編程語言相比領域特定語言,在設計時是為了解決更加抽象的問題,而關注點并不只是在某一個領域。


上面的幾個例子有著一些共同的特點:


沒有計算和執行的概念;

其本身并不需要直接表示計算;

使用時只需要聲明規則、事實以及某些元素之間的層級和關系;

雖然了解了 DSL 以及 DSL 的一些特性,但是,到目前為止,我們對于如何構建一個 DSL 仍然不是很清楚。


構建 DSL

DSL 的構建與編程語言其實比較類似,想想我們在重新實現編程語言時,需要做那些事情;實現編程語言的過程可以簡化為定義語法與語義,然后實現編譯器或者解釋器的過程,而 DSL 的實現與它也非常類似,我們也需要對 DSL 進行語法與語義上的設計。


總結下來,實現 DSL 總共有這么兩個需要完成的工作:


設計語法和語義,定義 DSL 中的元素是什么樣的,元素代表什么意思

實現 parser,對 DSL 解析,最終通過解釋器來執行

以 HTML 為例,HTML 中所有的元素都是包含在尖括號 <> 中的,尖括號中不同的元素代表了不同的標簽,而這些標簽會被瀏覽器解析成 DOM 樹,再經過一系列的過程調用 Native 的圖形 API 進行繪制。



再比如,我們使用下面這種方式對一個模型進行定義,實現一個 ORM 領域的 DSL:


define :article do

  attr :name

  attr :content

  attr :upvotes, :int

  

  has_many :comments

end

在上面的 DSL 中,使用 define 來定義一個新的模型,使用 attr 來為模型添加屬性,使用 has_many 建立數據模型中的一對多關系;我們可以使用 DSL 對這段“字符串”進行解析,然后交給代碼生成器來生成代碼。


public struct Article {

    public var title: String

    public var content: String

    public var createdAt: Date

    

    public init(title: String, content: String, createdAt: Date)


    static public func new(title: String, content: String, createdAt: Date) -> Article

    static public func create(title: String, content: String, createdAt: Date) -> Article?

    ...

}

這里創建的 DSL 中的元素數量非常少,只有 define attr 以及 has_many 等幾個關鍵字,但是通過這幾個關鍵字就可以完成在模型層需要表達的絕大部分語義。


設計原則和妥協

DSL 最大的設計原則就是簡單,通過簡化語言中的元素,降低使用者的負擔;無論是 Regex、SQL 還是 HTML 以及 CSS,其說明文檔往往只有幾頁,非常易于學習和掌握。但是,由此帶來的問題就是,DSL 中缺乏抽象的概念,比如:??榛?、變量以及方法等。


抽象的概念并不是某個領域所關注的問題,就像 Regex 并不需要有???、變量以及方法等概念。

由于抽象能力的缺乏,在我們的項目規模變得越來越大時,DSL 往往滿足不了開發者的需求;我們仍然需要編程語言中的??榛雀拍疃?DSL 進行補充,以此解決 DSL 并不是真正編程語言的問題。



在當今的 Web 前端項目中,我們在開發大規模項目時往往不會直接手寫 CSS 文件,而是會使用 Sass 或者 Less 為 CSS 帶來更強大的抽象能力,比如嵌套規則,變量,混合以及繼承等特性。


nav {

  ul {

    margin: 0;

    padding: 0;

    list-style: none;

  }


  li { display: inline-block; }


  a {

    display: block;

    padding: 6px 12px;

    text-decoration: none;

  }

}

也就是說,在使用 DSL 的項目規模逐漸變大時,開發者會通過增加抽象能力的方式,對已有的 DSL 進行拓展;但是這種擴展往往需要重新實現通用編程語言中的特性,所以一般情況下都是比較復雜的。


Embedded DSL(嵌入式 DSL)

那么,是否有一種其它的方法為 DSL 快速添加抽象能力呢?而這也就是這一小節的主題,嵌入式 DSL。


在上一節講到的 DSL 其實可以被稱為外部 DSL;而這里即將談到的嵌入式 DSL 也有一個別名,內部 DSL。


這兩者最大的區別就是,內部 DSL 的實現往往是嵌入一些編程語言的,比如 iOS 的依賴管理組件 CocoaPods 和 Android 的主流編譯工具 Gradle,前者的實現是基于 Ruby 語言的一些特性,而后者基于 Groovy。



CocoaPods 以及其它的嵌入式 DSL 使用了宿主語言(host language)的抽象能力,并且省去了實現復雜語法分析器(Parser)的過程,并不需要重新實現???、變量等特性。


嵌入式 DSL 的產生其實模糊了框架和 DSL 的邊界,不過這兩者看起來也沒有什么比較明顯的區別;不過,DSL 一般會使用宿主語言的特性進行創造,在設計 DSL 時,也不會考慮宿主語言中有哪些 API 以及方法,而框架一般都是對語言中的 API 進行組合和再包裝。


我們沒有必要爭論哪些是框架,哪些是 DSL,因為這些爭論并沒有什么意義。

Rails 和 Embedded DSL

最出名也最成功的嵌入式 DSL 應該就是 Ruby on Rails 了,雖然對于 Rails 是否是 DSL 有爭議,不過 Rails 為 Web 應用的創建提供大量的內置的支撐,使我們在開發 Web 應用時變得非常容易。



Ruby、 DSL 和 iOS

為了保證這篇文章的完整性,這一小節中有的一些內容都出自上一篇文章 CocoaPods 都做了什么?。

筆者同時作為 iOS 和 Rails 開發者接觸了非常多的 DSL,而在 iOS 開發中最常見的 DSL 就是 CocoaPods 了,而這里我們以 CocoaPods 為例,介紹如何使用 Ruby 創造一個嵌入式 DSL。


Why Ruby?

看到這里有人可能會問了,為什么使用 Ruby 創造嵌入式 DSL,而不是使用 C、Java、Python 等等語言呢,這里大概有四個原因:


一切皆對象的特性減少了語言中的元素,不存在基本類型、操作符;

向 Ruby 方法中傳入代碼塊非常方便;

作為解釋執行的語言,eval 模糊了數據和代碼的邊界;

不對代碼的格式進行約束,同時一些約定減少了代碼中的噪音。

一切皆對象


在許多語言,比如 Java 中,數字與其他的基本類型都不是對象,而在 Ruby 中所有的元素,包括基本類型都是對象,同時也不存在運算符的概念,所謂的 1 + 1,其實只是 1.+(1) 的語法糖而已。


得益于一切皆對象的概念,在 Ruby 中,你可以向任意的對象發送 methods 消息,在運行時自省,所以筆者在每次忘記方法時,都會直接用 methods 來“查閱文檔”:


2.3.1 :003 > 1.methods

 => [:%, :&, :*, :+, :-, :/, :<, :>, :^, :|, :~, :[email protected], :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_s, :to_f, :div, :divmod, :fdiv, :modulo, ...]

比如在這里向對象 1 調用 methods 就會返回它能響應的所有方法。


一切皆對象不僅減少了語言中類型的數量,消滅了基本數據類型與對象之間的邊界;這一概念同時也簡化了組成語言的元素,這樣 Ruby 中只有對象和方法,這兩個概念,極大降低了這門語言的復雜度:


使用對象存儲狀態

對象之間通過方法通信

block


Ruby 對函數式編程范式的支持是通過 block,這里的 block 和 Objective-C 中的 block 有些不同。


首先 Ruby 中的 block 也是一種對象,即 Proc 類的實例,也就是所有的 block 都是 first-class 的,可以作為參數傳遞,返回。


下面的代碼演示了兩種向 Ruby 方法中傳入代碼塊的方式:


def twice(&proc)

    2.times { proc.call() } if proc

end


def twice

    2.times { yield } if block_given?

end

yield 會調用外部傳入的 block,block_given? 用于判斷當前方法是否傳入了 block。


twice do 

    puts "Hello"

end


twice { puts "hello" }

向 twice 方法傳入 block 也非常簡單,使用 do、end 或者 {、} 就可以向任何的 Ruby 方法中傳入代碼塊。


eval


早在幾十年前的 Lisp 語言就有了 eval 這個方法,這個方法會將字符串當做代碼來執行,也就是說 eval 模糊了代碼與數據之間的邊界。


> eval "1 + 2 * 3"

 => 7

有了 eval 方法,我們就獲得了更加強大的動態能力,在運行時,使用字符串來改變控制流程,執行代碼并可以直接利用當前語言的解釋器;而不需要去手動解析字符串然后執行代碼。


格式和約定


編寫 Ruby 腳本時并不需要像 Python 一樣對代碼的格式有著嚴格的規定,沒有對空行、Tab 的要求,完全可以想怎么寫就怎么寫,這樣極大的增加了 DSL 設計的可能性。


同時,在一般情況下,Ruby 在方法調用時并不需要添加括號:


puts "Wello World!"

puts("Hello World!")

這樣減少了 DSL 中的噪音,能夠幫助我們更加關心語法以及語義上的設計,降低了使用者出錯的可能性。


最后,Ruby 中存在一種特殊的數據格式 Symbol:


> :symbol.to_s

 => "symbol"

> "symbol".to_sym

 => :symbol

Symbol 可以通過 Ruby 中內置的方法與字符串之間無縫轉換。那么作為一種字符串的替代品,它的使用也能夠降低使用者出錯的成本并提升使用體驗,我們并不需要去寫兩邊加上引號的字符串,只需要以 : 開頭就能創建一個 Symbol 對象。


Podfile 是什么

對 Ruby 有了一些了解之后,我們就可以再看一下使用 CocoaPods 的工程中的 Podfile 到底是什么了:


source 'https://github.com/CocoaPods/Specs.git'


target 'Demo' do

    pod 'Mantle', '~> 1.5.1'

    ...

end

如果不了解 iOS 開發后者沒有使用過 CocoaPods,筆者在這里簡單介紹一下這個文件中的一些信息。


source 可以看作是存儲依賴元信息(包括依賴的對應的 GitHub 地址)的源地址;


target 表示需要添加依賴的工程的名字;


pod 表示依賴,Mantle 為依賴的框架,后面是版本號。

上面是一個使用 Podfile 定義依賴的一個例子,不過 Podfile 對約束的描述其實是這樣的:


source('https://github.com/CocoaPods/Specs.git')


target('Demo') do

    pod('Mantle', '~> 1.5.1')

    ...

end

Podfile 中對于約束的描述,其實都可以看作是代碼的簡寫,在解析時會當做 Ruby 代碼來執行。


簡單搞個 Embedded DSL

使用 Ruby 實現嵌入式 DSL 一般需要三個步驟,這里以 CocoaPods 為例進行簡單介紹:


創建一個 Podfile 中“代碼”執行的上下文,也就是一些方法;

讀取 Podfile 中的內容到腳本中;

使用 eval 在上下文中執行 Podfile 中的“代碼”;

原理


CocoaPods 對于 DSL 的實現基本上就是我們創建一個 DSL 的過程,定義一系列必要的方法,比如 source、pod 等等,創造一個執行的上下文;然后去讀存儲 DSL 的文件,并且使用 eval 執行。


信息的傳遞一般都是通過參數來進行的,比如:


source 'https://github.com/CocoaPods/Specs.git'

source 方法的參數就是依賴元信息 Specs 的 Git 地址,在 eval 執行時就會被讀取到 CocoaPods 中,然后進行分析。


實現


下面是一個非常常見的 Podfile 內容:


source '//source.git'

platform :ios, '8.0'


target 'Demo' do

    pod 'AFNetworking'

    pod 'SDWebImage'

    pod 'Masonry'

    pod "Typeset"

    pod 'BlocksKit'

    pod 'Mantle'

    pod 'IQKeyboardManager'

    pod 'IQDropDownTextField'

end

因為這里的 source、platform、target 以及 pod 都是方法,所以在這里我們需要構建一個包含上述方法的上下文:


# eval_pod.rb

$hash_value = {}


def source(url)

end


def target(target)

end


def platform(platform, version)

end


def pod(pod)

end

使用一個全局變量 hash_value 存儲 Podfile 中指定的依賴,并且構建了一個 Podfile 解析腳本的骨架;我們先不去完善這些方法的實現細節,先嘗試一下讀取 Podfile 中的內容并執行 eval 看看會不會有問題。


在 eval_pod.rb 文件的最下面加入這幾行代碼:


content = File.read './Podfile'

eval content

p $hash_value

這里讀取了 Podfile 文件中的內容,并把其中的內容當做字符串執行,最后打印 hash_value 的值。


$ ruby eval_pod.rb

運行這段 Ruby 代碼雖然并沒有什么輸出,但是并沒有報出任何的錯誤,接下來我們就可以完善這些方法了:


def source(url)

    $hash_value['source'] = url

end


def target(target)

    targets = $hash_value['targets']

    targets = [] if targets == nil

    targets << target

    $hash_value['targets'] = targets

    yield if block_given?

end


def platform(platform, version)

end


def pod(pod)

    pods = $hash_value['pods']

    pods = [] if pods == nil

    pods << pod

    $hash_value['pods'] = pods

end

在添加了這些方法的實現之后,再次運行腳本就會得到 Podfile 中的依賴信息了,不過這里的實現非常簡單的,很多情況都沒有處理:


$ ruby eval_pod.rb

{"source"=>"//source.git", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]}

不過使用 Ruby 構建一個嵌入式 DSL 的過程大概就是這樣,使用語言內建的特性來進行創作,創造出一個在使用時看起來并不像代碼的 DSL。


寫在后面

在最后,筆者想說的是,當我們在某一個領域經常需要解決重復性問題時,可以考慮實現一個 DSL 專門用來解決這些類似的問題。


而使用嵌入式 DSL 來解決這些問題是一個非常好的辦法,我們并不需要重新實現解釋器,也可以利用宿主語言的抽象能力。


同時,在嵌入式 DSL 擴展了 DSL 的范疇之后,不要糾結于某些東西到底是框架還是領域特定語言,這些都不重要,重要的是,在遇到了某些問題時,我們能否跳出來,使用文中介紹的方法減輕我們的工作量。


來源:segmentfault

上一篇: 蘋果更新Safari技術預覽 帶來TouchBar支持

下一篇: 微信 iOS SQLite 源碼優化實踐

分享到: 更多
qq游戏欢乐二人雀神 重庆时时彩龙虎口诀 抢庄牛牛牛安卓版下载 抢庄斗牛看牌 时时彩5星玩法最稳赚 北京pk10哪种最稳 三公扑克牌出千技巧 皇家彩世界 开奖软件下载 快三计划软件怎么下载地址 彩名堂计划软件# 大乐透最近200期 双色球中奖查询 福彩七乐彩开奖走势图 三分pk10在线计划网站 北京pk赛车4码计划最准 双色球为什么延迟开奖