Các lập trình viên liên tục tranh cãi về việc ngôn ngữ nào là tốt nhất. Có lần chúng tôi so sánh C và Pascal, nhưng thời gian trôi qua. Cuộc chiến / và /C# đã ở phía sau chúng ta. Mỗi ngôn ngữ đều có ưu và nhược điểm, đó là lý do tại sao chúng tôi so sánh chúng. Lý tưởng nhất là chúng tôi muốn mở rộng ngôn ngữ để phù hợp với nhu cầu của mình. Các lập trình viên đã có cơ hội này từ rất lâu rồi. Chúng tôi biết các cách lập trình siêu dữ liệu khác nhau, tức là tạo chương trình để tạo chương trình. Ngay cả các macro tầm thường trong C cũng cho phép bạn tạo ra các đoạn mã lớn từ các mô tả nhỏ. Tuy nhiên, các macro này không đáng tin cậy, bị hạn chế và không có tính biểu cảm cao. Các ngôn ngữ hiện đại có nhiều phương tiện mở rộng biểu cảm hơn. Một trong những ngôn ngữ này là Kotlin. Python Ruby Java Định nghĩa của một ngôn ngữ dành riêng cho miền là ngôn ngữ được phát triển riêng cho một lĩnh vực chủ đề cụ thể, không giống như các ngôn ngữ có mục đích chung như Java, C#, C++ và các ngôn ngữ khác. Điều này có nghĩa là việc mô tả các nhiệm vụ của môn học sẽ dễ dàng hơn, thuận tiện hơn và biểu cảm hơn, nhưng đồng thời cũng bất tiện và không thực tế trong việc giải quyết các công việc hàng ngày, tức là nó không phải là một ngôn ngữ phổ quát. Như một ví dụ về DSL, bạn có thể sử dụng ngôn ngữ biểu thức chính quy. Lĩnh vực chủ đề của biểu thức chính quy là định dạng của chuỗi. Ngôn ngữ dành riêng cho miền (DSL) Để kiểm tra xem chuỗi có tuân thủ định dạng hay không, chỉ cần sử dụng thư viện triển khai hỗ trợ cho các biểu thức chính quy là đủ: private boolean isIdentifierOrInteger(String s) { return s.matches("^\\s*(\\w+\\d*|\\d+)$"); } Nếu bạn kiểm tra xem chuỗi có tuân thủ định dạng đã chỉ định trong ngôn ngữ phổ quát, ví dụ: Java, bạn sẽ nhận được mã sau: 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(); } Đoạn mã trên khó đọc hơn biểu thức thông thường, dễ mắc lỗi hơn và khó thực hiện thay đổi hơn. Các ví dụ phổ biến khác về DSL là HTML, CSS, SQL, UML và BPMN (hai ví dụ sau sử dụng ký hiệu đồ họa). DSL không chỉ được sử dụng bởi các nhà phát triển mà còn bởi những người thử nghiệm và những người không chuyên về CNTT. Các loại DSL DSL được chia thành hai loại: bên ngoài và bên trong. Các ngôn ngữ có cú pháp riêng và chúng không phụ thuộc vào ngôn ngữ lập trình chung mà chúng được hỗ trợ. DSL bên ngoài Ưu và nhược điểm của DSL bên ngoài: 🟢 Tạo mã bằng các ngôn ngữ khác nhau / thư viện tạo sẵn 🟢 Thêm tùy chọn để thiết lập cú pháp của bạn 🔴 Sử dụng các công cụ chuyên dụng: ANTLR, yacc, lex 🔴 Đôi khi khó diễn tả ngữ pháp 🔴 Không có hỗ trợ IDE, bạn cần viết plugin của mình dựa trên một ngôn ngữ lập trình phổ quát cụ thể (ngôn ngữ máy chủ). Nghĩa là, với sự trợ giúp của các công cụ tiêu chuẩn của ngôn ngữ máy chủ, các thư viện được tạo ra cho phép bạn viết gọn hơn. Ví dụ: hãy xem xét cách tiếp cận Fluent API. DSL nội bộ Ưu và nhược điểm của DSL nội bộ: 🟢 Sử dụng cách diễn đạt của ngôn ngữ chủ nhà làm cơ sở 🟢 Dễ dàng nhúng DSL vào mã ở ngôn ngữ chủ nhà và ngược lại 🟢 Không yêu cầu tạo mã 🟢 Có thể được gỡ lỗi dưới dạng chương trình con trong ngôn ngữ máy chủ 🔴 Khả năng thiết lập cú pháp bị hạn chế Một ví dụ thực tế Gần đây, công ty chúng tôi phải đối mặt với nhu cầu tạo DSL của mình. Sản phẩm của chúng tôi đã thực hiện chức năng chấp nhận mua hàng. Mô-đun này là một công cụ nhỏ của BPM (Quản lý quy trình nghiệp vụ). Quy trình kinh doanh thường được thể hiện bằng đồ họa. Ví dụ: ký hiệu BPMN bên dưới hiển thị một quy trình bao gồm thực hiện Nhiệm vụ 1 và sau đó thực hiện song song Nhiệm vụ 2 và Nhiệm vụ 3. Điều quan trọng đối với chúng tôi là có thể tạo các quy trình kinh doanh theo chương trình, bao gồm xây dựng lộ trình một cách linh hoạt, chỉ định người thực hiện cho các giai đoạn phê duyệt, đặt thời hạn thực hiện giai đoạn, v.v. Để thực hiện điều này, trước tiên chúng tôi đã cố gắng giải quyết vấn đề này bằng API thông thạo tiếp cận. Sau đó, chúng tôi kết luận rằng việc thiết lập các tuyến chấp nhận bằng Fluent API vẫn còn phức tạp và nhóm của chúng tôi đã cân nhắc tùy chọn tạo DSL của riêng mình. Chúng tôi đã nghiên cứu xem lộ trình chấp nhận sẽ trông như thế nào trên DSL bên ngoài và DSL nội bộ dựa trên Kotlin (vì mã sản phẩm của chúng tôi được viết bằng Java và ). Kotlin DSL bên ngoài: 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 nội bộ: 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 } } Ngoại trừ dấu ngoặc nhọn, cả hai tùy chọn đều gần như giống nhau. Vì vậy, người ta quyết định không lãng phí thời gian và công sức vào việc phát triển DSL bên ngoài mà tạo ra DSL nội bộ. Triển khai cấu trúc cơ bản của DSL Hãy bắt đầu phát triển một mô hình đối tượng 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 } } Lambda Đầu tiên, chúng ta hãy nhìn vào lớp . Nó được thiết kế để lưu trữ một tập hợp các phần tử tuyến đường và được sử dụng để thể hiện toàn bộ sơ đồ cũng như các khối . AcceptanceContext parallel Các phương thức và lấy lambda với bộ thu làm tham số. addStep parallel Lambda với bộ thu là một cách để xác định biểu thức lambda có quyền truy cập vào một đối tượng bộ thu cụ thể. Bên trong phần thân của hàm đen, đối tượng người nhận được truyền cho cuộc gọi sẽ trở thành ẩn , để bạn có thể truy cập các thành viên của đối tượng người nhận đó mà không cần bất kỳ vòng loại bổ sung nào hoặc truy cập đối tượng người nhận bằng cách sử dụng biểu thức . this this Ngoài ra, nếu đối số cuối cùng của lệnh gọi phương thức là lambda thì lambda có thể được đặt bên ngoài dấu ngoặc đơn. Đó là lý do tại sao trong DSL, chúng ta có thể viết mã như sau: parallel { addStep { executor = FINANCE_DEPARTMENT ... } addStep { executor = CTO ... } } Điều này tương đương với một mã không có đường cú pháp: parallel({ this.addStep({ this.executor = FINANCE_DEPARTMENT ... }) this.addStep({ this.executor = CTO ... }) }) Lambda có bộ thu và Lambda nằm ngoài dấu ngoặc đơn là các tính năng của Kotlin đặc biệt hữu ích khi làm việc với DSL. Khai báo đối tượng Bây giờ chúng ta hãy xem thực thể. là một đối tượng. Trong Kotlin, khai báo đối tượng là một cách để xác định một singleton — một lớp chỉ có một phiên bản. Vì vậy, việc khai báo đối tượng định nghĩa cả lớp và thể hiện đơn lẻ của nó cùng một lúc. acceptance acceptance Quá tải toán tử “gọi” Ngoài ra, toán bị quá tải đối với đối tượng . Toán là một hàm đặc biệt mà bạn có thể định nghĩa trong các lớp của mình. Khi bạn gọi một thể hiện của một lớp như thể nó là một hàm, thì hàm toán gọi sẽ được gọi. Điều này cho phép bạn coi các đối tượng là các hàm và gọi chúng theo cách giống như hàm. invoke accreditation invoke invoke Lưu ý rằng tham số của phương thức cũng là lambda có bộ thu. Bây giờ chúng ta có thể xác định lộ trình chấp nhận… invoke val acceptanceRoute = acceptance { addStep { executor = HEAD_OF_DEPARTMENT ... } parallel { addStep { executor = FINANCE_DEPARTMENT ... } addStep { executor = CTO ... } } addStep { executor = SECRETARY ... } } …và bước qua nó val headOfDepartmentStep = acceptanceRoute.elements[0] as StepContext val parallelBlock = acceptanceRoute.elements[1] as AcceptanceContext val ctoStep = parallelBlock.elements[1] as StepContext Thêm chi tiết Hàm trung tố Hãy nhìn vào mã này addStep { executor = FINANCE_DEPARTMENT or CTO or CEO ... } Chúng ta có thể thực hiện điều này bằng cách sau: 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) Lớp cho phép chúng ta thiết lập một số trình thực thi tác vụ có thể có. Trong chúng ta xác định hàm infix . Hàm infix là một loại hàm đặc biệt cho phép bạn gọi nó bằng cách sử dụng ký hiệu infix tự nhiên hơn. ExecutorCondition ExecutorCondition or Nếu không sử dụng đặc điểm này của ngôn ngữ, chúng ta sẽ phải viết như sau: addStep { executor = FINANCE_DEPARTMENT.or(CTO).or(CEO) ... } Các hàm Infix cũng được sử dụng để đặt trạng thái yêu cầu của giao thức và thời gian theo múi giờ. 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) } Các hàm mở rộng là một hàm mở rộng. Trong Kotlin, các hàm mở rộng cho phép bạn thêm các hàm mới vào các lớp hiện có mà không cần sửa đổi mã nguồn của chúng. Tính năng này đặc biệt hữu ích khi bạn muốn tăng cường chức năng của các lớp mà bạn không có quyền kiểm soát, chẳng hạn như các lớp từ thư viện tiêu chuẩn hoặc bên ngoài. String.timezone Cách sử dụng trong DSL: addStep { ... protocol shouldBe formed dueDate = "2022-12-08 08:00" timezone PST ... } Ở đây là một đối tượng máy thu, trên đó của hàm mở rộng được gọi và là tham số. Đối tượng người nhận được truy cập bằng từ khóa . "2022-12-08 08:00" timezone PST this Quá tải toán tử Tính năng Kotlin tiếp theo mà chúng tôi sử dụng trong DSL là nạp chồng toán tử. Chúng ta đã xem xét tình trạng quá tải của toán . Trong Kotlin, bạn có thể nạp chồng các toán tử khác, bao gồm cả toán tử số học. invoke addStep { ... +canChange } Ở đây toán tử một ngôi bị quá tải. Dưới đây là mã thực hiện điều này: + class StepContext : AcceptanceElement { ... var canChange = ChangePermission() } data class ChangePermission( var canChange: Boolean = true, ) { operator fun unaryPlus() { canChange = true } operator fun unaryMinus() { canChange = false } } Chi tiết cuối cùng Bây giờ chúng ta có thể mô tả các tuyến chấp nhận trên DSL của mình. Tuy nhiên, người dùng DSL cần được bảo vệ khỏi các lỗi có thể xảy ra. Ví dụ: trong phiên bản hiện tại, đoạn mã sau được chấp nhận: val acceptanceRoute = acceptance { addStep { executor = HEAD_OF_DEPARTMENT duration = days(7) protocol shouldBe signed addStep { executor = FINANCE_DEPARTMENT } } } bên trong trông có vẻ lạ phải không? Hãy cùng tìm hiểu lý do tại sao mã này biên dịch thành công mà không có bất kỳ lỗi nào. Như đã đề cập ở trên, các phương thức và lấy lambda với bộ thu làm tham số và đối tượng bộ thu có thể truy cập được bằng từ khóa . Vì vậy, chúng ta có thể viết lại mã trước đó như thế này: addStep addStep acceptance#invoke AcceptanceContext#addStep 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 } } } Bây giờ bạn có thể thấy được gọi cả hai lần. Đặc biệt đối với những trường hợp như vậy, Kotlin có chú thích . Bạn có thể sử dụng để xác định chú thích tùy chỉnh. Các máy thu được đánh dấu bằng cùng một chú thích như vậy không thể được truy cập bên trong nhau. this@acceptance.addStep DslMarker @DslMarker @DslMarker annotation class AcceptanceDslMarker @AcceptanceDslMarker class AcceptanceContext : AcceptanceElement { ... } @AcceptanceDslMarker class StepContext : AcceptanceElement { ... } Bây giờ mã val acceptanceRoute = acceptance { addStep { ... addStep { ... } } } sẽ không biên dịch do lỗi 'fun addStep(init: StepContext.() -> Unit): Unit' can't be called in this context by implicit receiver. Use the explicit one if necessary Liên kết Dưới đây là các liên kết đến tài liệu chính thức của Kotlin về các tính năng ngôn ngữ đã được xem xét trong bài viết này: Hàm chữ có bộ thu https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver Vượt qua lambdas https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas Khai báo đối tượng https://kotlinlang.org/docs/object-declarations.html#object-declarations-overview Quá tải toán tử https://kotlinlang.org/docs/operator-overloading.html Ký hiệu infix https://kotlinlang.org/docs/functions.html#infix-notation Các hàm mở rộng https://kotlinlang.org/docs/extensions.html#extension-functions Kiểm soát phạm vi: @DslMarker https://kotlinlang.org/docs/type-safe-builders.html#scope-control-dslmarker Phần kết luận Các ngôn ngữ dành riêng cho miền cung cấp một phương tiện mạnh mẽ để nâng cao năng suất, giảm lỗi và cải thiện sự cộng tác bằng cách cung cấp một cách chuyên biệt và biểu cảm để mô hình hóa và giải quyết các vấn đề trong một miền cụ thể. Kotlin là một ngôn ngữ lập trình hiện đại với nhiều tính năng và cú pháp dễ hiểu nên rất tuyệt vời khi làm ngôn ngữ chủ cho DSL nội bộ.