-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Remove MenuRegistry in favour of dependency injection #4527
Conversation
…d local state Signed-off-by: Janne Savolainen <janne.savolainen@live.fi> Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
onQuitCleanup.push( | ||
initMenu(windowManager), | ||
initMenu(windowManager, menuItems), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be cleaner if function initMenu
was injectable
. @panuhorsmalahti would you like to do this with us? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess, what is the benefit of this being injectable here? Instead of just computing the entries?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file shouldn’t need to know what are the dependencies for initMenu
. Simplicity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant, why is menuItems
not just a computed without this injection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same argument:
menuItems
has dependencies that I shouldn’t need to know when I use it.
All this code should care is that the required interface is somehow fulfilled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jansav I'm afraid I'm a bit busy with other Lens 🔥 issues for the time being..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries @panuhorsmalahti :)
We can share you the result, if you want.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that sounds interesting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do believe the promised result will satisfy @Nokel81, as it will extract responsibilities related to instantiation away from this file, where it doesn't quite belong. @Nokel81 how about you, wanna have a go doing this with us? It may be trivial here, but having this freedom somewhere else will be a big deal.
Or the beer's on me ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
freedom?
|
||
return removeItems; | ||
}); | ||
this.autoInitExtensions(() => Promise.resolve([])); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice how we no longer need to dispose items from a state when extension is disabled or removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I have wanted to do this for some time, I even made #2245 about 10 months ago, but no one wanted to me to pursue it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Glad to see that you have had same kind of changes in mind. However, I don’t see a reason not to pursue simplicity that reactive programming gives. Current state is less than optimal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome :)
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
1c7d29a
to
41837e8
Compare
@@ -61,11 +61,15 @@ import { FilesystemProvisionerStore } from "./extension-filesystem"; | |||
import { SentryInit } from "../common/sentry"; | |||
import { ensureDir } from "fs-extra"; | |||
import { Router } from "./router"; | |||
import { initMenu } from "./menu"; | |||
import { initMenu } from "./menu/menu"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why change this import?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved file to separate directory. Prefer not to create index.ts
when really not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay I guess, I find index
files much cleaner to read especially since the rest of this repo we use them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have seen both used in this repo. We prefer it this way so that we don’t need so much boilerplate code.
Not that big deal though for us
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it seems that more common practice is to use "index" in this repo but that has a downside because it requires tree-shaking (which might not work in practice too well). Without tree-shaking we are actually "loading" all dependencies from index ... of course import shouldn't have side-effects in a perfect world (we are not there yet).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. The index-files do feel like an unnecessary indirection and complication. We could consider if the benefits of encapsulation really overweigh them. As said, not a bid deal, as there's far bigger fish out there :)
...but in general, when it comes to side-effects on import, I'm afraid it's not something our team should tolerate at all. Let's make a hard line for this, right guys? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, it also makes circular dependencies too easy
import { initTray } from "./tray"; | ||
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions"; | ||
import { AppPaths } from "../common/app-paths"; | ||
import { ShellSession } from "./shell-session/shell-session"; | ||
import { getDi } from "./getDi"; | ||
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this "electron" menu items?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since menuItems
is not enough to make it obvious. Other option was appMenuItems
but that has same effect. App can be anything. Using Electron in name is not best, but still lot better than without it. Maybe mainAppMenuItems
?
{ extensionLoader: ExtensionLoader } | ||
> = { | ||
getDependencies: () => ({ | ||
extensionLoader: ExtensionLoader.createInstance(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No you aren't supposed to create the instances in more than once place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this is the issue with current Singleton
. I shouldn’t need to know how and when instance is created.
If we change this to .getInstance()
we need to be sure that .createInstance()
has been called.
This is something that we need to fix by getting rid of Singleton
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I completely disagree, just because something is a singleton doesn't mean there aren't dependency's on other instances, and those instances might need arguments to be created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solution is to use DI to make instances. That’s how I don’t need to worry about creating instance and getting arguments somewhere.
In this case, it’s clear that ExtensionLoader
needs to be injected. Therefore injectable
should be created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @Nokel81 means that we shouldn't call ExtensionLoader.createInstance()
in multiple places (see main/index.ts for other call). That said, it's a bit unfortunate that caller needs to know anything about this -> maybe instance should always come via injectable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know and I agree.
To be honest, I’m not sure why it was left as .createInstance()
since .getInstance()
is more than enough.
However it works as a great example of an issue caused Singleton
and permits this kind discussion.
I’ll change it tomorrow anyway :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because not every place that consumes an instance needs/should know how to create it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This we know ;)
src/extensions/extension-loader.ts
Outdated
@@ -47,7 +46,7 @@ const logModule = "[EXTENSIONS-LOADER]"; | |||
*/ | |||
export class ExtensionLoader extends Singleton { | |||
protected extensions = observable.map<LensExtensionId, InstalledExtension>(); | |||
protected instances = observable.map<LensExtensionId, LensExtension>(); | |||
instances = observable.map<LensExtensionId, LensExtension>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be public. A getter for the list of instances could be useful if it doesn't already exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, this being exposed for external mutation is scary. Good catch.
@@ -0,0 +1,132 @@ | |||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please put test files under a __test__
folder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? There are tests already in codebase that are not in __tests__
directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd argue for having tests in a tests folder (being a bit vague to not bikeshed too much). If Lens codebase is not consistent wrt, we should strive to make it so. I think file navigation becomes less convenient if test files are located next to application code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You might be both right:
Sometimes unit tests specify real life behaviour instead of workings of certain technical entrypoint (i.e. a file). For this, __tests__
makes sense, as the entrypoint becomes less relevant. This does not happen in current codebase yet. Some call this integration testing or functional testing, but let's not get into semantics.
At other times unit tests specify workings of a technical detail. For this, placing the test file next to entrypoint of unit makes sense, as the presence of tests (or lack thereof) is a valuable piece of information not to be swept away in a directory. When doing this, inconvenience in navigation is oftentimes sign of another problem, namely files not being organised in directories properly. Tests should not make directories feel cluttered.
}), | ||
|
||
instantiate: ({ extensions }) => | ||
computed(() => extensions.get().flatMap(extension => extension.appMenus)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about disabled extensions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just noticed that there is something weird happening here. Disabled extensions work already, but it seems that is not enough. We need to be sure that .init()
has been called for extension as well.
I have required changes done already and pushing those here tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. In general, with DI we will have more guarantees that temporal requirements, such as .init()
this or .createInstance()
that, will have been called when expected. Good.
onQuitCleanup.push( | ||
initMenu(windowManager), | ||
initMenu(windowManager, menuItems), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess, what is the benefit of this being injectable here? Instead of just computing the entries?
@@ -18,15 +18,8 @@ | |||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |||
*/ | |||
|
|||
// Extensions API -> Global menu customizations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why remove a comment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It serves no purpose -> dead weight.
@@ -18,15 +18,8 @@ | |||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, why move this file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was located in a file which was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah well the git diff implied that it was moved here instead. Then why delete that file in registries instead of just deleting the registry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be silly to leave empty file there? (Or file that doesn’t do anything and doesn’t have anything to do with registries.)
Before it was the place that this interface was used first time, but since that doesn’t exist anymore, it makes sense to move it closer to useplace.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is also used in LensMainExtension
and I would argue that is its "first use" and this is merely its consumer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that interface MenuRegistration
exists for making sure that Menu
has items with proper interface. That being said, Menu
is the one that defines the requirements for the interface -> logical place for the interface is next to Menu
.
for (const { parentId, ...menuItem } of MenuRegistry.getInstance().getItems()) { | ||
if (!appMenu.has(parentId)) { | ||
logger.error(`[MENU]: cannot register menu item for parentId=${parentId}, parent item doesn't exist`, { menuItem }); | ||
for (const menuItem of electronMenuItems) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it is cleaner to not pass menuItem.parentId
to the list of submenus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I suppose I considered the complication in typing not worth this. Do you agree? I see your point of encapsulation, but this case is trivial, and therefore simplicity is more pragmatic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is the new code more simple?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see this issue as good time investment. I can change it back if you feel strongly about it :)
…as enabled extensions Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
…h no changes required on use places Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
…ld be created Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
…lass with no changes required on use places" This reverts commit 34a47f5 Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
…y-obsolete Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Conflicts have been resolved. A maintainer will review the pull request shortly. |
Alright! With the latest changes, this will be one of those bigger PR's we hinted the team about. The big push was getting Here's our sincerest hopes to fast-track this PR, as it will directly support the next cool stuff to come. Onwards! |
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
2c13b5d
to
8337dfd
Compare
@Nokel81, I fixed failing tests and linting errors. Needs to be approved again. |
What we did
MenuRegistry
and replaced it with an injectableElectronMenuItems
. The injectable instantiates a MobX-computed observed from all applicable extensions. With this, we propose dependency injection over shared global state, and reactivity over statefulness.Singleton
-base-class got removed. With this, we also limit shared global state.ElectronMenuItems
.Main
process similar toRenderer
.Rationale
We recognized implementations of base-classes
Singleton
andBaseRegistry
as main causes of problems like code rigidity, non-testability, tendency to bug etc. These classes describe shared global state, logical dependencies, and statefulness over reactivity. Now that we have infrastructure in place to inject dependencies (i.e.injectable
), we can replace these anti-patterns with dependency injection. It is also handy that outsourcing lifecycles (e.g. singleton) to DI comes naturally, without the dependee even knowing about it.What’s next:
Closes #4475.