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

[RFC] Service Locators #185

Open
Blacksmoke16 opened this issue Jun 3, 2022 · 1 comment
Open

[RFC] Service Locators #185

Blacksmoke16 opened this issue Jun 3, 2022 · 1 comment

Comments

@Blacksmoke16
Copy link
Member

Blacksmoke16 commented Jun 3, 2022

Service Locators

Athena's current IoC model currently only supports DI. Another way to go about it is the service locator pattern, which allows dynamically fetching services based on some key. A good use case for when this could be useful is if you have a service that needs access to several other services when not all of them will be used. This pattern can allow for the lazily instantiation of the only the service(s) you need, which is not possible with DI since they would need to be instantiated and injected as an array/hash or something.

This issue is intended to act as both an RFC, and a way for me to document my thoughts/ideas on how to go about implementing this feature.

Requirements

The services MUST be provided in such as way where they are lazily initialized. The locator type should expose methods to check if it has a service given a key, and the ability to return the related service based on that key. The key can either be the metaclass of the type of a service, or an arbitrary string. Ideally the locator could provide disparate types in a way that does not return a union of all the possible types. It would also be nice to support the @service_name syntax it should be able to provide multiple services based on the same type based on diff keys. Similary, it should support resolving interfaces, but can forgo bindings.

The locator type itself should be auto generated based on either a static set of services it should provide. This set of services should also be able to be "dynamically" generated via some means. Ideally some sort of macro that can yield/expose information from each service with a specific tag; allowing the user to build out the mapping based on the type of each service, or some value exposed on it (class var, annotation value, tag value, etc).

Challenges

Ultimately this feature has some challenges that will need to be figured out in order to implement it. The main issue is since the container is entirely built at compile time. This makes it so the service map(s) need to be known at compile time as well; which makes the "dynamic" mapping harder as well.

The other main challenge in order for things to be made lazy, the container itself needs to be used, which means that the locator types need to be generated/managed internally via the container. It could be exposed as a service itself, but that's probably not necessary.

Implementation

I think the easiest way to go about this is have some macro that generates some type that accepts the container as an ivar. Overloads on the metaclass of each service could be used to handle returning the proper service in a type safe way. String based keys are going to be tricker, probably will need to leverage some sort of overload like def get(key : String, type : T.class) forall T. This of course wouldn't really work if all the services are of different types. Tho will have to see how it works in practice.

Static registration would be easy enough as the macro could be called with a named tuple/hash literal. Might also be able to introduce a new annotation to allow annotating a type directly, using the macro internally, but macro would need to be used if you need the same locator in multiple services.

The "dynamic" registration side of things is where I'm not 100% sure how to handle yet. For a few reasons:

  1. Custom/user provided data in tags isn't really supported as there isn't really a way to access them
  2. Don't really have a good way to access all types with a given tag outside of when the container is being built
  3. Don't have a way to tap into the creation of the container

Concessions

Ultimately crystal-lang/crystal#8835 would solve the vast majority of these challenges. As such, I think the best path forward would be to forgo some of the ideal features in favor of getting something working to support the framework's needs, and implement everything else when there is more flexibility. The primary use case for this feature is the integration of the console component into the framework. I.e. such that only the console command being invoked needs to be instantiated.

Proposed Plan

After typing this all out, I'm thinking the best solution, for now, would be to support the static mapping context, and just generate the mapping for the console component integration manually via iterating over children of the base type. This should probably be tied with maybe introducing an ACONA::AsCommand annotation such that all services will support lazily initialization.

@Blacksmoke16
Copy link
Member Author

I did figure out another way to implement this (ab)using a combination of a macro included hook with a finished hook, along with mutable constants to create a sort of "pipeline" of execution that can also invoke user defined code via iterating over and including modules that can interact with the consts.

https://play.crystal-lang.org/#/r/d9nl

Tho I'm still not sure if I actually want to use it as it feels quite hacky :/

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

No branches or pull requests

1 participant