-
-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add glossary to docs, provide trial run for some terminology changes #198
base: main
Are you sure you want to change the base?
Conversation
@solnic @flash-gordon Are you happy with these naming changes? If so, I'd like to start rolling them out as part of my 1.0 preparation work across the next two weeks. |
@jodosha Pinging you here too since this is a really critical foundation of Hanami, and I want to make sure you're comfortable with all the terms here too. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great! 🎉 👏🏻
Hi @timriley , just as an observer here, I have always struggled a bit to understand some of the dry-* concepts in the past, and this document makes things clearer about dry-system Something troubled me: finalize / finalization is used without being introduced, is this expected ? And one question for my understanding: would lazy loading be used more in dev mode, and finalize used more on a prod app? Thanks 🙏 |
Co-authored-by: Peter Solnica <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! This is great. I learned a ton. Made a number of suggestions. I had the gist of what dry-system
did before, but now I feel like I understand it a lot better. I hope my perspective is useful!
Should we add an entry for Manual replacements, this is a pretty fundamental feature and it's mentioned once (twice after my suggestions)?
|
||
**Container:** The container is central to dry-system. When you use dry-system, your very first step is to create your own `Dry::System::Container` subclass, which will be the class you use to manage and access all of your application’s **components**. | ||
|
||
**Component key:** A component’s **key** is a string that uniquely identifies that component within the **container**. You can **resolve** a component from the **container** by passing its key to the container’s `.[]` method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might make sense to put the Component key definition after the Component definition. (Else readers might have the experience of: "the what key?")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also specify that a namespace for a key looks like "foo.bar" here? We reference key namespaces but not that they're separated from the rest of the key with a period.
|
||
**Component:** A component is an item registered in the **container**, representing an object within your application. Each component has a **key**, which you can pass to `.[]` to **resolve** its **instance** from the container. | ||
|
||
**Component resolution:** You can **resolve** a component from the container by providing its **key** to the container’s `.[]` method. Whenever you resolve a component, it will either build and return a new component **instance** (when the component is **auto-registered**, or when **manually registered** with a block), or return a single instance (when the component is **manually registered** with an object instead of a block, or when the component is **auto-registered** and is configured to be **memoized** or is determined to be a **singleton**). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Component resolution:** You can **resolve** a component from the container by providing its **key** to the container’s `.[]` method. Whenever you resolve a component, it will either build and return a new component **instance** (when the component is **auto-registered**, or when **manually registered** with a block), or return a single instance (when the component is **manually registered** with an object instead of a block, or when the component is **auto-registered** and is configured to be **memoized** or is determined to be a **singleton**). | |
**Component resolution:** You can **resolve** a component from the container by providing its **key** to the container’s `.[]` method. Whenever you resolve a component, it will either build and return a new component **instance** (when the component is **auto-registered without memoization** , or when **manually registered** with a block), or return a single instance (when the component is **manually registered** with an object instead of a block, or when the component is **auto-registered with memoization** or is determined to be a **singleton**). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also wonder if we could break this into bulleted list for each option? It's a bit hard to parse when there are several lists of nested "or" items
|
||
**Component instance**: A component instance is a simple object from your application, with all its dependencies already provided (courtesy of **auto-injection**), ready for you to use. You receive a component instance when you **resolve** it from the container. If you had **manually registered** the component, then its instance will be the object you provided when registering it. Otherwise, it will typically be an instance of a class that has been **auto-registered** from one of your **component dirs**. | ||
|
||
**Auto-injection:** The auto-injector is a module from the **container** that you can mix into your own classes to declare their dependencies using **container keys**. The auto-injector will automatically define an initializer that **resolves** those dependencies from the container. This means you can initialize your object with `.new` alone, with its default dependencies resolved and assigned to instance variables automatically, while still allowing you to provide manual replacements for zero or more of those dependencies as explicit arguments to `.new`. Auto-injection combined with **auto-registration** means you can resolve a single component from your container and have all of its dependencies auto-registered and resolved in turn. When the container is **lazy loading**, this also allows an individual component to be resolved in the shortest possible time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Auto-injection:** The auto-injector is a module from the **container** that you can mix into your own classes to declare their dependencies using **container keys**. The auto-injector will automatically define an initializer that **resolves** those dependencies from the container. This means you can initialize your object with `.new` alone, with its default dependencies resolved and assigned to instance variables automatically, while still allowing you to provide manual replacements for zero or more of those dependencies as explicit arguments to `.new`. Auto-injection combined with **auto-registration** means you can resolve a single component from your container and have all of its dependencies auto-registered and resolved in turn. When the container is **lazy loading**, this also allows an individual component to be resolved in the shortest possible time. | |
**Auto-injection:** The auto-injector is a module from the **container** that you can mix into your own classes to declare their dependencies using **container keys**. The auto-injector will automatically define an initializer that **resolves** those dependencies from the container. This means you can initialize your object with `.new` alone, with its default dependencies resolved and assigned to instance variables automatically, while still allowing you to provide manual replacements for one or more of those dependencies as explicit arguments to `.new`. Auto-injection combined with **auto-registration** means you can resolve a single component from your container and have all of its dependencies auto-registered and resolved in turn. When the container is **lazy loading**, this also allows an individual component to be resolved in the shortest possible time. |
Wouldn't overriding zero of the variables be the default situation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the container is **lazy loading**, this also allows an individual component to be resolved in the shortest possible time.
How is the shortest possible time? My understanding of lazy loading is that it allows the container to be initialized faster (since it doesn't load lazy components). And the effect of that is that lazy loaded components actually take longer to resolve than non-lazy ones since they're loaded the first time they're called.
Wouldn't it be more accurate to say "this also allows a container to be finalized in the shortest possible time", though that might better belong in the "lazy loading" section?
|
||
**Provider:** A provider manages the lifecycle around configuring and registering one or more components (or setting global state if necessary) required for distinct parts of your application to work. When you register a provider (by creating a **provider file** in your **provider path**), you provide code to run for one or more of its **lifecycle hooks**, `prepare`, `start`, and `stop`. You typically create a provider when you need to register components with particular configuration (such as a client for a third party service, requiring API keys and other connection details) or when components are particularly heavyweight (such as a database persistence system). Every provider has a unique **name** that also corresponds to a **container key** **namespace**. Whenever any **component** in that namespace is **resolved** from the container, then the provider will be **started** (with its `prepare` and `start` hooks run in sequence). Providers can also be individually controlled via the container's `.prepare`, `.start`, and `.stop` methods. | ||
|
||
**Manual registration**: You can manually register a **component** in the **container** via its `#register` method, passing the component's **key**, along with either a block that returns a new **instance** of the component (which will be called whenever the component is **resolved**), or a single object (to be returned as the **instance** value whenever the component is resolved). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Manual registration**: You can manually register a **component** in the **container** via its `#register` method, passing the component's **key**, along with either a block that returns a new **instance** of the component (which will be called whenever the component is **resolved**), or a single object (to be returned as the **instance** value whenever the component is resolved). | |
**Manual registration**: You can manually register a **component** in the **container** via its `#register` method, passing the component's **key**, along with either a block that returns a new **instance** of the component (which will be called whenever the component is **resolved**), or a single object (to be returned as the **instance** whenever the component is resolved). |
|
||
**Manual registration**: You can manually register a **component** in the **container** via its `#register` method, passing the component's **key**, along with either a block that returns a new **instance** of the component (which will be called whenever the component is **resolved**), or a single object (to be returned as the **instance** value whenever the component is resolved). | ||
|
||
**Manifest registration:** You can create one or more registration manifest files, containing code that **manually registers** one or more components in the container. These files are searched during **auto-registration** to allow those components to be registered and **resolved** during both **finalization** and **lazy loading**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the only way, or the only practical way, or the preferred way, of doing manual registration?
|
||
**Manifest registration:** You can create one or more registration manifest files, containing code that **manually registers** one or more components in the container. These files are searched during **auto-registration** to allow those components to be registered and **resolved** during both **finalization** and **lazy loading**. | ||
|
||
**Container imports:** You can specify other containers to import into your own. The components in the imported container will be registered in your own, with a key prefix you provide. The container will be imported in full as part of **finalization**, or individual components from the container will be imported as required when **lazy loading**. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is a "key prefix"? Is it separated with an underscore or a period (though I think that's a namespace)? E.g. for a car
container, if I had an engine
, and the prefix was car
, would it be car_engine
?
|
||
**Container root**: This is the path for the root of your application. All **component dirs** are expected to be under this root. For typical applications, this will be the same as the your project directory. For a minimal container setup, you should configure both the container root and at least one **component dir**. | ||
|
||
**Component dir namespaces**: You can configure one or more namespaces within each **component dir**. Each namespace corresponds to a path within the dir, and sets configuration for loading components from the source files under that path: a **key namespace** (a prefix for the component key) and a **const namespace** (an expected Ruby constant to be used as the base namespace for the component's class constant). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Component dir namespaces**: You can configure one or more namespaces within each **component dir**. Each namespace corresponds to a path within the dir, and sets configuration for loading components from the source files under that path: a **key namespace** (a prefix for the component key) and a **const namespace** (an expected Ruby constant to be used as the base namespace for the component's class constant). | |
**Component dir namespaces**: You can configure one or more namespaces within each **component dir**. Each namespace corresponds to a path within the dir, and sets configuration for loading components from the source files under that path. This consists of both a **key namespace** (a prefix for the component key) and a **const namespace** (an expected Ruby constant to be used as the base namespace for the component's class constant). |
|
||
**Component dir namespaces**: You can configure one or more namespaces within each **component dir**. Each namespace corresponds to a path within the dir, and sets configuration for loading components from the source files under that path: a **key namespace** (a prefix for the component key) and a **const namespace** (an expected Ruby constant to be used as the base namespace for the component's class constant). | ||
|
||
**Provider paths**: You can configure one or more paths to be searched when the container loads **providers**. By default, this is a single `"system/providers/"` path only. The container will search these paths in the order specified, with the first match for a given provider name being used to load that provider. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Provider paths**: You can configure one or more paths to be searched when the container loads **providers**. By default, this is a single `"system/providers/"` path only. The container will search these paths in the order specified, with the first match for a given provider name being used to load that provider. | |
**Provider paths**: You can configure one or more paths to be searched when the container loads **providers**. By default, this is a single `"system/providers/"` path within the **container root** only. The container will search these paths in the order specified, with the first match for a given provider name being used to load that provider. |
|
||
**Plugins**: You can activate additional functionality for your container by activating one or more plugins. Plugins can add additional settings and methods on your container, register their own components, and otherwise act in response to container lifecycle events (such as before/after initial configuration). When you activate a plugin, you may also pass additional options to tailor the plugin’s behavior. | ||
|
||
**Provider packs**: You can use prebuilt providers offering useful functionality by accessing **provider packs**. These are typically distributed via 3rd party Ruby gems. When you use a provider from a provider pack, you still create your own **provider file**, but instead of specifying the provider lifecycle yourself, you instead specify a provider from a pack, and supply any required configuration for the provider. You can also enhance the provider's own lifecycle by adding your own "before" and "after" hooks for the provider lifecycle events. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Provider packs**: You can use prebuilt providers offering useful functionality by accessing **provider packs**. These are typically distributed via 3rd party Ruby gems. When you use a provider from a provider pack, you still create your own **provider file**, but instead of specifying the provider lifecycle yourself, you instead specify a provider from a pack, and supply any required configuration for the provider. You can also enhance the provider's own lifecycle by adding your own "before" and "after" hooks for the provider lifecycle events. | |
**Provider packs**: You can use prebuilt providers offering useful functionality by accessing **provider packs**. These are typically distributed via 3rd party Ruby gems. When you use a provider from a provider pack, you still create your own **provider file**, but instead of specifying the provider lifecycle yourself, you instead specify a provider from a pack, and supply any required configuration for the provider. You can also enhance the provider's own lifecycle by adding your own `before` and `after` hooks for the provider lifecycle events. |
|
||
## Testing | ||
|
||
**Container stubs**: You can enable stubbing on the container to replace a particular component (via its key) for the sake of certain integration tests. You should hopefully only need this approach sparingly, since most classes designed to work well with the container (especially those using **auto-injection**) should be able to be tested in isolation using dependency injection, with particular dependencies replaced via test doubles passed to the class’ constructor. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Container stubs**: You can enable stubbing on the container to replace a particular component (via its key) for the sake of certain integration tests. You should hopefully only need this approach sparingly, since most classes designed to work well with the container (especially those using **auto-injection**) should be able to be tested in isolation using dependency injection, with particular dependencies replaced via test doubles passed to the class’ constructor. | |
**Container stubs**: You can enable stubbing on the container to replace a particular component (via its key) for the sake of certain integration tests. You should hopefully only need this approach sparingly, since most classes designed to work well with the container (especially those using **auto-injection**) should be able to be tested in isolation using dependency injection, with particular dependencies replaced via test doubles passed to the class’ constructor as manual replacements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to fix one of the diffs (highlighting deletions/additions) but it's not working, so listed out the changes.
|
||
**Auto-registration:** Auto-registration is one of the main reasons to use dry-system: it makes it easy to work with applications consisting of a large number of components. The container **auto-registers** components both when you **finalize** it, as well as when you **resolve** a component while the container is **lazy loading.** When auto-registering, the container automatically registers a **component** for the class defined in each **source file** in each of its **component dirs**. The container matches the component to its source file based on the source file’s name and any **namespaces** you have configured in the component dirs. | ||
|
||
**Provider:** A provider manages the lifecycle around configuring and registering one or more components (or setting global state if necessary) required for distinct parts of your application to work. When you register a provider (by creating a **provider file** in your **provider path**), you provide code to run for one or more of its **lifecycle hooks**, `prepare`, `start`, and `stop`. You typically create a provider when you need to register components with particular configuration (such as a client for a third party service, requiring API keys and other connection details) or when components are particularly heavyweight (such as a database persistence system). Every provider has a unique **name** that also corresponds to a **container key** **namespace**. Whenever any **component** in that namespace is **resolved** from the container, then the provider will be **started** (with its `prepare` and `start` hooks run in sequence). Providers can also be individually controlled via the container's `.prepare`, `.start`, and `.stop` methods. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**Provider:** A provider manages the lifecycle around configuring and registering one or more components (or setting global state if necessary) required for distinct parts of your application to work. When you register a provider (by creating a **provider file** in your **provider path**), you provide code to run for one or more of its **lifecycle hooks**, `prepare`, `start`, and `stop`. You typically create a provider when you need to register components with particular configuration (such as a client for a third party service, requiring API keys and other connection details) or when components are particularly heavyweight (such as a database persistence system). Every provider has a unique **name** that also corresponds to a **container key** **namespace**. Whenever any **component** in that namespace is **resolved** from the container, then the provider will be **started** (with its `prepare` and `start` hooks run in sequence). Providers can also be individually controlled via the container's `.prepare`, `.start`, and `.stop` methods. | |
**Provider:** A provider manages the lifecycle around configuring and registering one or more components required for distinct parts of your application to work. When you register a provider (by creating a **provider file** in your **provider path**), you provide code to run for one or more of its **lifecycle hooks**: `prepare`, `start`, and `stop`. You typically create a provider when you need to register components with particular configuration (such as a client for a third party service, requiring API keys and other connection details, or setting global state if necessary) or when components are particularly heavyweight (such as a database persistence system). Every provider has a unique **name** that also corresponds to a **namespace** in a **container key**. Whenever any **component** in that namespace is **resolved** from the container, then the provider will be **started** (with its `prepare` and `start` hooks run in sequence). Providers can also be individually controlled via the container's `.prepare`, `.start`, and `.stop` methods. |
GitHub isn't previewing the diff properly for me so just in case it's not clear:
1: Add a colon before the lifecycle hooks to show it's an exhaustive list
2. Move the 'setting global state' part to later, since it's not something we want to encourage but may be necessary.
3. **container key** **namespace**
renders as the same as **container key namespace**
. I'm not sure if the wording I used here, of **namespace ** in a **container key**
is quite right either though.
|
||
**Container:** The container is central to dry-system. When you use dry-system, your very first step is to create your own `Dry::System::Container` subclass, which will be the class you use to manage and access all of your application’s **components**. | ||
|
||
**Component key:** A component’s **key** is a string that uniquely identifies that component within the **container**. You can **resolve** a component from the **container** by passing its key to the container’s `.[]` method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also specify that a namespace for a key looks like "foo.bar" here? We reference key namespaces but not that they're separated from the rest of the key with a period.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@timriley This is an epic work. 👏 LGTM.
Only one thing is puzzling me: the difference between Plugin and Provider Packs.
Next iteration for this documentation should be working on:
- Formatting: split those long paragraphs to ease the read.
- Linking: use anchors to link concepts within the same doc. Readers shouldn't hold all the information in their head, but be able to jump back and forth between information.
- Code examples/snippers: how the reader applies what they just read?
@jodosha a plugin extends dry-system itself to enable more functionality, a provider pack gives you components that you use within your app. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not only glossary is extremely helpful, this doc alone is enough to become a "confident user" :)
This PR adds a glossary (readable version here) covering all key dry-system concepts and features.
This should eventually be merged into the documentation, ideally as part of a revamp that includes dedicated pages for most of the features noted in the glossary.
For the time being, however, this glossary also serves as a trial run for some terminology changes I'd like to apply before 1.0, notably:
register
on the container itself and ensures it is not confused with these files, which indeed are "manifests" of a series of components to be registered.To see all these changes in context, please take a read of overall glossary.
Getting these terms as clear as possible will be important as we prepare for much wider dry-system use through the upcoming Hanami 2.0 release. And I'd like to make it so that Hanami doesn't have the carry the weight of providing its own friendlier mix of names, since all our lives will be made easier if it can just use the same terms as dry-system.