成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

如何使用Kotlin開發DSL?

譯文
開發
程序員總是在爭論哪種語言是最好的。我們曾比較過C和Pascal,但時過境遷。Python與Ruby之爭和Java與C#之爭早已遠去。每種語言有其優缺點。理想情況下,我們希望擴展語言以滿足自己的需要。

譯者 | 布加迪

審校 | 重樓

程序員總是在爭論哪種語言是最好的。我們曾比較過CPascal,但時過境遷。Python與Ruby之爭Java與C#之爭遠去。每種語言有其優缺點。理想情況下,我們希望擴展語言以滿足自己的需要。程序員早就有這樣的機會。我們知道元編程(即創建用來創建程序的程序)的不同方式。在C中,連不起眼的宏允許您小的描述生成大代碼。然而,這些宏是不可靠的、有限的,表達力不強。現代語言擁有極富表現力的擴展方式其中一種語言是Kotlin

一、領域特定語言的定義

領域特定語言DSL一種專門為特定主題領域開發的語言,與JavaC#和C++等通用語言不同。這意味著它描述主題領域的任務更容易、更方便、更有表現力,但同時它解決日常任務也不方便、不實用,即它不是一種通用語言。作為DSL的一個例子,您可以使用正則表達式語言。正則表達式的主題領域是字符串格式。

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

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(); 
}

上面的代碼比正則表達式更難閱讀,更容易出錯,更難以變更

DSL的其他常見例HTMLCSSSQLUMLBPMN后兩使用圖形符號。不僅開發人員使用DSL,測試人員和非IT專家也使用DSL

二、DSL的類型

DSL分為兩種類型外部和內部。外部DSL語言有自己的語法,它們不依賴用來實現持的通用編程語言。

外部DSL的優缺點

  • 使用不同語言/現成庫生成代碼
  • 設置語法方面擁有更多選項
  1. 使用專門的工具ANTLRyacclex
  2. 有時很難描述語法
  3. 沒有IDE支持,您需要編寫插件

內部DSL基于特定的通用編程語言(宿主語言。也就是說,在宿主語言的標準工具的幫助下,創建允許您編寫更緊湊的庫。Fluent API方法就是一個例子。

內部DSL優缺點:

  • 使用宿主語言表達式為基礎
  • 很容易將DSL嵌入到宿主語言的代碼中,反之亦然
  • 不需要生成代碼
  • 可以作為宿主語言的子程序進行調試
  1. 設置語法方面機會有限

三、一個真實的例子

最近,我們公司需要創建DSL。我們的產品已經實現了購買驗收功能。該模塊是BPM業務流程管理的一個小型引擎。業務流程常以圖形方式表示。比如說,下面的BPMN標注顯示了一個由執行任務1,然后并行執行任務2和任務3組成的流程。

能夠以編程方式創建業務流程對我們來說非常重要,包括動態構建路徑、為審批階段設置執行者、為階段執行設置截止日期等。為此,我們先嘗試使用Fluent API方法來解決這個問題。

然后我們得出結論使用Fluent API設置驗收路徑仍然很麻煩,我們的團隊考慮了創建自己的DSL這種方案。我們研究了基于Kotlin外部DSL和內部DSL的驗收路徑是什么樣子(因為我們的產品代碼是用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
內部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
 }
}

除了花括號外,這兩個選項幾乎一樣。因此,決定不浪費時間和精力開發外部DSL,而是創建內部DSL。

四、實施DSL的基本結構

不妨開始開發一個對象模型

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<AcceptanceElement>()

 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類。它旨在用于存儲路徑元素的集合,并用于表示整個圖以及parallel-blocks。addStep和parallel方法接受帶有接收者的lambda作為參數。

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

外,如果方法調用的最后一個參數是lambda,可以將lambda放在括號之外。這就是為什么在DSL中我們可以按如下方式編寫代碼

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

這相當于沒有語法糖的代碼:

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

帶接收者的Lambda和括號外的Lambda是Kotlin在處理DSL時特別有用的特性。

對象聲明

現在不妨看看實體acceptanceacceptance是一個對象。在Kotlin中,對象聲明是定義單例的一種方式,單例指只有一個實例的類。因此,對象聲明同時定義了類及其單個實例。

“invoke”操作符重載

此外,為accreditation對象重載了invoke操作符invoke操作符是一個可以在類中定義的特殊函數。當您像調用函數一樣調用類的實例時,調用invoke操作符函數。這允許您將對象作為函數來處理,并以類似函數的方式調用它們。

注意,invoke方法的參數也是帶接收者的lambda。現在我們可以定義驗收路徑:

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

五、添加細節

中綴函數

看看這段代碼

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

我們可以按以下方式實現這個:

enum class ExecutorConditionType { 
EQUALS, OR 
} 

data class ExecutorCondition( 
private val name: String, 
private val values: Set<ExecutorCondition>, 
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類允許我們設置幾個可能的任務執行器。在ExecutorCondition中定義了中綴函數or。中綴函數是一種特殊的函數,允許您使用更自然的中綴符號來調用它。

如果不使用語言的這個特性,我們將不得不這樣寫

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

中綴函數還用于設置協議的所需狀態和時區時間。

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)
}

擴展函數

String.timezone是一個擴展函數。在Kotlin中,擴展函數允許您向現有類添加新函數,而無需修改它們的源代碼。當您想要增強無法控制的類的功能時,比如來自標準庫或外部庫的類,這項特性特別有用。

DSL中的用法

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

這里的“2022-12-08 08:00”是接收者對象,針對它調用擴展函數timezone,而PST是參數。使用this關鍵字訪問接收者對象。

操作符重載

我們在DSL中使用的下一個Kotlin特性是操作符重載。我們已經考慮了invoke操作符的重載。在Kotlin中,您可以重載其他操作符,包括算術操作符。

addStep {
 ...
 +canChange
}

這里,一元操作符+被重載。下面是實現這個重載的代碼

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

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

operator fun unaryMinus() { 
canChange = false 
}
 }

結語

現在我們可以描述DSL上的驗收路徑然而,應該保護DSL用戶避免可能的錯誤。比如在當前版本中,以下代碼是可以接受的

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

addStep { 
executor = FINANCE_DEPARTMENT 
}
 } 
}

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

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 
} 
}
 }

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

@DslMarker 
annotation class AcceptanceDslMarker 

@AcceptanceDslMarker 
class AcceptanceContext : AcceptanceElement { 
... 
} 

@AcceptanceDslMarker 
class StepContext : AcceptanceElement { 
...
 }

現在

val acceptanceRoute = acceptance {
 addStep {
  ...

 addStep {
 ...
 }
 }
}

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

原文標題:How to Develop a DSL in Kotlin,作者:Fedor Yaremenko


責任編輯:華軒 來源: 51CTO
相關推薦

2018-04-24 15:00:59

Kotlin語言函數

2023-01-04 12:17:07

開源攜程

2011-05-11 09:47:14

mobl移動web開發

2016-11-24 17:21:30

2019-10-23 14:34:15

KotlinAndroid協程

2019-04-01 14:17:36

kotlin開發Java

2009-12-29 13:56:07

DSL技術

2011-05-06 15:31:28

moblweb開發DSL

2021-09-16 16:08:43

KotlinAndroidAOSP

2016-11-03 09:59:38

kotlinjavaspring

2023-04-09 14:49:57

開發語言Kotlin

2022-01-06 09:55:19

鴻蒙HarmonyOS應用

2017-05-22 11:09:53

KotlinAndroid

2020-03-04 11:20:22

DSL開發領域特定語言

2017-10-20 10:19:49

Kotlin語言陷阱

2009-12-14 18:23:38

Ruby DSL測試

2014-12-25 10:15:37

DockerJava

2009-12-14 18:30:59

Ruby DSL特點

2009-12-14 18:14:27

Ruby DSL

2020-04-20 12:45:20

編程語言JavaScriptKotlin
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 99reav| 一区二区三区四区视频 | 国产精品久久九九 | 秋霞影院一区二区 | 欧美一级二级三级视频 | 日韩福利| 99精品一区二区 | 米奇狠狠鲁| 亚洲一区二区在线视频 | 成人三级网址 | 久久久久久久久毛片 | 日韩国产在线 | 日韩在线欧美 | 99久久久无码国产精品 | 国产专区免费 | 国产精品久久久 | 成年人视频在线免费观看 | 日本精品裸体写真集在线观看 | 天堂久久av| 一级黄色在线 | 国产激情视频在线观看 | 亚洲一一在线 | www视频在线观看 | 国产乱码精品一区二区三区忘忧草 | 日韩在线精品 | 天堂资源 | 午夜精品久久久久久久久久久久 | 欧美一区二区三区一在线观看 | 九九免费 | 欧美人成在线视频 | 国产片淫级awww | 美女一级a毛片免费观看97 | 日韩欧美在线视频播放 | 永久av| 天天躁日日躁狠狠很躁 | 欧美日韩国产中文 | 亚洲成人精品久久久 | 成人免费视频观看视频 | 中文字幕一区二区三区四区 | 一区二区精品 | 亚洲欧美综合精品另类天天更新 |