Modul Ketergantungan Injeksi, JavaScript, dan ES6

Implementasi lain dari Dependency Injection dalam JavaScript adalah dengan modul ES6, dengan kemampuan untuk menggunakan kode yang sama di browser dan di nodejs dan tidak menggunakan transpiler.


gambar


Di bawah potongan adalah pandangan saya tentang DI, tempatnya dalam aplikasi web modern, implementasi mendasar dari wadah DI yang dapat membuat objek baik di depan dan belakang, serta penjelasan tentang apa yang harus dilakukan Michael Jackson dengan itu.


Saya sangat meminta mereka yang merasa sepele dalam artikel untuk tidak memperkosa diri sendiri dan tidak membaca sampai akhir, sehingga nanti, ketika mereka kecewa, mereka tidak memberi "minus". Saya tidak menentang "minus" - tetapi hanya jika minus disertai dengan komentar, apa yang sebenarnya di publikasi menyebabkan reaksi negatif. Ini adalah artikel teknis, jadi cobalah untuk merendahkan gaya presentasi, dan mengkritik dengan tepat komponen teknis di atas. Terima kasih


Objek dalam aplikasi


Saya sangat menghargai pemrograman fungsional, tetapi saya telah mencurahkan sebagian besar aktivitas profesional saya untuk membuat aplikasi yang terdiri dari objek. JavaScript membuat saya terkesan dengan fakta bahwa fungsi di dalamnya juga objek. Saat membuat aplikasi, saya memikirkan objek, ini adalah deformasi profesional saya.


Menurut masa hidup, objek dalam aplikasi dapat dibagi ke dalam kategori berikut:


  • permanen - muncul pada beberapa tahap aplikasi dan dihancurkan hanya ketika aplikasi selesai;
  • sementara - timbul ketika perlu untuk melakukan beberapa operasi dan dihancurkan ketika operasi ini selesai;

Dalam hal ini, dalam pemrograman ada pola desain seperti:



Artinya, dari sudut pandang saya, aplikasi terdiri dari penyendiri yang ada secara permanen yang melakukan sendiri operasi yang diperlukan atau menghasilkan objek sementara untuk menjalankannya.


Wadah Benda


Ketergantungan injeksi adalah suatu pendekatan yang membuatnya mudah untuk membuat objek dalam suatu aplikasi. Artinya, dalam aplikasi ada objek khusus yang β€œtahu” cara membuat semua objek lain. Objek seperti itu disebut Object Container (kadang-kadang Object Manager).


Wadah Objek bukan Obyek Ilahi , karena tugasnya hanya untuk membuat objek aplikasi yang signifikan dan menyediakan akses ke objek lain untuk mereka. Sebagian besar objek aplikasi, yang dihasilkan oleh Container dan terletak di dalamnya, tidak tahu tentang Container itu sendiri. Mereka dapat ditempatkan di lingkungan lain, disediakan dengan dependensi yang diperlukan, dan mereka juga akan berfungsi dengan sangat baik di sana (penguji tahu apa yang saya maksud).


Tempat implementasi


Secara umum, ada dua cara untuk menyuntikkan dependensi ke dalam objek:


  • melalui konstruktor;
  • melalui properti (atau aksesornya);

Saya pada dasarnya menggunakan pendekatan pertama, jadi saya akan melanjutkan deskripsi dengan sudut pandang injeksi ketergantungan melalui konstruktor.


Katakanlah kita memiliki aplikasi yang terdiri dari tiga objek:


gambar


Dalam PHP (bahasa ini dengan tradisi DI yang sudah lama ada, saya saat ini memiliki bawaan aktif, saya akan pindah ke JS sedikit kemudian) situasi serupa dapat tercermin dengan cara ini:


class Config { public function __construct() { } } class Service { private $config; public function __construct(Config $config) { $this->config = $config; } } class Application { private $config; private $service; public function __construct(Config $config, Service $service) { $this->config = $config; $this->service = $service; } } 

Informasi ini harus cukup sehingga wadah DI (misalnya, liga / wadah ), jika dikonfigurasi dengan tepat, dapat, atas permintaan untuk membuat objek Application , membuat Service dependensi dan Config dan meneruskannya parameter ke konstruktor objek Application .


Pengidentifikasi ketergantungan


Bagaimana Kontainer Obyek memahami bahwa konstruktor objek Application memerlukan dua objek Config dan Service ? Dengan menganalisis objek melalui API Refleksi ( Java , PHP ) atau dengan menganalisis kode objek secara langsung (anotasi kode). Artinya, dalam kasus umum, kita dapat menentukan nama-nama variabel yang diharapkan oleh konstruktor objek pada input, dan jika bahasa dapat diketik, kita juga bisa mendapatkan jenis variabel ini.


Dengan demikian, sebagai pengidentifikasi objek, Wadah dapat beroperasi dengan nama parameter input konstruktor atau jenis parameter input.


Buat Objek


Objek dapat secara eksplisit dibuat oleh pemrogram dan ditempatkan di Kontainer di bawah pengidentifikasi yang sesuai (misalnya, "konfigurasi")


 /** @var \League\Container\Container $container */ $container->add("configuration", $config); 

dan dapat dibuat oleh Kontainer sesuai dengan aturan khusus tertentu. Aturan-aturan ini, pada umumnya, turun untuk mencocokkan pengidentifikasi objek ke kode. Aturan dapat diatur secara eksplisit (pemetaan dalam bentuk kode, XML, JSON, ...)


 [ ["object_id_1", "/path/to/source1.php"], ["object_id_2", "/path/to/source2.php"], ... ] 

atau dalam bentuk beberapa algoritma:


 public function getSource($id) {. return "/path/to/source/${id}.php"; } 

Dalam PHP, aturan untuk mencocokkan nama kelas ke file dengan kode sumbernya adalah standar ( PSR-4 ), di Jawa, pencocokan dilakukan pada tingkat konfigurasi JVM ( pemuat kelas ). Jika Wadah menyediakan pencarian sumber secara otomatis saat membuat objek, maka nama kelas adalah pengidentifikasi yang cukup baik untuk objek dalam Wadah tersebut.


Ruang nama


Biasanya dalam suatu proyek, di samping kode sendiri, modul pihak ketiga juga digunakan. Dengan munculnya manajer dependensi (pakar, komposer, npm), penggunaan modul telah sangat disederhanakan, dan jumlah modul dalam proyek telah meningkat pesat. Ruang nama memungkinkan elemen kode dengan nama yang sama ada dalam satu proyek tunggal dari berbagai modul (kelas, fungsi, konstanta).


Ada bahasa di mana namespace dibangun pada awalnya (Java):


 package vendor.project.module.folder; 

Ada bahasa di mana namespace ditambahkan selama pengembangan bahasa (PHP):


 namespace Vendor\Project\Module\Folder; 

Implementasi namespace yang baik memungkinkan Anda untuk secara jelas membahas setiap elemen kode:


 \Doctrine\Common\Annotations\Annotation\Attribute::$name 

Namespace memecahkan masalah mengatur banyak elemen perangkat lunak dalam suatu proyek, dan struktur file memecahkan masalah mengatur file pada disk. Oleh karena itu, tidak hanya ada banyak kesamaan di antara mereka, dan kadang-kadang sangat banyak - di Jawa, misalnya, kelas publik di namespace harus secara unik dilampirkan ke file dengan kode kelas ini.


Dengan demikian, menggunakan pengidentifikasi kelas objek di namespace proyek sebagai pengidentifikasi objek dalam Container adalah ide yang baik dan dapat berfungsi sebagai dasar untuk membuat aturan untuk deteksi otomatis kode sumber saat membuat objek yang diinginkan.


 $container->add(\Vendor\Project\Module\ObjectType::class, $obj); 

Startup kode


Dalam composer PHP composer namespace modul dipetakan ke sistem file di dalam modul di deskriptor modul composer.json :


 "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } } 

Komunitas JS bisa melakukan pemetaan serupa di package.json jika ada ruang nama di JS.


Pengidentifikasi ketergantungan JS


Di atas, saya menunjukkan bahwa Wadah dapat menggunakan nama parameter input konstruktor atau jenis parameter input sebagai pengidentifikasi. Masalahnya adalah:


  1. JS adalah bahasa dengan pengetikan dinamis dan tidak menyediakan untuk menunjukkan jenis ketika mendeklarasikan suatu fungsi.
  2. JS menggunakan minifiers yang dapat mengubah nama parameter input.

Pengembang wadah awilix DI menyarankan untuk menggunakan objek sebagai satu-satunya parameter input ke konstruktor, dan properti objek ini sebagai dependensi:


 class UserController { constructor(opts) { this.userService = opts.userService } } 

Pengidentifikasi properti objek di JS dapat terdiri dari karakter alfanumerik, "_" dan "$", dan mungkin tidak dimulai dengan angka.


Karena kita perlu memetakan pengidentifikasi ketergantungan ke jalur ke sumber mereka di sistem file untuk autoload, lebih baik untuk meninggalkan penggunaan "$" dan menggunakan pengalaman PHP. Sebelum operator namespace muncul di beberapa kerangka kerja (misalnya, dalam Zend 1), nama-nama berikut digunakan untuk kelas:


 class Zend_Config_Writer_Json {...} 

Dengan demikian, kita dapat merefleksikan aplikasi kita dari tiga objek ( Application , Config , Service ) pada JS seperti ini:


 class Vendor_Project_Config { constructor() { } } class Vendor_Project_Service { constructor({Vendor_Project_Config}) { this.config = Vendor_Project_Config; } } class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

Jika kami memposting kode dari setiap kelas:


 export default class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

dalam file Anda di dalam modul proyek kami:


  • ./src/
    • ./Application.js
    • ./Config.js
    • ./Service.js

Kemudian kita dapat menghubungkan direktori root modul dengan root "namespace" dari modul dalam konfigurasi Container:


 const ns = "Vendor_Project"; const path = path.join(module_root, "src"); container.addSourceMapping(ns, path); 

dan kemudian, mulai dari informasi ini, buat path ke sumber yang sesuai ( ${module_root}/src/Config.js ) berdasarkan pengenal dependensi ( ${module_root}/src/Config.js ).


Modul ES6


ES6 menawarkan desain umum untuk memuat modul ES6:


 import { something } from 'path/to/source/with/something'; 

Karena kita perlu melampirkan satu objek (kelas) ke satu file, masuk akal di sumber untuk mengekspor kelas ini secara default:


 export default class Vendor_Project_Path_To_Source_With_Something {...} 

Pada prinsipnya, mungkin untuk tidak menulis nama yang panjang untuk kelas, hanya Something akan bekerja juga, tetapi dalam Zend 1 mereka menulis dan tidak pecah, dan keunikan nama kelas dalam proyek secara positif mempengaruhi kemampuan IDE (autocomplete dan kontekstual yang diminta), jadi dan ketika debugging:


gambar


Mengimpor kelas dan membuat objek dalam kasus ini terlihat seperti ini:


 import Something from 'path/to/source/with/something'; const something = new Something(); 

Impor Depan & Belakang


Impor berfungsi baik di browser dan di nodejs, tetapi ada nuansa. Misalnya, browser tidak memahami impor modul simpuljs:


 import path from "path"; 

Kami mendapatkan kesalahan di browser:


 Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../". 

Artinya, jika kita ingin kode kita berfungsi di browser dan nodejs, kita tidak dapat menggunakan konstruksi yang tidak dipahami oleh browser atau nodejs. Saya secara khusus fokus pada hal ini, karena kesimpulan seperti itu terlalu alami untuk dipikirkan. Cara bernafas.


Tempat DI dalam aplikasi web modern


Ini murni pendapat pribadi saya, karena pengalaman pribadi saya, seperti semua hal lain dalam publikasi ini.


Dalam aplikasi web, JS praktis mengambil tempat di bagian depan, di browser, hampir tanpa alternatif. Di sisi server, Java, PHP, .Net, Ruby, python, digali dengan kuat ... Tetapi dengan munculnya nodejs, JavaScript juga menembus server. Dan teknologi yang digunakan dalam bahasa lain, termasuk DI, mulai menembus JS server-side.


Pengembangan JavaScript disebabkan oleh perilaku kode yang tidak sinkron di browser. Asynchrony bukan fitur yang luar biasa dari JS, melainkan bawaan. Sekarang kehadiran JS baik di server dan di bagian depan tidak mengejutkan siapa pun, tetapi lebih merangsang penggunaan pendekatan yang sama di kedua ujung aplikasi web. Dan kode yang sama. Tentu saja, bagian depan dan belakang pada dasarnya terlalu berbeda dan dalam tugas yang harus diselesaikan untuk menggunakan kode yang sama baik di sana maupun di sana. Tetapi kita dapat mengasumsikan bahwa dalam aplikasi yang kurang lebih kompleks akan ada browser, server dan kode umum.


DI sudah digunakan di depan, di RequireJS :


 define( ["./config", "./service"], function App(Config, Service) {} ); 

Benar, di sini pengidentifikasi dependensi ditulis secara eksplisit dan segera dalam bentuk tautan ke sumber (Anda dapat mengonfigurasi pemetaan pengidentifikasi dalam konfigurasi bootloader).


Dalam aplikasi web modern, DI ada tidak hanya di sisi server, tetapi juga di browser.


Apa hubungannya Michael Jackson dengan itu?


Ketika Anda mengaktifkan dukungan modul-ES di nodejs (flag --experimental-modules ), mesin mengidentifikasi konten file dengan *.mjs sebagai modul EcmaScript (tidak seperti modul-Common dengan *.cjs ).


Pendekatan ini kadang-kadang disebut " Solusi Michael Jackson ", dan skrip disebut Michael Jackson Scripts ( *.mjs ).


Saya setuju bahwa intrik yang begitu-begitu saja dengan KDPV telah diselesaikan, tetapi ... para pria itu, Michael Jackson ...


Namun Implementasi DI Lainnya


Yah, seperti yang diharapkan, milikmu sendiri sepeda Modul DI - @ teqfw / di


Ini bukan solusi yang siap untuk diperjuangkan, tetapi lebih merupakan implementasi mendasar. Semua dependensi harus merupakan modul ES dan menggunakan fitur umum untuk browser dan nodejs.


Untuk mengatasi dependensi, modul menggunakan pendekatan awilix :


 constructor(spec) { /** @type {Vendor_Module_Config} */ const _config = spec.Vendor_Module_Config; /** @type {Vendor_Module_Service} */ const _service = spec.Vendor_Module_Service; } 

Untuk menjalankan contoh kembali:


 import Container from "./src/Container.mjs"; const container = new Container(); container.addSourceMapping("Vendor_Module", "../example"); container.get("Vendor_Module_App") .then((app) => { app.run(); }); 

di server:


 $ node --experimental-modules main.mjs 

Untuk menjalankan contoh depan ( example.html ):


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>DI in Browser</title> <script type="module" src="./main.mjs"></script> </head> <body> <p>Load main script './main.mjs', create new DI container, then get object by ID from container.</p> <p>Open browser console to see output.</p> </body> </html> 

Anda perlu meletakkan modul di server dan membuka halaman example.html di browser (atau menggunakan kemampuan IDE). Jika Anda membuka example.html secara langsung, maka kesalahan di Chrom adalah:


 Access to script at 'file:///home/alex/work/teqfw.di/main.mjs' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https. 

Jika semuanya berjalan dengan baik, maka di konsol (browser atau nodejs) akan ada sesuatu seperti ini:


 Create object with ID 'Vendor_Module_App'. Create object with ID 'Vendor_Module_Config'. There is no dependency with id 'Vendor_Module_Config' yet. 'Vendor_Module_Config' instance is created. Create object with ID 'Vendor_Module_Service'. There is no dependency with id 'Vendor_Module_Service' yet. 'Vendor_Module_Service' instance is created (deps: [Vendor_Module_Config]). 'Vendor_Module_App' instance is created (deps: [Vendor_Module_Config, Vendor_Module_Service]). Application 'Vendor_Module_Config' is running. 

Ringkasan


AMD, CommonJS, UMD?


ESM !

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


All Articles