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

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

由於 DSL 以及 DSL 的界定自己就是一個比較模糊的概念,因此不免有與他人觀點意見相左的地方,若是有不一樣的意見,咱們能夠具體討論。html

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

沒有銀彈?

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

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.git

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

silver-bullet

而今天要介紹的 DSL 就能夠真正的提高生產力,減小沒必要要的工做,在一些領域幫助咱們更快的實現需求。正則表達式

DSL 是什麼?

筆者是在兩年之前,在大一的一次分享上聽到 DSL 這個詞的,可是當時並無對這個名詞有多深的理解與認識,聽過也就忘記了,可是最近作的一些開源項目讓我從新想起了 DSL,也是此次分享題目的由來。數據庫

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

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 判斷當前字符串跟正則表達式是否匹配。 regex
  • SQL
    • SQL 語句在使用時也並無真正的執行,咱們輸入的 SQL 語句最終還要交給數據庫來進行處理,數據庫會從 SQL 語句中讀取有用的信息,而後從數據庫中返回使用者指望的結果。
  • HTML & CSS
    • HTML 和 CSS 只是對 Web 界面的結構語義和樣式進行描述,雖然它們在構建網站時很是重要,可是它們並不是是一種編程語言,正相反,咱們能夠認爲 HTML 和 CSS 是在 Web 中的領域特定語言。

Features

上面的幾個🌰明顯的縮小了通用編程語言的概念,可是它們確實在本身領域表現地很是出色,由於這些 DSL 就是根據某一個特定領域的特色塑造的;而通用編程語言相比領域特定語言,在設計時是爲了解決更加抽象的問題,而關注點並不僅是在某一個領域。

上面的幾個例子有着一些共同的特色:

  • 沒有計算和執行的概念;
  • 其自己並不須要直接表示計算;
  • 使用時只須要聲明規則、事實以及某些元素之間的層級和關係;

雖然瞭解了 DSL 以及 DSL 的一些特性,可是,到目前爲止,咱們對於如何構建一個 DSL 仍然不是很清楚。

構建 DSL

DSL 的構建與編程語言其實比較相似,想一想咱們在從新實現編程語言時,須要作那些事情;實現編程語言的過程能夠簡化爲定義語法與語義,而後實現編譯器或者解釋器的過程,而 DSL 的實現與它也很是相似,咱們也須要對 DSL 進行語法與語義上的設計。

compile

總結下來,實現 DSL 總共有這麼兩個須要完成的工做:

  1. 設計語法和語義,定義 DSL 中的元素是什麼樣的,元素表明什麼意思
  2. 實現 parser,對 DSL 解析,最終經過解釋器來執行

以 HTML 爲例,HTML 中全部的元素都是包含在尖括號 <> 中的,尖括號中不一樣的元素表明了不一樣的標籤,而這些標籤會被瀏覽器解析成 DOM 樹,再通過一系列的過程調用 Native 的圖形 API 進行繪製。

dom-tree

再好比,咱們使用下面這種方式對一個模型進行定義,實現一個 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 並非真正編程語言的問題。

css-sass

在當今的 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

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

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

咱們沒有必要爭論哪些是框架,哪些是 DSL,由於這些爭論並無什麼意義。

Rails 和 Embedded DSL

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

rails

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
 => [:%, :&, :*, :+, :-, :/, :<, :>, :^, :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :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 也很是簡單,使用 doend 或者 {} 就能夠向任何的 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 的過程,定義一系列必要的方法,好比 sourcepod 等等,創造一個執行的上下文;而後去讀存儲 DSL 的文件,而且使用 eval 執行。

信息的傳遞通常都是經過參數來進行的,好比:

source 'https://github.com/CocoaPods/Specs.git'複製代碼

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

實現

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

source 'http://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複製代碼

由於這裏的 sourceplatformtarget 以及 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"=>"http://source.git", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]}複製代碼

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

寫在後面

在最後,筆者想說的是,當咱們在某一個領域常常須要解決重複性問題時,能夠考慮實現一個 DSL 專門用來解決這些相似的問題。

而使用嵌入式 DSL 來解決這些問題是一個很是好的辦法,咱們並不須要從新實現解釋器,也能夠利用宿主語言的抽象能力。

同時,在嵌入式 DSL 擴展了 DSL 的範疇以後,不要糾結於某些東西究竟是框架仍是領域特定語言,這些都不重要,重要的是,在遇到了某些問題時,咱們可否跳出來,使用文中介紹的方法減輕咱們的工做量。

Reference

其它

GitHub Repo:iOS-Source-Code-Analyze

Follow: Draveness · GitHub

Source: draveness.me/dsl

相關文章
相關標籤/搜索