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:
Containeris 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".Environmentis relegated to gateway configurationSetupbecomes a logic-free class that only registers relations/commands/etc for collection later.- A
CreateContainerfactory class ties togetherEnvironmentandSetup, passing their properties toFinalizeto produce the finalContainer. - A shorthand
ROM.create_containermethod 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 betweenSetupandEnvironmenteliminated. - Hopefully is more functional?
EnvironmentI believe can be immutable,Setupwhile not immutable is also not long-lived, either.Containercreation pulled out of both, for better separation of responsibilities. Basically, the idea is to turnEnvironmentandSetupinto isolated data types that are consumed by the finalization process.
- Non-block form now has two objects that co-exist until the final
Containercreation
Setupis currently envisioned as a very simple object:attr_readers 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.