How I code without service objects

by Jason Swett,

What service objects are

Service objects and Interactors are fashionable among Rails developers today. I can’t speak for everyone’s motives but the impression I get is that service objects are seen as a way to avoid the “fat models” you’re left with if you follow the “skinny controllers, fat models” guideline. If your models are too fat you can move some of their bulk to some service objects to thin them out.

Unfortunately it’s not quite possible to say what service objects are because the term “service object” means different things to different people. To some developers, “service object” is just a synonym for plain old Ruby object (PORO). To other developers, a service object is more or less an implementation of the command pattern. Interactors are similarly close to the command pattern.

For the purposes of this post we’ll go with the “command pattern” conception of a service object: a Ruby class with a single method, call, which carries out a sequence of steps.

I have some gripes with the way service objects are advocated in the Ruby community. I’ll explain why.

Pattern vs. paradigm

The command pattern has its time and place. Sometimes it makes plenty of sense to bundle up an action into a single object and let that object orchestrate the steps that need to happen for that action.

But the command pattern is a pattern, not a paradigm. A software design pattern is a specific solution meant to meet a specific problem. A programming paradigm (like object-oriented programming or functional programming) is a style that can be applied at the level of entire applications.

What pains me a little bit to see is that many developers seem to have confused the idea of a pattern with the idea of a paradigm. They see service objects or Interactors and think, “This is how we’ll code now!” They have a service-object-shaped hammer and now everything is a nail.

Imperative vs. declarative

An imperative programming style expresses code mainly in terms of what to do. A declarative programming style expresses code mainly in terms of what things are.

For those not very familiar with imperative vs. declarative, here’s an explanation by way of example.

An imperative sandwich

If I were to tell a robot to make me a peanut butter and jelly sandwich using the imperative style, I would say, “Hey robot, get two pieces of bread, put some peanut butter on one, put some jelly on the other, put them together, put it on a plate, and give it to me.”

A declarative sandwich

If I were to do the same thing in a declarative style, I would say, “A sandwich is a food between two pieces of bread. It comes on a plate. A peanut butter and jelly sandwich is a sandwich that contains peanut butter and jelly. Make me one.”

My preference for declarative

I’m strongly of the view that declarative code is almost always easier to understand than imperative code. I think for the human brain it’s much easier to comprehend a static thing than a fleeting action.

At some point the things do have to take action (the peanut butter and jelly sandwich has to get made) but we can save the action part for the very highest level, the tip of the pyramid. Everything below can be declarative.

Service objects are all imperative

Service objects and Interactors are basically all imperative. Their names tend to be verbs, like NotifySlack or AuthenticateUser. Their guts tend to be imperative as well. To me this is a pity because, like I said above, I think most things are more clearly expressed in a declarative style. Even things that seem at first glance to lend themselves more naturally to an imperative style can often be expressed even more naturally in a declarative way.

An example of my style of coding

With debates around nuanced topics like this, small, contrived examples don’t really cut it. We need something with a little bit of complexity in order to see the differences that will be felt in production projects between service objects and my style of coding.

Before we get into the example, I’ll explain what my style of coding is.

Service objects vs. object-oriented programming

Instead of service objects or Interactors I just use regular old OOP. Not everything in Rails has to use a fancy tool or concept. Plain old objects are actually quite powerful and can get you a very long way.

Service objects might seem like OOP because they take the form of classes, but the service objects are objects only superficially. “Real” objects are abstractions of concepts that make it easier to deal with those concepts. If an object is just a wrapper for some procedural code, most of the benefits of objects are being left on the table.

What this example is

I work for a medical clinic. One of the things my application has to do is send a special file to an organization that helps facilitate the payment of insurance claims. This file is in a proprietary format and it’s called a ZirMed 837P File.

The job of bundling up a bunch of insurance claims, sticking them into a file one-by-one, and sending the file off to its destination might seem like a perfect use case for a service object. We could call it BuildZirMed837PFile and run BuildZirMed837PFile.call. But no, I choose to express this job in declarative terms. I hope that once you see the code you’ll agree that it’s better.

The tip of the pyramid

We’ll start with the highest-level code and work our way down. The tip of the pyramid is in fact imperative. It pretty much has to be. I’m creating an instance of a ZirMed837PFile::File and calling the write method on it, passing in the applicable appointments and the path of the destination file.

(Side note: all the following classes live inside the ZirMed837PFile namespace. Because this is true, I’m able to call my root object just File and it’s fine because it’s not going to be ambiguous or conflict with anything, thanks to the namespace.)

ZirMed837PFile::File.new(appointments).write(file.path)

The files we’ll be taking a look at are as follows.

app/models/zir_med_837_p_file/file.rb
app/models/zir_med_837_p_file/line.rb
app/models/zir_med_837_p_file/charge_collection.rb
app/models/zir_med_837_p_file/charge_slot.rb
app/models/zir_med_837_p_file/line_serializer.rb

Let’s take a look at the first class, File.

The File class

This class is pretty small. We’re mainly interested in the write method since that’s what’s being called above. The write delegates most of its responsibility to the to_s method.

# app/models/zir_med_837_p_file/file.rb

module ZirMed837PFile
  class File
    def initialize(appointments)
      @appointments = appointments
    end

    def self.from_queued
      new(Appointment.joins(:charges).merge(Charge.queued))
    end

    def write(filename)
      ::File.write(filename, to_s)
    end

    def filename
      Rails.root.join("tmp", "charges.txt")
    end

    def to_s
      @appointments.map { |appointment| Line.new(appointment).to_s }.join("\n")
    end
  end
end

The to_s method in turn delegates most of its work to a class called Line. Let’s take a look at that one.

The Line class

The Line object is a bit more involved. Line knows much more than File regarding the peculiarities of the way the data needs to be formatted. But despite the complexity, there are just three relatively small methods: to_s (declarative), fields_with_charges (declarative), and an initializer.

# app/models/zir_med_837_p_file/line.rb

module ZirMed837PFile
  class Line
    TOTAL_NUMBER_OF_FIELDS = 213
    FIRST_DIAGNOSIS_CODE_INDEX = 66

    ATTRIBUTE_SLOTS = {
      date_of_service: 77,
      procedure_code: 101,
      procedure_code_modifier: 107,
      diagnosis_code_pointer: 113,
      charge: 119,
      units: 125,
      ndc_code: 167,
    }.freeze

    def initialize(appointment)
      @appointment = appointment
      @charges = ChargeCollection.new(appointment.charges)
    end

    def fields_with_charges
      LineSerializer.new(@appointment).fields.tap do |fields|
        @charges.unique_diagnosis_codes.each_with_index do |diagnosis_code, offset|
          fields[FIRST_DIAGNOSIS_CODE_INDEX + offset] = diagnosis_code
        end

        ATTRIBUTE_SLOTS.each do |attr, starting_index|
          @charges.charge_slots.map do |charge_slot|
            fields[starting_index + charge_slot.offset] = charge_slot.value[attr]
          end
        end
      end
    end

    def to_s
      TOTAL_NUMBER_OF_FIELDS.times.map do |index|
        fields_with_charges[index + 1]
      end.join("|")
    end
  end
end

We’re making use of a couple other even finer-grained classes here too, ChargeCollection and LineSerializer. Let’s see what these each do, starting with ChargeCollection.

ChargeCollection

This is a tiny class. Just two methods, unique_diagnosis_codes and charge_slots, both declarative.

# app/models/zir_med_837_p_file/charge_collection.rb

module ZirMed837PFile
  class ChargeCollection
    def unique_diagnosis_codes
      map(&:diagnosis_code).map(&:code).uniq
    end

    def charge_slots
      ChargeSlot.charge_slots(self)
    end
  end
end

ChargeCollection also makes use of another class, ChargeSlot. Let’s see what this class does.

ChargeSlot

The “central” method of this class is value, which basically just maps values to particular hash keys. Notice how yet again, all the methods in this class are declarative, not imperative.

# app/models/zir_med_837_p_file/charge_slot.rb

module ZirMed837PFile
  class ChargeSlot
    NUMBER_OF_CHARGE_SLOTS = 6
    attr_reader :offset

    def initialize(charges, offset)
      @charges = charges
      @offset = offset
      @charge = charges[offset]
    end

    def value
      {
        date_of_service: I18n.l(@charge.date_of_service),
        procedure_code: @charge.cpt_code.code,
        procedure_code_modifier: @charge.modifier,
        diagnosis_code_pointer: (diagnosis_code_index + 1).to_s,
        charge: @charge.cpt_code.fee_per_unit.to_s,
        units: @charge.units.to_s,
        ndc_code: @charge.cpt_code.ndc_code,
      }
    end

    def diagnosis_code_index
      @charges.unique_diagnosis_codes.index(@charge.diagnosis_code.code)
    end

    def self.charge_slots(charges)
      charge_slot_offsets.select { |offset| charges[offset].present? }
        .map { |offset| new(charges, offset) }
    end

    def self.charge_slot_offsets
      (0..(NUMBER_OF_CHARGE_SLOTS - 1))
    end
  end
end

This class doesn’t refer to any other classes so we’ve apparently reached a “leaf” of this dependency tree. Let’s now take a look at LineSerializer which is used in the Line class which we saw earlier.

LineSerializer

This is the final class in the group. The “central” method of this class is fields which matches values with numbers slots in the file. (The strange proprietary format of this file is based on numbered slots.)

To cut down in what would have been a lot of noise inside of the fields method, I’ve created a number of “helper” methods that get the data into the exact format they need to be in. Many slots and helpers have been omitted because they are numerous and wouldn’t help make the example any clearer.

I try to avoid class names that end in “-er” because that implies that the class doesn’t really represent an actual crisp concept. But sometimes “-er” is fine. (Heck, think about Rails’ ApplicationController.) And plus, the benefits of a declarative style aren’t lost just because of the class name in this case. All the methods inside the class are of a declarative style.

# app/models/zir_med_837_p_file/line_serializer.rb

module ZirMed837PFile
  class LineSerializer
    PLACE_OF_SERVICE_CODES = {
      "Office" => 11,
      "Out Patient - Hospital" => 22,
      "Out Patient - Surgery Center" => 24,
    }.freeze

    def initialize(appointment)
      @appointment = appointment
    end

    def fields
      {
        1 => insurance_account.insurance_identifier,
        2 => patient_name,
        3 => patient_birth_date,
        57 => rendering_provider_name,
        89 => place_of_service_code,
        184 => @appointment.physician.last_name,
        185 => @appointment.physician.first_name,
        188 => @appointment.location.name,
        189 => address.line_1,
        190 => address.line_2,
        191 => "#{address.city}, #{address.state} #{address.zip_code}",
      }
    end

    def patient_name
      patient.name_on_insurance_card.presence || formatted_patient_name
    end

    def patient_birth_date
      I18n.l(patient.birth_date)
    end

    def address
      @appointment.location.address
    end

    def place_of_service_code
      PLACE_OF_SERVICE_CODES[@appointment.location.type].to_s.tap do |code|
        raise "Unable to find place of service code for location type \"#{@appointment.location.type}\"" if code.blank?
      end
    end

    def rendering_provider_name
      return "" unless insurance_account.insurance_type.medicare?

      "#{@appointment.physician.first_name} #{@appointment.physician.last_name}"
    end
  end
end

End of example

That’s all. This job of packaging a set of appointments’ insurance claims into a proprietary file format, although it might seem like a decent candidate for a procedural/imperative chunk of code, is actually expressed (I think) quite nicely in a declarative OOP style.

Takeaways

  • The command pattern is a pattern, not a paradigm. It’s not a style, like OOP or FP, that should be applied to all your code.
  • Declarative code is often easier to understand than imperative code.
  • Regular old OOP is a fine way to program. Not everything in Rails has to utilize a fancy tool or concept.

16 thoughts on “How I code without service objects

  1. Josh Cheek

    I can’t tell from this comment box if it will format the markdown correctly, so I uploaded my comment to a gist, as well: https://gist.github.com/JoshCheek/936c17369a9325e87a9caeb68e0052e1

    Guessing that `ZirMed837PFile::File.new(appointments).write(file.path)` is supposed to be something like this:

    “`ruby
    file = ZirMed837PFile::File.from_queued
    file.write(file.filename)
    “`

    Otherwise the `.from_queued` and `#filename` methods don’t make much sense. But
    the `filename` method probably shouldn’t be an instance method, given that it’s
    passed in to `#write` as an argument.

    If we follow the appointments from `.from_queued` down into `to_s` and then into
    `Line#initialize`, we see that each one has `.charges` called on it, which appears
    to be an `ActiveRecord::Relation`. This will have an `N+1` query. Looking around
    more, there are a bunch of these, there will be a lot of `N+1` queries (and probably
    worse in some places)

    “`ruby
    def ZirMed837PFile::File.from_queued
    new Appointment
    .joins(:charges)
    .includes(
    :patient, # Just a guess, it wasn’t clear where this came from
    :physician,
    location: :address,
    charges: [:diagnosis_code, :cpt_code],
    )
    .merge(Charge.queued)
    end
    “`

    `Line#fields_with_charges` would be better private since its only called by `Line#to_s`.

    `Line#fields_with_charges` appears to do a fairly expensive operation, and it
    gets called 213 times by `#to_s`, so it would be good to memoize it. Then again,
    it’s a hash, and the block just asks for keys, so you could also get away with
    not memoizing it if you only call it once, which you can do with `Hash#to_proc`
    (using a range to avoid needing to add 1 in the block):

    “`ruby
    def to_s
    (1..TOTAL_NUMBER_OF_FIELDS).map(&fields_with_charges).join(“|”)
    end
    “`

    `ChargeSlot.charge_slot_offsets` can be simpler if you use three dots, which will
    exclude the final index

    “`ruby
    def self.charge_slot_offsets
    0…NUMBER_OF_CHARGE_SLOTS
    end
    “`

    `ChargeSlot.charge_slots` does a `select` that makes it look like some of the
    charges could be empty, but it only does that b/c it’s iterating over a range that’s
    potentially larger than the array. You can use `head` instead. It doesn’t fit the best
    with the code as it currently is, b/c the code is more interested in the indexes than
    the charges. But after refactoring those things out (passing it the charge instead of
    the array and index), it will work nicely.

    `ChargeCollection#charge_slots` winds up limiting the number of slots to 6 (because
    of that above `charge_slots_offsets`), but `ChargeCollection#unique_diagnosis_codes`
    does not. So if there were enough charges with different diagnosis codes, it would
    write into other fields. It should probably be like
    `unique_diagnosis_codes.take(NUMBER_OF_DIAGNOSIS_SLOTS)`

    `I18n.l` is probably a hidden bug (meaning it could unexpectedly change the date format in prod)
    because it renders localized dates. I’d check the spec, it probably specifies a format.

    You don’t need to explicitly call `to_s` on the hash values, `join` will call it for you.

    Note that the joining in `ZirMed837PFile::File#to_s` means there will not be
    a newline on the last line. Seemed potentially incorrect.

    With a hash, you can call fetch and it will invoke the block if the key DNE,
    so you can refactor `place_of_service_code`

    Also use `inspect` in the error message instead of typing quotes in it.
    Because if there were quotes in `type.to_s`, then the displayed value would not
    be correctly escaped. This will also prevent `nil` from looking like an empty string.

    The values in `location.type` seem like an unwise choice, BTW. (1) it’ll be painful
    to call an ActiveRecord column type (I assume it’s ActiveRecord), b/c AR will think
    it’s single table inheritance. (2) The values look like they’re for presentation,
    which makes them ill suited as a data representation, might convert it to an enum,
    and look up the presentation string through `I18n`.

    “`ruby
    def place_of_service_code
    PLACE_OF_SERVICE_CODES.fetch @appointment.location.type do |type|
    raise KeyError, “Unable to find place of service code for location type #{type.inspect}”
    end
    end
    “`

    While reading it, I started tweaking it a bit. This is what I wound up at.
    I didn’t really go into the `LineSerializer` because it was unclear where some
    of that data was coming from. Note that I have no way of running any of this,
    so who knows how correct it is.

    “`ruby
    # Opening the file allows it to write each line as it is generated so it doesn’t
    # have to build up a potentially massive string in memory.
    File.open Rails.root.join(‘tmp’, ‘charges.txt’), “w” do |file|
    ZirMed837PFile::Document.new(Appointment.all).write file
    end

    require ‘csv’
    module ZirMed837PFile
    class Document
    def initialize(appointments_relation)
    @appointments = appointments_relation
    .joins(:charges)
    .includes( # `includes` fixes the N+1 queries
    :patient, # Just a guess that this is here, it wasn’t clear where it came from
    :physician,
    location: :address,
    charges: [:diagnosis_code, :cpt_code],
    )
    .merge(Charge.queued)
    end

    def to_s
    write “” # Should work b/c `write` uses `<<`, which both IO and String implement
    end

    def write(stream)
    # Receive the stream so you can write it into, eg, a socket,
    # or some specific part of a currently open file.
    each_line { |line| stream << line.to_s }
    stream
    end

    def each_line
    return to_enum __method__ unless block_given?
    appointments.each { |appt| yield Line.new appt }
    end
    end
    end

    require 'csv'
    module ZirMed837PFile
    class Line
    TOTAL_NUMBER_OF_FIELDS = 213
    DIAGNOSIS_CODES_OFFSET = 65 # Starting it 1 earlier b/c document seems to count from 1 instead of 0
    NUMBER_OF_DIAGNOSIS_SLOTS = 12 # I added this b/c first date of service is @ 77, which is 12 more than 65
    NUMBER_OF_CHARGE_SLOTS = 6 # What happens if you have more than 6 charges? I'd expect that happens a lot
    DATE_FORMAT = '%Y%m%d' # Not sure what it should be, but almost certainly not `I18n.l`

    def initialize(appointment)
    @appointment = appointment
    end

    def to_s
    # Swapped the join("|") with CSV b/c it was just so sus to me, like you
    # there's user submitted data in there, what if the user enters a pipe?
    # that said, it's a total guess.
    CSV.generate_line to_a
    end

    def to_a
    (1..TOTAL_NUMBER_OF_FIELDS).map &to_h
    end

    def to_h
    other_fields.merge diagnosis_code_fields, charges_fields
    end

    private

    def other_fields
    @other_fields ||= LineSerializer.new(@appointment).fields, :merge
    end

    # Maybe sort these to put the most expensive ones first? Someone might get denied their insurance claim b/c the aspirin was charge 1 and the MRI was charge 7 and thus omitted, making the bill look sus
    def charges
    @charges ||= appointment.charges.to_a
    end

    def diagnosis_code_pointers
    @diagnosis_code_pointers ||= charges
    .map(&:diagnosis_code).map(&:code).uniq
    .take(NUMBER_OF_DIAGNOSIS_SLOTS)
    .each.with_index(1).to_h
    end

    def diagnosis_code_fields
    @diagnosis_code_fields ||= diagnosis_code_pointers.to_h do |code, offset|
    [DIAGNOSIS_CODES_OFFSET+offset, code]
    end
    end

    def charges_fields
    @charges_fields ||= charges
    .take(NUMBER_OF_CHARGE_SLOTS)
    .flat_map.with_index { |charge, offset|
    [ [offset+77, charge.date_of_service.strftime(DATE_FORMAT)], # date of service
    [offset+101, charge.cpt_code.code], # procedure code
    [offset+107, charge.modifier], # procedure code modifier
    [offset+113, diagnosis_code_pointers[charge.diagnosis_code.code]], # diagnosis code pointer
    [offset+119, charge.cpt_code.fee_per_unit], # charge
    [offset+125, charge.units], # units
    [offset+167, charge.cpt_code.ndc_code], # ndc code
    ]
    }.to_h
    end

    end
    end
    “`

    Reply
  2. Peter Schröder

    I like that you are explaining your style through a more nuanced, real world example that is still reasonably easy to follow.

    The way you are adressing the topic is pretty much how I’ve made up my mind about this topic.

    Another thing that really botheres me with the `.call` pattern in the case of interactors is the hassle with discoverability. If all your methods are just named `.call` it’s hard to search for locally, in github, in your exception tracker…

    One thing that I’d like to comment on is your `ChargeCollection` class. I understand the desire to add functionality to collections, but I generally refute when someone extends from base-classes like Array, Hash etc. It’s usually more appropriate to put this logic elsewhere. I think the only exception to this is when you are building a framework where all objects are interacting with a same set of base-classes-on-steroids.

    Reply
  3. Simon

    Great article!
    I agree that sometimes service objects get a little abused.
    However, in most of the cases they are used as part of the CRUD set of actions, where the majority of the logic revolves around building the object to be stored in the DB + maybe sending some notifications etc.
    How would you model something like that with this OOP approach?

    Also, in your example, who would call the initial #write method? The controller?

    One final thing, I’m not sure that app/models is the right place to put this kind of code. app/models is usually reserved for Active Record models. I would expect your code to be under /lib instead.

    Reply
    1. Jason Swett Post author

      Thanks! I’ll answer your questions in reverse order. Regarding where to put things, I put all my domain logic in app/models and all my application logic in lib. The #write method happens to get called just by another model in this case. Regarding how I would model something like that in OOP, I’m not sure. Maybe I wouldn’t. I’d have to understand more about the scenario in order to know how I would code it.

      Reply
  4. Todd

    Jason – Thanks for sharing this article. I definitely agree with you that an OOP design often is a better fit and helps with creating more understandable/manageable code.

    Have you considered combining OOP design with Service Objects/Interactors? Doesn’t really fit the more common definitions of Service Objects but that is what I’ve been doing for a while.

    The basic structure still includes a `call` class-level method but what the `call` method does is where things tend to diverge. For me, the `call` method is just a simple way to create an instance of the Service Object, run it by calling an instance method (I use `go` but could be anything), and return the created instance.

    There are many reasons for this Service Object design but it primarily came out of trying to figure out what a Service Object should return. I’ve always thought that suggestions for a Service Object return value were either too complex (creating a special return object) or too opaque (no return value, check the database to make sure it worked).

    Returning the Service Object instance allows for an OOP design on top of the Service Object pattern. Then you can design the Service Object so that it’s easy to inspect what the Service Object did (ex: new objects created or updated). In the case of report generation, like in your example, you would have public methods for retrieving the part of the report that the Service Object is responsible for.

    Perhaps keeping the Service Object pattern isn’t that important to you but it wouldn’t change too much of your code. You would keep most of the same classes, add a few standard methods to each object, and maybe do some minor restructuring. I also like to name my Service Objects as verbs (`LineSerializer` -> `SerializeLine`) to keep me in the right frame of mind when writing the code but I don’t see that as important as the basic structure.

    A couple of other things I think about when designing this type of code is writing automated tests and background jobs. The OOP Service Object works well for those situations too.

    Reply
  5. Augusts

    Thanks for writing this up Jason. I must say I have contracted a bit of a service-object-itis in that lately almost everything I’ve written, that is not a model or a controller, has been a service.

    My justification for that is that web applications lend themselves to the (over)use of command pattern well. Action code works well by making a command call and rendering the return value. Job code works the same way because return value is irrelevant. Ditto for callbacks. Perhaps the only place for objects with several methods in Rails’ MVC is the missing “view” object.

    From the perspective of a service-object-junky

    ZirMed837PFile::File.new(appointments).write(file.path)

    looks hard to test (will have to assert/mock instantiation and call to write), and requires the developer to remember both how to instantiate and then what to call on the instance. The API can be KISSed down to one method call.

    A service call enthusiast would say that for the purposes of writing the ZirMed837PFile, the calling object does not care, indeed ideally should not have access to, any writing or aggregating instances. Merely a writer service that, when called with appropriate params performs the needed operation, some

    ZirMed837PFileWriter.call(
    appointments: appointments,
    path: file.path
    )

    This is simple to contract-test – assert that writer was called with correct params, mock returned value, if any, and assert that the value was used correctly.

    Perhaps this is dumbing Ruby down to BASIC and losing some OOP magic, but I find that when working in a team the understanding that everyone writes services that have just one public method and only use kwargs to be liberating. Understanding another’s service generally only requires reading the “.call” spec.

    Reply
  6. Alex

    Thanks for the article! It’s always nice to have a look on well structured code. But there is a one point in my mind about this approach vs .call . The deal is you approach is basically a freedom, it fits to experienced programmers who knows how to decompose their code and not mess it up and the .call approach pushes less experienced developers to more simple and narrow interface so their code looks similar so new developers can stick to it and save good structure instead of practising in decomposing task to plain objects which is not simple at all for less experienced developers. Anyway I think both approaches can be used in the same project with good balance.

    Reply
    1. Jason Swett Post author

      Thanks! I would argue that if the .call approach results in a more simple and narrow interface, and if a more simple and narrow interface is easier to understand, then everyone should do it that way, not just less experienced developers. But I don’t think using .call does result in more understandable code. I think using .call gives the illusion of simplicity and organization without genuinely providing simplicity and organization. What I would prefer is that less experienced developers are taught good OOP practices.

      Reply
  7. Ajay

    Interesting and thought-provoking.

    What do you think about “SerializedLine” instead of “LineSerializer”, to preserve the declarative naming convention and avoiding “er”.

    Reply
  8. Denys

    Thank you, interesting opinion.But I think those classes shouldn’t be in models, it rather lib folder, since you could re-use it in other projects, or even extract it to gem. Models just for AR, plain objects there are confusing.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *