-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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 #425
Comments
I recently had some thoughts about "plugin" classes, I want to share, loosely connected to this topic. 1. Plugins are controllersFor every part of the editor (except core and engine) which need a controller, a plugin should be created. I was thinking if we are no overusing plugins, if we should create plugins for instance for UI components, like a contextual toolbar. Such component might be an integrated part of the editor, not very pluggable (you should not be able to disable it). But I think that even in such cases using plugins is not very costly and give us a nice architecture for splitting the controlling flow, handling dependencies. Each plugin/controller can create a nice API for other which can be reached by 2. There can be separate plugins for separate UI elementsWe did it for upload. There is a separate plugin for the engine part of the upload, separate for the button which opens system file input dialog and separates for the upload animation. Thanks to it every part of the UI can be easily replaced, but also it gave us very nice code splitting (see https://github.com/ckeditor/ckeditor5-upload/blob/be7cf3942b1ea236bef07062ff39ab726b2e4f0a/src/imageupload.js and imagine how it would look like if this plugin would handle the whole UI). 3. There is nothing wrong in the plugin which only aggregates other pluginsNote that, thanks to our architecture, we can create a plugin which does nothing but requires a list of other plugins. Thanks to it the "feature" can be a set of concrete behaviors (engine, all needed UI elements). |
And I need to add that injecting |
Related: https://github.com/ckeditor/ckeditor5-upload/issues/22#issuecomment-333090113
This is one more thing which we should avoid. Plugins should require other plugins as infrequently as possible. I think that requiring other plugin is good when that other plugin is a library (like Then, we may have "load them all" plugins which role will be to load all necessary pieces of a feature to make it easy for the developers to enable all the parts. |
An example application of these rules to the |
Another example how the As you can see it will be really simple to reuse the link UI's logic with a different view (see the class LinkUIController {
constructor( editor, options ) {
this.formView = options.formView;
...
}
}
class LinkUI extends Plugin {
init() {
this.controller = new LinkUIController( editor, {
formView: new LinkFormView()
} );
}
}
class Link {
static get requires() {
return [ LinkEditing, LinkUI ];
}
}
class MyLink extends Plugin {
static get requires() {
return [ LinkEditing ];
}
init() {
this.controller = new LinkUIController( editor, {
formView: new MyLinkFormView()
} );
}
} |
I finally found some time to read about DI ideas and I don't like the above concept, and there are reasons why: It will growThe main problem is, that when the dependencies chain groves you need to pass such reference down to all children, or, thinking contrariwise, pass the constructor up and up. It means that if you have, for instance, a UI component for comments sidebar, which is built from the comments thread components, and each has form components for comments, edit form, etc., then you need to pass all of them in the main constructor: class CommentSidebar extends Plugin {
static get requires() {
return [ CommentsEditing ];
}
init() {
this.controller = new CommentSidebarUIController( editor, {
commentView: new CommentView(),
commentEditView: new CommentEditView(),
commentRemoveViev: new CommentRemoveViev(),
commentThreadView: new CommentThreadView(),
addCommentToThreadView: new AddCommentToThreadView(),
confirmCommentRemovealView: new ConfirmCommentRemovealView(),
commentSidebarView: new CommentSidebarView(),
// ...
} );
}
}
class CommentSidebarUIController {
constructor( editor, options ) {
this.commentThreadController = new CommentThreadController( editor, {
commentView: options.commentView,
commentEditView: options.commentEditView,
commentRemoveViev: options.commentRemoveViev,
commentThreadView: options.commentThreadView,
addCommentToThreadView: options.addCommentToThreadView,
confirmCommentRemovealView: options.confirmCommentRemovealView,
// ...
} );
}
}
class CommentThreadController{
constructor( editor, options ) {
this.commentUIController = new CommentUIController( editor, {
commentView: options.commentView,
commentEditView: options.commentEditView,
commentRemoveViev: options.commentRemoveViev
} );
}
}
// etc. I think this is the reason, why all frameworks introduce an advanced mechanism for dependency injection. Note that, this is a pretty simple case where the list of child views is static. If it will be dynamic the code we get even more complicated. Interface have to be the sameAnother problem is that injected view have to have the same interface that the original class. It's not that simple for instance for the I prefer creating class connections in the logic level, not on the interface level. As original MVC proposed, it is a controller who should combine model and view. This way you will know how your views interface is used. One more mechanismWe already have 2 mechanisms in CKE5 to decrease the number of dependencies: plugins and events (including observables). Here you introduce another one. Note that it won't be independent. For instance, a toolbar will have injected its view this way, but buttons will be injected using plugins. And the buttons view will be injected using this mechanism but within injected plugins. In fact, as I think about it, named plugins available through the context ("editor.plugins") is the mechanism very similar to the injector in many DI solutions. One more classImplementing DI as you proposed you need to introduce one more class in each, even a simple plugin. There is no other reason not to have plugin being controller, as I mentioned before. You need to create the separate controller class and special Not much in exchangeQuoting another smart guy. "Don't go crazy. Not everything needs to be reusable and not every component must be replaceable.". When we will move more logic to the "editing" part of the plugin, it might be relatively simple to create your own UI part. Moreover, at the end of the day, if you really need to overwrite views, you can always use webpack. I think that DI comes in pair with the module system. So, even if we find it ugly, it might be the proper way to go, when you really need to overwrite a view. SummaryI would stick to the original plan: improve the split between editing and UI parts of a plugin, create proper utils, recommend users to use webpack to overwrite classes and... wait and see if there are any problems that cannot be solved this way. We may realize that all problems are solved or will find more specific issues, for instance, it might be enough to make templates overwritable. |
From the current perspective, it seems that the split to editing and UI parts + the glue plugin is satisfactory. I don't think that we need more work here on the platform level. |
When doing some research on DI I stumbled upon this short, Angular related article. Despite explaining the problems with Angular's DI, it contains a very good explanation of what DI is:
To be honest, it's like the first time when I saw someone taking the easy way when talking about DI. Usually, DI is presented in context of some specific tooling and this is how people approach it and on what they focus.
So, even though in another article we can read:
the whole article is about how to inject dependencies (techniques to do that). And of course it makes a lot of sense – how could we talk about DI if we don't talk about injecting dependencies? However, I feel that a really important aspect of DI is being diminished in all this tool-oriented talk – that the step 1 is always to create the right abstractions and leave code decoupled as long as possible.
How you inject your modules is a secondary aspect. It can be done using some nice programmatic tools which allow you to easily rewire code for tests but this is not critical. If you bind modules late you will be able to recompose most of the significant parts of the code without any tools. It will all boil down to passing different dependencies when initialising your components.
To the point
I started the topic because of https://github.com/ckeditor/ckeditor5-link/issues/108 – "How to override LinkFormView?".
My first reaction was – use Webpack! You want a different view, why bothering us to add more tools – use the existing tools. Configure Webpack to load a different module instead of the original
LinkFormView
and that's it. (I'm quite sure it is doable, but I'm too lazy now to check it. I only mention this as a possible solution if someone wants to try. Especially that this is a good solution taken the code size – this allows totally skipping the original module's code.)Then I started thinking about late binding – why we even talk about overriding anything? If you don't want
LinkFormView
then you should implement your ownLink
plugin which uses a different view. Recomposing features is just so nicer than overriding something.However, this approach is not really a viable option in the current state of things. The
Link
plugin can be neither given theLinkFormView
nor easily reimplemented.The first option is simple (even without special DI tools) – let's allow overriding
Link#createFormView()
(it'd need to be public). This is an OK solution, but it's a bit too limited. This is not the only dependency that anyone will want to replace so a more general solution should be found. Also, this doesn't cut off the dependency to the standard view so its code will be included in the bundle.I like the second option much more – reimplementing the
Link
plugin. Right now, that would be painful – theLink
not only initialises theLinkFormView
and other subcomponents, but it also contains a lot of code.Plugins are the perfect place to glue together a set of modules by initialising and injecting proper dependencies. A job of a plugin should be to, using a minimal code, build a feature out of available building blocks. The building blocks used by plugins should be left decoupled for as long as possible.
In other words, plugins should be focused on initialising and configuring other modules.
I wanted to show an example of how
Link
could be refactored, but, TBH, this is too much work for now because I'd also have to think how to improve the way buttons are created and how other components work together. However, if we take a look at the currentLink#init()
code we can see that it initialises 3 things (form and 2 buttons) and glue them together. It should do exactly that but the initialisation code should be minimal and the glue should be in a form of external modules (simple functions most likely).You may remember that it sounds very similar to what I've been proposing a few months ago – that certain behaviours should be enclosed in form of simple helpers. That makes them reusable. We have the same situation here.
To sum up
If you want to override part of some existing plugin this should be your way of thinking:
If you are implementing a plugin:
Plans for the future:
The text was updated successfully, but these errors were encountered: