feat: add greeter extension to illustrate extension point/extension pattern#2249
feat: add greeter extension to illustrate extension point/extension pattern#2249raymondfeng merged 2 commits intomasterfrom
Conversation
|
@raymondfeng Good to have an example repository for extension. What's the difference between this example repo and the log extension in https://github.com/strongloop/loopback-next/tree/master/examples/log-extension? |
fc53201 to
53ae291
Compare
167668d to
a0f6b74
Compare
+1, I'd like to better understand the differences too. |
|
Also IIUC, this pull request contains commits from other pull requests, for example #2122. Am I assuming correctly that you will rebase those commit away once the other PRs are landed? |
7c5e45a to
a4c9ffb
Compare
Yes. The PR is against |
The log-extension focuses on demonstrating the common LB4 constructs for extensions, such as decorators, mixins, providers, and components. The greeter-extension is dedicated to the extension point/extension pattern. It illustrates how to solve the common extensibility issue by implementing the pattern on top of LB4 constructs. See README for more details. |
765f316 to
b25d990
Compare
a4c9ffb to
25ad6d9
Compare
b25d990 to
b9518c1
Compare
44742f9 to
b75bc99
Compare
01866ed to
878e523
Compare
878e523 to
d738173
Compare
accacaf to
112dea3
Compare
3ea2f02 to
180ccb9
Compare
112dea3 to
8c8585e
Compare
e5d289c to
f81045f
Compare
1a47923 to
5047ec1
Compare
e5340fd to
bf5cf26
Compare
bajtos
left a comment
There was a problem hiding this comment.
There are too many unrelated changes included in the pull request, it makes the patch difficult to review. I'll wait with further review until related Context improvements are landed and this PR is rebased on top of them.
examples/greeter-extension/README.md
Outdated
| /** | ||
| * An extension point for greeters that can greet in different languages | ||
| */ | ||
| export class GreeterExtensionPoint { |
There was a problem hiding this comment.
I find this class name problematic. This class is not the extension point, it's a service providing multilingual greet function and tapping into the greeter extension point. The actual extension point is just a convention on how greeter implementations should be bound on the context and how the greeter service is looking them up.
If we want to better show the implementation details of an extension point, then I am proposing a slightly different design:
Greeterinterface remains without changes, this part is already designed wells.GreeterExtensionPointthat accepts the requested language and returnsGreeterinstance matching the language.GreeterServicethat providesgreetmethod and usesGreeterExtensionPointto obtain the greeter instance to use.
export class GreeterExtensionPoint {
constructor(
@inject.getter(bindingTagFilter({extensionPoint: 'greeter'}))
private greeters: Getter<Greeter[]>,
) {}
findByLanguage(language: string): Greeter | undefined {
const greeters = await this.greeters();
return greeters.find(g => g.language === language);
}
}
class GreeterService {
constructor(
@inject('extensions.greeter')
private greeterRegistry: GreeterExtensionPoint
// ...
) {}
async greet(language: string, name: string): Promise<string> {
const greeter = await greeterRegistry.findByLanguage(language);
const greeting = greeter ? greeter.greet(name) : `Hello, ${name}`;
// etc.
}
}There was a problem hiding this comment.
I feel it would be even better if we could find a way how to refactor GreeterExtensionPoint into something more generic that can be configured via @inject decorator. Below is the first version that comes to my mind. It's not as pretty as I would like it to be, we may need to look for a different approach.
type GreeterLocator = (lang: string) => Greeter;
class GreeterService {
constructor(
@inject.extensionLocator(
'extensions.greeter',
(lang: string) => (ext: Greeter) => ext.language === lang)
private findGreeterByLanguage: GreeterLocator) {}
async greet(language: string, name: string): Promise<string> {
const greeter = await findGreeterByLanguage(language);
// etc.
}
}
// resolver for extensionLocator
// the next two values will be obtained from injection metadata
const extensionPointName = // 'extensions.greeter';
const filterFactory = // (lang: string) => (ext: Greeter) => ext.language === lang);
const extensions = await new ContextView(ctx, filterByExtension(extensionPointName)).values();
return (...args) => {
const filter = filterFactory(...args);
return extensions.find(filter);
};Maybe the complexity is not worth the benefits? IDK.
Anyways, this is probably out of scope of this pull request.
There was a problem hiding this comment.
I was experimenting generalization of extension points into a base class at https://github.com/strongloop/loopback-next/tree/extension-point. My current assessment is that the benefit is not significant enough.
| key: 'greeter-extension-point', | ||
| }), | ||
| createBindingFromClass(EnglishGreeter, {namespace: 'greeters'}), | ||
| createBindingFromClass(ChineseGreeter, {namespace: 'greeters'}), |
There was a problem hiding this comment.
Can we use asGreeter template please?
There was a problem hiding this comment.
I mean why the built-in greeters are created with {namespace: 'greeters'} but without applying the changes made by asGreeter and why asGreeter does not set any namespace?
| * | ||
| * @param extensionPoint Name/id of the extension point | ||
| */ | ||
| export function extensions(extensionPoint: string) { |
There was a problem hiding this comment.
Perhaps injectExtensions to be promoted to @inject.extensions may be a better name?
There was a problem hiding this comment.
Yes. Once we are settled on the final approach.
b5ae9a8 to
2dac020
Compare
Decoupling |
14fccf0 to
3ff92dd
Compare
2dac020 to
b71eedb
Compare
3ff92dd to
ab70385
Compare
b71eedb to
cc5ace0
Compare
I am ok to allow users to combine the responsibility of two into one class, if they wish so. However, I am arguing that such design
|
|
@bajtos PTAL |
bajtos
left a comment
There was a problem hiding this comment.
The patch is starting to look very reasonable 👍
Please get at least one more (preferably 2-3) people from @strongloop/loopback-maintainers to review this proposal too.
examples/greeter-extension/src/__tests__/greeter-extension.acceptance.ts
Outdated
Show resolved
Hide resolved
nabdelgadir
left a comment
There was a problem hiding this comment.
I think as far as I understand, this looks good to me, besides some minor comments. Is this example also going to be added to the docs/cli?
examples/greeter-extension/src/__tests__/greeter-extension.acceptance.ts
Show resolved
Hide resolved
jannyHou
left a comment
There was a problem hiding this comment.
The signature of registering an extension point and extensions LGTM 👏
I left a few questions.
emonddr
left a comment
There was a problem hiding this comment.
Great stuff!
The only thing to fix would be the test case 'supports options for the extension point' where there is usage of chalk to test an extension point option is in place. I have a comment which explains the problem.
jannyHou
left a comment
There was a problem hiding this comment.
Thanks for clarifying all the questions! LGTM.
The example illustrates how to implement extension point/extenion pattern in LoopBack 4 to provide great extensibility.
The PR adds an example project to demonstrate how to implement extensibility using the extension point/extension pattern on top of LoopBack 4's IoC and DI foundation.
See https://github.com/strongloop/loopback-next/blob/greeter-extension/examples/greeter-extension/README.md.
Checklist
npm testpasses on your machinepackages/cliwere updatedexamples/*were updated