Open waterlink opened 8 years ago
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).
This sounds good but i wonder how it applies to reality in practice :eyeglasses:
@sdogruyol
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 record
s. 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
Will write an example for domain object soon.
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
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.
@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 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 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. :)
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.
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"} })
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.
Needless to say, that plugins should not depend on existence of other plugins and should not be using the interfaces of other plugins.
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.
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.
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, asto_h
,to_json
,to_yaml
, etc. are implemented.#save
,#update
,.find
,.where
behaviors (data storage related).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).DataStructure
.DataStructure
.WDYT?