active_record.cr icon indicating copy to clipboard operation
active_record.cr copied to clipboard

RFC: Data Structures vs Domain Objects

Open waterlink opened this issue 8 years ago • 15 comments

My experience tells me, that mixing database related behaviors with business behaviors (or any other behaviors, like representation, UI, etc) is a bad idea long-term, and is an AntiPattern.

To avoid that problem, we could split current base class into 2 ones, and give users a conscious choice, what they want to do with ActiveRecord, in each particular case:

  • ActiveRecord::DataStructure - a data structure, all data is exposed, common serialization methods, as to_h, to_json, to_yaml, etc. are implemented.
    • It has only #save, #update, .find, .where behaviors (data storage related).
    • Any other behavior is forbidden (still need to figure out if it is possible to do at the code level).
  • ActiveRecord::DomainObject - a domain object, all data is hidden, serialization is not possible, unless defined by hand as a custom behavior (probably not a good idea, though).
    • Data storage related behaviors are provided by internal instance of DataStructure.
    • By default, no behavior is exposed. User is free to expose some or all of the data storage related behaviors by delegating to the internal DataStructure.
    • User is free to add any business domain behavior.

WDYT?

waterlink avatar Apr 03 '16 15:04 waterlink

Vote for DataStructure, but probably relations(belongs_to, has_many etc should be implemented as well), so it should cover the SQL layer, all the business logic should be placed somewhere else: Serializers, Vaidators, Guards, Services etc(separated shards).

dsounded avatar Apr 03 '16 15:04 dsounded

This sounds good but i wonder how it applies to reality in practice :eyeglasses:

sdogruyol avatar Apr 03 '16 17:04 sdogruyol

@sdogruyol

Using Data Structures in Practice

Basically, data structures are just there to map data from data storage (database or smth else) to the memory and vice versa. They are not suited for any business domain behavior, and basically just act as records. All the business domain behavior goes to other objects, that use these data structure, either by: 1) wrapping over them (OO style), or 2) passing them around, and preferably not mutating (FP style).

class OrderData < ActiveRecord::DataStructure
  primary id : Int
  field owner_id : Int
  field contractor_id : Int
  field name : String

  # Notice, there is no behavior here
end

class Order
  def initialize(@data : OrderData)
  end

  # .. here goes the domain-wide behavior ..
end

# Custom classes for behavior that is needed only for certain use cases
class EmailOrderToContractor
  def initialize(@order : Order)
  end

  def execute
    # .. do stuff here ..
  end

  # .. maybe some private methods here too ..
end

waterlink avatar Apr 03 '16 18:04 waterlink

Will write an example for domain object soon.

waterlink avatar Apr 03 '16 18:04 waterlink

Using Domain Objects in practice

On the other hand, domain objects, by default will hide all access to the data from the outside and hide all the database related methods from the outside too. The user will have to implement some logic on top of that.

class Order < ActiveRecord::DomainObject
  data_structure do
    primary id : Int
    field owner_id : Int
    field contractor_id : Int
    field name : String
  end

  def self.find_by_name(name)
    from_or_nil(
      data_structure.where(criteria("name") == name).first?
    )
  end

  def self.find(id)
    from_or_nil(data_structure.find(id))
  end

  private def self.from_or_nil(data)
    unless data.nil?
      new(data)
    end
  end

  def email_contractor
    contractor = Contractor.find(data_structure.contractor_id)
    # do necessary things to send email to contractor
  end
end

# This is impossible to do, because there are no such methods:
order.contractor_id    # No method contractor_id defined on Order
order.save   # No method save defined on Order
# and so on

waterlink avatar Apr 03 '16 18:04 waterlink

Maybe it makes sense for me to try to make an ad-hoc implementation (in a branch or separate shard) for these 2 patterns and try to write a simple application with them (~ 3-4 domain models).

Then after some tweaking, I will publish some sort of post/tutorial (s) on patterns involved.

Then the community can read them, maybe try them out, and judge if that is something we want to adopt.

waterlink avatar Apr 03 '16 18:04 waterlink

@sdogruyol

On the question of practicality/reality, I was writing applications using such patterns for quite a while (1.5 years), and it feels much better than your usual Web MVC, active record anti-patterns (I did spent some time with canonical projects, that have these anti-patterns, and it was a horrible experience.., I am pretty convinced, that it does not scale after certain threshold of the complexity/size).

waterlink avatar Apr 03 '16 18:04 waterlink

@waterlink that looks pretty reasonable yet cumbersome to me. (i don't have that horrible experience like you do with AR :smile: )

I agree that we should create some sample apps with these pattern and see how it goes.

sdogruyol avatar Apr 03 '16 18:04 sdogruyol

@sdogruyol It makes a lot of sense, because AR in Rails fails a lot of applications, nowadays a lot of people in Ruby world talk about patterns and other cool design stuff, but in the same time they have 1_000 + lines models in production. :)

dsounded avatar Apr 03 '16 19:04 dsounded

Not only AR fails, but the whole Web-version of MVC is one big AntiPattern.

Original MVC was about programming User Interfaces, without any business logic inside. And Model-View-Controller triples were very small and granular. For example, one triple for a submit button. Another triple for a search query text input field, another triple for a email text input field, and so on.

The pattern, schematically looked as simple as:

![digraph](http://gravizo.com/g? digraph G { controller -> model view -> model [style=dotted,label="observes"] })

Each triple was completely independent of each other one. This worked really well for UI interfaces.

Then the WEB came around. And this happened to "MVC":

![digraph](http://gravizo.com/g? digraph G { "A bunch of controllers" -> "A lot of business objects" [label="change state"] "A bunch of views" -> "A lot of business objects" [label="interrogate to show results"] })

As a result of this, business objects start to get more and more behaviors, that they shouldn't have any idea of (and what is not a part of business domain logic), including WEB related behaviors, which is really-really conceptually wrong, and violates so much principles of good design and in practice impedes development of any project at long run (ranging from 3+ months to 2+ years, depending on how quick team makes a mess).

Business objects start to acquire some controller-like behaviors and some view-like behaviors.

waterlink avatar Apr 03 '16 20:04 waterlink

General overview of architecture I prefer looks like this:

![digraph](http://gravizo.com/g? digraph G { {WebUI; RESTfulAPI; DatabaseStorage; ExternalPaymentService} -> "Business Domain Code" [label=plugin] })

plugin means that application code itself doesn't know anything about it, and plugin implements some sort of interface and gives object, that implements this interface to the application (probably plain-old Dependency Injection). By the way the interface is owned by the Application code (caller), not by implementations (plugins).

In more detail, Business Domain Code looks like this:

![digraph](http://gravizo.com/g? digraph G { ".. Use Cases .." -> {EntityA; EntityB; ".. other entities .."} {".. Use Cases .."; EntityA; EntityB; ".. other entities .."} -> {"abstract DataStorage"; "abstract Request"; "abstract Response"; "abstract PaymentProvider"} })

Entities are business objects, that hold general business logic, used in different parts of application. They DO NOT need to correspond to database tables/rows/collections/whatever. They can be more fine-grained (one row is actually 5 different objects), or less-grained (one object represents the whole table, or a list of rows from different tables), depending on the situation, and what makes more sense from business domain's point of view.

Usecases are business objects, that hold very specific business logic, that is used only in that particular, well, use case.

And plugins depend on relevant abstractions:

![digraph](http://gravizo.com/g? digraph G { {WebUI; RESTfulAPI; CLI; AgentOnMacOSX_UI} -> {"abstract Request"; "abstract Response"; ".. Use Cases .."} })

![digraph](http://gravizo.com/g? digraph G { {MemoryBasedStorage; FileBasedStorage; PostgresStorage} -> {"abstract DataStorage"} })

![digraph](http://gravizo.com/g? digraph G { {StripePaymentProvider; PayPalPaymentProvider; ".. other payment providers .."} -> {"abstract PaymentProvider"} })

waterlink avatar Apr 03 '16 21:04 waterlink

It should be clear, that there are very distinct boundaries between business domain logic and non-domain logic. These boundaries have their code dependencies inverted, as opposed to their runtime dependencies. Additionally, only logic-less data structures should be passed through the boundaries.

waterlink avatar Apr 03 '16 21:04 waterlink

Needless to say, that plugins should not depend on existence of other plugins and should not be using the interfaces of other plugins.

waterlink avatar Apr 03 '16 21:04 waterlink

Honestly, just leave it as it did. Having them coupled is more or less the standard , and its fairly easy on the brain-socket to have it all in the modelling context. No need to architecture astronaut it.

shayneoneill avatar May 05 '16 13:05 shayneoneill

Vote for Data Structure. Maybe it's not good pattern or something like that, but it's simple, and we all love simplicity. I don't like AR in Rails, but I fell in love with Sequel. This is a simple and powerful tool, a little more convenient for me.

AlexWayfer avatar May 29 '16 20:05 AlexWayfer