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
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 71 additions & 0 deletions docsite/source/glossary.html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
title: Glossary
layout: gem-single
name: dry-system
---

## The basics

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


## Component registration

**Component dir:** You can configure a container with one or more component dirs, which are the directories containing the **source files** for your components. The container **auto-registers** its components from the source files in these component dirs.

**Source file:** A source file is a `.rb` Ruby source file inside a **component dir**, defining a single class with a name corresponding to the file. The container loads this file during **auto-registration** to register a matching **component**. (TODO: A source file may have **magic comments** to control its registration behavior)

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


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


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


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


## Component loading

**Loader**: The loader is an internal facility responsible for creating the **component instances** when a component is **resolved**. Specifically, the loader will `require` the component's source file (unless **autoloading**), determine the class constant for the component, then create a new instance of the class (or the single instance of the class is a **singleton**).

**Autoloading**: When you’re using an autoloader like Zeitwerk, you can configure dry-system with the **autoloading loader**, which is a specialization of the default **loader** that does not `require` any source files, which allows the autoloader to load the component’s class constant according to its own rules.

**Memoized components**: By default, the **loader** will create a new **component instance** every time a component is **resolved**. However, you can also configure a component to be memoized (via **component dir** configuration or **magic comments**), in which case an instance is created the first time, and then reused for all subsequent resolutions.

## Container lifecycle

**Lazy loading:** Containers start in lazy loading mode, loading individual components just in time when you resolve them (as well as all the components for their dependencies, for components using **auto-injection**). This uses the same **auto-registration** process as when you **finalize** the container, ensuring your components are consistently available in both modes. Typically, you leave the container to lazy load when you want to optimize for fastest container load time, such as when running tests or during local development.

**Finalization:** When you **finalize** a container, it **imports** other containers as required, starts each of its own **providers**, then begins a one-off process to **auto-register** components for all the **source files** in its **components dirs**, as well as any components specified in registration **manifests**. Once a container is finalized, it is frozen and no additional components can be registered. Typically, you finalize a container as part of booting a long-running application process.

**Shutdown:** When you shut down a container, it runs the **stop hook** for each of its providers.

## Container configuration

**Container configuration**: You configure your container subclass via a class-level `config` object.

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


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


## Extensibility

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