Programcılar sürekli olarak hangi dilin en iyi olduğu konusunda tartışıyorlar. Bir keresinde C ile Pascal'ı karşılaştırmıştık ama zaman geçti. / ve /C# savaşları artık geride kaldı. Her dilin artıları ve eksileri vardır, bu yüzden onları karşılaştırıyoruz. İdeal olarak, dilleri kendi ihtiyaçlarımıza uyacak şekilde genişletmek isteriz. Programcılar bu fırsata çok uzun zamandır sahip. Metaprogramlamanın, yani program oluşturmak için program oluşturmanın farklı yollarını biliyoruz. C'deki önemsiz makrolar bile küçük açıklamalardan büyük kod parçaları oluşturmanıza olanak tanır. Ancak bu makrolar güvenilmezdir, sınırlıdır ve pek anlamlı değildir. Modern diller çok daha anlamlı genişleme araçlarına sahiptir. Bu dillerden biri de Kotlin'dir. Python Ruby Java Etki alanına özgü bir dilin tanımı Java, C#, C++ ve diğerleri gibi genel amaçlı dillerin aksine, belirli bir konu alanı için özel olarak geliştirilmiş bir dildir. Bu, konu alanındaki görevleri tanımlamanın daha kolay, daha kullanışlı ve daha anlamlı olduğu anlamına gelir, ancak aynı zamanda günlük görevleri çözmek için uygun değildir ve pratik değildir, yani evrensel bir dil değildir. DSL'de normal ifade dilini kullanabilirsiniz. Düzenli ifadelerin konu alanı dizelerin biçimidir. Etki alanına özgü dil (DSL), Dizenin formata uygunluğunu kontrol etmek için, normal ifadeler için destek uygulayan bir kitaplığın kullanılması yeterlidir: private boolean isIdentifierOrInteger(String s) { return s.matches("^\\s*(\\w+\\d*|\\d+)$"); } Dizenin evrensel bir dilde, örneğin Java'da belirtilen formata uygunluğunu kontrol ederseniz, aşağıdaki kodu alırsınız: 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(); } Yukarıdaki kodu okumak normal ifadelere göre daha zordur, hata yapmak daha kolaydır ve değişiklik yapmak daha zordur. DSL'in diğer yaygın örnekleri HTML, CSS, SQL, UML ve BPMN'dir (son ikisi grafiksel gösterim kullanır). DSL'ler yalnızca geliştiriciler tarafından değil aynı zamanda test uzmanları ve BT dışı uzmanlar tarafından da kullanılır. DSL Türleri DSL'ler iki türe ayrılır: harici ve dahili. dillerinin kendi sözdizimleri vardır ve desteklerinin uygulandığı evrensel programlama diline bağlı değildirler. Harici DSL Harici DSL'lerin artıları ve eksileri: 🟢 Farklı dillerde / hazır kütüphanelerde kod üretimi 🟢 Sözdiziminizi ayarlamak için daha fazla seçenek 🔴 Özel araçların kullanımı: ANTLR, yacc, lex 🔴 Bazen dil bilgisini anlatmak zordur 🔴IDE desteği yoktur eklentilerinizi yazmanız gerekmektedir belirli bir evrensel programlama dilini (ana bilgisayar dili) temel alır. Yani, ana dilin standart araçlarının yardımıyla, daha derli toplu yazmanıza olanak tanıyan kütüphaneler oluşturulur. Örnek olarak Fluent API yaklaşımını düşünün. Dahili DSL'ler Dahili DSL'lerin artıları ve eksileri: 🟢 Ana dilin ifadelerini temel alır 🟢 DSL'i ana dillerdeki koda yerleştirmek kolaydır ve bunun tersi de geçerlidir 🟢 Kod oluşturma gerektirmez 🟢 Ana dilde alt program olarak hata ayıklanabilir 🔴 Sözdizimini ayarlamada sınırlı olanaklar Gerçek hayattan bir örnek Son zamanlarda şirket olarak DSL'imizi oluşturma ihtiyacıyla karşı karşıya kaldık. Ürünümüz satın alma kabulü işlevini hayata geçirmiştir. Bu modül BPM'nin (İş Süreçleri Yönetimi) mini motorudur. İş süreçleri genellikle grafiksel olarak temsil edilir. Örneğin, aşağıdaki BPMN gösterimi, Görev 1'in yürütülmesinden ve ardından Görev 2 ile Görev 3'ün paralel olarak yürütülmesinden oluşan bir süreci göstermektedir. Dinamik olarak rota oluşturmak, onay aşamaları için uygulayıcıları ayarlamak, aşamanın yürütülmesi için son tarihi belirlemek vb. dahil olmak üzere iş süreçlerini programlı bir şekilde oluşturabilmek bizim için önemliydi. Bunu yapmak için öncelikle bu sorunu Fluent API kullanarak çözmeye çalıştık. yaklaşmak. Daha sonra Fluent API'yi kullanarak kabul rotaları belirlemenin hala zahmetli olduğu sonucuna vardık ve ekibimiz kendi DSL'sini oluşturma seçeneğini değerlendirdi. Harici bir DSL ve Kotlin tabanlı dahili bir DSL'de kabul yolunun nasıl görüneceğini araştırdık (çünkü ürün kodumuz Java ve ile yazılmıştır). Kotlin Harici 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 Dahili 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 } } Kıvrımlı parantezlerin dışında her iki seçenek de hemen hemen aynıdır. Bu nedenle harici bir DSL geliştirmek için zaman ve çaba harcamamaya, dahili bir DSL oluşturmaya karar verildi. DSL'in temel yapısının uygulanması Bir nesne modeli geliştirmeye başlayalım 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 } } Lambdalar Öncelikle sınıfına bakalım. Rota elemanlarının bir koleksiyonunu depolamak için tasarlanmıştır ve blokların yanı sıra tüm diyagramı temsil etmek için kullanılır. AcceptanceContext parallel ve yöntemler, parametre olarak alıcılı bir lambda alır. addStep parallel Alıcılı bir lambda, belirli bir alıcı nesnesine erişimi olan bir lambda ifadesini tanımlamanın bir yoludur. İşlev değişmezinin gövdesi içinde, bir çağrıya iletilen alıcı nesnesi örtük bir haline gelir, böylece bu alıcı nesnesinin üyelerine herhangi bir ek niteleyici olmadan erişebilir veya alıcı nesnesine ifadesini kullanarak erişebilirsiniz. this this Ayrıca, eğer bir yöntem çağrısının son argümanı lambda ise lambda parantezlerin dışına yerleştirilebilir. Bu nedenle DSL'imizde aşağıdaki gibi bir kod yazabiliriz: parallel { addStep { executor = FINANCE_DEPARTMENT ... } addStep { executor = CTO ... } } Bu, sözdizimsel şeker içermeyen bir koda eşdeğerdir: parallel({ this.addStep({ this.executor = FINANCE_DEPARTMENT ... }) this.addStep({ this.executor = CTO ... }) }) Alıcıları olan Lambdalar ve parantezlerin dışındaki Lambdalar, DSL'lerle çalışırken özellikle yararlı olan Kotlin özellikleridir. Nesne bildirimi Şimdi varlık bakalım. bir nesnedir. Kotlin'de nesne bildirimi, tek bir örneği olan bir sınıf olan singleton'u tanımlamanın bir yoludur. Yani nesne bildirimi hem sınıfı hem de onun tek örneğini aynı anda tanımlar. acceptance acceptance Operatörün aşırı yüklenmesini “çağırmak” Ayrıca, nesnesi için operatörü aşırı yüklenmiştir. operatörü, sınıflarınızda tanımlayabileceğiniz özel bir fonksiyondur. Bir sınıfın örneğini bir işlevmiş gibi çağırdığınızda, operatörü işlevi çağrılır. Bu, nesneleri işlev olarak ele almanıza ve onları işlev benzeri bir şekilde çağırmanıza olanak tanır. accreditation invoke invoke invoke yönteminin parametresinin de alıcılı bir lambda olduğuna dikkat edin. Artık bir kabul rotası tanımlayabiliriz… invoke val acceptanceRoute = acceptance { addStep { executor = HEAD_OF_DEPARTMENT ... } parallel { addStep { executor = FINANCE_DEPARTMENT ... } addStep { executor = CTO ... } } addStep { executor = SECRETARY ... } } …ve onun üzerinden yürüyün val headOfDepartmentStep = acceptanceRoute.elements[0] as StepContext val parallelBlock = acceptanceRoute.elements[1] as AcceptanceContext val ctoStep = parallelBlock.elements[1] as StepContext Ayrıntılar ekleniyor Infix işlevleri Bu koda bir göz atın addStep { executor = FINANCE_DEPARTMENT or CTO or CEO ... } Bunu aşağıdaki yöntemlerle uygulayabiliriz: 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) sınıfı birkaç olası görev yürütücüsü ayarlamamıza olanak tanır. infix işlevini . Bir infix işlevi, onu daha doğal bir infix gösterimi kullanarak çağırmanıza olanak tanıyan özel bir işlev türüdür. ExecutorCondition ExecutorCondition or Dilin bu özelliğini kullanmasaydık şu şekilde yazmamız gerekirdi: addStep { executor = FINANCE_DEPARTMENT.or(CTO).or(CEO) ... } Infix işlevleri aynı zamanda protokolün gerekli durumunu ve saat dilimini ayarlamak için de kullanılır. 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) } Uzatma işlevleri bir uzantı işlevidir. Kotlin'de eklenti işlevleri, mevcut sınıflara kaynak kodlarını değiştirmeden yeni işlevler eklemenizi sağlar. Bu özellik, standart veya harici kitaplıklardaki sınıflar gibi, üzerinde kontrol sahibi olmadığınız sınıfların işlevselliğini artırmak istediğinizde özellikle kullanışlıdır. String.timezone DSL'de kullanım: addStep { ... protocol shouldBe formed dueDate = "2022-12-08 08:00" timezone PST ... } Burada , üzerinde genişletme işlevi çağrıldığı bir alıcı nesnesidir ve parametredir. Alıcı nesnesine anahtar sözcük kullanılarak erişilir. "2022-12-08 08:00" timezone PST this Operatör aşırı yüklemesi DSL'imizde kullandığımız bir sonraki Kotlin özelliği operatör aşırı yüklemesidir. operatörünün aşırı yükünü zaten değerlendirdik. Kotlin'de aritmetik olanlar da dahil olmak üzere diğer operatörlere aşırı yükleme yapabilirsiniz. invoke addStep { ... +canChange } Burada tekli operatör aşırı yüklenmiştir. Bunu uygulayan kod aşağıdadır: + class StepContext : AcceptanceElement { ... var canChange = ChangePermission() } data class ChangePermission( var canChange: Boolean = true, ) { operator fun unaryPlus() { canChange = true } operator fun unaryMinus() { canChange = false } } Bitirici dokunuş Artık DSL'imizde kabul yollarını tanımlayabiliriz. Ancak DSL kullanıcılarının olası hatalardan korunması gerekmektedir. Örneğin, mevcut sürümde aşağıdaki kod kabul edilebilir: val acceptanceRoute = acceptance { addStep { executor = HEAD_OF_DEPARTMENT duration = days(7) protocol shouldBe signed addStep { executor = FINANCE_DEPARTMENT } } } içindeki tuhaf görünüyor, değil mi? Bu kodun neden hatasız bir şekilde başarıyla derlendiğini bulalım. Yukarıda bahsedildiği gibi, ve yöntemleri parametre olarak alıcı içeren bir lambda alır ve alıcı nesnesine anahtar sözcüğüyle erişilebilir. Böylece önceki kodu şu şekilde yeniden yazabiliriz: 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 } } } Artık iki seferde de çağrıldığını görebilirsiniz. Özellikle bu gibi durumlar için Kotlin'in açıklaması bulunmaktadır. Özel ek açıklamalar tanımlamak için kullanabilirsiniz. Aynı açıklamayla işaretlenen alıcılara birbirlerinin içinden erişilemez. this@acceptance.addStep DslMarker @DslMarker @DslMarker annotation class AcceptanceDslMarker @AcceptanceDslMarker class AcceptanceContext : AcceptanceElement { ... } @AcceptanceDslMarker class StepContext : AcceptanceElement { ... } Şimdi kod val acceptanceRoute = acceptance { addStep { ... addStep { ... } } } 'fun addStep(init: StepContext.() -> Unit): Unit' can't be called in this context by implicit receiver. Use the explicit one if necessary Bağlantılar Aşağıda, bu makalede dikkate alınan dil özelliklerine ilişkin resmi Kotlin belgelerine bağlantılar bulunmaktadır: Alıcı ile işlev değişmezleri https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver Sondaki lambdaları geçmek https://kotlinlang.org/docs/lambdas.html#passing-trailing-lambdas Nesne bildirimleri https://kotlinlang.org/docs/object-declarations.html#object-declarations-overview Operatör aşırı yüklemesi https://kotlinlang.org/docs/operator-overloading.html Ek gösterimi https://kotlinlang.org/docs/functions.html#infix-notation Uzantı işlevleri https://kotlinlang.org/docs/extensions.html#extension-functions Kapsam kontrolü: @DslMarker https://kotlinlang.org/docs/type-safe-builders.html#scope-control-dslmarker Çözüm Etki alanına özgü diller, belirli bir etki alanındaki sorunları modellemek ve çözmek için özelleştirilmiş ve etkileyici bir yol sağlayarak üretkenliği artırmak, hataları azaltmak ve işbirliğini geliştirmek için güçlü bir araç sunar. Kotlin birçok özelliği ve sözdizimsel yapısıyla modern bir programlama dilidir, dolayısıyla dahili DSL için ana dil olarak mükemmeldir.