[RUBY] Refactor Fat Controller pour le faire ressembler à DDD

Tout à coup, disons que vous avez une action Fat Controller comme celle-ci.

reports_controller.rb


class ReportsController < ApplicationController
  def create
    @report = Report.new(report_params)
    if @report.status == Report::STATUS_DRAFT
      if @report.save
        redirect_to @report
      else
        render :new
      end
    else
      result = false
      ApplicationRecord.transaction do
        #Signaler une demande d'examen
        if @report.report_type == Report::REPORT_TYPE_REVIEW_REQUEST
          #Traitement tel que la notification du réviseur et l'envoi d'un e-mail ...
        end
        @report.save!
        result = true
      end
      if result
        redirect_to @report
      else
        render :new
      end
    end
  rescue StanderdError
    render :new
  end
end

Il peut être amélioré un peu en créant une méthode dans la classe Early Return ou Report, mais cette fois, je vais introduire une autre approche.

Préparer un test avant de refactoriser?

Je pense que la classe qui est Fat Controller est dans un état où il n'y a pas de test ou seule la partie qui est facile à faire est testée. À la lumière des principes généraux, j'aimerais avoir un test prêt avant de refactoriser. Cependant, si le test est facile à écrire, le test devrait déjà exister, et si vous essayez de le faire, vous ressentirez une douleur qui n'est pas à moitié enthousiaste et votre cœur sera brisé. Donc, écrivez un test uniquement pour le code après refactoring, et lorsque le refactoring est terminé, revenez au code avant refactoring et vérifiez que le test réussit. C'est plus risqué que d'écrire un test à l'avance, mais je pense que c'est une méthode réaliste.

Préparer le service d'application

Tout d'abord, préparez la classe ApplicationService et copiez exactement l'action.

app/services/create_report_service.rb


class CreateReportService
  def create
    @report = Report.new(report_params)
    if @report.status == Report::STATUS_DRAFT
      if @report.save
        redirect_to @report
      else
        render :new
      end
    else
      result = false
      ApplicationRecord.transaction do
        #Signaler une demande d'examen
        if @report.report_type == Report::REPORT_TYPE_REVIEW_REQUEST
          #Traitement tel que la notification du réviseur et l'envoi d'un e-mail ...
        end
        @report.save!
        result = true
      end
      if result
        redirect_to @report
      else
        render :new
      end
    end
  rescue StanderdError
    render :new
  end
end

Bien sûr, cela ne fonctionne pas, mais une fois que vous validez ce code. Remplacez ensuite le processus qui dépend du contrôleur pour que ce code fonctionne. Remplacez redirect_to et render par return, et n'acceptez que les valeurs requises pour session, params, etc. comme arguments.

app/services/create_report_service.rb


class CreateReportService
  def create(report_params)
    @report = Report.new(report_params)
    if @report.status == Report::STATUS_DRAFT
      if @report.save
        return { result: true, data: { report: @report }}
      else
        return { result: false, data: { report: @report }}
      end
    else
      result = false
      ApplicationRecord.transaction do
        #Signaler une demande d'examen
        if @report.report_type == Report::REPORT_TYPE_REVIEW_REQUEST
          #Traitement tel que la notification du réviseur et l'envoi d'un e-mail ...
        end
        @report.save!
        result = true
      end
      return { result: result, data: { report: @report }}
    end
  rescue StanderdError
    return { result: false, data: { report: @report }}
  end
end

Engagez-vous ici et demandez un avis à un collègue. Demandez-leur de confirmer que le traitement qui dépend du contrôleur est correctement remplacé. Puisqu'il n'y a pas de test, l'examen garantit la qualité. Si vous obtenez OK dans l'examen, nous commencerons la refactorisation. Si vous avez demandé un examen avec une demande d'extraction, veuillez la fermer sans fusionner.

Créer un ValueObject

Je pense que cela dépend du code par où commencer. Cette fois, nous allons commencer avec un objet de valeur facile à comprendre. status et report_type ont des expressions conditionnelles fixes, alors utilisons ValueObjecct.

app/models/report.rb


class Report < ApplicationRecord
  #Il y a beaucoup d'autres traitements ici...

  def status_value_object
    StatusValueObject.new(status)
  end

  def report_type_value_object
    ReportTypeValueObject.new(report_type)
  end

  class StatusValueObject
    attr_accessor :status
    DRAFT = 1 # Report::STATUS_PROJET de déplacement
    def initialize(status)
      self.status = status
    end

    def draft?
      status == DRAFT
    end
  end

  class ReportTypeValueObject
    #Signaler de la même manière_Créer un ValueObject de type
  end
end

Définissez temporairement ValueObject sous la classe Report. Une fois que vous avez créé un test ValueObject, validons-le.

Entité de sauvegarde et de publication de brouillon séparée

Je pense qu'il y a souvent une demande pour ignorer la validation lors de l'enregistrement d'un brouillon.

app/models/report.rb


class Report < ApplicationRecord
  STATUS_DRAFT = 1

  validates :status, presence: true
  validates :title, presence: true, unless: :draft?

  def draft?
    status == STATUS_DRAFT
  end
end

Divisez cela en une classe pour enregistrer les brouillons et une classe pour la publication.

app/domains/research_module/draft_report.rb


#Je l'appellerai la fonction de soumission de rapport de l'enquête de recherche
module ResearchModule
  class DraftReport
    attr_accessor :report
    def initialize(report)
      self.report = report
    end

    def valid?
      report.valid?
    end
  end
end

app/domains/research_module/publish_report.rb


module ResearchModule
  class PublishReport
    attr_accessor :report
    #Rendez possible d'appeler uniquement ceux que vous utilisez avec le délégué. Il sera plus facile de comprendre quelle colonne vous utilisez.
    delegate :title, to: :report
    def initialize(report)
      self.report = report
    end

    def valid?
      report.valid?
      report.errors.add(:title, :blank) if title.blank?
      report.errors.blank? # valid?Si tel est le cas, le contenu des erreurs sera réinitialisé
    end
  end
end

Créez une méthode Report # to_research_module et associez la classe DraftReport et la classe PublishReport à Report.

app/models/report.rb


class Report < ApplicationRecord
  #Laissez la validation ici pour vérifier si vous enregistrez un brouillon ou un message
  validates :status, presence: true

  def to_research_module
    if status_value_object.draft?
      return ResearchModule::DraftReport.new(self)
    end
    #Si vous avez besoin de l'associer à l'enregistrement, transmettez-le en tant qu'argument de constructeur.
    # report.Les appels comme l'utilisateur sont interdits dans PublishReport
    ResearchModule::PublishReport.new(self)
  end

  #Autre traitement...
end

Créez un test de validation pour vous assurer qu'il fonctionne et validez [^ 1]. [^ 1]: S'il y a une autre partie qui utilise la validation de rapport, elle peut être cassée. Veuillez vérifier si une action est requise.

Déplacer ValueObject vers le module

Déplacez le ValueObject qui a été temporairement créé dans la classe Report vers le ResearchModule.

app/domains/research_module/report_status_value_object.rb


module ResearchModule
  #Renommé depuis StatusValueObject
  class ReportStatusValueObject
    #réduction
  end
end

app/domains/research_module/report_type_value_object.rb


module ResearchModule
  class ReportTypeValueObject
    #réduction
  end
end

Créez une classe parent Report dans DraftReport et PublishReport. Puisqu'il s'agit d'un ResearchModule :: Report, il n'est pas en conflit avec le rapport de la classe ActiveRecord. Déplacez Report # status_value_object etc. vers ResearchModule :: Report.

app/domains/research_module/report.rb


module ResearchModule
  class Report
    attr_accessor :report
    delegate :status, :report_type, to: :report
    def initialize(report)
      self.report = report
    end

    def status_value_object
      ReportStatusValueObject.new(status)
    end

    def report_type_value_object
      ReportTypeValueObject.new(report_type)
    end
  end
end

app/domains/research_module/draft_report.rb


module ResearchModule
  class DraftReport < Report
    #Puisqu'il est courant, le constructeur etc. est supprimé
    def valid?
      report.valid?
    end
  end
end

app/domains/research_module/publish_report.rb


module ResearchModule
  class PublishReport < Report
    delegate :title, to: :report
    def valid?
      report.valid?
      report.errors.add(:title, :blank) if title.blank?
      report.errors.blank? # valid?Si tel est le cas, le contenu des erreurs sera réinitialisé
    end
  end
end

La méthode status_value_object est appelée dans Report # to_research_module, mais remplacez-la parResearchModule :: ReportStatusValueObject.new (status). L'emplacement de la classe a changé, alors modifiez le test pour vous assurer qu'il fonctionne et validez.

Refactorisation de la décision de soumission de la demande d'examen

S'il s'agit de DraftReport, la demande de révision n'est pas toujours envoyée, et si c'est PublishReport, si report_type est la demande de révision, elle est envoyée, alors créez la méthode send_review_request?.

app/domains/research_module/draft_report.rb


module ResearchModule
  class DraftReport < Report
    #Autres omis...

    def send_review_request?
      false
    end
  end
end

app/domains/research_module/publish_report.rb


module ResearchModule
  class PublishReport < Report
    #Autres omis...

    def send_review_request?
      report_type_value_object.review_request?
    end
  end
end

Écrivez un test et validez.

Début de la refactorisation du service d'application

Avant de vous lancer dans la refactorisation, copiez la méthode entière et laissez-la avec un nom de méthode tel que ʻold_create. Il s'agit de remplacer le nom de la méthode une fois la refactorisation effectuée pour s'assurer que le test réussit même avec l'ancien code. Tout d'abord, appelez Report # to_research_module` pour utiliser la classe DraftReport et la classe PublishReport.

app/services/create_report_service.rb


class CreateReportService
  def create(report_params)
    @report = Report.new(report_params)
    report_entity = @report.to_research_module #Ajoutée
    if @report.status == Report::STATUS_DRAFT
      if @report.save
        return { result: true, data: { report: @report }}
      else
        return { result: false, data: { report: @report }}
      end
    else
      result = false
      ApplicationRecord.transaction do
        #Signaler une demande d'examen
        if @report.report_type == Report::REPORT_TYPE_REVIEW_REQUEST
          #Traitement tel que la notification du réviseur et l'envoi d'un e-mail ...
        end
        @report.save!
        result = true
      end
      return { result: result, data: { report: @report }}
    end
  rescue StanderdError
    return { result: false, data: { report: @report }}
  end
end

Ensuite, utilisez send_review_request? Pour supprimer la branche conditionnelle du brouillon car vous n'avez pas à vérifier s'il s'agit d'un brouillon.

app/services/create_report_service.rb


class CreateReportService
  def create(report_params)
    @report = Report.new(report_params)
    report_entity = @report.to_research_module
    result = false
    #Suppression du brouillon de cas et unification avec le traitement normal des écritures
    ApplicationRecord.transaction do
      #Signaler une demande d'examen
      if report_entity.send_review_request? #Modifiez les conditions ici
        #Traitement tel que la notification du réviseur et l'envoi d'un e-mail ...
      end
      @report.save!
      result = true
    end
    return { result: result, data: { report: @report }}
  rescue StanderdError
    return { result: false, data: { report: @report }}
  end
end

Vous devez appeler report_entity.valid? Parce que la validation a changé avec le refactoring de la classe Report.

app/services/create_report_service.rb


class CreateReportService
  def create(report_params)
    @report = Report.new(report_params)
    report_entity = @report.to_research_module
    result = false
    ApplicationRecord.transaction do
      #Validation d'appel de l'entité
      unless report_entity.valid?
        return { result: false, data: { report: @report }}
      end
      #Signaler une demande d'examen
      if report_entity.send_review_request?
        #Traitement tel que la notification du réviseur et l'envoi d'un e-mail ...
      end
      @report.save!
      result = true
    end
    return { result: result, data: { report: @report }}
  rescue StanderdError
    return { result: false, data: { report: @report }}
  end
end

Si vous mettez en forme le contenu du rapport pour notification au réviseur, vous souhaiterez peut-être le déplacer vers la classe PublishReport. Il n'est pas nécessaire de le stocker dans une variable d'instance comme @ report, changeons-le donc en variable locale.

Créez un test et assurez-vous qu'il réussit. Si vous utilisez FactoryBot, il est recommandé de le définir en l'associant à l'entité du module comme factory: research_module_draft_report, class: Report. Validez une fois à ce stade (si vous ne souhaitez pas valider une copie de l'ancien code, supprimez-le).

Exécutez des tests avec l'ancien code avant de refactoriser

Assurez-vous que l'ancien code que vous avez laissé passera. Tout ce que vous avez à faire est de remplacer le nom de la méthode et d'exécuter le test. S'il ne réussit pas, il est cassé par la refactorisation, alors corrigez-le pour qu'il réussisse le test. Revenez ensuite au code refactoré et modifiez le code produit pour que le test réussisse.

Refactoring du contrôleur

Le contrôleur appelle simplement ApplicationService et bascule entre l'appel redirect_to et render sur le résultat.

reports_controller.rb


class ReportsController < ApplicationController
  def create
    service = CreateReportService.new
    service_result = service.create(report_params)
    @report = service_result[:data][:report]
    if service_result[:result]
      redirect_to @report
      return
    end
    render :new
  end
end

J'ai changé la méthode de validation, mais comme elle ne s'écarte pas du mécanisme d'ActiveRecord, View n'a pas du tout besoin d'être modifié.

finalement

On dit que Rails ne convient pas pour DDD, mais je pense que vous pouvez faire n'importe quoi si vous préparez une méthode qui correspond à Entity comme Report # to_research_module. Dans cet exemple, seul le modèle de rapport est utilisé, mais il peut être dérivé de différentes manières.

app/models/report.rb


class Report < ApplicationRecord
  belongs_to :reporter
  def to_review_module
    #Si vous souhaitez utiliser une association, passez-la dans le constructeur
    ReviewModule::Report.new(self, reporter)
  end

  def to_review_module2
    #Si vous souhaitez créer en tant qu'agrégat, également dans Reporter_review_Créer une méthode de module
    ReviewModule::Report.new(self, reporter.to_review_module)
  end


  def to_review_module3
    #Tout ce dont vous avez besoin est le titre, le corps et le nom du journaliste.
    ReviewModule::Report.new(title, body, reporter.name)
  end
end

En fin de compte, le modèle ActiveRecord ne devrait avoir que des associations et des validations telles que «appartient à», des étendues et des méthodes telles que «to_research_module» qui correspondent à Entity. Cela signifie ne pas écrire de logique de domaine dans le modèle ActiveRecord. De plus, en limitant la référence d'association à des méthodes comme to_research_module, il vous suffit de vérifier la méthode comme to_research_module pour voir quelle table vous utilisez.


Cette entrée est pour Use Case (User Story), Swim Lane dans Requirements Analysis Driven Design. requirements_analysis_driven_desgin / #% E3% 83% A6% E3% 83% BC% E3% 82% B9% E3% 82% B1% E3% 83% BC% E3% 82% B9% E3% 83% A6% E3% 83% BC% E3% 82% B6% E3% 83% BC% E3% 82% B9% E3% 83% 88% E3% 83% BC% E3% 83% AA% E3% 83% BC% E3% 82% B9% E3% 82% A4% E3% 83% A0% E3% 83% AC% E3% 83% BC% E3% 83% B3) et modélisation des données (https://linyclar.github.io/software_development/requirements_analysis_driven_desgin/ #% E3% 83% 87% E3% 83% BC% E3% 82% BF% E3% 83% A2% E3% 83% 87% E3% 83% AA% E3% 83% B3% E3% 82% B0) A été réorganisé et réécrit dans la perspective de la refactorisation du contrôleur et des modèles ActiveRecord. C'est une longue histoire, mais c'est une histoire de choses de type DDD dans Rails.

Recommended Posts

Refactor Fat Controller pour le faire ressembler à DDD