プッシー:リファクタリング。 パート3または櫛の粗さ

image 一連の記事の第1部と第2部では、そのコードと1つの関数で記述した追加のアクションを分離するために少し作業を行いました。 基本的に、api-pussyのテストクライアントを作成するために、HttpClientクラスとCacheクラス、およびそれらの異なる実装を処理していました。



データ表示



その前に、コードの動作と一般的な構造に多くの注意を払いましたが、扱っているデータを忘れていました。 現在、戻り値CatApi :: getRandomImage()を含むすべてが文字列です。 つまり、このメソッドを呼び出すことで、文字列を取得することを「認識」します。 PHPはオブジェクト、リソース、配列など、すべてを返すことができるので、「知っています」と言います。 それにもかかわらず、RealCatApi :: getRandomImage()の場合でも、値が明示的にキャストされているため、この文字列が「有用」であるとは言えません。このメソッドを呼び出した人の場合:空の文字列、URLを含まない文字列(「私はURLではない」など)などです。



class RealCatApi implements CatAPi { ... /** * @return string URL of a random image */ public function getRandomImage() { try { $responseXml = ...; } catch (HttpRequestFailed $exception) { ... } $responseElement = new \SimpleXMLElement($responseXml); return (string) $responseElement->data->images[0]->image->url; } }
      
      





コードをより正確で信頼性の高いものにするための良い解決策は、正しい値を返すようにすることです。



最初にできることは、メソッドの事後条件を確認することです。



 $responseElement = new \SimpleXMLElement($responseXml); $url = (string) $responseElement->data->images[0]->image->url; if (filter_var($url, FILTER_VALIDATE_URL) === false) { throw new \RuntimeException('The Cat Api did not return a valid URL'); } return $url;
      
      





正しいとはいえ、読みにくいことがあります。 同様のチェックを必要とする機能がさらにいくつかある場合、さらに悪化します。 この検証ロジックを何らかの形で再利用する必要があります。 いずれにせよ、戻り値はまだ同じ役に立たない文字列であり、これが本当にURLであることを全員に保証すればクールです。 この場合、CatApi :: getRandomImage()メソッドを使用するプログラムのどの部分でも、それがURLであることが確実にわかり、エラーがなく、これはメールではありません。



URL値オブジェクト



CatApi :: getRandomImage()実装の事後条件を書く代わりに、画像のURLの事前条件を書くことができます。 画像のURLが有効な場合にのみ存在できることをどのように確認しますか? そうです、もう1つのオブジェクトを作成し、有効なアドレスではないものを使用して作成されないようにします。



 class Url { private $url; public function __construct($url) { if (!is_string($url)) { throw new \InvalidArgumentException('URL was expected to be a string'); } if (filter_var($url, FILTER_VALIDATE_URL) === false) { throw new \RuntimeException('The provided URL is invalid'); } $this->url = $url; } }
      
      





このタイプのオブジェクトはvalue-objectと呼ばれます



そのようなオブジェクトは正しく作成できません。



 new Url('I am not a valid URL'); // RuntimeException: "The provided URL is invalid"
      
      





これで、URLクラスのオブジェクトは有効なアドレスを正確に表しますが、他のオブジェクトは存在できません。 関数コードを再度変更して、新しいオブジェクトを返すことができます。



 $responseElement = new \SimpleXMLElement($responseXml); $url = (string) $responseElement->data->images[0]->image->url; return new Url($url);
      
      





通常、値オブジェクトには、プリミティブ型からオブジェクトを作成し、保存/ロード、または他の値オブジェクトから作成できるように変換するためのメソッドがあります。 この場合、fromString()および__toString()メソッドが必要です。 また、これらのメソッドにより、並列作成メソッド(fromParts($スキーム、$ホスト、$パス、...)など)および特別なゲッター(ホスト()、isSecure()..)を実装できるようになります。 もちろん、これらのメソッドは実際に必要になる前に作成しないでください。



 class Url { private $url; private function __construct($url) { $this->url = $url; } public static function fromString($url) { if (!is_string($url)) { ... } ... return new self($url); } public function __toString() { return $this->url; } }
      
      





getRandomImage()メソッドを変更し、画像のデフォルト値もURLオブジェクトとして返されるようにする必要があります。



 class RealCatApi implements CatAPi { ... /** * @return Url URL of a random image */ public function getRandomImage() { try { $responseXml = ...; } catch (HttpRequestFailed $exception) { return Url::fromString('http://cdn.my-cool-website.com/default.jpg'); } $responseElement = new \SimpleXMLElement($responseXml); return Url::fromString((string) $responseElement->data->images[0]->image->url); } }
      
      





当然、そのような変更はCacheインターフェースとそれを実装するクラス(FileCacheなど)に反映されます。したがって、URLオブジェクトを受け入れて返す必要があります。



 class FileCache implements Cache { ... public function put(Url $url) { file_put_contents($this->cacheFilePath, (string) $url); } public function get() { return Url::fromString(file_get_contents($this->cacheFilePath)); } }
      
      





XML応答を解析する



コードのこの部分を変更することは残っています。



 $responseElement = new \SimpleXMLElement($responseXml); $url = (string) $responseElement->data->images[0]->image->url;
      
      





正直なところ、私自身はSimpleXMLが好きではありませんが、問題はそこにありません。 ここでの問題は、データと呼ばれる1つの要素を含むルート要素を含む有効な応答を常に取得することです。この要素には、少なくとも1つの画像要素が含まれ、1つの画像要素が含まれ、文字列はURLだと思います。 このチェーンのどの時点でも、仮定は間違っている可能性があり、これはエラーにつながります。



現時点でのタスクは、PHPが例外をスローする代わりに、これらのエラーを処理することです。 これを行うために、再び例外を定義し、それをキャッチします。 繰り返しますが、XML応答に存在する要素名と階層に関する詳細をすべて非表示にする必要があります。 このようなオブジェクトは、例外も処理する必要があります。 最初のステップは、単純なDTO(データ転送オブジェクト)を導入することです。これは、api pussyからのイメージの本質を表します。



 class Image { private $url; public function __construct($url) { $this->url = $url; } /** * @return string */ public function url() { return $this->url; } }
      
      





このDTOは、そのような概念やその他の要素を答えから隠していることがわかります。 私たちはURLにのみ興味があるので、単純なgetter url()を介してアクセスできるようにします



これで、getRandomImage()のコードは次のようになります。



 $responseElement = new \SimpleXMLElement($responseXml); $image = new Image((string) $responseElement->data->images[0]->image->url); $url = $image->url();
      
      





ご覧のとおり、これはまだこのXML要素のチェーンに依存しているため、あまり役に立ちません。



DTOを直接作成する代わりに、XML応答の構造を知っているファクトリを介してこれを行う方が適切です。



 class ImageFromXmlResponseFactory { public function fromResponse($response) { $responseElement = new \SimpleXMLElement($response); $url = (string) $responseElement->data->images[0]->image->url; return new Image($url); } }
      
      





RealCatApiクラスにImageFromXmlResponseFactoryインスタンスを実装するだけです。これにより、RealCatApi :: getRandomImage()メソッドのコードが次のような状態になります。



 $image = $this->imageFactory->fromResponse($responseXml); $url = $image->url(); return Url::fromString($url);
      
      





この方法でコードを配置すると、いくつかのことを最適な方法で書くことができます。 少人数クラスはテストが簡単です。 先に指摘したように、私たちのテストはまだ主に「理想的なシナリオ」をテストしており、ほとんどの境界ケースはテストの対象外です。 ここで、たとえば、何が起こる可能性があります。



  1. サーバーからの空の応答
  2. 打たれたXML
  3. 構造が変更された有効なXML
  4. ...


XML処理ロジックを個別のクラスに配置することにより、この特定のタスクを担当するものにのみ集中できます。 これにより、実際のTDDを使用して、状況(上記のリストのように)と予想される結果を決定できます。



おわりに



これで、Cat APIクライアントのリファクタリングシリーズ( プッシー:リファクタリング )が終わりに近づいています。 改善のための提案があれば、投稿へのコメントを書いてください。 頑張って



All Articles