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).
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 configurationSetup
becomes a logic-free class that only registers relations/commands/etc for collection later.- A
CreateContainer
factory class ties togetherEnvironment
andSetup
, passing their properties toFinalize
to produce the finalContainer
. - A shorthand
ROM.create_container
method is a bit of sugar, wrappingCreateContainer
—no global state.
|
|
Issue 3: The block form of #setup
is non-idiomatic:
- Implicit receiver obscures what's actually going on.
- 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 - as a consequence of instance_exec the user code gets internal/private access to Environment.
- No actual functionality is gained,
- but the semantics differ, e.g.,
auto_registration
. - Configuration-specific interface hangs around after execution, and as such
- 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.
- 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 betweenSetup
andEnvironment
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 turnEnvironment
andSetup
into isolated data types that are consumed by the finalization process.
- Non-block form now has two objects that co-exist until the final
Container
creation
Setup
is currently envisioned as a very simple object:attr_reader
s for relations, mappers, and commands, and theirregister_
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.