Skip to content

Instantly share code, notes, and snippets.

@endash
Last active October 20, 2015 19:28
Show Gist options
  • Save endash/3b436aa0d90fedc41c8b to your computer and use it in GitHub Desktop.
Save endash/3b436aa0d90fedc41c8b to your computer and use it in GitHub Desktop.
Ruby Object Mapper configuration

N.B. This is a strawman proposal This is primarily based on the stand-alone use-case and not part of any framework integration. As such, there are possibly edge or corner cases not taken into account. That said, this is mostly shifting class boundaries around a bit and clarifying semantics, rather than modifying the underlying logic.

The main context here is present-day use of Environment.new, not the global ROM.setup.

Very basic, slapdash first cut at a refactor: https://github.com/endash/rom/tree/rework-env (this really just served to test that the existing code could be re-arranged as intended and still work, not as a genuine attempt at a refactor).

Current issues

Issue 1: We instantiate Environment, but Environment is throw-away. We actually want the Container at the end, in all cases, so that we can get at relations, commands, etc. We even access the Container via Environment#env, so users are encouraged to mentally conflate the two. Environment is, essentially, a single-use factory.

Related Issue 2: Because configuration is done through Environment configuration methods remain in the public interface after finalization.

Resolution: Somewhat tricky as, ideally, we want to tease these classes apart, not introduce another coupling. My current thinking is:

  • Container is positioned, in guides and documentation, as the main interface for clarity [note: only insofar as it actually remains the main interface]. This just means describing the setup process as "creating a container" rather than "configuring an environment".
  • Environment is relegated to gateway configuration
  • Setup becomes a logic-free class that only registers relations/commands/etc for collection later.
  • A CreateContainer factory class ties together Environment and Setup, passing their properties to Finalize to produce the final Container.
  • A shorthand ROM.create_container method is a bit of sugar, wrapping CreateContainer—no global state.

|


|

Issue 3: The block form of #setup is non-idiomatic:

  1. Implicit receiver obscures what's actually going on.
  2. Weird scope means method calls might be harder to reason about, e.g. there's a potential conflict between a user-defined method/attribute outside of the block and a method/attribute on Environment, and
  3. as a consequence of instance_exec the user code gets internal/private access to Environment.
  4. No actual functionality is gained,
  5. but the semantics differ, e.g., auto_registration.
  6. Configuration-specific interface hangs around after execution, and as such
  7. the block leaks harmful/useless state contrary to expectations about block semantics

Resolution: A block form of the aforementioned ROM.create_container method would invoke #call, passing to the block a Setup instance. An Environment will be instantiated from the non-block arguments. The two objects will then be passed to CreateContainer, returning a Container. DSL methods removed from this code path entirely.

See an example of the potential syntax below.

|


|

Issue 3 is primarily a consequence of Issue 4:

Issue 4: The block form of #setup does double-duty as the DSL-based 80% use case dev-friendly configuration method. This is a conflation of a perfectly valid use case (manually configuring an Environment with block syntax) and an attempt at a simplified global interface.

Resolution: DSL is removed from the main code path entirely.

|


|

Issue 5: Environment#setup is redundant, should only be called once, and, from a functional point of view, involves mutating an object's state (extensively) after initialization.

Resolution: At least as current envisioned, Environment is set up on initialization and is immutable. Furthermore, while Environment exists to be consumed in the creation of a Container, the Container is selfsufficient and Environment can be allowed to pass out of scope.

Result

Pros

  • Basic-use API now structured around Container, which currently is the ultimately resulting object that the user interacts with
  • DSL pulled out of the main code path, so basic setup is much simplified.
  • Configuration interfaces contained in the appropriate classes.
  • Finalization pulled out of Setup, and some cross-talk between Setup and Environment eliminated.
  • Hopefully is more functional? Environment I believe can be immutable, Setup while not immutable is also not long-lived, either. Container creation pulled out of both, for better separation of responsibilities. Basically, the idea is to turn Environment and Setup into isolated data types that are consumed by the finalization process.

Cons

  • Non-block form now has two objects that co-exist until the final Container creation

Thoughts

  • Setup is currently envisioned as a very simple object: attr_readers for relations, mappers, and commands, and their register_ methods. This is continued, rather than using a hash, to maintain the semantic API; to allow for the block & method call form instead of manually constructing a hash; to future-proof against the need for additional methods or functionality; and finally, because in all likelihood it'll need to regain some complexity to resolve any issues with the rest of the proposal.
  • The more explicit, non-block form might be cleaned up by having a wrapper Configuration class, which instantiates an Environment internally, as well as proxies to a Setup internally.
  • I'm going through add-on repos to see if the existing API is used in unforeseen (by me) ways.
# Block-based (no additional semantics or DSL behaviour)
#
# Looks superficially similar to ROM.setup, but implementation is very different. Also,
# avoids setup/Setup confusion and is more explicit about purpose
container = ROM.create_container(:sql, 'postgres://localhost/db') do |config| #config is a Setup
config.register_relation(:foos)
end
container.register_relation(:bars) # NoMethodError
foos = container.relation(:foos).to_a # [{...}, {...}]
# Time-separated setup (e.g., as part of a framework boot process)
rom_environment = ROM::Environment.new(:sql, 'postgres://localhost/db')
rom_setup = ROM::Setup.new
#...
rom_setup.register_relation(:bars)
#...
container = ROM.create_container(rom_environment, rom_setup)
bars = container.relation(:bars).to_a # [{...}, {...}]
# create_container immediately delegates to a class which could be used directly:
container = ROM::CreateContainer.new(rom_environment, rom_setup).container
# A potential wrapper class around `Environment` and `Setup` to simplify basic usage.
rom_configuration = ROM::Configuration.new(:sql, 'postgres://localhost/db')
#...
rom_configuration.register_relation(:bars)
#...
container = ROM.create_container(rom_configuration)
bars = container.relation(:bars).to_a # [{...}, {...}]
# create_container immediately delegates to a class which could be used directly:
container = ROM::CreateContainer.new(rom_configuration).container
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment