Am Anfang des Artikels möchte ich sofort darauf hinweisen, dass ich nicht vorgebe, neu zu sein, sondern nur eine Gelegenheit wie IoC DI teilen / zurückrufen möchte.
Außerdem habe ich fast keine Erfahrung mit dem Schreiben von Artikeln, dies ist meine erste. Ich habe mein Bestes versucht, wenn Sie nicht streng urteilen.
Worüber reden wir?
Die meisten Rails-Projekte, auf die ich gestoßen bin, haben ein großes Problem. Entweder haben sie überhaupt keine Tests oder ihre Tests überprüfen einen unbedeutenden Teil, während die Qualität dieser Tests zu wünschen übrig lässt.
Der Hauptgrund dafür ist, dass der Entwickler einfach nicht weiß, wie er den Code schreiben soll, so dass in Komponententests nur der von ihm geschriebene Code getestet wird und nicht der Code, der beispielsweise in einem anderen Dienstobjekt oder einer anderen Bibliothek enthalten ist.
Außerdem bildet sich im Kopf des Programmierers eine logische Kette. Warum sollte ich den Geschäftslogikcode überhaupt in eine andere Ebene einfügen, füge ich hier nur ein paar Zeilen hinzu und alles wird den Anforderungen des Kunden entsprechen.
Und das ist sehr schlecht, weil Unit-Tests ihre Resistenz gegen Refactoring verlieren und die Verwaltung von Änderungen in einem solchen Code schwierig wird. Die Entropie nimmt allmählich zu. Und wenn Sie bereits Angst haben, Ihren Code umzugestalten, sind die Dinge sehr schlecht.
Um solche Probleme in der Java-Welt zu lösen, gibt es seit langem eine Reihe von Bibliotheken, und es ist wenig sinnvoll, das Rad neu zu erfinden, obwohl zu beachten ist, dass diese Lösungen sehr umständlich sind und es nicht immer einen Grund gibt, sie zu verwenden. Rubististen scheinen solche Probleme irgendwie anders zu lösen, aber ehrlich gesagt habe ich immer noch nicht verstanden, wie. Deshalb habe ich mich entschlossen zu teilen, wie ich mich dazu entschlossen habe.
Die allgemeine Idee, wie dies in Ruby-Projekten gelöst werden kann
Die Grundidee ist, dass wir Objekte mit Abhängigkeiten verwalten können sollten.
Betrachten Sie ein Beispiel:
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
Um die block_user-Methode zu testen, befinden wir uns in einem unangenehmen Moment, da die Benachrichtigung von NotificationService für uns funktioniert und wir gezwungen sind, einen minimalen Teil dieser Methode zu verarbeiten.
Inversion ermöglicht es uns, einfach aus dieser Situation herauszukommen, wenn wir beispielsweise einen UserService wie folgt implementieren:
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
Beim Testen übergeben wir nun ein Objekt als NotificationService-Mock und stellen sicher, dass block_user die Methoden notification_service in der richtigen Reihenfolge und mit den richtigen Argumenten abruft.
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
Fallstudie für Schienen
Wenn das System viele Serviceobjekte enthält, wird es schwierig, alle Abhängigkeiten selbst zu erstellen. Der Code wächst mit zusätzlichen Codezeilen, die die Lesbarkeit beeinträchtigen.
In diesem Zusammenhang kam mir der Gedanke, ein kleines Modul zu schreiben, das das Abhängigkeitsmanagement automatisiert.
module Services module Injector def self.included(base)
Es gibt eine Einschränkung: Der Dienst sollte Singleton sein, d. H. habe eine Instanzmethode. Der einfachste Weg, dies zu tun, besteht darin, include Singleton
in die Serviceklasse aufzunehmen.
Jetzt im ApplicationController hinzufügen
require 'services' class ApplicationController < ActionController::Base include Services::Injector end
Und jetzt können wir dies in den Controllern tun
class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end
In der Spezifikation dieses Controllers erhalten wir automatisch instance_double (WelcomeService) als Abhängigkeit.
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
Was kann verbessert werden
Stellen Sie sich zum Beispiel vor, dass es in unserem System verschiedene Möglichkeiten gibt, wie wir Benachrichtigungen senden können, zum Beispiel nachts, wenn es sich um einen Anbieter und tagsüber um einen anderen handelt. Gleichzeitig haben Anbieter völlig unterschiedliche Sendeprotokolle.
Im Allgemeinen bleibt die NotificationService-Schnittstelle dieselbe, es gibt jedoch zwei spezifische Implementierungen.
class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end
Jetzt können wir eine Klasse schreiben, die eine bedingte Zuordnung von Diensten durchführt
class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end
Wenn wir nun die Dienstinstanz in Services :: Helpers :: Service.instance verwenden, müssen wir überprüfen, ob ein * Mapper-Objekt vorhanden ist. Wenn ja, nehmen Sie die Klassenkonstante durch take.