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.
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.
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.
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.
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é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.
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.
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).
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.
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é.
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