Skip to content

Commit 7ea6414

Browse files
committed
Adding dependency graph analysis
1 parent 17a652c commit 7ea6414

File tree

3 files changed

+251
-94
lines changed

3 files changed

+251
-94
lines changed

readme.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ All above configuration is required.
5252
# Features
5353
My Own IoC Container support most of common IoC Container features:
5454

55-
- [x] Single file, copy - paste able, less then 15kb/300 lines of code
55+
- [x] Single file, copy - paste able, less then 15kb/400 lines of code
5656
- [x] Lifestyle support: Singleton and Transient (Transient is default)
5757
- [x] Constructor injection
5858
- [x] Inject by type
@@ -61,8 +61,8 @@ My Own IoC Container support most of common IoC Container features:
6161
- [x] Inject instance with factory function
6262
- [x] Inject Auto factory
6363
- [x] Component creation hook
64-
- [ ] Collection injection / Multi injection
65-
- [ ] Circular dependency graph analysis
64+
- [x] Dependency graph analysis for non registered component
65+
- [x] Dependency graph analysis for circular dependency
6666

6767
Things that will not supported because it introduce more code base and complexity
6868
* Advanced dependency graph analysis such as captive dependency etc

src/ioc-container.ts

+100-88
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,29 @@ import "reflect-metadata";
44
/* --------------------------------- TYPES --------------------------------------- */
55
/* ------------------------------------------------------------------------------- */
66

7-
/**
8-
* Alias for constructor of type of T
9-
*/
107
type Class<T> = new (...args: any[]) => T
11-
12-
/**
13-
* Life time style of component.
14-
* Singleton: the same instance returned on each resolve.
15-
* Transient: different instance returned on each resolve (default registration)
16-
*/
178
type LifetimeScope = "Singleton" | "Transient"
18-
19-
/**
20-
* Internal use, interface which contains of index and name
21-
*/
22-
interface IndexNameType {
23-
index: number,
24-
name: string
25-
}
26-
27-
/**
28-
* Internal use, Abstraction of resolver type
29-
*/
30-
interface Resolver {
31-
/**
32-
* Resolve a registered component model
33-
* @param config ComponentModel that will be resolved
34-
*/
35-
resolve<T>(config: ComponentModel): T
36-
}
37-
38-
/**
39-
* Alias type for ResolverBase constructor
40-
*/
419
type ResolverConstructor = new (kernel: Kernel, cache: { [key: string]: any }) => Resolver
4210

43-
/**
44-
* Abstraction of container which only expose resolve<T>() method
45-
*/
46-
interface Kernel {
47-
/**
48-
* Resolve a registered component
49-
* @param type Type or Name of the component that will be resolved
50-
*/
51-
resolve<T>(type: Class<T> | string): T
52-
}
11+
interface IndexNameType { index: number, name: string }
12+
interface Resolver { resolve<T>(config: ComponentModel): T }
13+
interface Kernel { resolve<T>(type: Class<T> | string): T }
14+
interface AutoFactory<T> { get(): T }
15+
interface Analyzer { analyze(path: (string | Class<any>)[], model?: ComponentModel): string | undefined }
5316

54-
/**
55-
* ComponentModel modifier that will be exposed on fluent registration
56-
*/
5717
interface ComponentModelModifier<T> {
58-
/**
59-
* Set a component model as singleton life style, default lifestyle is transient
60-
*/
61-
singleton(): ComponentModelModifier<T>
18+
singleton(): ComponentModelModifier<T>,
6219
onCreated(callback: (instance: T, kernel: Kernel) => T): ComponentModelModifier<T>
6320
}
6421

65-
/**
66-
* Abstraction of ComponentModel
67-
*/
6822
interface ComponentModel {
6923
kind: string,
7024
name: string,
71-
scope: LifetimeScope
25+
scope: LifetimeScope,
26+
analyzed: boolean
7227
onCreatedCallback?: (instance: any, kernel: Kernel) => any
7328
}
7429

75-
/**
76-
* Factory that returned registered component
77-
*/
78-
interface AutoFactory<T> {
79-
get(): T
80-
}
81-
82-
8330
/* ------------------------------------------------------------------------------- */
8431
/* ----------------------------- CONSTANTS/CACHE --------------------------------- */
8532
/* ------------------------------------------------------------------------------- */
@@ -89,10 +36,6 @@ interface AutoFactory<T> {
8936
*/
9037
const NAME_DECORATOR_KEY = "my-own-ioc-container:named-type"
9138

92-
/**
93-
* Registry of Resolvers will be used by Container. This constant retrieve value
94-
* from @resolver decorator
95-
*/
9639
const RESOLVERS: { [kind: string]: ResolverConstructor } = {}
9740

9841

@@ -139,6 +82,10 @@ function traverseConstructorParameters(target: Class<any>): (string | Class<any>
13982
return traverseConstructorParameters(Object.getPrototypeOf(target))
14083
}
14184

85+
function getComponentName(component: string | Class<any>) {
86+
return typeof component == "string" ? component : component.prototype.constructor.name
87+
}
88+
14289
/* ------------------------------------------------------------------------------- */
14390
/* --------------------------------- DECORATORS ---------------------------------- */
14491
/* ------------------------------------------------------------------------------- */
@@ -161,10 +108,6 @@ namespace inject {
161108
}
162109
}
163110

164-
/**
165-
* Only for internal use. Register resolver thus automatically added to the Container
166-
* @param kind Kind of resolver, will automatically match with ComponentModel.kind
167-
*/
168111
function resolver(kind: string) {
169112
return (target: ResolverConstructor) => {
170113
RESOLVERS[kind] = target
@@ -178,13 +121,13 @@ function resolver(kind: string) {
178121
abstract class ComponentModelBase<T> implements ComponentModel, ComponentModelModifier<T> {
179122
abstract kind: string;
180123
abstract name: string;
124+
analyzed: boolean = false;
181125
scope: LifetimeScope = "Transient"
182126
onCreatedCallback?: (instance: any, kernel: Kernel) => any;
183127
singleton(): ComponentModelModifier<T> {
184128
this.scope = "Singleton"
185129
return this
186130
}
187-
188131
onCreated(callback: (instance: T, kernel: Kernel) => T): ComponentModelModifier<T> {
189132
this.onCreatedCallback = callback
190133
return this
@@ -209,23 +152,90 @@ abstract class ResolverBase implements Resolver {
209152
}
210153
}
211154

155+
class DependencyGraphAnalyzer {
156+
constructor(private models: ComponentModel[]) { }
157+
158+
private getModelByNameOrType(type: string | Class<any>): ComponentModel | undefined {
159+
const filter = (x: ComponentModel) =>
160+
typeof type == "function" && x instanceof TypeComponentModel ?
161+
x.type == type : x.name == type
162+
return this.models.filter(filter)[0]
163+
}
164+
165+
private traverseAnalyze(path: (string | Class<any>)[], model?: ComponentModel): string | undefined {
166+
/*
167+
Traverse component model recursively to get issue in dependency graph
168+
path: is traversal path for example:
169+
class A {}
170+
class B { constructor(a:A){} }
171+
class C {}
172+
class D { constructor(b:B, c:C){} }
173+
if we traverse through D then the path will be:
174+
1: [D, B, A]
175+
2: [D, C]
176+
177+
model: the current model
178+
*/
179+
if (model && model.analyzed) return
180+
const curName = getComponentName(path[path.length - 1])
181+
const curPath = path.map(x => getComponentName(x)).join(" -> ")
182+
if (!model) return `Trying to resolve ${curPath} but ${curName} is not registered in container`
183+
else {
184+
if (this.hasCircularDependency(path, model)) return `Circular dependency detected on: ${curPath}`
185+
if (model instanceof TypeComponentModel) {
186+
for (let dependency of model.dependencies) {
187+
path.push(dependency)
188+
const analysis = this.traverseAnalyze(path, this.getModelByNameOrType(dependency))
189+
if (analysis) return analysis
190+
}
191+
}
192+
}
193+
}
194+
195+
private hasCircularDependency(path: (string | Class<any>)[], model: ComponentModel){
196+
/*
197+
Graph has circular dependency if the model is inside the path exclude the last path.
198+
Example:
199+
path: [Computer, LGMonitor], model: {type: LGMonitor} -> Not Circular
200+
path: [Computer, Computer], model: {type: Computer} -> Circular
201+
path: [Computer, LGMonitor, Computer], model: {type: Computer} -> Circular
202+
path: [Computer], model: {type:Computer} -> Not Circular
203+
*/
204+
const matchName = (x: string | Class<any>) => (typeof x == "string" && x == model.name)
205+
const matchType = (x: string | Class<any>) => (typeof x == "function" && model instanceof TypeComponentModel && x == model.type)
206+
//exclude the last path
207+
const testPath = path.slice(0, -1)
208+
//check if the model is inside path
209+
return (testPath.some(matchName) || testPath.some(matchType))
210+
}
211+
212+
analyze(request: string | Class<any>) {
213+
const model = this.getModelByNameOrType(request)
214+
const analysis = this.traverseAnalyze([request], model)
215+
if (analysis) throw new Error(analysis)
216+
}
217+
}
218+
212219
class Container implements Kernel {
213220
private singletonCache: { [name: string]: any } = {}
214221
private models: ComponentModel[] = []
215222
private resolver: { [kind: string]: Resolver } = {}
223+
private analyzer: DependencyGraphAnalyzer
216224

217225
constructor() {
218226
//setup all registered RESOLVERS
219227
//Resolver should be marked with @resolver decorator
220228
Object.keys(RESOLVERS).forEach(x => {
221229
this.resolver[x] = new RESOLVERS[x](this, this.singletonCache)
222230
})
231+
this.analyzer = new DependencyGraphAnalyzer(this.models)
223232
}
224233

225-
private resolveModel<T>(model: ComponentModel): T {
226-
const resolver = this.resolver[model.kind]
227-
if (!resolver) throw new Error(`No resolver registered for component model kind of ${model.kind}`)
228-
return resolver.resolve(model)
234+
private getModelByNameOrType(type: string | Class<any>): ComponentModel | undefined {
235+
const filter = (x: ComponentModel) =>
236+
typeof type == "function" && x instanceof TypeComponentModel ?
237+
x.type == type : x.name == type
238+
return this.models.filter(filter)[0]
229239
}
230240

231241
/**
@@ -260,21 +270,19 @@ class Container implements Kernel {
260270
}
261271
}
262272

273+
private resolveModel<T>(model: ComponentModel): T {
274+
const resolver = this.resolver[model.kind]
275+
if (!resolver) throw new Error(`No resolver registered for component model kind of ${model.kind}`)
276+
return resolver.resolve(model)
277+
}
278+
263279
/**
264-
* Resolve a registered component
265-
* @param type Type or Name of the component that will be resolved
266-
*/
280+
* Resolve a registered component
281+
* @param type Type or Name of the component that will be resolved
282+
*/
267283
resolve<T>(type: Class<T> | string): T {
268-
if (typeof type == "string") {
269-
const model = this.models.filter(x => x.name == type)[0];
270-
if (!model) throw Error(`Trying to resolve ${type}, but its not registered in the container`)
271-
return this.resolveModel(model)
272-
}
273-
else {
274-
const model = this.models.filter(x => x.kind == "Type" && x instanceof TypeComponentModel && x.type == type)[0]
275-
if (!model) throw Error(`Trying to resolve type of ${type.prototype.constructor.name}, but its not registered in the container`)
276-
return this.resolveModel(model)
277-
}
284+
this.analyzer.analyze(type)
285+
return this.resolveModel(this.getModelByNameOrType(type)!)
278286
}
279287
}
280288

@@ -386,5 +394,9 @@ export {
386394
inject,
387395
Container,
388396
ComponentRegistrar,
389-
ComponentModel
397+
ComponentModel,
398+
DependencyGraphAnalyzer,
399+
TypeComponentModel,
400+
InstanceComponentModel,
401+
AutoFactoryComponentModel
390402
}

0 commit comments

Comments
 (0)