Browse Source

chore: initial commit

e22m4u 2 years ago
commit
899e75e1f5
20 changed files with 1045 additions and 0 deletions
  1. 9 0
      .c8rc
  2. 5 0
      .commitlintrc
  3. 13 0
      .editorconfig
  4. 20 0
      .eslintrc.cjs
  5. 17 0
      .gitignore
  6. 4 0
      .husky/commit-msg
  7. 9 0
      .husky/pre-commit
  8. 7 0
      .mocharc.cjs
  9. 7 0
      .prettierrc
  10. 21 0
      LICENSE
  11. 20 0
      README.md
  12. 10 0
      mocha.setup.js
  13. 47 0
      package.json
  14. 1 0
      src/errors/index.js
  15. 6 0
      src/errors/invalid-argument-error.js
  16. 2 0
      src/index.js
  17. 97 0
      src/service-container.js
  18. 613 0
      src/service-container.spec.js
  19. 68 0
      src/service.js
  20. 69 0
      src/service.spec.js

+ 9 - 0
.c8rc

@@ -0,0 +1,9 @@
+{
+  "all": true,
+  "include": [
+    "src/**/*.js"
+  ],
+  "exclude": [
+    "src/**/*.spec.js"
+  ]
+}

+ 5 - 0
.commitlintrc

@@ -0,0 +1,5 @@
+{
+  "extends": [
+    "@commitlint/config-conventional"
+  ]
+}

+ 13 - 0
.editorconfig

@@ -0,0 +1,13 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+charset = utf-8
+indent_style = space
+indent_size = 2
+max_line_length = 80

+ 20 - 0
.eslintrc.cjs

@@ -0,0 +1,20 @@
+module.exports = {
+  env: {
+    es2021: true,
+    node: true
+  },
+  parserOptions: {
+    sourceType: 'module',
+    ecmaVersion: 13,
+  },
+  plugins: [
+    'mocha',
+    'chai-expect',
+  ],
+  extends: [
+    'eslint:recommended',
+    'prettier',
+    'plugin:mocha/recommended',
+    'plugin:chai-expect/recommended',
+  ],
+}

+ 17 - 0
.gitignore

@@ -0,0 +1,17 @@
+# OS
+.DS_Store
+
+# IDE
+.idea
+
+# Npm
+node_modules
+npm-debug.log
+package-lock.json
+
+# Yarn
+.yarn/
+.yarn*
+
+# c8
+coverage

+ 4 - 0
.husky/commit-msg

@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npx --no -- commitlint --edit "${1}"

+ 9 - 0
.husky/pre-commit

@@ -0,0 +1,9 @@
+#!/usr/bin/env sh
+. "$(dirname -- "$0")/_/husky.sh"
+
+npm run lint:fix
+npm run format
+
+npm run test
+
+git add -A

+ 7 - 0
.mocharc.cjs

@@ -0,0 +1,7 @@
+const path = require('path');
+
+module.exports = {
+  extension: ['js'],
+  spec: 'src/**/*.spec.js',
+  require: [path.join(__dirname, 'mocha.setup.js')],
+}

+ 7 - 0
.prettierrc

@@ -0,0 +1,7 @@
+{
+  "bracketSpacing": false,
+  "singleQuote": true,
+  "printWidth": 80,
+  "trailingComma": "all",
+  "arrowParens": "avoid"
+}

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 e22m4u@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 20 - 0
README.md

@@ -0,0 +1,20 @@
+## @e22m4u/service
+
+Разновидность сервис-локатора для инкапсуляции процесса разрешения
+зависимостей.
+
+## Установка
+
+```bash
+npm install @e22m4u/service
+```
+
+## Тесты
+
+```bash
+npm run test
+```
+
+## Лицензия
+
+MIT

+ 10 - 0
mocha.setup.js

@@ -0,0 +1,10 @@
+import chai from 'chai';
+import chaiSpies from 'chai-spies';
+import chaiSubset from 'chai-subset';
+import chaiAsPromised from 'chai-as-promised';
+
+process.env['NODE_ENV'] = 'test';
+
+chai.use(chaiSpies);
+chai.use(chaiSubset);
+chai.use(chaiAsPromised);

+ 47 - 0
package.json

@@ -0,0 +1,47 @@
+{
+  "name": "@e22m4u/service",
+  "version": "0.0.1",
+  "description": "Разновидность сервис-локатора",
+  "type": "module",
+  "main": "src/index.js",
+  "scripts": {
+    "lint": "eslint .",
+    "lint:fix": "eslint . --fix",
+    "format": "prettier --write \"./src/**/*.js\"",
+    "test": "eslint . && c8 --reporter=text-summary mocha",
+    "test:coverage": "eslint . && c8 --reporter=text mocha",
+    "prepare": "npx husky install"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/e22m4u/service.git"
+  },
+  "keywords": [
+    "DI",
+    "Service",
+    "Locator",
+    "Container"
+  ],
+  "author": "e22m4u <e22m4u@gmail.com>",
+  "license": "MIT",
+  "homepage": "https://github.com/e22m4u/service",
+  "dependencies": {
+    "@e22m4u/format": "0.0.2"
+  },
+  "devDependencies": {
+    "@commitlint/cli": "^17.7.1",
+    "@commitlint/config-conventional": "^17.7.0",
+    "c8": "^8.0.1",
+    "chai": "^4.3.7",
+    "chai-as-promised": "^7.1.1",
+    "chai-spies": "^1.0.0",
+    "chai-subset": "^1.6.0",
+    "eslint": "^8.47.0",
+    "eslint-config-prettier": "^9.0.0",
+    "eslint-plugin-chai-expect": "^3.0.0",
+    "eslint-plugin-mocha": "^10.1.0",
+    "husky": "^8.0.3",
+    "mocha": "^10.2.0",
+    "prettier": "^3.0.1"
+  }
+}

+ 1 - 0
src/errors/index.js

@@ -0,0 +1 @@
+export * from './invalid-argument-error.js';

+ 6 - 0
src/errors/invalid-argument-error.js

@@ -0,0 +1,6 @@
+import {Errorf} from '@e22m4u/format';
+
+/**
+ * Invalid argument error.
+ */
+export class InvalidArgumentError extends Errorf {}

+ 2 - 0
src/index.js

@@ -0,0 +1,2 @@
+export * from './service.js';
+export * from './service-container.js';

+ 97 - 0
src/service-container.js

@@ -0,0 +1,97 @@
+import {Service} from './service.js';
+import {InvalidArgumentError} from './errors/index.js';
+
+/**
+ * Service container.
+ */
+export class ServiceContainer {
+  /**
+   * Services map.
+   *
+   * @type {Map<Function, any>}
+   * @private
+   */
+  _services = new Map();
+
+  /**
+   * Get.
+   *
+   * @param {Function} ctor
+   * @param {any} args
+   * @return {any}
+   */
+  get(ctor, ...args) {
+    if (!ctor || typeof ctor !== 'function')
+      throw new InvalidArgumentError(
+        'The first argument of ServicesContainer.get must be ' +
+          'a class constructor, but %v given.',
+        ctor,
+      );
+    let service = this._services.get(ctor);
+    // instantiates if no service or args given
+    if (!service || args.length) {
+      service =
+        'prototype' in ctor && ctor.prototype instanceof Service
+          ? new ctor(this, ...args)
+          : new ctor(...args);
+      this._services.set(ctor, service);
+      // instantiates from a factory function
+    } else if (typeof service === 'function') {
+      service = service();
+      this._services.set(ctor, service);
+    }
+    return service;
+  }
+
+  /**
+   * Has.
+   *
+   * @param {Function} ctor
+   * @return {boolean}
+   */
+  has(ctor) {
+    return this._services.has(ctor);
+  }
+
+  /**
+   * Add.
+   *
+   * @param {Function} ctor
+   * @param {any} args
+   * @return {this}
+   */
+  add(ctor, ...args) {
+    if (!ctor || typeof ctor !== 'function')
+      throw new InvalidArgumentError(
+        'The first argument of ServicesContainer.add must be ' +
+          'a class constructor, but %v given.',
+        ctor,
+      );
+    const factory = () =>
+      ctor.prototype instanceof Service
+        ? new ctor(this, ...args)
+        : new ctor(...args);
+    this._services.set(ctor, factory);
+    return this;
+  }
+
+  /**
+   * Find.
+   *
+   * @param {Function} ctor
+   * @return {any[]}
+   */
+  find(ctor) {
+    if (!ctor || typeof ctor !== 'function')
+      throw new InvalidArgumentError(
+        'The first argument of ServicesContainer.find must be ' +
+          'a class constructor, but %v given.',
+        ctor,
+      );
+    const keys = Array.from(this._services.keys());
+    const ctors = keys.filter(
+      key => typeof key === 'function' && key.prototype instanceof ctor,
+    );
+    return ctors.map(c => this.get(c));
+  }
+}

+ 613 - 0
src/service-container.spec.js

@@ -0,0 +1,613 @@
+import {expect} from 'chai';
+import {Service} from './service.js';
+import {format} from '@e22m4u/format';
+import {ServiceContainer} from './service-container.js';
+
+describe('ServiceContainer', function () {
+  describe('get', function () {
+    it('throws an error if no constructor given', function () {
+      const container = new ServiceContainer();
+      const throwable = v => () => container.get(v);
+      const error = v =>
+        format(
+          'The first argument of ServicesContainer.get must be ' +
+            'a class constructor, but %s given.',
+          v,
+        );
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+    });
+
+    describe('Service', function () {
+      it('passes itself and given arguments to the given constructor', function () {
+        let executed = 0;
+        let givenContainer;
+        let givenArgs;
+        class MyService extends Service {
+          constructor(container, ...args) {
+            super(container);
+            executed++;
+            givenContainer = container;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        const service = container.get(MyService, 'foo', 'bar');
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenContainer).to.be.eq(container);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+
+      it('instantiates from an existing factory function', function () {
+        let executed = 0;
+        let givenContainer;
+        let givenArgs;
+        class MyService extends Service {
+          constructor(container, ...args) {
+            super(container);
+            executed++;
+            givenContainer = container;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        const service = container.get(MyService);
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenContainer).to.be.eq(container);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+
+      it('overrides an existing factory function', function () {
+        let executed = 0;
+        let givenContainer;
+        let givenArgs;
+        class MyService extends Service {
+          constructor(container, ...args) {
+            super(container);
+            executed++;
+            givenContainer = container;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        const service = container.get(MyService, 'baz', 'qux');
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenContainer).to.be.eq(container);
+        expect(givenArgs).to.be.eql(['baz', 'qux']);
+      });
+
+      it('caches a new instance', function () {
+        let executed = 0;
+        class MyService extends Service {
+          constructor(container) {
+            super(container);
+            ++executed;
+          }
+        }
+        const container = new ServiceContainer();
+        const myService1 = container.get(MyService);
+        const myService2 = container.get(MyService);
+        expect(myService1).to.be.instanceof(MyService);
+        expect(myService2).to.be.instanceof(MyService);
+        expect(myService1).to.be.eq(myService2);
+        expect(executed).to.be.eq(1);
+      });
+
+      it('overrides the cached instance', function () {
+        let executed = 0;
+        const givenArgs = [];
+        class MyService extends Service {
+          constructor(container, arg) {
+            super(container);
+            ++executed;
+            givenArgs.push(arg);
+          }
+        }
+        const container = new ServiceContainer();
+        const myService1 = container.get(MyService, 'foo');
+        const myService2 = container.get(MyService);
+        const myService3 = container.get(MyService, 'bar');
+        const myService4 = container.get(MyService);
+        expect(myService1).to.be.instanceof(MyService);
+        expect(myService2).to.be.instanceof(MyService);
+        expect(myService3).to.be.instanceof(MyService);
+        expect(myService4).to.be.instanceof(MyService);
+        expect(myService1).to.be.eq(myService2);
+        expect(myService2).to.be.not.eq(myService3);
+        expect(myService3).to.be.eq(myService4);
+        expect(executed).to.be.eq(2);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+    });
+
+    describe('non-Service', function () {
+      it('passes given arguments to the given constructor', function () {
+        let executed = 0;
+        let givenArgs;
+        class MyService {
+          constructor(...args) {
+            executed++;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        const service = container.get(MyService, 'foo', 'bar');
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+
+      it('instantiates from an existing factory function', function () {
+        let executed = 0;
+        let givenArgs;
+        class MyService {
+          constructor(...args) {
+            executed++;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        const service = container.get(MyService);
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+
+      it('overrides an existing factory function', function () {
+        let executed = 0;
+        let givenArgs;
+        class MyService {
+          constructor(...args) {
+            executed++;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        const service = container.get(MyService, 'baz', 'qux');
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenArgs).to.be.eql(['baz', 'qux']);
+      });
+
+      it('caches a new instance', function () {
+        let executed = 0;
+        class MyService {
+          constructor() {
+            ++executed;
+          }
+        }
+        const container = new ServiceContainer();
+        const myService1 = container.get(MyService);
+        const myService2 = container.get(MyService);
+        expect(myService1).to.be.instanceof(MyService);
+        expect(myService2).to.be.instanceof(MyService);
+        expect(myService1).to.be.eq(myService2);
+        expect(executed).to.be.eq(1);
+      });
+
+      it('overrides the cached instance', function () {
+        let executed = 0;
+        const givenArgs = [];
+        class MyService {
+          constructor(arg) {
+            ++executed;
+            givenArgs.push(arg);
+          }
+        }
+        const container = new ServiceContainer();
+        const myService1 = container.get(MyService, 'foo');
+        const myService2 = container.get(MyService);
+        const myService3 = container.get(MyService, 'bar');
+        const myService4 = container.get(MyService);
+        expect(myService1).to.be.instanceof(MyService);
+        expect(myService2).to.be.instanceof(MyService);
+        expect(myService3).to.be.instanceof(MyService);
+        expect(myService4).to.be.instanceof(MyService);
+        expect(myService1).to.be.eq(myService2);
+        expect(myService2).to.be.not.eq(myService3);
+        expect(myService3).to.be.eq(myService4);
+        expect(executed).to.be.eq(2);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+    });
+  });
+
+  describe('has', function () {
+    describe('Service', function () {
+      it('returns true when a given constructor has its cached instance or false', function () {
+        const container = new ServiceContainer();
+        class MyService extends Service {}
+        expect(container.has(MyService)).to.be.false;
+        container.get(MyService);
+        expect(container.has(MyService)).to.be.true;
+      });
+
+      it('returns true when a given constructor has its factory function or false', function () {
+        const container = new ServiceContainer();
+        class MyService extends Service {}
+        expect(container.has(MyService)).to.be.false;
+        container.add(MyService);
+        expect(container.has(MyService)).to.be.true;
+      });
+    });
+
+    describe('non-Service', function () {
+      it('returns true when a given constructor has its cached instance or false', function () {
+        const container = new ServiceContainer();
+        class MyService {}
+        expect(container.has(MyService)).to.be.false;
+        container.get(MyService);
+        expect(container.has(MyService)).to.be.true;
+      });
+
+      it('returns true when a given constructor has its factory function or false', function () {
+        const container = new ServiceContainer();
+        class MyService {}
+        expect(container.has(MyService)).to.be.false;
+        container.add(MyService);
+        expect(container.has(MyService)).to.be.true;
+      });
+    });
+  });
+
+  describe('add', function () {
+    it('throws an error if no constructor given', function () {
+      const container = new ServiceContainer();
+      const throwable = v => () => container.add(v);
+      const error = v =>
+        format(
+          'The first argument of ServicesContainer.add must be ' +
+            'a class constructor, but %s given.',
+          v,
+        );
+      expect(throwable()).to.throw(error('undefined'));
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+    });
+
+    describe('Service', function () {
+      it('provides given arguments to the factory function', function () {
+        let executed = 0;
+        let givenContainer;
+        let givenArgs;
+        class MyService extends Service {
+          constructor(container, ...args) {
+            super(container);
+            executed++;
+            givenContainer = container;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        const service = container.get(MyService);
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenContainer).to.be.eq(container);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+
+      it('overrides a cached instance of the given constructor', function () {
+        class MyService extends Service {}
+        const container = new ServiceContainer();
+        const service1 = container.get(MyService);
+        const service2 = container.get(MyService);
+        expect(service1).to.be.eq(service2);
+        container.add(MyService);
+        const service3 = container.get(MyService);
+        const service4 = container.get(MyService);
+        expect(service3).to.be.eq(service4);
+        expect(service3).to.be.not.eq(service1);
+      });
+
+      it('overrides constructor arguments of the factory function', function () {
+        let executed = 0;
+        let givenContainer;
+        let givenArgs;
+        class MyService extends Service {
+          constructor(container, ...args) {
+            super(container);
+            executed++;
+            givenContainer = container;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        container.add(MyService, 'baz', 'qux');
+        const service = container.get(MyService);
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenContainer).to.be.eq(container);
+        expect(givenArgs).to.be.eql(['baz', 'qux']);
+      });
+    });
+
+    describe('non-Service', function () {
+      it('provides given arguments to the factory function', function () {
+        let executed = 0;
+        let givenArgs;
+        class MyService {
+          constructor(...args) {
+            executed++;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        const service = container.get(MyService);
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+
+      it('overrides a cached instance of the given constructor', function () {
+        class MyService {}
+        const container = new ServiceContainer();
+        const service1 = container.get(MyService);
+        const service2 = container.get(MyService);
+        expect(service1).to.be.eq(service2);
+        container.add(MyService);
+        const service3 = container.get(MyService);
+        const service4 = container.get(MyService);
+        expect(service3).to.be.eq(service4);
+        expect(service3).to.be.not.eq(service1);
+      });
+
+      it('overrides constructor arguments of the factory function', function () {
+        let executed = 0;
+        let givenArgs;
+        class MyService {
+          constructor(...args) {
+            executed++;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(MyService, 'foo', 'bar');
+        expect(executed).to.be.eq(0);
+        container.add(MyService, 'baz', 'qux');
+        const service = container.get(MyService);
+        expect(service).to.be.instanceof(MyService);
+        expect(executed).to.be.eq(1);
+        expect(givenArgs).to.be.eql(['baz', 'qux']);
+      });
+    });
+  });
+
+  describe('find', function () {
+    it('throws an error if no constructor given', function () {
+      const container = new ServiceContainer();
+      const throwable = v => () => container.find(v);
+      const error = v =>
+        format(
+          'The first argument of ServicesContainer.find must be ' +
+            'a class constructor, but %s given.',
+          v,
+        );
+      expect(throwable('str')).to.throw(error('"str"'));
+      expect(throwable(10)).to.throw(error('10'));
+      expect(throwable(true)).to.throw(error('true'));
+      expect(throwable(false)).to.throw(error('false'));
+      expect(throwable(undefined)).to.throw(error('undefined'));
+      expect(throwable(null)).to.throw(error('null'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable({})).to.throw(error('Object'));
+    });
+
+    describe('Service', function () {
+      it('returns an empty array if no bound classes of the given super-class', function () {
+        const container = new ServiceContainer();
+        class MyService extends Service {}
+        const result1 = container.find(MyService);
+        expect(result1).to.be.instanceof(Array);
+        expect(result1).to.be.empty;
+      });
+
+      it('returns cached instances of the given super-class', function () {
+        class MyService extends Service {}
+        class FooService extends MyService {}
+        class BarService extends MyService {}
+        const container = new ServiceContainer();
+        const foo = container.get(FooService);
+        const bar = container.get(BarService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.have.lengthOf(2);
+        expect(result[0]).to.be.instanceof(FooService);
+        expect(result[0]).to.be.eq(foo);
+        expect(result[1]).to.be.instanceof(BarService);
+        expect(result[1]).to.be.eq(bar);
+      });
+
+      it('returns instances from factory functions of the given super-class', function () {
+        class MyService extends Service {}
+        class FooService extends MyService {}
+        class BarService extends MyService {}
+        const container = new ServiceContainer();
+        container.add(FooService);
+        container.add(BarService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.have.lengthOf(2);
+        expect(result[0]).to.be.instanceof(FooService);
+        expect(result[1]).to.be.instanceof(BarService);
+      });
+
+      it('does not return a cached instance of the given constructor', function () {
+        class MyService extends Service {}
+        const container = new ServiceContainer();
+        container.get(MyService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.be.empty;
+      });
+
+      it('does not return an instance from a factory function of the given constructor', function () {
+        class MyService extends Service {}
+        const container = new ServiceContainer();
+        container.add(MyService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.be.empty;
+      });
+
+      it('caches resolved instances from factory functions', function () {
+        class MyService extends Service {}
+        class FooService extends MyService {}
+        class BarService extends MyService {}
+        const container = new ServiceContainer();
+        container.add(FooService);
+        container.add(BarService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.have.lengthOf(2);
+        expect(result[0]).to.be.instanceof(FooService);
+        expect(result[1]).to.be.instanceof(BarService);
+        const foo = container.get(FooService);
+        const bar = container.get(BarService);
+        expect(foo).to.be.eq(result[0]);
+        expect(bar).to.be.eq(result[1]);
+      });
+
+      it('uses constructor arguments provided to the factory function', function () {
+        class MyService extends Service {}
+        let givenContainer;
+        let givenArgs;
+        class ChildService extends MyService {
+          constructor(container, ...args) {
+            super(container);
+            givenContainer = container;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(ChildService, 'foo', 'bar');
+        const result = container.find(MyService);
+        expect(result[0]).to.be.instanceof(ChildService);
+        expect(givenContainer).to.be.eq(container);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+    });
+
+    describe('non-Service', function () {
+      it('returns an empty array if no bound classes of the given super-class', function () {
+        const container = new ServiceContainer();
+        class MyService {}
+        const result1 = container.find(MyService);
+        expect(result1).to.be.instanceof(Array);
+        expect(result1).to.be.empty;
+      });
+
+      it('returns cached instances of the given super-class', function () {
+        class MyService {}
+        class FooService extends MyService {}
+        class BarService extends MyService {}
+        const container = new ServiceContainer();
+        const foo = container.get(FooService);
+        const bar = container.get(BarService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.have.lengthOf(2);
+        expect(result[0]).to.be.instanceof(FooService);
+        expect(result[0]).to.be.eq(foo);
+        expect(result[1]).to.be.instanceof(BarService);
+        expect(result[1]).to.be.eq(bar);
+      });
+
+      it('returns instances from factory functions of the given super-class', function () {
+        class MyService {}
+        class FooService extends MyService {}
+        class BarService extends MyService {}
+        const container = new ServiceContainer();
+        container.add(FooService);
+        container.add(BarService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.have.lengthOf(2);
+        expect(result[0]).to.be.instanceof(FooService);
+        expect(result[1]).to.be.instanceof(BarService);
+      });
+
+      it('does not return a cached instance of the given constructor', function () {
+        class MyService {}
+        const container = new ServiceContainer();
+        container.get(MyService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.be.empty;
+      });
+
+      it('does not return an instance from a factory function of the given constructor', function () {
+        class MyService {}
+        const container = new ServiceContainer();
+        container.add(MyService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.be.empty;
+      });
+
+      it('caches resolved instances from factory functions', function () {
+        class MyService {}
+        class FooService extends MyService {}
+        class BarService extends MyService {}
+        const container = new ServiceContainer();
+        container.add(FooService);
+        container.add(BarService);
+        const result = container.find(MyService);
+        expect(result).to.be.instanceof(Array);
+        expect(result).to.have.lengthOf(2);
+        expect(result[0]).to.be.instanceof(FooService);
+        expect(result[1]).to.be.instanceof(BarService);
+        const foo = container.get(FooService);
+        const bar = container.get(BarService);
+        expect(foo).to.be.eq(result[0]);
+        expect(bar).to.be.eq(result[1]);
+      });
+
+      it('uses constructor arguments provided to the factory function', function () {
+        class MyService {}
+        let givenContainer;
+        let givenArgs;
+        class ChildService extends MyService {
+          constructor(...args) {
+            super();
+            givenContainer = container;
+            givenArgs = args;
+          }
+        }
+        const container = new ServiceContainer();
+        container.add(ChildService, 'foo', 'bar');
+        const result = container.find(MyService);
+        expect(result[0]).to.be.instanceof(ChildService);
+        expect(givenContainer).to.be.eq(container);
+        expect(givenArgs).to.be.eql(['foo', 'bar']);
+      });
+    });
+  });
+});

+ 68 - 0
src/service.js

@@ -0,0 +1,68 @@
+import {ServiceContainer} from './service-container.js';
+
+/**
+ * Service.
+ */
+export class Service {
+  /**
+   * Container.
+   *
+   * @type {ServiceContainer}
+   */
+  container;
+
+  /**
+   * Constructor.
+   *
+   * @param container
+   */
+  constructor(container) {
+    this.container =
+      container instanceof ServiceContainer
+        ? container
+        : new ServiceContainer();
+  }
+
+  /**
+   * Get service.
+   *
+   * @param {any} ctor
+   * @param {any} args
+   * @return {any}
+   */
+  getService(ctor, ...args) {
+    return this.container.get(ctor, ...args);
+  }
+
+  /**
+   * Has service.
+   *
+   * @param {any} ctor
+   * @return {boolean}
+   */
+  hasService(ctor) {
+    return this.container.has(ctor);
+  }
+
+  /**
+   * Add service.
+   *
+   * @param {any} ctor
+   * @param {any} args
+   * @return {this}
+   */
+  addService(ctor, ...args) {
+    this.container.add(ctor, ...args);
+    return this;
+  }
+
+  /**
+   * Find services.
+   *
+   * @param {any} ctor
+   * @return {any[]}
+   */
+  findServices(ctor) {
+    return this.container.find(ctor);
+  }
+}

+ 69 - 0
src/service.spec.js

@@ -0,0 +1,69 @@
+import chai from 'chai';
+import {expect} from 'chai';
+import {Service} from './service.js';
+import {ServiceContainer} from './service-container.js';
+const {spy} = chai;
+
+describe('Service', function () {
+  describe('constructor', function () {
+    it('instantiates with a services container', function () {
+      const service = new Service();
+      expect(service.container).to.be.instanceof(ServiceContainer);
+    });
+
+    it('sets a given service container', function () {
+      const container = new ServiceContainer();
+      const service = new Service(container);
+      expect(service.container).to.be.eq(container);
+    });
+  });
+
+  describe('getService', function () {
+    it('calls the container "get" method', function () {
+      const service = new Service();
+      spy.on(service.container, 'get', (ctor, ...args) => {
+        expect(ctor).to.be.eq(Date);
+        expect(args).to.be.eql(['foo', 'bar', 'baz']);
+        return 'OK';
+      });
+      const res = service.getService(Date, 'foo', 'bar', 'baz');
+      expect(res).to.be.eq('OK');
+    });
+  });
+
+  describe('hasService', function () {
+    it('calls the container "has" method', function () {
+      const service = new Service();
+      spy.on(service.container, 'has', ctor => {
+        expect(ctor).to.be.eq(Date);
+        return 'OK';
+      });
+      const res = service.hasService(Date);
+      expect(res).to.be.eq('OK');
+    });
+  });
+
+  describe('addService', function () {
+    it('calls the container "add" method', function () {
+      const service = new Service();
+      spy.on(service.container, 'add', (ctor, ...args) => {
+        expect(ctor).to.be.eq(Date);
+        expect(args).to.be.eql(['foo', 'bar', 'baz']);
+      });
+      const res = service.addService(Date, 'foo', 'bar', 'baz');
+      expect(res).to.be.eq(service);
+    });
+  });
+
+  describe('findServices', function () {
+    it('calls the container "find" method', function () {
+      const service = new Service();
+      spy.on(service.container, 'find', ctor => {
+        expect(ctor).to.be.eq(Date);
+        return 'OK';
+      });
+      const res = service.findServices(Date);
+      expect(res).to.be.eq('OK');
+    });
+  });
+});