Posted at

Hackで作る簡単サービスロケーター / Generics

前回 Hackで作る簡単サービスロケーター では、

簡単なDI・サービスロケーターライブラリを実装しました。

利用する上では十分機能しますが、

まだ制約を設けることができます。

今回は前回の内容から引き続き、もう少しだけ手を加えていきます。


前回のgetメソッドは様々な型を返却するため、

戻りの型宣言がmixedとしていました。

  <<__Rx>>

public function get(string $id): mixed {
if ($this->has($id)) {
list($scope, $callable) = $this->map[$id];
if ($callable is nonnull) {
if ($scope === Scope::SINGLETON) {
return $this->shared($id);
}
return $callable($this);
}
}
throw new NotFoundException(
Str\format('Identifier "%s" is not binding.', $id),
);
}

このままでも利用することができますが

利用時に都度返却される型を確認しなければなりません。

$instance = $container->get(\stdClass::class);

if($instance is \stdClass) {
$hoge = $instance->hoge;
// なにか処理
}

mixedが返却される場合、nullを含む様々な型が返却されます。

実行時に保証されるものが何もないため常に型を保証する様に記述する必要があります。

こうした場合にどうしたらいいでしょうか。

文字列などではなく、確実に何かしらのインスタンスが返却される様な場合は、

以前紹介した classname<\Hoge> の型宣言が利用できます。

ただしこのHogeの部分は様々なインスタンスになるため、固定で記述することができません。

こうした場合にGenericsを利用することができます。


setメソッドを変更

元のsetメソッドは第一引数にstringを指定していました。

文字列であれば様々なものが利用できます。

  public function set(

string $id,
TCallable $callback,
Scope $scope = Scope::PROTOTYPE,
): void {
$this->map[$id] = tuple($scope, $callback);
}

コンテナからインスタンスのみを取得できる様に、

指定する型を変更します。

  public function set<T>(

classname<T> $id,
TCallable $callback,
Scope $scope = Scope::PROTOTYPE,
): void {
$this->map[$id] = tuple($scope, $callback);
}

上記の様にすることで、setメソッド利用時に存在しないクラス名や適当な文字列は利用できなくなります。

    $container = new Container();

$container->set(
'hoge', // クラス名ではないためエラーに
($container) ==> new \stdClass()
);


getメソッド変更

setメソッドに合わせてgetメソッドも変更します。

前回は下記の様に実装しましたが、今回はこれを変更します。

  <<__Rx>>

public function get(string $id): mixed {
if ($this->has($id)) {
list($scope, $callable) = $this->map[$id];
if ($callable is nonnull) {
if ($scope === Scope::SINGLETON) {
return $this->shared($id);
}
return $callable($this);
}
}
throw new NotFoundException(
Str\format('Identifier "%s" is not binding.', $id),
);
}

指定した値と戻りの型を以下の様に記述します。

  <<__Rx>>

public function get<T>(classname<T> $t): T;

ただしgetメソッドの場合は、Singletonを表現するメソッドも内包しているため

すんなりgetメソッドの変更ができません。

getメソッド利用時に型を保証できる様に記述できればいいだけですので、

いくつか方法はありますが、今回はインスタンス取得のメソッドと、

型チェックを行い、指定したクラスのインスタンスで返却することを保証するメソッドに分割します。

  <<__Rx>>

protected function resolve<T>(classname<T> $id): mixed {
if ($this->has($id)) {
list($scope, $callable) = $this->map[$id];
if ($callable is nonnull) {
if ($scope === Scope::SINGLETON) {
return $this->shared($id);
}
return $callable($this);
}
}
throw new Exception\NotFoundException(
Str\format('Identifier "%s" is not binding.', $id),
);
}

<<__Rx>>
public function get<T>(classname<T> $t): T {
$mixed = $this->resolve($t);
invariant($mixed instanceof $t, "invalid use of incomplete type %s", $t);
return $mixed;
}

getメソッドの利用方法は前回のものと全く変わりませんが、

setメソッドと同じくクラス名以外を利用することはできない様になりました。

インスタンス生成の処理を別メソッドと切り出し、mixedで返却されることを許容しました。

そのかわり、getメソッドで引数に指定したクラス名の型が返却されるかどうかをチェックするように実装します。

invariant関数wp問題なく通過すると指定した型が返却されることが保証されますので、

Typechckerで指定したクラスが返却される、と判断されます。

これによりmixedではなくなる、ということになります。

この対応をすることでIDE上でもそのまま補完が効く様になり、

戻りの型チェックも不要になります。


Lock

最後に前回実装時にはなかったコンテナのロック機構を追加します。

これにより不確実な値の返却を防ぎます。

例ではlockしていない場合はインスタンス生成を行わず、Exceptionをスローする様にしましたが、

利用用途によって細部は変更してしまいましょう。

ロックを含めた実装コードは次の通りです。

<?hh // strict

namespace Acme\HackDi;

use namespace Acme\HackDi\Exception;
use namespace HH\Lib\{C, Str};
type TCallable = (function(\Acme\HackDi\Container): mixed);

class Container {
private bool $lock = false;
private dict<string, (Scope, TCallable)> $map = dict[];

public function set<T>(
classname<T> $id,
TCallable $callback,
Scope $scope = Scope::PROTOTYPE,
): void {
if(!$this->lock) {
$this->map[$id] = tuple($scope, $callback);
}
}

<<__Rx>>
protected function resolve<T>(classname<T> $id): mixed {
if ($this->has($id)) {
list($scope, $callable) = $this->map[$id];
if ($callable is nonnull) {
if ($scope === Scope::SINGLETON) {
return $this->shared($id);
}
return $callable($this);
}
}
throw new Exception\NotFoundException(
Str\format('Identifier "%s" is not binding.', $id),
);
}

<<__Rx>>
public function get<T>(classname<T> $t): T {
$mixed = $this->resolve($t);
invariant($mixed instanceof $t, "invalid use of incomplete type %s", $t);
return $mixed;
}

<<__Memoize>>
protected function shared<T>(classname<T> $id): mixed {
list($_, $callable) = $this->map[$id];
return $callable($this);
}

<<__Rx>>
public function has(string $id): bool {
if($this->lock) {
return C\contains_key($this->map, $id);
}
throw new Exception\ContainerNotLockedException(
Str\format('Container was not locked.'),
);
}

public function lock(): void {
$this->lock = true;
}

public function unlock(): void {
$this->lock = false;
}
}

これによりコンテナに登録されたインスタンス生成方法などが不必要に変更されることを防ぐことができます。

Genericsを利用することで、より強い制約をかけて確実にインスタンス生成が行える様になりました。

Hackを実際に利用する際に有用な方法ですので、活用していきましょう!!!