Au début de l'article, je tiens à noter immédiatement que je ne prétends pas être nouveau, mais seulement partager / rappeler une opportunité comme IoC DI.
De plus, je n'ai pratiquement aucune expérience dans la rédaction d'articles, c'est ma première. J'ai fait de mon mieux, si vous ne jugez pas strictement.
De quoi parle-t-on
La plupart des projets Rails que j'ai rencontrés ont un gros problème. Soit ils n'ont pas du tout de tests, soit leurs tests vérifient une partie insignifiante, tandis que la qualité de ces tests laisse beaucoup à désirer.
La raison principale en est que le développeur ne sait tout simplement pas comment écrire le code de sorte que dans les tests unitaires, testez uniquement le code écrit par lui, et non pas le code, qui, par exemple, est contenu dans un autre objet de service ou bibliothèque.
De plus, une chaîne logique est formée dans la tête du programmeur, mais pourquoi dois-je transférer le code logique métier vers une autre couche, j'ajouterai juste quelques lignes ici et tout répondra aux exigences du client.
Et c'est très mauvais, car les tests unitaires perdent leur résistance au refactoring, et la gestion des changements dans un tel code devient difficile. L'entropie augmente progressivement. Et si vous avez déjà peur de refactoriser votre code, les choses vont très mal.
Pour résoudre de tels problèmes dans le monde Java, un certain nombre de bibliothèques existent depuis longtemps et il n'y a pas de raison particulière de réinventer la roue, même s'il convient de noter que ces solutions sont très lourdes et qu'il n'y a pas toujours de raison de les utiliser. Les rubististes semblent en quelque sorte résoudre ces problèmes d'une manière différente, mais honnêtement, je ne comprenais toujours pas comment. Par conséquent, j'ai décidé de partager comment j'ai décidé de le faire.
L'idée générale de la façon de résoudre ce problème dans les projets rubis
L'idée de base est que pour les objets qui ont des dépendances, nous devrions pouvoir les gérer.
Prenons un exemple:
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
Pour tester la méthode block_user, nous nous trouvons à un moment désagréable, car la notification de NotificationService fonctionnera pour nous et nous sommes obligés de traiter une partie minimale que cette méthode effectue.
L'inversion nous permet de simplement sortir de cette situation si nous implémentons un UserService, par exemple, comme ceci:
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
Maintenant, lors des tests, nous passons un objet en tant que maquette NotificationService et vérifions que block_user extrait les méthodes notification_service dans le bon ordre et avec les bons arguments.
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
Étude de cas pour les rails
Lorsqu'il y a beaucoup d'objets de service dans le système, il devient difficile de construire toutes les dépendances vous-même, le code commence à croître avec des lignes de code supplémentaires qui réduisent la lisibilité.
À cet égard, il m'est venu à l'esprit d'écrire un petit module qui automatise la gestion des dépendances.
module Services module Injector def self.included(base)
Il y a une mise en garde, le service devrait être Singleton, c'est-à-dire avoir une méthode d'instance. Pour ce faire, la méthode la plus simple consiste à écrire include Singleton
dans la classe de service.
Maintenant, dans ApplicationController, ajoutez
require 'services' class ApplicationController < ActionController::Base include Services::Injector end
Et maintenant, dans les contrôleurs, nous pouvons le faire
class WelcomeController < ApplicationController inject_service :welcome def index render plain: welcome_service.text end end
Dans la spécification de ce contrôleur, nous obtenons automatiquement instance_double (WelcomeService) en tant que dépendance.
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
Ce qui peut être amélioré
Imaginez, par exemple, que dans notre système, il existe plusieurs options pour envoyer des notifications, par exemple, la nuit, ce sera un fournisseur et un autre pendant la journée. Dans le même temps, les fournisseurs ont des protocoles d'envoi complètement différents.
En général, l'interface NotificationService reste la même, mais il existe deux implémentations spécifiques.
class NightlyNotificationService < NotificationService end class DailyNotificationService < NotificationService end
Maintenant, nous pouvons écrire une classe qui effectuera un mappage conditionnel des services
class NotificationServiceMapper include Singleton def take now = Time.now ((now.hour >= 00) and (now.hour <= 8)) ? NightlyNotificationService : DailyNotificationService end end
Maintenant, lorsque nous prenons l'instance de service dans Services :: Helpers :: Service.instance, nous devons vérifier s'il existe un objet * Mapper, et si c'est le cas, puis prendre la constante de classe via take.