-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Discuss] New Platform Service vs Plugin Architecture #47065
Comments
Pinging @elastic/kibana-app-arch (Team:AppArch) |
Pinging @elastic/kibana-platform (Team:Platform) |
How would plugins and services differ aside from their position in the hierarchy? |
one of the suggestions at the time was the possibility for two level folder structure, where we could have actual plugins nested inside a subfolder inside
i think it shouldn't be too much work to make this possible. this way we could have multiple small plugins without polluting the plugins folder. |
If we went with the future proposals which include dependency injection, they wouldn't. The only difference is that a plugin is a top level domain. And as such I suppose that plugin can be disabled/enabled but not individual services (at least not using the same mechanism). If we go with just the "now" proposals, plugins would need to initialize sub services/domains manually. E.g.
@ppisljar - I like that proposal as well. If they are real plugins then that solves the circular references and you get the automatic dependency injection. I think then the discussion is still though, should these be called nested services or nested plugins? Though one downside (?) is that access paths then don't match folder structure. e.g.
|
I'd describe this topic as IMO, unless there is a significant difference (yes, thats open to interpretation) between plugins and services I'd just assume refer to them as plugins and nested (or sub? something else?) plugins as it communicates similarity and hierarchy in a way that |
There are concrete differences between plugins and services today: Services, by convention, are classes with lifecycle functions that instantiate the state of their particular domain and expose APIs for others to use. Plugins are effectively services that also:
In essence, a "plugin" is a service that you've wrapped with a pattern that allows it to integrate with the core plugin service. I also want to help frame this discussion: we're proposing changes to the model designed in the original rearchitecture and now new platform efforts. That's OK because we learn news things and we certainly have no illusions that Kibana's new architecture is perfect, but this should mean we're approaching this in terms of what is problematic with the conventions, terminology, or general architecture that we have today and what are the things we should do to fix that. |
This is a good point to consider. If we group thinks solely based on logical, domain, hierarchies, the disable/enable flag can end up covering a lot of smaller discrete services. I think I actually just came up with a real circular dependency issue with grouping by domain. Consider the following top level domains:
we have another x-pack plugin called If it was, we now have a circular dependency because ...actually that still isn't a real issue because |
One more benefit of the modular approach is an obvious place to put a single README.md file that can cover both server and public details. |
Another similar question. Should routes mimick the path? e.g. should it be |
This sounds like a good idea. And I think we can take it further by emphasizing that the type LifecycleMethod = (...args: any[]) => any;
interface Service<TSetup extends LifecycleMethod, TStart extends LifecycleMethod> {
setup(...args: Parameters<TSetup>): ReturnType<TSetup>;
start(...args: Parameters<TStart>): ReturnType<TStart>;
stop(): void;
}
type Plugin<
TSetup = void,
TStart = void,
TPluginsSetup extends object = object,
TPluginsStart extends object = object
> = Service<
(core: CoreSetup, plugins: TPluginsSetup) => TSetup,
(core: CoreStart, plugins: TPluginsStart) => TStart
>;
It seems to me we would get the same benefits of "portability" if we kept all // my_plugin/server/plugin.ts
import { DataService } from './services/data';
interface MyPluginSetup = { data: ReturnType<DataService['setup']> };
interface MyPluginStart = { data: ReturnType<DataService['start']> };
export class MyPlugin implements Plugin<MyPluginSetup, MyPluginStart> {
private readonly dataService = new DataService();
public setup(core: CoreSetup) {
return { data: this.dataService.setup(core) };
}
public start(core: CoreStart) {
return { data: this.dataService.start(core) };
}
public stop() {
this.dataService.stop(core);
}
} With this pattern, you'd never really need to "move" For the hierarchy considerations, I think the main consideration we should be making here is whether or not a domain or sub-domain should be togglable (enable/disable from kibana.yml) on its own. If yes, then that service/domain should be its own plugin. If not, then it should belong to a broader plugin. If a top-level plugin also wishes to allow it's sub-domains/sub-services to be disable-able, it still can. It just needs to implement this functionality itself. I don't think this is a concern that Core could solve in any meaningful way beyond what plugins can already do today by reading config. Regarding circular dependencies, I think when these arise it's more indicative of a code smell than an architectural problem with the Platform. Typically, this indicates tight-coupling or a need for reorganization. I would like to see a good example that couldn't be solved by either:
The more circular dependencies we have, the harder refactoring and iterating on new solutions becomes. I think introducing any magic dependency injector beyond what we already have would be a mistake that we would pay for for years to come. |
👍 that makes sense to me @joshdover. |
👍
I see value in creating a formalized interface for services where a service encompasses a particular domain and agree with @joshdover's comment about this providing portability so that services can be moved between plugins. However, I'm cautious about extending this to "sub-services" or "leaf-services" unless the sub-service has an obvious lifecycle that benefits from having lifecycle methods defined. There are a couple of places in Core where it feels like we've forced this pattern a bit too much. One could argue this improves portability, but I think when it comes to tiny services this reason alone would be over-engineering. If we ever need to move a sub-service into a service it would be trivial to refactor ("You A'int Gonna Need It"). Adhering to the lifecycle methods adds additional type boilerplate and makes it hard not to separate documentation from the implementation. It also constrains API's to a lifecycle, while a service like the ChromeService might choose not to expose Some concrete examples: Doc links servicekibana/src/core/public/doc_links/doc_links_service.ts Lines 28 to 37 in 8571d56
Do we loose anything if we use a function? export function getDocLinks(injectedMetadata: InjectedMetadataStart) {
const DOC_LINK_VERSION = injectedMetadata.getKibanaBranch();
const ELASTIC_WEBSITE_URL = 'https://www.elastic.co/';
const ELASTICSEARCH_DOCS = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`;
return deepFreeze({
DOC_LINK_VERSION,
ELASTIC_WEBSITE_URL,
links: { Doc title servicekibana/src/core/public/chrome/doc_title/doc_title_service.ts Lines 43 to 87 in ccd01a3
As an ES6 class: export class DocTitleService {
private document = { title: '' };
private baseTitle = '';
public constructor(document: Document){
this.document = document;
this.baseTitle = document.title;
}
/**
* Changes the current document title.
*
* @example
* How to change the title of the document
* ```ts
* chrome.docTitle.change('My application title')
* chrome.docTitle.change(['My application', 'My section'])
* ```
*
* @param newTitle The new title to set, either a string or string array
*/
public change(title: string | string[]) {
this.applyTitle(title);
}
/**
* Resets the document title to it's initial value.
* (meaning the one present in the title meta at application load.)
*/
public reset() {
this.applyTitle(defaultTitle);
} In short, I like the idea of standardising the service lifecycle pattern, but we should only apply it where it adds value. The deeper down the plugin/service tree we go the less value I see it adding and the more it gets in the way. |
I know I'm a month late to catch up on this, but I wanted to voice support for the idea of a formalized service interface as well, which could serve as a base to the plugin interface as Josh describes above.
100% agree with this. If we hit a circular dependency, we should have a bias toward finding out whether it truly needs to be there in the first place, rather than having the solution be creating another domain/plugin. If down the road we end up with a giant flat As an aside: My sense is that |
We talked more about circular dependencies in meeting today. There are more use case of dependencies, though in most cases a registry should solve the problem. For example, we talked about embeddables and cross linking between different solutions:
You can solve this via registries because you don't have to actually add the other plugin as a dependency, you can do something like:
It was mentioned will could cause us to lose some type safety (though I'm not sure I follow how). This also forces us to always use registries these ways, which maybe is too restrictive? You could solve this by importing static code, but this is not always possible. You can also solve this by breaking your plugin into multiple plugins (the current workaround), but this creates the flat domain structure. In addition to the cross solution embedding, is the cross solution linking, which is a common scenario. |
Noting some things that have changed since my last comment.
Good: const fooBarService = fooBarPlugin.getFooBarService(); Discouraged: const fooBarService = fooPlugin.getService(BAR); Why? The former makes it obvious you need to declare a dependency on plugin
|
@stacey-gammon do you think we should keep this issue open, or are we good closing? |
Good to close! |
Summary
I'd like to discuss some challenges with our current paradigms for plugin and service development and propose some ideas.
Terminology
search services domain
could be within thedata services domain
.A plugin is a domain, but a domain is not always a plugin, depending on where the folder is located.
Current Goals
**1. [Requirement] Avoid circular dependencies. **
This isn't a goal, it's a requirement, but I have it listed here because there may be times we can't support 2. because of 1.
2. An organized, hierarchical folder structure for services to ease discoverability
We want to avoid 1,000s of folders at any given level.
Proposed goals
3. Ease of changing this hierarchy at a later time.
We've already started changing our minds for some structures. It's inevitable that as more services and domains come about, better organizational structures will appear and some re-arrangement will be necessary.
4. Consistency between domains at any level (only difference between a top and nested domain is access path)
You hit this goal, you get 3 for free. If you want to move a sub-domain to be a top level domain, or a top-level domain to be a sub domain, you should only have to move folders and fix references. There isn't any reason a top level domain should be treated specially or written in a different way.
5. Leaf services should be highly focused, small services
This makes them easy to read, test and reason about.
Proposals for now
1. Formalize and encourage the Service interface
Supports goal 3 and 4.
Just like we have the Plugin service. Even though the platform team has followed this pattern consistently with core, other teams are exposing their sub services differently.
2. Replace Plugin terminology with Service terminology
Supports goal 3 and 4.
Current:
Proposed:
Proposals for the future
3. Allow for modularity within a sub service that has both client and server side code
Supports goal 3 and 4.
Current:
Proposed:
4. Use dependency injection for services
I'm not sure of the technical feasibility of this one, or if it's even desired, but I suspect we will want this to avoid circular dependencies. We can punt on this until/if it actually becomes a problem, but wanted to mention it in case others start to see circular references as a pain point.
Discussion
I bring this up now because I started working on the
search
service inside thedata
plugin and realized it has no dependencies on anything else that is supposed to be inside thedata
plugin. It felt like a very small and focused "plugin" and conforming to the plugin architecture made it consistent, familiar, and easy to read. Along the same line, I madeesSearch
a plugin as well. It depended on thesearch
plugin, but by separating it out, it made it familiar and easy to read as well. TheesSearch
plugin.ts
file was small and focused - just a few lines to register the ES search strategy. Thesearch
plugin.ts
file was also small and focused - just a few lines to expose the search strategy registration points.I decided to make them top level plugins because by conforming to this structure it made them focused, structured, and consistent.
But the problem with this approach is that it contradicts 2. - going this way you'll end up with 1,000s of plugins at the top level, making code hard to navigate and find.
I want to find a way where we can continue to make small, focused, isolated "services", no matter where in the hierarchy of domains they fall. Choosing a parent domain then should have no affect on the code you are writing inside that service. Write your code, decide where it belongs, change it later if a better organizational path ends up making more sense. I'm pretty sure that as we add more services, we'll come up with better organizational structures. It's easy now to have everything within a single plugin built completely differently from another plugin.
In addition, I suspect that we will start to encounter circular dependencies within plugins which will force us to change our hierarchies. Consider this:
We may end up having highly specialized services (
TopNavQueryBar
) mixed in with super generic services (Search
) which could end up causing circular dependencies. In the above example, hypothetically speaking we decide to add a "create alert" button inside the query bar and now it has a dependency onAlerting
which has a dependency onsearch
(this isn't a great real life example because it'd actually be pluggable and alerting would register the button in it's own plugin).Some of these are hypothetical problems that may never actually arise. I could be totally wrong. So my proposal is: do some of these now, consider some of these for later if it does start to be a problem. I bring it up now so developers who might encounter the hypothetical issues can bring it up and come back here. And also to get others thoughts on this! Maybe there are developers out there who can come up with a concrete example for circular dependencies.
Do now:
plugin.ts
call itdata_services.ts
Maybe do later:
Thoughts?
cc @epixa @peterschretlen
The text was updated successfully, but these errors were encountered: