Edited at

Hack Factory Pattern Explained

今回はMcrouterとHackの予定を変更してお届け!

ここまでPHPとHackの違いの一つにTypecheckerがある、

という点に触れてきました。

それでは少し実践的なパターンを取り入れながら、

strictモードでPHPと異なる点をさらに紹介していきます。


PHP: Factory Pattern

フレームワークやライブラリでも多用されることも多いパターンですが、

PHPの場合は次のような実装にすることが多いと思います。

例えば何かしらの開発言語を返却するものとしましょう。

まずはインターフェースです。

<?php

declare(strict_types=1);

namespace Acme;

interface LanguageInterface
{
public function getName(): string;
}

次にこのインターフェースを実装した開発言語返却クラスです。


PHPを返却するクラス (Acme\Php)

getNameをコールするとPHPと返却されます。

簡単ですね!


Acme/Php.php

<?php

declare(strict_types=1);

namespace Acme;

class Php implements LanguageInterface
{
public function getName(): string
{
return 'PHP';
}
}



Hackを返却するクラス (Acme\Hack)

getNameをコールするとHackと返却されます。

<?php

declare(strict_types=1);

namespace Acme;

class Hack implements LanguageInterface
{
public function getName(): string
{
return 'Hack';
}
}


Goを返却するクラス (Acme\Go)

getNameをコールするとHackと返却されます。

<?php

declare(strict_types=1);

namespace Acme;

class Go implements LanguageInterface
{
public function getName(): string
{
return 'Go';
}
}


具象クラス生成

引数などを与えて各言語クラスのインスタンス生成を担当します。

<?php

declare(strict_types=1);

namespace Acme;

use function ucfirst;
use function strtolower;
use function trim;
use function method_exists;

class LanguageFactory
{
public function make(string $type): LanguageInterface
{
$className = 'make' . ucfirst(strtolower(trim($type)));
if(method_exists($this, $className)) {
return $this->$className();
}
throw new \RuntimeException('class not found.');
}

protected function makePhp(): LanguageInterface
{
return new Php();
}

protected function makeHack(): LanguageInterface
{
return new Hack();
}

protected function makeGo(): LanguageInterface
{
return new Go();
}
}

このクラスを利用する場合は以下となります。

<?php

declare(strict_types=1);

$factory = new Acme\LanguageFactory();
$factory->make('php'); // Acme\Php
$factory->make('hack')->getName(); // Hack

PHPで実装する場合は非常に簡単で、シンプルに作ることができます。

実はHackで同様に記述しても、

Typecheckerで警告になり実行できないことがほとんどです。

そのままHackに置き換えると次のエラーとなります。

$ hh_client

src/LanguageFactory.php:17:21,30: Dynamic method call (Naming[2011])

残念ながらHackでは動的メソッドのコールはあまり利用ができません。

どうすれば同じ様にこのパターンを用いることができるのでしょうか。


Hack: Factory Pattern

HackにはTypecheckerという強力なツールがセットになっています。

このTypecheckerを無効にすれば・・・

そうではありません!

PHPと異なり、

HackではTypecheckerが理解できる様にコードを記述しなければならないため、

少しだけ工夫する必要があります。

難しい実装な度が必要ではありませんが、Hackの仕組みを理解しておく必要があるため、

この機会に覚えておきましょう。


HH_FIXME 最も簡単な解決

HH_FIXMEは、実装コードにコメントとして記述することで、

Typecheckerのチェックをその部分だけ回避することができます。

動的メソッドコールに対して、下記の通りにエラー番号を記述します。

<?hh // strict

namespace Acme;

use function ucfirst;
use function strtolower;
use function trim;
use function method_exists;

class LanguageFactory {

public function make(string $type): LanguageInterface {
$className = 'make' . ucfirst(strtolower(trim($type)));
if(method_exists($this, $className)) {
/* HH_FIXME[2011] */
return $this->$className();
}
throw new \RuntimeException('class not found.');
}

// 省略
}

あまり複雑なアプリケーションでない場合はこれで十分なこともほとんどです。

ただ複数人で開発している場合は、エンジニアのスキルも様々です。

静的片付言語に近いHackと言えども

HH_FIXMEだけでは不確実な実装が入り込むことがあります。

より厳格な実装に変更してみましょう。


classname<T>

PHPやHackでも利用するクラス名の完全修飾名を取得するclassキーワードがあります。

おなじみの Foo::class ですね。

PHPではこれは文字列として作用しますが、

Hackではこのclassキーワード記述は文字列と異なるものとして扱うことができます。

それが classname<T> です。

<T>は、クラス、インターフェースを記述します。

下記の様に記述すると、動的メソッドコールができます。

<?hh

public function make(
classname<\stdClass> $class
): \stdClass {
return new $class();
}

$factory = new Acme\LanguageFactory();

$factory->example(\stdClass::class);

この仕組みをうまく活用することができそうです。

exampleメソッドを開発言語インターフェースを指定してみます。

<?hh

public function make(
classname<LanguageInterface> $class
): LanguageInterface {
return new $class();
}

残念ながらこれだけではTypechackerが理解できるコードにはなりません。

LanguageInterfaceを実装したクラス名を指定したとしても、

クラスのインスタンス生成にどんなものが必要なのか、

保証するものが何もありません。

src/LanguageFactory.php:30:16,21: Can't use new on classname<Acme\LanguageInterface>; __construct arguments are not guaranteed to be consistent in child classes (Typing[4060])

src/LanguageFactory.php:28:15,31: This declaration neither defines an abstract/final __construct nor uses <<__ConsistentConstruct>> attribute

エラーによると、<<__ConsistentConstruct>> を利用することができそうです。


<<__ConsistentConstruct>>

このAttributeは、コンスタラクタに対するものです。

このAttributeが記述されたクラスは、

継承する場合にかならずコンストラクタの引数などは同一のものとならなければなりません。

記述することでインスタンス生成でコンストラクタに必要な引数が保証されます。

Hackの公式マニュアルにある例を参考にしましょう。

<<__ConsistentConstruct>>

abstract class A {
}

class B extends A {
}

class C extends A {
// __ConsistentConstruct applied to A will cause the typechecker to throw
// an error when you have constructor with a different signature.
private int $x;
public function __construct(int $x) {
$this->x = x;
}
}

クラスAに <<__ConsistentConstruct>> が記述されているため、

クラスBも、コンストラクタは同じものとなります。

つまりインスタンス生成時に引数は利用しない、ということになります。

クラスCは、クラスAと異なり、コンストラクタに引数を利用する様に記述されています。

これはTypechckerでエラーと判定されますので、実行できません。

classname\<T> と、 <<__ConsistentConstruct>> を組み合わせることで、

Hackで堅いFactory Patternが実装できそうです。


Map、classname<T>、<<__ConsistentConstruct>>

PHP感が全くなくてワクワクしてくるでしょう!w

Acme\LanguageInterfaceに記述することもできますが、

ConsistentConstruct自体は抽象クラスなどで利用した方がいいでしょう。

ここでは例としてインターフェースを下記の様にします

以前紹介したSealedなどを使って、インターフェースの利用範囲を指示していますが、

場合によっては不必要なものです(Hackっぽくするために例として記述しています)

<?hh // strict

namespace Acme;

<<__Sealed(\Acme\AbstractLanguage::class)>>
interface LanguageInterface {

public function getName(): string;

}

抽象クラスを作成し、このクラスを継承するクラスに対して制約を与えます。

<?hh // strict

namespace Acme;

use type Acme\LanguageInterface;

<<__ConsistentConstruct>>
abstract class AbstractLanguage implements LanguageInterface {

}


PHPを返却するクラス (Acme\Php)

PHPのものから少しだけ変更しています。

他の言語返却クラスも同様です。

<?hh // strict

namespace Acme;

class Php extends AbstractLanguage {

public function getName(): string {
return 'PHP';
}
}


具象クラス生成(Hack版)

せっかくなのでここでもこれまでに紹介した hhvm/hsl

も混ぜています。

処理自体は非常にシンプルです。

<?hh // strict

namespace Acme;

use namespace HH\Lib\Str;

class LanguageFactory {

private Map<string, classname<AbstractLanguage>> $m = Map{
'php' => \Acme\Php::class,
'hack' => \Acme\Hack::class,
'go' => \Acme\Go::class,
};

public function make(string $type): AbstractLanguage {
$name = Str\lowercase(Str\trim($type));
if($this->m->containsKey($name)) {
$className = $this->m->at($name);
return new $className();
}
throw new \RuntimeException('class not found.');
}
}

Mapとクラスの完全修飾名を使い、キー名で指定できる様になっています。

private Map<string, classname<AbstractLanguage>> $m = Map{

'php' => \Acme\Php::class,
'hack' => \Acme\Hack::class,
'go' => \Acme\Go::class,
};

Mapのメソッドを使うことで、確実にMapから値を取得し、

かつ完全修飾名、完全修飾名で記述しているクラスには、<<__ConsistentConstruct>> が記述されていますので、

動的なインスタンス生成としても動作が保証されています。


public function make(string $type): AbstractLanguage {
$name = Str\lowercase(Str\trim($type));
if($this->m->containsKey($name)) {
$className = $this->m->at($name);
return new $className();
}
throw new \RuntimeException('class not found.');
}

makeメソッドは文字列となっていますので、ここにenumを使って厳格さをますこともできます。

拡張可能なクラスとする場合は、enumを引数として利用することはできないため(enumは継承できない)

文字列などにする必要があるということを覚えておきましょう。