doc/development/fe_guide/design_patterns.md
This page covers suggested design patterns and also anti-patterns.
[!note] When adding a design pattern to this document, be sure to clearly state the problem it solves. When adding a design anti-pattern, clearly state the problem it prevents.
The following design patterns are suggested approaches for solving common problems. Use discretion when evaluating if a certain pattern makes sense in your situation. Just because it is a pattern, doesn't mean it is a good one for your problem.
Anti-patterns may seem like good approaches at first, but it has been shown that they bring more ills than benefits. These should generally be avoided.
Throughout the GitLab codebase, there may be historic uses of these anti-patterns. Use discretion when figuring out whether or not to refactor, when touching code that uses one of these legacy patterns.
[!note] For new features, anti-patterns are not necessarily prohibited, but it is strongly suggested to find another approach.
A shared global object is an instance of something that can be accessed from anywhere and therefore has no clear owner.
Here's an example of this pattern applied to a Vuex Store:
const createStore = () => new Vuex.Store({
actions,
state,
mutations
});
// Notice that we are forcing all references to this module to use the same single instance of the store.
// We are also creating the store at import-time and there is nothing which can automatically dispose of it.
//
// As an alternative, we should export the `createStore` and let the client manage the
// lifecycle and instance of the store.
export default createStore();
Shared Global Objects are convenient because they can be accessed from anywhere. However, the convenience does not always outweigh their heavy cost:
Here are some historic examples where this pattern was identified to be problematic:
Shared Global Object's solve the problem of making something globally accessible. This pattern could be appropriate:
Even in these scenarios, consider avoiding the Shared Global Object pattern because the side-effects can be notoriously difficult to reason with.
For more information, see Global Variables Are Bad on the C2 wiki.
The classic Singleton pattern is an approach to ensure that only one instance of a thing exists.
Here's an example of this pattern:
class MyThing {
constructor() {
// ...
}
// ...
}
MyThing.instance = null;
export const getThingInstance = () => {
if (MyThing.instance) {
return MyThing.instance;
}
const instance = new MyThing();
MyThing.instance = instance;
return instance;
};
It is a big assumption that only one instance of a thing should exist. More often than not, a Singleton is misused and causes very tight coupling amongst itself and the modules that reference it.
Here are some historic examples where this pattern was identified to be problematic:
Here are some ills that Singletons often produce:
RepoEditor is now forced to be a Singleton as well. Multiple instances of this component
would cause production issues because no one truly owns the instance of Editor.This is because of the limitations of languages like Java where everything has to be wrapped in a class. In JavaScript we have things like object and function literals where we can solve many problems with a module that exports utility functions.
Singletons solve the problem of enforcing there to be only 1 instance of a thing. It's possible that a Singleton could be appropriate in the following rare cases:
Even in these scenarios, consider avoiding the Singleton pattern.
When no state needs to be managed, we can export utility functions from a module without messing with any class instantiation.
// bad - Singleton
export class ThingUtils {
static create() {
if(this.instance) {
return this.instance;
}
this.instance = new ThingUtils();
return this.instance;
}
bar() { /* ... */ }
fuzzify(id) { /* ... */ }
}
// good - Utility functions
export const bar = () => { /* ... */ };
export const fuzzify = (id) => { /* ... */ };
Dependency Injection is an approach which breaks
coupling by declaring a module's dependencies to be injected from outside the module (for example, through constructor parameters, a bona-fide Dependency Injection framework, and even in Vue provide/inject).
// bad - Vue component coupled to Singleton
export default {
created() {
this.mediator = MyFooMediator.getInstance();
},
};
// good - Vue component declares dependency
export default {
inject: ['mediator']
};
// bad - We're not sure where the singleton is in it's lifecycle so we init it here.
export class Foo {
constructor() {
Bar.getInstance().init();
}
stuff() {
return Bar.getInstance().doStuff();
}
}
// good - Lets receive this dependency as a constructor argument.
// It's also not our responsibility to manage the lifecycle.
export class Foo {
constructor(bar) {
this.bar = bar;
}
stuff() {
return this.bar.doStuff();
}
}
In this example, the lifecycle and implementation details of mediator are all managed
outside the component (most likely the page entrypoint).