[RUBY] A story that separates business logic and model

Overview

It is a story that cuts out access to a specific data resource from business logic and processing processing in the service that is being operated and standardizes it.

Is this really a model?

I do not understand. Is Entity or domain object better? Please let me know.

Before modeling

For example, this is the code. The following is a class that manages the page of a prefecture, and it is assumed that you have acquired an event associated with that prefecture.

area/pref.rb


module Area
  class Pref
    def event
      events = Api.call('/events', params)
      events.each_with_object([]) do |event, memo|
        memo << {
          name: event[:name],
          address: create_full_address(event)
        }
      end
    end

    def create_full_address(event)
      #Returns the full string of the address, combining some information
    end
  end
end

Next, the page of cities, wards, towns and villages under the prefecture was created. Below you will find events related to that city.

area/city.rb


module Area
  class City
    def event
      events = Api.call('/events', params)
      events.each_with_object([]) do |event, memo|
        memo << {
          name: event[:name],
          address: create_city_address(event)
        }
      end
    end

    def create_city_address(event)
      #I want to display the address short, so I will return the address string after the city
    end
  end
end

The processing that handles event information is duplicated. Let's cut this out as follows and make it common.

area/common.rb


module Area
  class Common
    class << self
      def events(params)
        Api.call('/events', params)
      end

      def create_full_address(event)
        #Returns the full string of the address, combining some information
      end

      def create_city_address(event)
        #I want to display the address short, so I will return the address string after the city
      end
    end
  end
end

The caller looks like the following.

area/pref.rb


require_relative 'common'

module Area
  class Pref
    def event
      events = Common.events(params)
      events.each_with_object([]) do |event, memo|
        memo << {
          name: event[:name],
          address: Common.create_city_address(event)
        }
      end
    end
  end
end

I thought it was okay, but next there was a new class to manage the station pages. Suppose you don't need address information on this page, you only need information about the station.

Last time, I made common on area pages like area / common.rb. Also, since the information acquired on the area page is unnecessary and the information I want is slightly different, I decided to implement access to the event information on my own.

traffic/station.rb


module Traffic
  class Station
    def event(params)
      events = Api.call('/events', params)
      events.each_with_object([]) do |event, memo|
        memo << {
          name: event[:name],
          stations: create_stations(event)
        }
      end
    end

    def create_stations(event)
      #Returns some station information
    end
  end
end

The above is the general situation before modeling. The example is long and I feel that it is not good enough. Let me make an excuse that the situation is actually a little more complicated.

Problems before modeling

To summarize the problems, it can be said that the situation was as follows.

If the convenient API in the company is a data resource, a response that is somewhat attentive will be returned, so there may be cases where it is called in the business logic without being properly separated. I think. Even if it can be implemented simply at first, it will become chaotic when various uses start in various places.

After modeling

As a solution, I decided to operate the model properly. I decided to cut the model for each data resource and always access the data resource via the model. By consolidating the decoration of the response into the model, we tried to standardize it and created a clear state where to implement it.

Specifically, it is as follows.

Define the model that manages each event as follows.

model/event.rb


module Model
  class Event
    attr_accessor :name, :address

    def initialtize(event)
      {
        'name' => '@name',
        'address' => '@address',
      }.each do |key, prop|
        instance_variable_set(prop, event[key])
      end
    end

    def full_address
      #Returns the full string of the address, combining some information
    end

    def city_address
      #I want to display the address short, so I will return the address string after the city
    end

    def stations
      #Returns some station information
    end
  end
end

The model that manages the list of events is as follows.

model/events.rb


require_relative 'event'

module Model
  module Events
    attr_accessor :events

    def initialize(events)
      @events = events.each_with_object([]) do |event, memo|
        memo << Model::Event.new(event)
      end
    end

    class << self
      def get(params)
        events = Api.call('/events', params)
        Model::Events.new(events)
      end
    end
  end
end

The user side looks like this.

area/pref.rb


module Area
  class Pref
    def event
      events = Model::Events.get(params)
      events.each_with_object([]) do |event, memo|
        memo << {
          name: event.name,
          address: event.full_address
        }
      end
    end
  end
end

We have defined model / event.rb as a model that handles event information. This is in charge of holding and decorating event information, and all access to event information goes through this model.

Since the API response to get the event is returned as a list type, define model / events.rb and manage the API response here. At this time, individual event information is passed to event.rb for decoration, and events.rb keeps the result as a list.

As a result, users will be able to access the decorated items without being aware of it.

Avoid FAT

By modeling as described above, we were able to consolidate access and decoration to data resources.

On the other hand, with this design, there is a concern that the model will swell. As an effort to avoid the FAT model, we are taking the following measures.

Although it is a delegate, it is effective when there are other models for festival other than event, and you want to have a similar method for each.

model/event.rb


module Model
  class Event
    #Hold to judge the event holding?The method is Model::Transfer to Condition and use that implementation
    delegate :hold?, to: Model::Condition
  end
end

Can be called as follows

area/pref.rb


module Area
  class Pref
    def hold_event?
      Model::Events.hold?
    end
  end
end

SpecialThanks Thank you to @nazomikan for providing the draft, HK and SS for promoting it, and all the members of the department!

Recommended Posts

A story that separates business logic and model
[MVC model] A memo that a beginner learned MVC
Business logic?
A story that took time to establish a connection
The story that docker had a hard time
Ruby sum is a sober and amazing story
[Ruby] A program that uses search and each_with_index
A story about Apache Wicket and atomic design
A story about making a Builder that inherits the Builder
A story that failed using "bundle exec rubocop -a"
Class and model
A small story that is sometimes useful in Maven
[Ruby] A program / concept that combines each_with_index and search
[Rails] Volume that displays favorites and a list of favorites
A story that deepened the understanding of devise's methods user_signed_in? And current_user through error resolution