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

Dependency injection writeup #333

Merged
merged 25 commits into from
May 8, 2024
Merged

Dependency injection writeup #333

merged 25 commits into from
May 8, 2024

Conversation

eliykat
Copy link
Member

@eliykat eliykat commented Apr 7, 2024

Objective

My initial goal here was to document safeProvider, however I decided that we needed a writeup about our DI patterns in general to properly contextualize it. This was tricky to structure, partly because our patterns in this area are varied and diffuse, so feedback is particularly welcome.

@eliykat eliykat requested a review from a team as a code owner April 7, 2024 23:39
@bitwarden-bot
Copy link

bitwarden-bot commented Apr 7, 2024

Logo
Checkmarx One – Scan Summary & Details9e10c7d6-47a0-4b0a-930d-d91d0eb6c903

No New Or Fixed Issues Found

Comment on lines 44 to 46
Angular contexts use ngModules
[dependency providers](https://angular.io/guide/dependency-injection-providers) to configure DI. For
example:
Copy link
Contributor

@coroiu coroiu Apr 8, 2024

Choose a reason for hiding this comment

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

I think there are some additional subtleties that we should probably mention here. This method of configuring DI services is mainly used for multi-client services. For single-client services and/or angular-only services, we should also mention:

provideIn

Services that are intended to be used globally can use the provideIn property which avoids having to create and manage service-modules. When exporting services through feature-modules it can be tricky to manage their lifetime, and it's easy to end up with instances of the service being created.

@Injectable({
    provideIn: "root"
})

Standalone components

Services that do are not stateful and/or are only supposed to be used locally to extract complex logic from a component can be injected directly into standalone components, completely bypassing modules.

@Component({
    providers: [SomeService],
    standalone: true
})

Copy link
Member Author

@eliykat eliykat Apr 8, 2024

Choose a reason for hiding this comment

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

providedIn assumes that there is no abstract interface for the service, right? Do we have any clear guidelines/rules as to when that's appropriate? The current practice seems to be "small, Angular-only services don't need abstractions if you don't feel like it" but I'm not really sure why.

Perhaps abstract interfaces are only used to manage different implementations across apps, and if a service is for a single app only, it doesn't need one? (This seems to be the approach SM Team have taken, I see providedIn: "root" for all their services.)

Copy link
Contributor

Choose a reason for hiding this comment

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

providedIn assumes that there is no abstract interface for the service, right?

Exactly!

Perhaps abstract interfaces are only used to manage different implementations across apps, and if a service is for a single app only, it doesn't need one?

This is the same approach I've been using! The component library might also export services without abstractions, not sure if they use providedIn though, but they probably should!

Copy link
Member Author

Choose a reason for hiding this comment

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

After discussing this on Slack, I've incorporated your suggestions. Unfortunately there are now a lot of options for how to register a service in Angular - but that reflects our current practice. Open to any further feedback re: structure or being more opinionated in the text.

Copy link

cloudflare-pages bot commented Apr 15, 2024

Deploying contributing-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7ea529f
Status: ✅  Deploy successful!
Preview URL: https://a6c15a16.contributing-docs.pages.dev
Branch Preview URL: https://dependency-injection.contributing-docs.pages.dev

View logs

@eliykat eliykat requested a review from coroiu April 24, 2024 03:44
Copy link
Contributor

@coroiu coroiu left a comment

Choose a reason for hiding this comment

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

I gave all of the parts a detailed read-through this time, and while I did have quite a few comments, it's all mostly just polish and I would've ok with merging as-is.

docs/architecture/clients/dependency-injection.md Outdated Show resolved Hide resolved
Comment on lines +31 to +34
- `libs/common` for services used by **multiple clients** in **non-Angular contexts (or a mix of
Angular and non-Angular contexts)**
- `libs/angular` for services used by **multiple clients** in **Angular contexts only**
- `apps/<client-name>` for services used by a single client only
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 suggestion: mention the team-specific libraries e.g. libs/auth, libs/vault

Comment on lines +124 to +129
:::note

The `useAngularDecorators` option is not type safe - it is your responsibility to ensure that the
class actually uses the `@Injectable` decorator.

:::
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 thought: we should keep an eye on microsoft/TypeScript#4881 which might be able to fix this

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice spot!

Copy link
Contributor

Choose a reason for hiding this comment

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

Keeping this open for future reference! :)

docs/architecture/clients/dependency-injection.md Outdated Show resolved Hide resolved
docs/architecture/clients/dependency-injection.md Outdated Show resolved Hide resolved
docs/architecture/clients/dependency-injection.md Outdated Show resolved Hide resolved
docs/architecture/clients/dependency-injection.md Outdated Show resolved Hide resolved
Comment on lines 186 to 188
However, the web module heavily uses [feature modules](https://angular.io/guide/feature-modules) to
divide its code by feature. Feature modules should configure their own DI wherever possible for
feature-specific (non-global) services.
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion(non-blocking): preferably in standalone components or using provideIn: 'root' and not in the actual feature module

Copy link
Member Author

Choose a reason for hiding this comment

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

Why not? If the component is standalone, then by definition it doesn't have a module, so advice about a feature module doesn't apply. And if you have a service that is only used in a feature module, I assume you'd want to register it in the more specific location rather than in root. Does this go to your earlier concern about accidentally instantiating duplicate services?

Copy link
Contributor

@coroiu coroiu Apr 25, 2024

Choose a reason for hiding this comment

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

If the component is standalone, then by definition it doesn't have a module, so advice about a feature module doesn't apply.

Good point, I read feature module as just a folder in an organize-by-feature structure. I think feature modules could (and should) in most cases be replaced by standalone component, but that discussion might be outside the scope of this PR comment :D

Does this go to your earlier concern about accidentally instantiating duplicate services?

Yes, and other things. It depends a little bit on how we define a "feature". If feature => 1 exported top-level component then we are basically talking about standalone components.

In the case where 1 feature module exports multiple components then the module becomes a bit rigid. It's a big blob containing all of the dependencies that any of it's child components might need. It makes it less obvious which component needs which service/pipe/etc. and also affects tree-shaking negatively. The lifetime of the service also becomes much easier to reason about, since it's tied to an actual DOM component, instead of a module that is somewhat magically instantiated by Angular in the background. Service tied to components also have access to the activated route in a way that service in modules never will (though this use-case might be a bit of an edge case).

Copy link
Member Author

Choose a reason for hiding this comment

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

I added a mention of standalone components when discussing the web vault client, otherwise I suggest this could be done in a subsequent revision of this page - which would definitely be welcome. It might require some discussion/consensus on stricter rules around feature modules though. I agree in many cases they can be replaced with standalone, but it's not what we do today.

eliykat and others added 7 commits April 25, 2024 11:55
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
@eliykat eliykat requested a review from coroiu April 25, 2024 03:29
@eliykat
Copy link
Member Author

eliykat commented Apr 25, 2024

Thanks for all the feedback @coroiu , I've addressed or responded to everything.

Can I please have a review from @bitwarden/dept-architecture as codeowners as well, thanks.

Copy link
Contributor

@coroiu coroiu left a comment

Choose a reason for hiding this comment

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

Looking good! One final issue

docs/architecture/clients/dependency-injection.md Outdated Show resolved Hide resolved
docs/architecture/clients/dependency-injection.md Outdated Show resolved Hide resolved
Comment on lines +124 to +129
:::note

The `useAngularDecorators` option is not type safe - it is your responsibility to ensure that the
class actually uses the `@Injectable` decorator.

:::
Copy link
Contributor

Choose a reason for hiding this comment

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

Keeping this open for future reference! :)

eliykat and others added 4 commits April 26, 2024 08:22
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
eliykat and others added 2 commits May 6, 2024 13:57
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
@eliykat eliykat requested review from coroiu and MGibson1 May 6, 2024 04:03
Copy link
Member

@MGibson1 MGibson1 left a comment

Choose a reason for hiding this comment

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

Responded to comments. Only thing really still outstanding is the factory method statements.

@eliykat eliykat requested a review from MGibson1 May 7, 2024 22:27
@eliykat eliykat merged commit 04ebd9e into main May 8, 2024
5 checks passed
@eliykat eliykat deleted the dependency-injection branch May 8, 2024 22:11
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

4 participants