Skip to content
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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

timriley
Copy link
Member

@timriley timriley commented Jan 4, 2022

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:

  • "Component" as the canonical term for an item registered in the container
  • "Component key" (instead of a vague mix of both "key" and "identifier") as the reference to a component in the container
  • "Provider" instead of "bootable component" — the job of a provider is provide a certain end state, typically one or more configured and then registered components, but in some cases (e.g. when working with inflexible 3rd party tools) also includes global configuration or other global state. "Bootable component" was problematic as a name because it overlaps with "component" itself, and the files embodying these are not "components" themselves at all.
  • "Provider pack" instead of "component provider" — to ensure clarity and consistency given the name change above.
  • A provider's first lifecycle event as "prepare" instead of "init" — this term is much easier to use in regular prose because it is a real verb (e.g. "has the provider prepared?" vs the awkward "has the provider inited?") and it also better expresses the intent of the lifecycle event, i.e. to prepare some basic environment ahead of some heavier-weight setup. Objects may be "initialized" in either of these steps, so it's better we not use a term that can confuse this.
  • "Manifest registration" instead of "manual registration" to describe the files containing one or more directives to manually register components in the container, plus the whole process of incorporating these files into the container lazy loading and finalization behaviors — this clarifies "manual registration" as specifically the act of calling 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.

@timriley timriley requested a review from solnic as a code owner January 4, 2022 05:24
@timriley timriley changed the title Add glossary to docs Add glossary to docs, provide trial run for some terminology changes Jan 4, 2022
@timriley
Copy link
Member Author

timriley commented Jan 4, 2022

@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.

@timriley
Copy link
Member Author

timriley commented Jan 4, 2022

@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.

Copy link
Member

@solnic solnic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 🎉 👏🏻

docsite/source/glossary.html.md Outdated Show resolved Hide resolved
docsite/source/glossary.html.md Outdated Show resolved Hide resolved
@inouire
Copy link

inouire commented Jan 4, 2022

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 🙏

timriley and others added 2 commits January 4, 2022 20:01
Co-authored-by: Peter Solnica <solnic@users.noreply.github.com>
Copy link
Member

@cllns cllns left a 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.
Copy link
Member

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?")

Copy link
Member

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**).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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**).

Copy link
Member

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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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?

Copy link
Member

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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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**.
Copy link
Member

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**.
Copy link
Member

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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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.

Copy link
Member

@cllns cllns left a 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**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.
Copy link
Member

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.

Copy link
Member

@jodosha jodosha left a 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?

@solnic
Copy link
Member

solnic commented Jan 5, 2022

Only one thing is puzzling me: the difference between Plugin and Provider Packs.

@jodosha a plugin extends dry-system itself to enable more functionality, a provider pack gives you components that you use within your app.

Copy link
Member

@flash-gordon flash-gordon left a 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" :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants