Skip to content

Flatter and FlatMap: What's Changed

Artem Kuzko edited this page Oct 4, 2015 · 6 revisions

Code

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.

Mappers

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.

Special cases are not part of the core

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.

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.

No nested traits

Nested traits lead to extra complication of the code and were not really used. One can always mix several traits when needed.

No owner and host methods

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.

No suffixes

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.

Well-defined processing and callbacks order

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 and development

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.