-
Notifications
You must be signed in to change notification settings - Fork 0
Flatter and FlatMap: What's Changed
Despite the fact FlatMap
code was modular, it's code was tightly coupled: low-level modules were using code that was defined in other higher-level modules. Flatter
was re-written to keep it's code decoupled and maintainable. Basically, by removing it's layers of functionality one by one, what's left will still work. Thus, when working on one specific module one can focus on it's dedicated functionality without thinking about how it's coupled with other modules.
In Flatter
there's no more OpenMapper
nor ModelMapper
. There's only Flatter::Mapper
. The original idea behaing OpenMapper
refactoring was to decouple mappers concepts and functionality from active-record related cases FlatMap
was working with. Despite the fact the idea was right, refactoring to OpenMapper
lead to pretty dirty and badly maintainable code. OpenMapper
's Factory
class had to handle every possible mounting and option available, having lots of logic, completely unrelated to OpenMapper
. With Flatter
this has been resolved by introducing only one Mapper
which accepts ActiveModel
-like targets. Flatter::Mapper
's Factory
's code is also modular and gets pumped alongside with Mapper
itself.
FlatMap
mappers were built around Rails code they were dealing with. And it's handling became part of core in a quite empiric, not generalized way: "to make this work, we need to save this mounting first, and then save this trait, and then save root mapper". Such approach, although worked well in most of the cases, was not intuitive and did not work well in other applications. Flatter
mappers are clean from handling corner-cases. They deal only what's they suppose to deal: mappings, mountings and traits, and do it well with a well-defined processing order (see bellow). This doesn't mean that Flatter
cannot do something FlatMap
did, because Flatter
has extensions.
Such things as skipping unnecessary mountings, manipulation of mappers saving order or dealing with ActiveRecord
targets (which was built-in in FlatMap
) is not part of the Flatter
core. Instead, they are provided in a flatter-extensions gem. They can be optionally included at a runtime:
Flatter.configure do |f|
f.use :multiparam
f.use :skipping
f.use :order
f.use :active_record
end
The only built-in extension is the Flatter::Mapping::Scribe
extension, which provides :reader
and :writer
options when mapping attributes.
Nested traits lead to extra complication of the code and were not really used. One can always mix several traits when needed.
owner
and host
methods represented 'main' mapper for a trait and a 'mounter' mapper for inner mapper. They were used mostly internally, but nevertheless it was easy to confuse one and another. In Flatter
there is only mounter
method, which returns mapper that mounted self
, no matter whether self
is trait or not.
There are no suffixes for mappings in Flatter
. They were used as workaround for handling limited collections. Instead, it's planned to add native collections support in the next version of Flatter
.
Finally, Flatter
mappers have a well-defined processing order of mountings. In FlatMap
there were things like before_mountings
, after_mountings
, and such. Traits were processed before root mapper was processed which causes execution of trait's after_save
callbacks before target was actually saved. That was completely counter-intuitive. Flatter
mappers have a well-defined saving order, it can be best shown on example. Suppose we have something like (in pseudo code):
MapperA
trait :trait_a1 do
mount :b, traits: :trait_b do
# some callbacks defined here
trait :trait_a2 do
mount :c
mount :d
Mappers are processed (validated and saved) from top to bottom. Let's have initialized
a = MapperA.new(a, :trait_a2, :trait_a1)
. Please note traits order, it is very important: :trait_a2
goes first, so it's callbacks and mountings will go first too. So if we call a.save
, we will have following execution order (suppose, we have defined callbacks for all traits and mappers):
trait_a2.before_save
trait_a1.before_save
A.before_save
A.save
A.after_save
trait_a1.after_save
trait_a2.after_save
C.before_save
C.save
C.after_save
trait_b.before_save
B_extension.before_save
B.before_save
B.save
B.after_save
B_extension.after_save
trait_b.after_save
D.before_save
D.save
D.after_save
You can manipulate with the processing order using :order
extension.
Debugging FlatMap
mappers was a pain. Classes were anonymous and inspection was completely uncomfortable. Flatter
mappers provide much better way to inspect mapper state, utilizing human-friendly naming and assigning created classes to constants, which dramatically increases readability. For example (some output was cut):
mapper.mappings
# => {"a1"=>#<Mapping @mapper=#<MapperA>, @name="a1", @options={}, @target_attribute=:a1>,
# "a2"=>#<Mapping @mapper=#<MapperA::TraitA1Trait>, @name="a2", @options={}, @target_attribute=:a2>,
# "b1"=>#<Mapping @mapper=#<MapperB>, @name="b1", @options={}, @target_attribute=:b1>,
# "b3"=>#<Mapping @mapper=#<#<Class:0x00000003e03050>>, @name="b3", @options={}, @target_attribute=:b3>,
# "b2"=>#<Mapping @mapper=#<MapperB::TraitBTrait>, @name="b2", @options={}, @target_attribute=:b2>}
mapper.mountings
# => {"trait_a1_trait"=>#<MapperA::TraitA1Trait:0x00000003e03f00>,
# "b"=>#<MapperB:0x00000003e032d0>,
# "b_extension_trait"=>#<#<Class:0x00000003e03050>:0x00000003e023d0>,
# "trait_b_trait"=>#<MapperB::TraitBTrait:0x00000003e01b38>}
as you can see, the only anonymous output corresponds to extension trait, which cannot be named by constantizing it, since it is defined by each distinct instance of the mapper class.