Pengantar Pemrograman Berorientasi Konteks Kotlin

Ini adalah terjemahan dari Pengantar pemrograman berorientasi konteks di Kotlin

Pada artikel ini saya akan mencoba untuk menggambarkan fenomena baru yang muncul sebagai produk sampingan dari perkembangan pesat bahasa Kotlin. Ini adalah pendekatan baru untuk merancang arsitektur aplikasi dan perpustakaan, yang saya sebut pemrograman berorientasi konteks.

Beberapa kata tentang izin fungsi


Seperti diketahui, ada tiga paradigma pemrograman utama ( catatan Pedant : ada paradigma lain):

  • Pemrograman Prosedural
  • Pemrograman berorientasi objek
  • Pemrograman fungsional

Semua pendekatan ini berfungsi dengan satu atau lain cara. Mari kita lihat ini dari sudut pandang resolusi fungsi, atau penjadwalan panggilan mereka (artinya pilihan fungsi yang harus digunakan di tempat ini). Pemrograman prosedural ditandai dengan penggunaan fungsi global dan resolusi statisnya berdasarkan nama fungsi dan tipe argumen. Tentu saja, jenis hanya dapat digunakan dalam kasus bahasa yang diketik secara statis. Misalnya, dalam Python, fungsi dipanggil dengan nama, dan jika argumennya tidak benar, pengecualian dilempar ke dalam runtime selama eksekusi program. Resolusi fungsi dalam bahasa dengan pendekatan prosedural hanya didasarkan pada nama prosedur / fungsi dan parameternya, dan dalam banyak kasus dilakukan secara statis.

Gaya pemrograman berorientasi objek membatasi ruang lingkup fungsi. Fungsinya bukan global, melainkan merupakan bagian dari kelas, dan hanya dapat dipanggil dengan instance kelas yang sesuai ( Catatan Pedant : beberapa bahasa prosedural klasik memiliki sistem modular dan, oleh karena itu, ruang lingkup; bahasa prosedural! = C).

Tentu saja, kita selalu dapat mengganti fungsi anggota suatu kelas dengan fungsi global dengan argumen tambahan dari jenis objek yang dipanggil, tetapi dari sudut pandang sintaksis, perbedaannya cukup signifikan. Misalnya, dalam kasus ini, metode dikelompokkan dalam kelas yang mereka rujuk, dan oleh karena itu lebih jelas terlihat perilaku seperti apa yang disediakan oleh objek jenis ini.

Tentu saja, enkapsulasi adalah yang paling penting di sini, karena beberapa bidang dari kelas atau perilakunya dapat bersifat pribadi dan hanya dapat diakses oleh anggota kelas ini (Anda tidak dapat memberikan ini dalam pendekatan yang murni prosedural), dan polimorfisme, berkat metode yang sebenarnya digunakan ditentukan tidak hanya berdasarkan nama. metode, tetapi juga didasarkan pada jenis objek dari mana ia dipanggil. Mengirim panggilan metode dalam pendekatan berorientasi objek tergantung pada jenis objek yang didefinisikan dalam runtime, nama metode, dan jenis argumen pada tahap kompilasi.

Pendekatan fungsional tidak membawa sesuatu yang secara fundamental baru dalam hal resolusi fungsi. Biasanya, bahasa berorientasi fungsi memiliki aturan yang lebih baik untuk membedakan antara area visibilitas ( catatan pedant : sekali lagi, C tidak semua bahasa prosedural, ada yang di mana area visibilitas dibatasi dengan baik) yang memungkinkan kontrol yang lebih ketat atas visibilitas fungsi berdasarkan sistem. modul, tetapi selain itu, resolusi dilakukan pada waktu kompilasi berdasarkan pada jenis argumen.

Apa ini


Dalam hal pendekatan objek, ketika memanggil metode pada objek, kami memiliki argumennya, tetapi selain itu kami memiliki eksplisit (dalam kasus Python) atau parameter implisit yang mewakili instance dari kelas yang dipanggil (selanjutnya semua contoh ditulis dalam Kotlin):

class A{ fun doSomething(){ println("    $this") } } 

Kelas dan penutupan bersarang sedikit rumit:

 interface B{ fun doBSomething() } class A{ fun doASomething(){ val b = object: B{ override fun doBSomething(){ println("    $this  ${this@A}") } } b.doBSomething() } } 

Dalam hal ini, ada dua implisit ini untuk fungsi doBSomething - satu sesuai dengan instance kelas B , dan yang lainnya muncul dari penutupan instance A. Hal yang sama terjadi pada kasus penutupan lambda yang jauh lebih umum. Penting untuk dicatat bahwa ini dalam kasus ini berfungsi tidak hanya sebagai parameter implisit, tetapi juga sebagai ruang lingkup atau konteks untuk semua fungsi dan objek yang disebut dalam lingkup leksikal. Jadi metode doBSomething sebenarnya memiliki akses ke anggota kelas A , publik atau swasta, serta anggota B itu sendiri.

Dan ini Kotlin


Kotlin memberi kita "mainan" yang sama sekali baru - fungsi ekstensi . ( Catatan oleh Pedant : Sebenarnya, mereka tidak begitu baru, mereka juga ada di C #). Anda dapat mendefinisikan fungsi seperti A.doASomething () di mana saja dalam program, bukan hanya di dalam A. Di dalam fungsi ini, kita memiliki parameter implisit ini , yang disebut penerima, menunjuk ke instance A di mana metode ini dipanggil:

 class A fun A.doASomthing(){ println(" -   $this") } fun main(){ val a = A() a.doASomthing() } 

Fungsi ekstensi tidak memiliki akses ke anggota pribadi penerima mereka, sehingga enkapsulasi tidak dilanggar.

Hal penting berikutnya yang dimiliki Kotlin adalah blok kode dengan penerima. Anda dapat menjalankan blok kode arbitrer menggunakan sesuatu sebagai penerima:

 class A{ fun doInternalSomething(){} } fun A.doASomthing(){} fun main(){ val a = A() with(a){ doInternalSomething() doASomthing() } } 

Dalam contoh ini, kedua fungsi dapat dipanggil tanpa " a " tambahan . Pada awalnya, karena fungsi with menempatkan semua kode dari blok selanjutnya di dalam konteks a. Ini berarti bahwa semua fungsi dalam blok ini dipanggil seolah-olah mereka dipanggil pada objek (yang dilewati secara eksplisit) a .

Langkah terakhir pada titik ini dalam pemrograman berorientasi konteks adalah kemampuan untuk mendeklarasikan ekstensi sebagai anggota kelas. Dalam hal ini, fungsi ekstensi didefinisikan di dalam kelas lain, seperti ini:

 class B class A{ fun B.doBSomething(){} } fun main(){ val a = A() val b = B() with(a){ b.doBSomething() //   } b.doBSomething() //   } 

Adalah penting bahwa di sini B mendapatkan beberapa perilaku baru, tetapi hanya ketika itu berada dalam konteks leksikal tertentu. Fungsi ekstensi adalah anggota reguler kelas A. Ini berarti bahwa resolusi fungsi dilakukan secara statis berdasarkan konteks di mana ia dipanggil, tetapi implementasi nyata ditentukan oleh instance A yang dilewatkan sebagai konteks. Fungsi tersebut bahkan dapat berinteraksi dengan keadaan objek a .

Pengiriman Berorientasi Konteks


Pada awal artikel, kami membahas berbagai pendekatan untuk mengirim panggilan fungsi, dan ini dilakukan karena suatu alasan. Faktanya adalah bahwa fungsi ekstensi di Kotlin memungkinkan Anda untuk bekerja dengan pengiriman dengan cara baru. Sekarang keputusan tentang fungsi tertentu mana yang harus digunakan didasarkan tidak hanya pada jenis parameternya, tetapi juga pada konteks leksikal panggilannya. Artinya, ungkapan yang sama dalam konteks yang berbeda dapat memiliki makna yang berbeda. Tentu saja, tidak ada yang berubah dari sudut pandang implementasi, dan kami masih memiliki objek penerima eksplisit yang mendefinisikan pengiriman untuk metode dan ekstensi yang dijelaskan dalam tubuh kelas itu sendiri (ekstensi anggota) - tetapi dari sudut pandang sintaksis, ini adalah pendekatan yang berbeda .

Mari kita lihat bagaimana pendekatan berorientasi konteks berbeda dari pendekatan berorientasi objek klasik, menggunakan masalah klasik operasi aritmatika pada angka di Jawa sebagai contoh. Kelas Number di Java dan Kotlin adalah induk untuk semua angka, tetapi tidak seperti angka khusus seperti Double, ia tidak mendefinisikan operasi matematisnya. Jadi Anda tidak bisa menulis, misalnya, seperti ini:

 val n: Number = 1.0 n + 1.0 //  `plus`     `Number` 

Alasannya di sini adalah bahwa tidak mungkin untuk secara konsisten mendefinisikan operasi aritmatika untuk semua tipe numerik. Misalnya, pembagian integer berbeda dari divisi floating point. Dalam beberapa kasus khusus, pengguna tahu jenis operasi apa yang diperlukan, tetapi biasanya tidak masuk akal untuk mendefinisikan hal-hal seperti itu secara global. Solusi berorientasi objek (dan, pada kenyataannya, fungsional) akan mendefinisikan tipe pewarisan baru dari kelas Number , operasi yang diperlukan di dalamnya, dan menggunakannya di mana diperlukan (di Kotlin 1.3 Anda dapat menggunakan kelas inline). Sebagai gantinya, mari kita mendefinisikan konteks dengan operasi ini dan menerapkannya secara lokal:

 interface NumberOperations{ operator fun Number.plus(other: Number) : Number operator fun Number.minus(other: Number) : Number operator fun Number.times(other: Number) : Number operator fun Number.div(other: Number) : Number } object DoubleOperations: NumberOperations{ override fun Number.plus(other: Number) = this.toDouble() + other.toDouble() override fun Number.minus(other: Number) = this.toDouble() - other.toDouble() override fun Number.times(other: Number) = this.toDouble() * other.toDouble() override fun Number.div(other: Number) = this.toDouble() / other.toDouble() } fun main(){ val n1: Number = 1.0 val n2: Number = 2 val res = with(DoubleOperations){ (n1 + n2)/2 } println(res) } 

Dalam contoh ini, perhitungan res dilakukan di dalam konteks yang mendefinisikan operasi tambahan. Konteks tidak harus didefinisikan secara lokal, melainkan dapat diteruskan secara implisit sebagai penerima fungsi. Misalnya, Anda dapat melakukan ini:

 fun NumberOperations.calculate(n1: Number, n2: Number) = (n1 + n2)/2 val res = DoubleOperations.calculate(n1, n2) 

Ini berarti bahwa logika operasi dalam konteks sepenuhnya terpisah dari implementasi konteks ini, dan dapat ditulis di bagian lain dari program atau bahkan dalam modul lain. Dalam contoh sederhana ini, suatu konteks adalah singleton tanpa kewarganegaraan, tetapi konteks negara juga dapat digunakan.

Perlu juga diingat bahwa konteks dapat disarangkan:

 with(a){ with(b){ doSomething() } } 

Ini memberikan efek menggabungkan perilaku kedua kelas, namun, fitur ini sulit untuk dikontrol hari ini karena kurangnya ekstensi dengan beberapa penerima ( KT-10468 ).

Kekuatan Coroutines Eksplisit


Salah satu contoh terbaik dari pendekatan berorientasi konteks digunakan di perpustakaan Kotlinx-coroutines. Penjelasan ide dapat ditemukan dalam sebuah artikel oleh Roman Elizarov. Di sini, saya hanya ingin menekankan bahwa CoroutineScope adalah kasus desain berorientasi konteks dengan konteks stateful. CoroutineScope memainkan dua peran:

  • Ini berisi CoroutineContext , yang diperlukan untuk menjalankan coroutine dan diwariskan ketika coroutine baru diluncurkan.
  • Ini berisi keadaan coroutine induk, yang memungkinkan Anda untuk membatalkannya jika coroutine yang dihasilkan melempar kesalahan.

Juga, konkurensi terstruktur memberikan contoh yang bagus dari arsitektur berorientasi konteks:

 suspend fun CoroutineScope.doSomeWork(){} GlobalScope.launch{ launch{ delay(100) doSomeWork() } } 

Di sini, doSomeWork adalah fungsi konteks, tetapi didefinisikan di luar konteksnya. Metode peluncuran membuat dua konteks bersarang yang setara dengan area leksikal dari fungsi yang sesuai (dalam hal ini, kedua konteks memiliki tipe yang sama, sehingga konteks dalam mengaburkan yang luar). Titik awal yang baik untuk mempelajari coroutine Kotlin adalah panduan resmi.

DSL


Ada kelas tugas yang luas untuk Kotlin, yang biasanya disebut sebagai tugas membangun DSL (Domain Specific Language). Dalam hal ini, DSL dipahami sebagai beberapa kode yang menyediakan pembangun yang ramah pengguna dari beberapa jenis struktur yang kompleks. Bahkan, penggunaan istilah DSL tidak sepenuhnya benar di sini, seperti dalam kasus seperti itu, sintaksis dasar Kotlin hanya digunakan tanpa trik khusus - tetapi mari kita masih menggunakan istilah umum ini.

Pembuat DSL berorientasi konteks dalam banyak kasus. Misalnya, jika Anda ingin membuat elemen HTML, Anda harus terlebih dahulu memeriksa apakah elemen khusus ini dapat ditambahkan ke tempat ini. Perpustakaan kotlinx.html melakukan ini dengan memberikan ekstensi kelas berbasis konteks yang mewakili tag tertentu. Bahkan, seluruh pustaka terdiri dari ekstensi konteks untuk elemen DOM yang ada.

Contoh lain adalah pembangun GUI TornadoFX . Seluruh pembangun grafik adegan diatur sebagai urutan pembangun konteks bersarang, di mana balok bagian dalam bertanggung jawab untuk membangun anak-anak untuk blok luar atau menyesuaikan parameter orang tua. Ini adalah contoh dari dokumentasi resmi:

 override val root = gridPane{ tabpane { gridpaneConstraints { vhGrow = Priority.ALWAYS } tab("Report", HBox()) { label("Report goes here") } tab("Data", GridPane()) { tableview<Person> { items = persons column("ID", Person::idProperty) column("Name", Person::nameProperty) column("Birthday", Person::birthdayProperty) column("Age", Person::ageProperty).cellFormat { if (it < 18) { style = "-fx-background-color:#8b0000; -fx-text-fill:white" text = it.toString() } else { text = it.toString() } } } } } } 

Dalam contoh ini, wilayah leksikal mendefinisikan konteksnya (yang logis, karena mewakili bagian GUI dan struktur internalnya), dan memiliki akses ke konteks induk.

Apa selanjutnya: banyak penerima


Pemrograman berorientasi konteks memberi pengembang Kotlin banyak alat dan membuka cara baru merancang arsitektur aplikasi. Apakah kita membutuhkan yang lain? Mungkin ya.

Saat ini, pengembangan dalam pendekatan kontekstual dibatasi oleh fakta bahwa Anda perlu mendefinisikan ekstensi untuk mendapatkan semacam perilaku kelas yang terbatas konteks. Ini bagus untuk kelas kustom, tetapi bagaimana jika kita menginginkan hal yang sama untuk kelas dari perpustakaan? Atau jika kita ingin membuat ekstensi untuk perilaku yang sudah terbatas cakupannya (misalnya, tambahkan beberapa jenis ekstensi di dalam CoroutineScope)? Kotlin saat ini tidak mengizinkan fungsi ekstensi memiliki lebih dari satu penerima. Tetapi beberapa penerima dapat ditambahkan ke bahasa tanpa melanggar kompatibilitas ke belakang. Kemungkinan menggunakan beberapa penerima saat ini sedang dibahas ( KT-10468 ) dan akan dikeluarkan sebagai permintaan KEEP (UPD: sudah dikeluarkan ). Masalahnya (atau mungkin sebuah chip) dari konteks bersarang adalah bahwa mereka memungkinkan Anda untuk menutupi sebagian besar, jika tidak semua, opsi untuk menggunakan kelas tipe ( tipe-kelas ), yang sangat diinginkan dari fitur yang diusulkan. Agak tidak mungkin kedua fitur ini akan diimplementasikan dalam bahasa pada saat yang sama.

Selain itu


Kami ingin berterima kasih kepada Pedant dan kekasih Haskell penuh-waktu kami Alexei Khudyakov atas komentarnya pada teks artikel dan amandemen penggunaan istilah yang agak bebas. Saya juga berterima kasih kepada Ilya Ryzhenkov untuk komentar yang berharga dan mengoreksi artikel versi bahasa Inggris.

Penulis artikel asli: Alexander Nozik , Wakil Kepala Laboratorium Metode Eksperimen Fisika Nuklir di Penelitian JetBrains .

Diterjemahkan oleh: Petr Klimay , Peneliti di Laboratorium Metode Percobaan Fisika Nuklir di Penelitian JetBrains

Source: https://habr.com/ru/post/id477898/


All Articles