白話 Ruby 與 DSL 以及在 iOS 開發中的運用

閱讀本文不須要預先掌握 Ruby 與 DSL 相關的知識html

何爲 DSL

DSL(Domain Specific Language) 翻譯成中文就是:「領域特定語言」。首先,從定義就能夠看出,DSL 也是一種編程語言,只不過它主要是用來處理某個特定領域的問題。ios

廣爲人知的編程語言有 C、Java、PHP 等,他們被稱爲 GPL(General Purpose Language),即通用目的語言。與這些語言相比,DSL 相對顯得比較神祕,他們中的大多數甚至連一個名字都沒有。這主要是由於 DSL 一般用來處理某個特定的、很小的領域中的問題,所以起名字這事沒有太大的必要和意義。git

說了這麼多廢話, 必定有讀者在想:「能不能舉個例子講解一下,什麼是 DSL」。實際上,DSL 只是對一類語言的描述,它能夠很是簡單:github

UIView (0, 0, 100, 100) black
UILabel (50, 50, 200, 200) yellow
……複製代碼

好比這就是我本身隨便編的一個語言。它的語法看上去很奇怪,不過這不是重點。語言的根本目的是傳遞信息。數據庫

爲何要用 DSL

其實從上面的代碼中已經能夠比較出 DSL 和 GPL 的特色了。DSL 語法更加簡潔,好比能夠沒有括號(這取決於你如何設計),所以開發、閱讀的效率更高。但做爲代價,DSL 調試很麻煩,很難作類型檢查,所以幾乎不可思議能夠用 DSL 開發一個大型的程序。編程

若是同時接觸過編譯型語言和腳本語言,你能夠把 DSL 理解爲一種比腳本語言更加輕量、靈活的語言。swift

DSL 的執行過程

瞭解過 C 語言的開發者應該知道,從 C 語言源碼到最後的可執行文件,須要通過預編譯、編譯(詞法分析、語法分析、語義分析)、彙編、連接等步驟,最終生成 CPU 相關的機器碼,也就是一堆 0 和 1。ruby

腳本語言不須要編譯(有些也能夠編譯),他們在運行時被解釋,固然也須要作詞法分析和語法分析,最終生成機器碼。閉包

因而問題來了,自定義的 DSL 如何被執行呢?app

對於詞法分析和語法分析,因爲語言簡單,一般只是少數關鍵字,即便使用最簡單的字符串解析,工做量和複雜度也在可接受的範圍內。然而最後生成彙編代碼就顯得不是頗有必要了,DSL 的特色不是追求執行效率,而是高效,對開發者友好。

所以一種常見的作法是,用別的語言(能夠理解爲宿主語言)來解析 DSL,並執行宿主語言。繼續以上面的 DSL 爲例,咱們能夠用 OC 讀取這個文本文件,瞭解到咱們要建立一個 UIView 對象,所以會執行如下代碼:

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view.backgroundColor = [UIColor blackColor];複製代碼

如何實現 DSL

能夠看到,DSL 的定義與實現毫無技術難度可言,與其說是一門語言,不如說是一種信息的標記格式,一種存儲信息的協議。從這個角度來講,JSON、XML 等數據格式也能夠被稱爲 DSL。

然而,隨着關鍵字數量的增多,對 DSL 的解析難度迅速提升。舉個簡單的例子,Cocoa 框架下的控件類型有不少,所以在解析上述 DSL 時就須要考慮不少狀況。這顯然與 DSL 的初衷不符。

有沒有一種快速實現 DSL 的方法呢?選擇 ruby 必定程度上能夠解決上述問題。在解釋爲何恰恰選擇 ruby 以前,首先介紹一些基礎知識。

Ruby

這篇文章不是用來介紹 Ruby 語法的,感興趣的讀者能夠閱讀 《七週七語言》 或者 《松本行弘的程序世界》這兩本書的前面幾個章節來入門 Ruby,進階教程推薦 《Ruby 元編程》。

本文主要介紹爲什麼 Ruby 常常做爲宿主語言,被用來實現 DSL,用一句話歸納就是:

DSL 其實就是 Ruby 代碼

上文說過,實現 DSL 的主要難度在於利用宿主語言解析 DSL 的語法,而藉助 Ruby 實現的 DSL,其自己就是 Ruby 代碼,只是看起來比較像 DSL。這樣在執行的時候,咱們徹底藉助了 Ruby 解釋器的力量,而不須要手動分析其中的語法結構。

借用 Creating a Ruby DSL 這篇文章中的例子,假設咱們想寫一段 HTML 代碼:

<html>
  <body>
    <div id="container">
      <ul class="pretty">
        <li class="active">Item 1</li>
        <li>Item 2</li>
      </ul>
    </div>
  </body>
</html>複製代碼

但又感受手寫代碼太麻煩,但願簡化它,因此使用一個自創的 DSL:

html = HTMLMaker.new.document do
  body do
    div id: "container" do
      ul class: "pretty" do
        li "Item 1", class: :active
        li "Item 2"
      end
    end
  end
end
# 這個 html 變量是一個字符串,值就是上面的 HTML 文檔複製代碼

不熟悉 Ruby 語法的讀者可能沒法一眼看出這段看上去像是文本的內容,實際上是 Ruby 代碼。爲何恰恰是 Ruby,而不是 Objective-C 或者 C++ 這些語言呢?我總結爲如下兩點:

  1. Ruby 自身的語法特性
  2. Ruby 具有元編程的能力

語法簡介

首先,Ruby 調用函數能夠不用把參數放在括號中:

def say(word)
  puts word
end

say "Hello"
# 調用 say 函數會輸出 "Hello"複製代碼

這就保證了語法的簡潔,看上去像是一門 DSL。

另外要提到的一點是 Ruby 中的閉包。與 Objective-C 和 Swift 不一樣的是,Ruby 的閉包能夠寫在 do … end 代碼塊,而不是必須放在大括號中:

(1..10).each do |i|
  puts "Number = #{i}"
end

# 輸出十行,每行的格式都是 "Number = i"複製代碼

大括號看上去就像是一門比較複雜的語言,而 do … end 會更容易閱讀一些。

Ruby 元編程

元編程是 Ruby 的精髓之一。咱們見過不少以「元」開頭的單詞,好比 「元數據」、「元類」、「元信息」。這些詞彙看上去很難理解,其實只要把 「元xx」 當作 「關於xx的xx」,就很容易理解了。

以元數據爲例,它表示「關於數據的數據」。好比個人 ID 是 bestswifter,它是一個數據。我還能夠說這個單詞中有兩個字母 s,一共有 11 個字母等等。這些也是數據,而且是關於數據(bestswifter)的數據,所以能夠被稱爲元數據。

在 runtime 中常常提到的元類,也就是關於類的類。因此存儲了類的方法和屬性。

而所謂的元編程,天然指的就是「關於編程的編程」。編程是指用一段代碼輸出某個結果,而關於編程的編程則能夠理解爲經過編程的方式來產生這段代碼。

在實際開發時,元編程一般以兩種形式體現出他的威力:

  1. 提供反射的功能,經過 API 來提供對運行時環境的訪問和修改能力
  2. 提供執行字符串格式代碼的能力

在 Ruby 中,咱們能夠隨意爲任何一個類添加、修改甚至刪除方法。調用不存在方法時,能夠統一進行轉發:

class TestMissing
  def method_missing(m, *args, &block)
    puts "方法名:#{m},參數:#{args},閉包:#{block}"
  end
end

TestMissing.new.say "Hello", "World" do
  puts "Hello, world"
end
# 方法名:say,參數:["Hello", "World"],閉包:#<Proc:0x007feeea03cb00@t.ruby:7>複製代碼

可見,當調用不存在的方法 say 時,會被轉發到類的 method_missing 方法中,而且能夠很容易的獲取到方法名稱和參數。

有必定 iOS 開發經驗的讀者會馬上想到,這哪是元編程,明明就是 runtime。確實,相比於靜態語言好比 Java、Swift 的反射機制而言,Objective-C 的 runtime 提供了更強大的功能,它不只能夠自省,還能動態的進行修改。固然這也是由語言特性決定的,對於靜態語言來講,早在編譯時期就生成了機器碼,而且隨後進行連接,能提供一個反射機制就很不錯了,至於修改仍是不要奢望。

實際上,若是咱們廣義的把元編程理解爲:「關於編程的編程」,那麼 runtime 能夠理解爲一種元編程的實現方式。若是狹義的把元編程理解爲用代碼生成代碼,而且動態執行,那 runtime 就不算了。

利用 Ruby 實現 DSL

分別介紹了 DSL 和 Ruby 的基礎概念後,咱們就能夠着手利用 Ruby 來實現本身的 DSL 了。

以上文生成 HTML 的 DSL 爲例進行分析,爲了說明問題,我把代碼再次簡化一下:

html = HTMLMaker.new.document do
  body do
    div id: "container"
  end
end複製代碼

首先咱們要定義一個 HTMLMaker 類,而且把 document 方法做爲入口。這個方法接收一個閉包,閉包中調用 body 函數,這個函數也提供了閉包,閉包中調用了 div 方法,而且有一個參數 id: "container"……

可見這實際上是一個遞歸調用,不管是 body 仍是 div,他們對應着 HTML 標籤,其實都是一些並列的方法,方法能夠接受若干個鍵值對,也就是 HTML 中標籤的屬性,最後再跟上一個閉包用來建立隸屬於本身的子標籤。

若是不用 Ruby,咱們須要事先知道全部的 HTML 標籤名,而後進行匹配,可想工做量有多大。而在 Ruby 中,他們都是並列關係,能夠統一轉發到 method_missing 方法中,獲取方法名、參數和閉包。

咱們首先解析參數,配合方法名拼湊出當前標籤的字符串,而後遞歸調用閉包便可,核心代碼以下:

def method_missing(m, *args, &block)
    tag(m, args, &block)
end

def tag(html_tag, args, &block)
  # indent 用來記錄行首的空格縮進
  # options 表示解析後的 HTML 屬性,好比 id="container", content 則是標籤中的內容
  html << "\n#{indent}<#{html_tag}#{options}>#{content}"
  if block_given? # 若是傳遞了閉包,遞歸執行
    instance_eval(&block) # 遞歸執行閉包
    html << "\n#{indent}"
  end
  html << "</#{html_tag}>"
end複製代碼

這裏的 instance_eval 也是一種元編程,表示以當前實例爲上下文,執行閉包,具體用途能夠參考這篇文章: Eval, module_eval, and instance_eval

完整的代碼意義不大,主要是細節的處理,若是感興趣,或者沒有徹底理解上面這段代碼的意思,能夠去原文中查看代碼,而且自行調試。

Ruby 在 iOS 開發中的運用

Ruby 主要用來實現一些自動化腳本,並且因爲 iOS 系統上沒有 Ruby 解釋器,因此它一般是在 Mac 系統上使用,在編譯前(絕非 app 的運行時)進行一些自動化工做。

你們最熟悉的 Cocoapods 的 podfile 其實就是一份 Ruby 代碼:

target 'target_name' do
    pod 'pod_name', '~> version'
end複製代碼

熟悉的 do end 代碼塊告訴咱們,這段聲明式的 pod 依賴關係,其實就是能夠執行的 ruby 代碼。Cocoapods 的具體實現原理能夠參考 @draveness 的這篇文章: CocoaPods 都作了什麼?

在 Ruby On Rails 中有一個著名的模塊: ActiveRecord,它提供了對象關係映射的功能(ORM)。

在面向對象的語言中,咱們用對象來存儲數據,對象是類的實例,而在關係型數據庫中,數據的抽象模型叫實體(Entity)。類和實體在必定程度上有類似性,好比均可以擁有多個屬性,類對屬性的增刪改查操做由類對外暴露的方法實現,在關係型數據庫中則是由 SQL 語句實現。

ORM 提供了對象和實體之間的對應關係,咱們再也不須要手寫 SQL 語句,而是直接調用對象的相關方法, 這些方法的內部會生成相應的 SQL 語句並執行。能夠說 ORM 框架屏蔽了數據庫的具體細節, 容許咱們以面向對象的方式對數據進行持久化操做。

MetaModel

MetaModel 這個框架借鑑了 ActiveRecord 的功能,致力於打造一個 iOS 開發中的 ORM 框架。

在沒有 ORM 時,假設有一個 Person 類,它有若干個屬性。即便咱們利用繼承等面向對象的特性封裝好了大量模板方法,每當增長或刪除屬性時,代碼改動量依然不算小。考慮到實體之間還有一對1、一對多、多對多等關係,一旦關係發生變化,相關代碼的變化會更大。

MetaModel 的原理就是利用 ruby 實現了一個 DSL,在 DSL 中規定了每一個實體的屬性和關係,這也是開發者惟一須要關心的內容。接下來的任務將徹底由 MetaModel 負責,首先它會解析每一個實體有哪些屬性,和別的實體有哪些關係,而後生成對應的 Swift/Objective-C 代碼,打包成靜態庫,最終以面向對象的方式向開發者暴露增刪改查的 API。

在實際使用時,咱們首先要寫一個 Metafile 文件,它相似於 Podfile,用於規定實體的屬性和關係:

define :Article do
  attr :title
  attr :content

  has_many :comments
end

define :Comment do
  attr :content

  belongs_to :article
end複製代碼

執行完 MetaModel 的腳本後,就會生成相關代碼,並封裝在靜態庫中,而後能夠這樣調用:

let article = Article.create(title: "title1", content: "content1")
article.save // 執行 INSERT 語句
article.update(title: "newTitle")
let anotherArticle = Article.find(content:"content1")
print(Article.all)複製代碼

MetaModel 的實現原理並不複雜,但真的作起來,仍是要考慮不少細節,本文不對它的內部實現作過多分析。MetaModel 已經開源,正在不斷的完善中,想了解具體使用步驟或參與到 MetaModel 完善工做中的朋友,能夠打開這個頁面

相關文章
相關標籤/搜索