日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
如何使用Kotlin開發(fā)DSL?

譯者 | 布加迪

十年的威縣網(wǎng)站建設(shè)經(jīng)驗(yàn),針對(duì)設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對(duì)一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。營銷型網(wǎng)站建設(shè)的優(yōu)勢(shì)是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整威縣建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。創(chuàng)新互聯(lián)從事“威縣網(wǎng)站設(shè)計(jì)”,“威縣網(wǎng)站推廣”以來,每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。

審校 | 重樓

程序員總是在爭論哪種語言是最好的。我們?cè)容^過C和Pascal,但時(shí)過境遷。Python與Ruby之爭和Java與C#之爭早已遠(yuǎn)去。每種語言有其優(yōu)缺點(diǎn)。理想情況下,我們希望擴(kuò)展語言以滿足自己的需要。程序員早就有這樣的機(jī)會(huì)。我們知道元編程(即創(chuàng)建用來創(chuàng)建程序的程序)的不同方式。在C中,連不起眼的宏都允許您用小的描述生成大段代碼。然而,這些宏是不可靠的、有限的,表達(dá)力不強(qiáng)?,F(xiàn)代語言擁有極富表現(xiàn)力的擴(kuò)展方式,其中一種語言是Kotlin。

一、領(lǐng)域特定語言的定義

領(lǐng)域特定語言(DSL)是一種專門為特定主題領(lǐng)域開發(fā)的語言,與Java、C#和C++等通用語言不同。這意味著它描述主題領(lǐng)域的任務(wù)更容易、更方便、更富有表現(xiàn)力,但同時(shí)它解決日常任務(wù)也不方便、不實(shí)用,即它不是一種通用語言。作為DSL的一個(gè)例子,您可以使用正則表達(dá)式語言。正則表達(dá)式的主題領(lǐng)域是字符串格式。

要檢查字符串是否符合格式,只需使用支持正則表達(dá)式的庫就夠了:

private boolean isIdentifierOrInteger(String s) {
 return s.matches("^\\s*(\\w+\\d*|\\d+)$"); 
}

如果您檢查字符串是否符合通用語言(比如Java)中的指定格式,您將得到以下代碼:

private boolean isIdentifierOrInteger(String s) { 
int index = 0; 

while (index < s.length() && isSpaceChar(s.charAt(index))) { 
index++; 
} 

if (index == s.length()) { 
return false;
 } 

if (isLetter(s.charAt(index))) { 
index++; 

while (index < s.length() && isLetter(s.charAt(index))) 
index++; 

while (index < s.length() && isDigit(s.charAt(index))) 
index++; 
} else if (Character.isDigit(s.charAt(index))) { 
while (index < s.length() && isDigit(s.charAt(index))) 
index++; 
}

 return index == s.length(); 
}

上面的代碼比正則表達(dá)式更難閱讀,更容易出錯(cuò),更難以變更。

DSL的其他常見例子有HTML、CSS、SQL、UML和BPMN(后兩種使用圖形符號(hào))。不僅開發(fā)人員使用DSL,測(cè)試人員和非IT專家也使用DSL。

二、DSL的類型

DSL分為兩種類型:外部和內(nèi)部。外部DSL語言有自己的語法,它們不依賴用來實(shí)現(xiàn)支持的通用編程語言。

外部DSL的優(yōu)缺點(diǎn):

  • 使用不同語言/現(xiàn)成庫生成代碼
  • 設(shè)置語法方面擁有更多的選項(xiàng)
  1. 使用專門的工具:ANTLR、yacc和lex
  2. 有時(shí)很難描述語法
  3. 沒有IDE支持,您需要編寫插件

內(nèi)部DSL基于特定的通用編程語言(宿主語言)。也就是說,在宿主語言的標(biāo)準(zhǔn)工具的幫助下,創(chuàng)建允許您編寫更緊湊的庫。Fluent API方法就是一個(gè)例子。

內(nèi)部DSL的優(yōu)缺點(diǎn):

  • 使用宿主語言的表達(dá)式作為基礎(chǔ)
  • 很容易將DSL嵌入到宿主語言的代碼中,反之亦然
  • 不需要生成代碼
  • 可以作為宿主語言中的子程序進(jìn)行調(diào)試
  1. 設(shè)置語法方面機(jī)會(huì)有限

三、一個(gè)真實(shí)的例子

最近,我們公司需要?jiǎng)?chuàng)建DSL。我們的產(chǎn)品已經(jīng)實(shí)現(xiàn)了購買驗(yàn)收功能。該模塊是BPM(業(yè)務(wù)流程管理)的一個(gè)小型引擎。業(yè)務(wù)流程常以圖形方式表示。比如說,下面的BPMN標(biāo)注顯示了一個(gè)由執(zhí)行任務(wù)1,然后并行執(zhí)行任務(wù)2和任務(wù)3組成的流程。

能夠以編程方式創(chuàng)建業(yè)務(wù)流程對(duì)我們來說非常重要,包括動(dòng)態(tài)構(gòu)建路徑、為審批階段設(shè)置執(zhí)行者、為階段執(zhí)行設(shè)置截止日期等。為此,我們先嘗試使用Fluent API方法來解決這個(gè)問題。

然后我們得出了結(jié)論:使用Fluent API設(shè)置驗(yàn)收路徑仍然很麻煩,我們的團(tuán)隊(duì)考慮了創(chuàng)建自己的DSL這種方案。我們研究了基于Kotlin外部DSL和內(nèi)部DSL上的驗(yàn)收路徑是什么樣子(因?yàn)槲覀兊漠a(chǎn)品代碼是用Java和Kotlin編寫的)。

外部DSL:

Acceptance
 addStep 
executor: HEAD_OF_DEPARTMENT 
duration: 7 days 
protocol should be formed 
parallel 
addStep 
executor: FINANCE_DEPARTMENT or CTO or CEO 
condition: ${!request.isInternal} 
duration: 7 work days after start date 
addStep 
executor: CTO 
dueDate: 2022-12-08 08:00 PST 
can change 
addStep 
executor: SECRETARY 
protocol should be signed
內(nèi)部DSL:
acceptance {
 addStep {
  executor = HEAD_OF_DEPARTMENT
  duration = days(7)
  protocol shouldBe formed
 }
 parallel {
  addStep {
   executor = FINANCE_DEPARTMENT or CTO or CEO
   condition = !request.isInternal
   duration = startDate() + workDays(7)
  }
  addStep {
   executor = CTO
 dueDate = "2022-12-08 08:00" timezone PST
   +canChange
  }
 }
 addStep {
  executor = SECRETARY
  protocol shouldBe signed
 }
}

除了花括號(hào)外,這兩個(gè)選項(xiàng)幾乎一樣。因此,決定不浪費(fèi)時(shí)間和精力開發(fā)外部DSL,而是創(chuàng)建內(nèi)部DSL。

四、實(shí)施DSL的基本結(jié)構(gòu)

不妨開始開發(fā)一個(gè)對(duì)象模型

interface AcceptanceElement

class StepContext : AcceptanceElement {

 lateinit var executor: ExecutorCondition
 var duration: Duration? = null
 var dueDate: ZonedDateTime? = null
 val protocol = Protocol()
 var condition = true
 var canChange = ChangePermission()

}

class AcceptanceContext : AcceptanceElement {

 val elements = mutableListOf()

 fun addStep(init: StepContext.() -> Unit) {
  elements += StepContext().apply(init)
 }

 fun parallel(init: AcceptanceContext.() -> Unit) {
  elements += AcceptanceContext().apply(init)
 }
}

object acceptance {

 operator fun invoke(init: AcceptanceContext.() -> Unit): 
AcceptanceContext {
  val acceptanceContext = AcceptanceContext()
  acceptanceContext.init()
  return acceptanceContext
 }

}

Lambdas

首先看一下AcceptanceContext類。它旨在用于存儲(chǔ)路徑元素的集合,并用于表示整個(gè)圖以及parallel-blocks。addStep和parallel方法接受帶有接收者的lambda作為參數(shù)。

帶有接收者的lambda是定義可以訪問特定接收者對(duì)象的lambda表達(dá)式的一種方式。在函數(shù)主體中,傳遞給調(diào)用的接收者對(duì)象變成了隱式的this,這樣您就可以在沒有任何附加限定符的情況下訪問該接收者對(duì)象的成員,或者使用this表達(dá)式訪問接收者對(duì)象。

此外,如果方法調(diào)用的最后一個(gè)參數(shù)是lambda,可以將lambda放在括號(hào)之外。這就是為什么在DSL中我們可以按如下方式編寫代碼:

parallel {
 addStep {
  executor = FINANCE_DEPARTMENT
  ...
 }
 addStep {
  executor = CTO
  ...
 }
}

這相當(dāng)于沒有語法糖的代碼:

parallel({
 this.addStep({
  this.executor = FINANCE_DEPARTMENT
  ...
 })
 this.addStep({
  this.executor = CTO
  ...
 })
})

帶接收者的Lambda和括號(hào)外的Lambda是Kotlin在處理DSL時(shí)特別有用的特性。

對(duì)象聲明

現(xiàn)在不妨看看實(shí)體acceptance。acceptance是一個(gè)對(duì)象。在Kotlin中,對(duì)象聲明是定義單例的一種方式,單例指只有一個(gè)實(shí)例的類。因此,對(duì)象聲明同時(shí)定義了類及其單個(gè)實(shí)例。

“invoke”操作符重載

此外,為accreditation對(duì)象重載了invoke操作符。invoke操作符是一個(gè)可以在類中定義的特殊函數(shù)。當(dāng)您像調(diào)用函數(shù)一樣調(diào)用類的實(shí)例時(shí),調(diào)用invoke操作符函數(shù)。這允許您將對(duì)象作為函數(shù)來處理,并以類似函數(shù)的方式調(diào)用它們。

注意,invoke方法的參數(shù)也是帶接收者的lambda?,F(xiàn)在我們可以定義驗(yàn)收路徑:

val acceptanceRoute = acceptance {
 addStep {
  executor = HEAD_OF_DEPARTMENT
  ...
 }
 parallel {
  addStep {
   executor = FINANCE_DEPARTMENT
   ...
  }
  addStep {
   executor = CTO
   ...
  }
 }
 addStep {
  executor = SECRETARY
  ...
 }
}

然后處理它

val headOfDepartmentStep = acceptanceRoute.elements[0] as StepContext 
val parallelBlock = acceptanceRoute.elements[1] as AcceptanceContext 
val ctoStep = parallelBlock.elements[1] as StepContext

五、添加細(xì)節(jié)

中綴函數(shù)

看看這段代碼:

addStep {
 executor = FINANCE_DEPARTMENT or CTO or CEO
 ...
}

我們可以按以下方式實(shí)現(xiàn)這個(gè):

enum class ExecutorConditionType { 
EQUALS, OR 
} 

data class ExecutorCondition( 
private val name: String, 
private val values: Set, 
private val type: ExecutorConditionType, 
) { 
infix fun or(another: ExecutorCondition) = 
ExecutorCondition("or", setOf(this, another), 
ExecutorConditionType.OR) 
} 

val HEAD_OF_DEPARTMENT = 
ExecutorCondition("HEAD_OF_DEPARTMENT", setOf(), 
ExecutorConditionType.EQUALS) 
val FINANCE_DEPARTMENT = 
ExecutorCondition("FINANCE_DEPARTMENT", setOf(), 
ExecutorConditionType.EQUALS) 
val CHIEF = ExecutorCondition("CHIEF", setOf(), 
ExecutorConditionType.EQUALS) 
val CTO = ExecutorCondition("CTO", setOf(), ExecutorConditionType.EQUALS) 
val SECRETARY = 
ExecutorCondition("SECRETARY", setOf(), ExecutorConditionType.EQUALS)

ExecutorCondition類允許我們?cè)O(shè)置幾個(gè)可能的任務(wù)執(zhí)行器。在ExecutorCondition中定義了中綴函數(shù)or。中綴函數(shù)是一種特殊的函數(shù),允許您使用更自然的中綴符號(hào)來調(diào)用它。

如果不使用語言的這個(gè)特性,我們將不得不這樣寫:

addStep { 
executor = FINANCE_DEPARTMENT.or(CTO).or(CEO) 
... 
}

中綴函數(shù)還用于設(shè)置協(xié)議的所需狀態(tài)和時(shí)區(qū)時(shí)間。

enum class ProtocolState {
 formed, signed
}

class Protocol {
 var state: ProtocolState? = null

 infix fun shouldBe(state: ProtocolState) {
 this.state = state
 }
}


enum class TimeZone {
 ...
 PST,
 ...
}

infix fun String.timezone(tz: TimeZone): ZonedDateTime {
 val format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z")
 return ZonedDateTime.parse("$this $tz", format)
}

擴(kuò)展函數(shù)

String.timezone是一個(gè)擴(kuò)展函數(shù)。在Kotlin中,擴(kuò)展函數(shù)允許您向現(xiàn)有類添加新函數(shù),而無需修改它們的源代碼。當(dāng)您想要增強(qiáng)無法控制的類的功能時(shí),比如來自標(biāo)準(zhǔn)庫或外部庫的類,這項(xiàng)特性特別有用。

DSL中的用法:

addStep { 
... 
protocol shouldBe formed 
dueDate = "2022-12-08 08:00" timezone PST 
... 
}

這里的“2022-12-08 08:00”是接收者對(duì)象,針對(duì)它調(diào)用擴(kuò)展函數(shù)timezone,而PST是參數(shù)。使用this關(guān)鍵字訪問接收者對(duì)象。

操作符重載

我們?cè)贒SL中使用的下一個(gè)Kotlin特性是操作符重載。我們已經(jīng)考慮了invoke操作符的重載。在Kotlin中,您可以重載其他操作符,包括算術(shù)操作符。

addStep {
 ...
 +canChange
}

這里,一元操作符+被重載。下面是實(shí)現(xiàn)這個(gè)重載的代碼:

class StepContext : AcceptanceElement { 
... 
var canChange = ChangePermission() 
} 

data class ChangePermission( 
var canChange: Boolean = true, 
) { 
operator fun unaryPlus() { 
canChange = true 
}

operator fun unaryMinus() { 
canChange = false 
}
 }

結(jié)語

現(xiàn)在我們可以描述DSL上的驗(yàn)收路徑。然而,應(yīng)該保護(hù)DSL用戶避免可能的錯(cuò)誤。比如在當(dāng)前版本中,以下代碼是可以接受的:

val acceptanceRoute = acceptance { 
addStep { 
executor = HEAD_OF_DEPARTMENT 
duration = days(7) 
protocol shouldBe signed 

addStep { 
executor = FINANCE_DEPARTMENT 
}
 } 
}

addStep中的addStep看起來很奇怪,是不是?不妨弄清楚為什么這段代碼成功編譯而沒有出現(xiàn)任何錯(cuò)誤。如上所述,acceptance#invoke和AcceptanceContext#addStep方法接受帶有接收者的lambda作為參數(shù),而接收者對(duì)象可以通過this關(guān)鍵字來訪問。所以我們可以像這樣重寫前面的代碼:

val acceptanceRoute = acceptance { 
this@acceptance.addStep { 
this@addStep.executor = HEAD_OF_DEPARTMENT 
this@addStep.duration = days(7) 
this@addStep.protocol shouldBe signed 

this@acceptance.addStep { 
executor = FINANCE_DEPARTMENT 
} 
}
 }

現(xiàn)在您可以看到this@acceptance.addStep兩次都被調(diào)用了。特別是對(duì)于這種情況,Kotlin有一個(gè)DslMarker注釋。您可以使用@DslMarker來定義自定義注釋。用相同此類注釋標(biāo)記的接收者無法在對(duì)方的內(nèi)部被訪問。

@DslMarker 
annotation class AcceptanceDslMarker 

@AcceptanceDslMarker 
class AcceptanceContext : AcceptanceElement { 
... 
} 

@AcceptanceDslMarker 
class StepContext : AcceptanceElement { 
...
 }

現(xiàn)在

val acceptanceRoute = acceptance {
 addStep {
  ...

 addStep {
 ...
 }
 }
}

由于錯(cuò)誤'fun addStep(init: StepContext.() -> Unit): Unit'無法通過隱式接收者在此上下文中調(diào)用,上面這段代碼無法編譯。必要時(shí)使用顯式接收者。

原文標(biāo)題:How to Develop a DSL in Kotlin,作者:Fedor Yaremenko


本文名稱:如何使用Kotlin開發(fā)DSL?
網(wǎng)站地址:http://www.dlmjj.cn/article/dppscpc.html