Menurunkan tingkat konektivitas menggunakan DI untuk meningkatkan testabilitas kode, contoh implementasi

Di awal artikel saya ingin segera mencatat bahwa saya tidak berpura-pura baru, tetapi hanya ingin berbagi / mengingat peluang seperti IoC DI.


Juga, saya hampir tidak memiliki pengalaman menulis artikel, ini adalah yang pertama. Saya mencoba yang terbaik, jika Anda tidak menghakimi dengan ketat.


Apa yang kita bicarakan


Sebagian besar proyek Rails yang saya temui memiliki satu masalah besar. Mereka tidak memiliki tes sama sekali, atau tes mereka memeriksa beberapa bagian yang tidak signifikan, sementara kualitas tes ini menyisakan banyak yang diinginkan.


Alasan utama untuk ini adalah bahwa pengembang sama sekali tidak tahu bagaimana menulis kode sehingga dalam unit test hanya menguji kode yang ditulis olehnya, dan tidak menguji kode itu, misalnya, terkandung dalam beberapa objek layanan lain atau perpustakaan.


Selanjutnya, rantai logis dibentuk di kepala programmer, tetapi mengapa saya perlu mentransfer kode logika bisnis ke lapisan lain, saya akan menambahkan hanya beberapa baris di sini dan semuanya akan memenuhi persyaratan pelanggan.


Dan ini sangat buruk, karena pengujian unit kehilangan ketahanannya terhadap refactoring, dan mengelola perubahan dalam kode tersebut menjadi sulit. Entropi secara bertahap meningkat. Dan jika Anda sudah takut untuk memperbaiki kode Anda, semuanya akan sangat buruk.


Untuk mengatasi masalah seperti itu di dunia Jawa, sejumlah perpustakaan telah lama ada dan tidak ada alasan khusus untuk menemukan kembali roda, meskipun perlu dicatat bahwa solusi ini sangat rumit dan tidak selalu ada alasan untuk menggunakannya. Rubistists tampaknya entah bagaimana menyelesaikan masalah seperti itu dengan cara yang berbeda, tetapi jujur, saya masih tidak mengerti caranya. Karena itu, saya memutuskan untuk membagikan bagaimana saya memutuskan untuk melakukannya.


Gagasan umum tentang bagaimana menyelesaikan ini dalam proyek ruby


Ide dasarnya adalah bahwa untuk objek yang memiliki dependensi, kita harus dapat mengelolanya.


Pertimbangkan sebuah contoh:


class UserService def initialize() @notification_service = NotificationService.new end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end 

Untuk menguji metode block_user, kami menemukan diri kami pada saat yang tidak menyenangkan, karena pemberitahuan dari NotificationService akan bekerja untuk kami dan kami dipaksa untuk memproses beberapa bagian minimal yang dilakukan oleh metode ini.
Pembalikan memungkinkan kita keluar dari situasi ini jika kita mengimplementasikan UserService, misalnya, seperti ini:


 class UserService def initialize(notification_service = NotificationService.new) @notification_service = notification_service end def block_user(user) user.block! @notification_service.send(user, 'you have been blocked') end end 

Sekarang, saat pengujian, kami melewatkan objek sebagai tiruan NotificationService, dan memverifikasi bahwa block_user menarik metode notification_service dalam urutan yang benar dan dengan argumen yang benar.


 RSpec.describe UserService, type: :service do let (:notification_service) { instance_double(NotificationService) } let (:service) { UserService.new(notification_service) } describe ".block_user" do let (:user) { instance_double(User) } it "should block user and send notification" do expect(user).to receive :block! expect(notification_service).to receive(:send).with(user, "you have been blocked") service.block_user(user) end end end 

Studi kasus untuk rel


Ketika ada banyak objek layanan dalam sistem, menjadi sulit untuk membangun semua dependensi sendiri, kode mulai tumbuh dengan baris kode tambahan yang mengurangi keterbacaan.


Dalam hal ini, terpikir oleh saya untuk menulis modul kecil yang mengotomatiskan manajemen ketergantungan.


 module Services module Injector def self.included(base) # TODO: check base, should be controller or service base.extend ClassMethods end module ClassMethods def inject_service(name) service = Services::Helpers::Service.new(name) attr_writer service.to_s define_method service.to_s do instance_variable_get("@#{service.to_s}").tap { |s| return s if s } instance_variable_set("@#{service.to_s}", service.instance) end end end end module Helpers class Service def initialize(name) raise ArgumentError, 'name of service should be a Symbol' unless name.is_a? Symbol @name = name.to_s.downcase @class = "#{@name.camelcase}Service".constantize unless @class.respond_to? :instance raise ArgumentError, "#{@name.to_s} should be singleton (respond to instance method)" end end def to_s "#{@name}_service" end def instance if Rails.env.test? if defined? RSpec::Mocks::ExampleMethods extend RSpec::Mocks::ExampleMethods instance_double @class else nil end else @class.instance end end end end end 

Ada satu peringatan, layanan harus Singleton, yaitu memiliki metode contoh. Cara termudah untuk melakukan ini adalah dengan menulis include Singleton di kelas layanan.


Sekarang di ApplicationController tambahkan


 require 'services' class ApplicationController < ActionController::Base include Services::Injector end 

Dan sekarang di controller kita bisa melakukan ini


 class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end 

Dalam spesifikasi controller ini, kita secara otomatis mendapatkan instance_double (WelcomeService) sebagai dependensi.


 RSpec.describe WelcomeController, type: :controller do describe "index" do it "should render text from test service" do allow(controller.welcome_service).to receive(:text).and_return "OK" get :index expect(response).to have_attributes body: "OK" expect(response).to have_http_status 200 end end end 

Apa yang bisa diperbaiki


Bayangkan, misalnya, bahwa dalam sistem kami ada beberapa opsi untuk bagaimana kami dapat mengirim pemberitahuan, misalnya pada malam hari akan menjadi satu penyedia, dan yang lain di siang hari. Pada saat yang sama, penyedia memiliki protokol pengiriman yang sama sekali berbeda.


Secara umum, antarmuka NotificationService tetap sama, tetapi ada dua implementasi spesifik.


 class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end 

Sekarang kita dapat menulis kelas yang akan melakukan pemetaan layanan bersyarat


 class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end 

Sekarang ketika kita mengambil instance layanan di Services :: Helpers :: Service.instance kita perlu memeriksa apakah ada objek * Mapper, dan jika demikian, maka ambil konstanta kelas melalui take.

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


All Articles