Skip to content

Commit

Permalink
refactor: boyscout rules
Browse files Browse the repository at this point in the history
  • Loading branch information
Evyweb committed Sep 8, 2024
1 parent 0219a7f commit 1f61413
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 48 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ Create a symbol for each dependency you want to register. It will be used to ide
```typescript
export const DI = {
HELLO_WORLD: Symbol('HELLO_WORLD'),
DEP1: Symbol('dep1'),
DEP2: Symbol('dep2'),
MY_SERVICE: Symbol('MyService'),
MY_SERVICE_WITH_DEPENDENCIES: Symbol('MyServiceWithDependencies'),
MY_USE_CASE: Symbol('MyUseCase'),
DEP1: Symbol('DEP1'),
DEP2: Symbol('DEP2'),
MY_SERVICE: Symbol('MY_SERVICE'),
MY_USE_CASE: Symbol('MY_USE_CASE'),
LOGGER: Symbol('LOGGER'),
MY_SERVICE_CLASS: Symbol('MyServiceClass'),
NOT_REGISTERED_VALUE: Symbol('NOT_REGISTERED_VALUE')
FUNCTION_WITH_DEPENDENCIES: Symbol('FUNCTION_WITH_DEPENDENCIES'),
FUNCTION_WITHOUT_DEPENDENCIES: Symbol('FUNCTION_WITHOUT_DEPENDENCIES'),
CLASS_WITH_DEPENDENCIES: Symbol('CLASS_WITH_DEPENDENCIES'),
CLASS_WITHOUT_DEPENDENCIES: Symbol('CLASS_WITHOUT_DEPENDENCIES'),
NOT_REGISTERED_VALUE: Symbol('NOT_REGISTERED_VALUE'),
};
```

Expand All @@ -43,10 +45,10 @@ container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);

// You can register functions without dependencies (e.g. a const sayHelloWorld = () => 'Hello World')
container.bind(DI.HELLO_WORLD).toFunction(sayHelloWorld);
container.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld);

// You can register functions with dependencies (any higher order function)
container.bind(DI.MY_SERVICE_WITH_DEPENDENCIES).toHigherOrderFunction(MyServiceWithDependencies, [DI.DEP1, DI.DEP2]);
container.bind(DI.HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES).toHigherOrderFunction(MyServiceWithDependencies, [DI.DEP1, DI.DEP2]);

// For more complexe cases, you can register a factory so dep1 and dep2 will be injected
container.bind(DI.MY_SERVICE).toFactory(() => {
Expand All @@ -64,7 +66,7 @@ container.bind(DI.MY_USE_CASE).toFactory(() => {
});

// You can register classes, the dependencies of the class will be resolved and injected in the constructor
container.bind(DI.MY_SERVICE_CLASS).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);
container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);

```

Expand Down
19 changes: 10 additions & 9 deletions specs/DI.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
export const DI = {
HELLO_WORLD: Symbol('HELLO_WORLD'),
DEP1: Symbol('dep1'),
DEP2: Symbol('dep2'),
MY_SERVICE: Symbol('MyService'),
MY_SERVICE_WITH_DEPENDENCIES: Symbol('MyServiceWithDependencies'),
MY_USE_CASE: Symbol('MyUseCase'),
DEP1: Symbol('DEP1'),
DEP2: Symbol('DEP2'),
LOGGER: Symbol('LOGGER'),
MY_SERVICE_CLASS: Symbol('MyServiceClass'),
MY_SERVICE: Symbol('MY_SERVICE'),
MY_USE_CASE: Symbol('MY_USE_CASE'),
SIMPLE_FUNCTION: Symbol('SIMPLE_FUNCTION'),
NOT_REGISTERED_VALUE: Symbol('NOT_REGISTERED_VALUE'),
SERVICE_WITHOUT_DEPENDENCY: Symbol('OtherService')
}
CLASS_WITH_DEPENDENCIES: Symbol('CLASS_WITH_DEPENDENCIES'),
CLASS_WITHOUT_DEPENDENCIES: Symbol('CLASS_WITHOUT_DEPENDENCIES'),
HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES: Symbol('HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES'),
HIGHER_ORDER_FUNCTION_WITHOUT_DEPENDENCIES: Symbol('HIGHER_ORDER_FUNCTION_WITHOUT_DEPENDENCIES')
} as const;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {MyServiceInterface} from "./MyServiceInterface";

export const MyServiceWithDependencies = (dep1: string, dep2: number): MyServiceInterface => ({
export const FunctionWithDependencies = (dep1: string, dep2: number): MyServiceInterface => ({
runTask() {
return `Executing with dep1: ${dep1} and dep2: ${dep2}`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ServiceWithoutDependencyInterface} from "./ServiceWithoutDependencyInterface";

export const ServiceWithoutDependency = (): ServiceWithoutDependencyInterface => ({
export const HigherOrderFunctionWithoutDependencies = (): ServiceWithoutDependencyInterface => ({
run() {
return 'OtherService';
}
Expand Down
7 changes: 7 additions & 0 deletions specs/MyServiceClassWithoutDependencies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {MyServiceClassInterface} from "./MyServiceClassInterface";

export class MyServiceClassWithoutDependencies implements MyServiceClassInterface {
runTask(): string {
return `Executing without dependencies`;
}
}
66 changes: 42 additions & 24 deletions specs/container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import {DI} from "./DI";
import {Container, createContainer} from "../src";
import {MyServiceClass} from "./MyServiceClass";
import {MyServiceClassInterface} from "./MyServiceClassInterface";
import {MyServiceWithDependencies} from "./MyServiceWithDependencies";
import {ServiceWithoutDependency} from "./ServiceWithoutDependency";
import {FunctionWithDependencies} from "./FunctionWithDependencies";
import {HigherOrderFunctionWithoutDependencies} from "./HigherOrderFunctionWithoutDependencies";
import {ServiceWithoutDependencyInterface} from "./ServiceWithoutDependencyInterface";
import {MyServiceClassWithoutDependencies} from "./MyServiceClassWithoutDependencies";

describe('Container', () => {

Expand All @@ -24,10 +25,10 @@ describe('Container', () => {
describe('When a function is registered using a symbol', () => {
it('should return the associated function', () => {
// Arrange
container.bind(DI.HELLO_WORLD).toFunction(sayHelloWorld);
container.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld);

// Act
const sayHello = container.get<SayHelloType>(DI.HELLO_WORLD);
const sayHello = container.get<SayHelloType>(DI.SIMPLE_FUNCTION);

// Assert
expect(sayHello()).toBe('hello world');
Expand All @@ -39,11 +40,11 @@ describe('Container', () => {
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);

container.bind(DI.MY_SERVICE_WITH_DEPENDENCIES)
.toHigherOrderFunction(MyServiceWithDependencies, [DI.DEP1, DI.DEP2]);
container.bind(DI.HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES)
.toHigherOrderFunction(FunctionWithDependencies, [DI.DEP1, DI.DEP2]);

// Act
const myService = container.get<MyServiceInterface>(DI.MY_SERVICE_WITH_DEPENDENCIES);
const myService = container.get<MyServiceInterface>(DI.HIGHER_ORDER_FUNCTION_WITH_DEPENDENCIES);

// Assert
expect(myService.runTask()).toBe('Executing with dep1: dependency1 and dep2: 42');
Expand All @@ -54,11 +55,11 @@ describe('Container', () => {
it('should consider the function as an higher order function', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.SERVICE_WITHOUT_DEPENDENCY)
.toHigherOrderFunction(ServiceWithoutDependency);
container.bind(DI.HIGHER_ORDER_FUNCTION_WITHOUT_DEPENDENCIES)
.toHigherOrderFunction(HigherOrderFunctionWithoutDependencies);

// Act
const myService = container.get<ServiceWithoutDependencyInterface>(DI.SERVICE_WITHOUT_DEPENDENCY);
const myService = container.get<ServiceWithoutDependencyInterface>(DI.HIGHER_ORDER_FUNCTION_WITHOUT_DEPENDENCIES);

// Assert
expect(myService.run()).toBe('OtherService');
Expand Down Expand Up @@ -91,7 +92,7 @@ describe('Container', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.HELLO_WORLD).toFunction(sayHelloWorld);
container.bind(DI.SIMPLE_FUNCTION).toFunction(sayHelloWorld);

container.bind(DI.MY_SERVICE).toFactory(() => {
return MyService({
Expand All @@ -109,7 +110,7 @@ describe('Container', () => {
return MyUseCase({
myService: container.get<MyServiceInterface>(DI.MY_SERVICE),
logger: container.get<LoggerInterface>(DI.LOGGER),
sayHello: container.get<SayHelloType>(DI.HELLO_WORLD)
sayHello: container.get<SayHelloType>(DI.SIMPLE_FUNCTION)
});
});

Expand Down Expand Up @@ -151,29 +152,46 @@ describe('Container', () => {
});

describe('When a class is registered using a symbol', () => {
it('should return the associated function', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.MY_SERVICE_CLASS).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);
describe('When the class has dependencies', () => {
it('should return the instance with the resolved dependencies', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);

// Act
const myService = container.get<MyServiceClassInterface>(DI.MY_SERVICE_CLASS);
// Act
const myService = container.get<MyServiceClassInterface>(DI.CLASS_WITH_DEPENDENCIES);

// Assert
expect(myService.runTask()).toBe('Executing with dep1: dependency1 and dep2: 42');
// Assert
expect(myService.runTask()).toBe('Executing with dep1: dependency1 and dep2: 42');
});
});

describe('When the class has no dependency', () => {
it('should return the instance', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.CLASS_WITHOUT_DEPENDENCIES).toClass(MyServiceClassWithoutDependencies);

// Act
const myService = container.get<MyServiceClassInterface>(DI.CLASS_WITHOUT_DEPENDENCIES);

// Assert
expect(myService.runTask()).toBe('Executing without dependencies');
});
});

describe('When the dependency is retrieved twice', () => {
it('should return the same instance', () => {
// Arrange
container.bind(DI.DEP1).toValue('dependency1');
container.bind(DI.DEP2).toValue(42);
container.bind(DI.MY_SERVICE_CLASS).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);
const myService1 = container.get<MyServiceClassInterface>(DI.MY_SERVICE_CLASS);
container.bind(DI.CLASS_WITH_DEPENDENCIES).toClass(MyServiceClass, [DI.DEP1, DI.DEP2]);
const myService1 = container.get<MyServiceClassInterface>(DI.CLASS_WITH_DEPENDENCIES);

// Act
const myService2 = container.get<MyServiceClassInterface>(DI.MY_SERVICE_CLASS);
const myService2 = container.get<MyServiceClassInterface>(DI.CLASS_WITH_DEPENDENCIES);

// Assert
expect(myService1).toBe(myService2);
Expand Down
6 changes: 3 additions & 3 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export interface Container {
toFunction: (fn: CallableFunction) => void;
toHigherOrderFunction: (fn: CallableFunction, dependencies?: symbol[]) => void;
toFactory: (factory: CallableFunction) => void;
toClass: <C>(constructor: new (...args: any[]) => C, dependencies: symbol[]) => void;
toClass: <C>(constructor: new (...args: any[]) => C, dependencies?: symbol[]) => void;
};

get<T>(key: symbol): T;
Expand All @@ -30,8 +30,8 @@ export function createContainer(): Container {

const toFactory = (factory: CallableFunction) => factories.set(key, factory);

const toClass = (AnyClass: new (...args: unknown[]) => unknown, dependencies: symbol[]) => {
factories.set(key, () => new AnyClass(...(resolveDependencies(dependencies))));
const toClass = (AnyClass: new (...args: unknown[]) => unknown, dependencies: symbol[] = []) => {
factories.set(key, () => new AnyClass(...resolveDependencies(dependencies)));
};

return {
Expand Down

0 comments on commit 1f61413

Please sign in to comment.