1

Ich habe drei Modelle: Aufgabe, Benutzer und Antwort.Erstellen eines anderen Datensatzes aus dem Modell: schlechte Übung?

Wenn ein Benutzer eine Aufgabe abschließt, werden die Ergebnisse als Antwort gespeichert. Während dieser Antwortzeit erhält ein Benutzer Punkte.

Meine erste Frage ist, wo sollte die Logik für die Aktualisierung der Punkte Attribut gehen? Im Task-, Benutzer- oder Antwortmodell? Momentan habe ich es im Response-Modell, wo es Response.task.points ergreift und diesen Wert zu User.task.points hinzufügt.

Response.create sieht dann aus wie:

# POST /responses 
    # POST /responses.json 
    def create 
    @response = Response.new(response_params) 

    respond_to do |format| 
     if @response.save 
     @response.reward_user 

     format.html { redirect_to @response, notice: 'Response was successfully created.' } 
     format.json { render :show, status: :created, location: @response } 
     else 
     format.html { render :new } 
     format.json { render json: @response.errors, status: :unprocessable_entity } 
     end 
    end 
    end 

Die zweite Sache ist, dass ich jeden Punkt Transaktion protokolliert werden soll. Also habe ich ein anderes Modell namens points_transaction erstellt. Meine andere Frage ist, wo soll ich die points_transaction erstellen? Im Response.create-Controller? Im Antwortmodell?

Es scheint falsch zu sein, eine PointsTransaction aus der Response create-Methode zu erstellen, aber es scheint auch falsch zu sein, sie innerhalb des Modells zu erstellen. Welcher ist MVC-korrekt?

My Response-Objekt sieht wie folgt aus:

class Response < ApplicationRecord 
    belongs_to :task, optional: true 
    belongs_to :user, optional: true 

    def reward_user 
    point_value = task.point_value 
    user.points += point_value 

    PointTransaction.new({/*params go here*/}) 
    end 
end 
+0

Sie haben eine gute Frage. Ich denke, Sie haben einen Punkt erreicht, als Sie auf die nächste Stufe Ihrer Architekturlösungen gehen müssen :) – AntonTkachov

Antwort

1

Ein gängiger Ansatz für diese Art von Problem besteht darin, Service-Objekt zu erstellen, die den gesamten Verarbeitungscode in eine "Einheit" Code einbinden können. Sie lassen das Serviceobjekt die Response überprüfen und dann die Punkte des Benutzers aktualisieren und die PointTransaction verfolgen, so dass Sie Ihren Controller dünn halten können und Sie Ihre Modelle daran hindern können, andere Modelle zu berühren oder andere, möglicherweise unklare Nebenwirkungen zu erzeugen.

Nehmen wir an, Sie erstellen einen Ordner namens Dienste innerhalb des App-Verzeichnisses und Sie legen Ihre Serviceklasse dort hinein.

# app/services/response_checker.rb 
class ResponseChecker 
    attr_reader :success 

    def initialize 
    end 

    def call(response, task, user) 
    @success = if response.save 
     user.points += task.point_value 
     point_trans = PointTransaction.new(/*params go here*/) 

     user.save && point_trans.save 
    else 
     false 
    end 
    end 
end 

Verwenden Sie dann den Dienst in Ihrem Controller:

# app/controllers/response_controller.rb 
def create 
    @response = Response.new(response_params) 
    @response_checker = ResponseChecker.new.call(@response, @response.task, @response.user) 

    respond_to do |format| 
    if @response_checker.success 
     # conditional controller response logic 
    end 
    end 
end 

Sie möchten, können ein Fehler Attribut auf dem Dienstobjekt erstellen Informationen über Fehler bei der Ausführung des Dienstes zu erfassen und sie dann aussetzen an wen/was auch immer den Service nutzt (in diesem Fall der Controller).

+0

Quick question. Was ist der Zweck der Initialisierungsfunktion? Was soll ich als initialisierte Variablen im Call angeben? – JCDJulian

+0

Ich lege die 'initialize' Methode an um klar zu sein, dass es hier wirklich nichts Besonderes macht. Wir könnten es weglassen und alles würde immer noch funktionieren. Es gibt einige verschiedene Taktiken, wie man Dienste instanziieren und mit ihnen interagieren kann, aber eine Art, die ich mag, ist, 'new' /' initialize' zu ​​verwenden, um irgendwelche anderen Diensttyp-Abhängigkeiten, die benötigt werden, zu injizieren und 'call' zu verwenden übergeben Sie die Objekte mit dem Zustand, den wir für die Arbeit benötigen. Es ist einfacher, so zu testen. Einige Beispiele hier: https://hackernoon.com/going-further-with-service-objects-in-ruby-on-rails-b8aac13a7271 – DRSE

1

Ich glaube, Sie auf jeden Fall eine Situation konfrontiert, wenn Sie über die einfache MVC gehen müssen.

Erstens, in idealen Welt sollten Modelle überhaupt nicht voneinander wissen. Sie sollten also keine anderen Modelle von Response wie Sie beziehen. Auf der anderen Seite ... Controller ist definitiv noch schlimmer, dann setzen Sie es in das Modell.

Zweitens, wenn Sie Zweifel haben, wo Sie Ihren Code zwischen 2 Stellen setzen. Dann sind beide nicht gut genug und du musst nach dem dritten suchen.

Das ist, wenn Service-Objekte zum Spiel kam. Es ist ziemlich weit verbreitet und in Rails üblich. Ich bin mir nicht sicher, ob es eine perfekte Lösung ist, aber es entkoppelt Ihren Code, hält Ihren Code sauber und leicht testbar. Ich habe noch keine Probleme mit diesem Ansatz gefunden, außer dass ich zu viele Service-Objekte habe :).

Hier ist ein Beispiel mit Logik, die mehrere Modelle aus unserem Projekt berührt (app/services/active_site_service.rb):

class ActivateSiteService 
    attr_reader :error 

    def initialize(user, template, password) 
    @user = user 
    @template = template 
    @activation = @user.activation_for(@template) 
    @password = password 
    end 

    def call 
    return false unless self.valid? 

    generate_site_service = GenerateSiteService.new(@user, @template) 
    generate_site_service.call 

    @activation.update(quantity: @activation.quantity - 1) 

    @user.transactions.create(status: :success, 
           target: generate_site_service.site, 
           amount: 0, 
           transaction_type: :site_activation) 
    true 
    end 

    protected 
    def valid? 
    validate_password && validate_activation 
    end 

    def validate_password 
    return true if @user.valid_password?(@password) 
    @error = 'Неправильный пароль' 
    false 
    end 

    def validate_activation 
    return true if @activation.present? && @activation.quantity > 0 
    @error = 'У вас нет предоплаченных активаций' 
    false 
    end 
end 

Regeln, die wir wie folgt vor:

  1. Konzeptionell Service-Objekt ist ein Geschäftsprozess, der mehrere Modelle
  2. Namen immer mit einem Verb
  3. Serviceobjekt nur starten umfassen hat 2 Methoden: initialize und call
  4. call immer zurück nur wahr/falsch
  5. Nur 2 attr_reader Variable erlaubt - result einige Daten oder das Objekt aus dem Dienst zu erhalten oder error einen Fehler

Controller zu bekommen:

class ActivationsController < ApplicationController 
    def create 
    template = Site.templates.find(params[:template_id]) 

    activate_site_service = ActivateSiteService.new(current_user, template, params[:password]) 

    if activate_site_service.call 
     redirect_to sites_path, notice: 'Активация сайта прошла успешно' 
    else 
     redirect_to new_purchase_path(template_id: template.id), alert: activate_site_service.error 
    end 
    end 
end 

Wenn Sie nicht in diese Regeln fallen können, dann es ist meistens kein Serviceobjekt. Sie können mehr über Service-Objekte googlen

+0

Hmmm. Interessant. Wohin würde das Servicemodell gehen? In der lib? – JCDJulian

+0

Wer ruft auch das Serviceobjekt an? Sollte das Serviceobjekt ein anderer Controller sein? Oder sollte ich es von Response.create anrufen? Bedeutet, dass es die Antwort protokolliert und den Benutzer als eine atomare Operation aus Response belohnt? – JCDJulian

+0

Alles in meiner Antwort enthalten. Warum ich nicht mit lib gehen würde. Lib ist meist ein Modul, das eine separate Bibliothek ist und in anderen Projekten verwendet werden kann. Für mich ist lib also meistens wie technischer Helfer und hat nie irgendeine Beziehung zur Geschäftslogik. Alles im Zusammenhang mit Geschäftslogik sollte in 'app /' Ordner gehen – AntonTkachov

Verwandte Themen