Twitter以上ブログ以下

ただの読み物

Constructor Injection でやるテストしやすいPHP

背景

もともとRailsRubyとJSを書いていた人間だったが、最近はPHPとGoをよく書くので、それっぽい記事を書いておこうと思った。

今回はConstructor Injectionを用いてテストしやすい、Mockに差し替えしやすい、なんか読みやすいコードを書くため小技を残す。

前提

PHPは7.2を期待して記述しています。あるいは、PHPDocによる型の宣言をサポートされているIDEや、それに準ずる環境を期待して記述しています。

この記事に存在しない要素

  • DI ツールを利用した記述方法

Constructor Injection ?

Dependency Injection

Dependency Injection を実装する手法の一つです。他には Properties Injection(Setter Injection) 、 Interface Injection といった実装手法があるようです。

Dependency injection - Wikipedia

Dependency Injection(以下DI) は日本語文献では「依存性の注入」と記述されることが多く、あるオブジェクトが依存する振る舞いや知識(これらを表現するものとしてオブジェクトがありますね)を外部から注入することによって、SOLID原則に対して容易に準ずる事ができるコードを記述できるようになります。

依存性の注入 - Wikipedia

Wikipedia日本語版いわく、https://ja.wikipedia.org/wiki/%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5

結合度の低下によるコンポーネント化の促進
単体テストの効率化
特定のフレームワークへの依存度低下

の効果があるとされています。今回は特に単体テストの効率化について記述します。

Constructor Injection

ではDIにおける Constructor Injection とは何でしょうか。その名の通り、Constructorで依存関係を定義、注入する手法です。

PHPでは、簡単に言えば以下のようなものです。

<?php

class K {
  private $dependency;
  public function __constructor(Dependency $dependency)
  {
    $this->dependency = $dependency;
  }
}

簡単ですね。実際には、 private $dependency のプロパティにPHPDocで型宣言をしましょう。また、PHP7.4 で Typed Properties がくるっぽいので、わくわくしましょう。

PHP: rfc:typed_properties_v2

より正しいDI

より高度に設計されたDIではDependency といった実Classに依存するのではなく、 DependencyInterfaceAbstractDependency といった抽象に依存するほうがよりよいとされています。

理由は、「結合度の低下によるコンポーネント化の促進」に対してより効果的に作用するためです。

また、この __constructor の定義自体も Interface に切り出すことで、より実Class間の依存度は下がり、上記効果が高まりますが、

僕はInterfaceがはじめからうまく作成できると思っていない ので、実クラス同士で Constructor Injection くらいが身体にあっています。

本当に凝集度を考慮するくらいに複雑になったら、その時にInterfaceの存在に思いを馳せるとより適切なInterfaceがそこに見いだせるでしょう。

実際に書く

サンプルコード

基本的には以下の点だけを気にしてコードを書いていきます。 1. 「処理」を知っているObjectは初期化時にのみ渡す 2. 「処理」への引数の受け渡しと、引数に依存する振る舞いのみ記述する

サンプルコードとして、Articleの情報を受け取って、Tweetを返却するクラスを考えてみます。

<?php
// 簡単のために、 namespace を省略して、 use も省略しています。

class TweetTextBuilder{
  $article_formatter;
  $user_repository;
  $tweet_validator;
  $tweet_builder;
  
  public function __constructor(ArticleFormatter $article_formatter, UserRepository $user_repository, TweetValidator $tweet_validator, TweetBuilder)
  {
    $this->article_formatter = $article_formatter;
    $this->user_repository = $user_repository;
    $this->tweet_validator = $tweet_validator;
    $this->tweet_builder = $tweet_builder;
  }

  public function build(Article $article): Tweet
  {
    $user = $this->user_repository->findById($article->getId());
    $text = $this->article_formatter->format($article, $user);
    $validate_result = $this->tweet_validator->validate($text);

    if (!$validate_result->isValid()) {
      throw new ValidateError($validate_result->getMessage()) // 140 文字overかもしれないし、User不正な空文字かもしれないし
    }

    // 本当は TwitterUser が別で存在すると思うんだけど、とりあえず同じContextのUserを共有することにした
    return $this->tweet_builder->build($user, $text)
  }
}

長い? 長いですね。とくに Constructor の引数が長いですね。でも現代のIDEやエディタを利用している我々は補完があるので、この程度の長さは特に気になりませんね(本当ですか?) さて、ではこのコードのテストコードを書いてみましょう。

<?php

// これまた PHP Unit の use を省略しています。Extendも省略
class TweetTextBuilderTest {
  public function testBuildWhenErrorOnTweetValidator() {
    $article_formatter = $this->createMock(ArticleFormatter::class);
    $user_repository = $this->createMock(UserRepository::class);
    $tweet_validator = $this->createMock(TweetValidator::class);
    $tweet_builder = $this->createMock(TweetBuilder::class);

    $user = new User();
    $validate_result = new ValidationResult(false, '140文字オーバーです');

    $article_formatter->expects($this->once())->method('format')->willReturn('example');
    $user_repository->expects($this->once())->method('findById')->willReturn($user);
    $tweet_validator->expects($this->once())->method('validate')->willReturn($validate_result);
    // validate_result が error なので、このオブジェクトのこのメソッドは呼ばれないことを確認する。
    $tweet_builder->expects($this->exactly(0))->method('build');

    $tweet_text_builder = new TweetTextBuilder($article_formatter, $user_repository, $tweet_validator, $tweet_builder);

    $article = new Article();
    $tweet_text_builder->build($article);
  }
}

mock ばっかり? そうですね。ただ、このクラスのこのメソッドについてテストしたい要素は2つです。 1. tweet_validator の返却値のオブジェクトの isValid() メソッドが true だった場合 - すべての依存先Classの該当メソッドが呼び出されること。 - よりちゃんとやるなら、各 mock の willReturn を異なる mock の with() に指定することで、「このメソッドの返却値がこのメソッドの引数になること」を宣言し、テストしてもよい 2. 上記メソッドが false だった場合 - TweetBuilder のオブジェクトのメソッドがコールされないこと - 1. の下の要素もやってもよい

ただし

ただし、各依存先の各メソッドが適切にテストされていることを期待しているので、そうでなければこの手法も意味がないです。 「このClass(このObject)は何をする(何を知っている)のですか?」という問いに「〜をします(を知っています)」と一言で答えられるまでに要素を分解していき、その小さい部品ごとにテストを書いて、それを結合するクラスがいる、そういう状態を目指しています。

メリット

  1. テストしやすい。特に Repository への直接依存しないことによってもちろん実データを用意する必要がない。(いろんな実装があるとは思うけどとりあえず)
  2. 条件分岐も mock の willReturn の返却値をいじればしゅっと書ける
  3. 各依存先Classをテストすれば容易にカバレッジもあげられるし、各々の仕様が変更されても(たとえばArticleFormatterは文頭100文字だけをSplitするだけだったが、「最初は自己紹介が定型文なので、本文はじまりから100文字だけを取得するようにする」みたいな仕様変更があっても、このClassや他のメソッドの振る舞いは変更しないで良い(事が多い)

デメリット

  1. テストが長く、同じようなことを何度も記述しなければならない
  2. mock の生成を短くしても、しょうがない。
  3. DateProvider を利用することによって、パターンは別で記述することもデキるが、 expects を利用したいので、悩ましいといえば悩ましい。
  4. 初見ではわかりにくい
  5. なんでこんなことしてるのかがわかりにくい。

終わりに

テストがしやすい、よく分離されているコードは問題がわかっても調査するポイントが絞りやすく、また仕様変更に対しても依存先のObjectに閉じたり、そもそもこのクラスまるごと捨てるかの二択になりやすい。

とはいえ、「常に140文字をオーバーするFormat結果しか帰ってこない」状態になったりしたら、常に失敗するのはそうなので、分離されていることに対して忌避感を持つことも十分にわかる。

いい感じの塩梅を目指し、常に改善するポイントを探していくのがソフトウェア開発における楽しいポイントでもあるといいなぁと思う。

2018年をまとめてみる

背景

特にない。気持ちの問題。

仕事について

今年前半は副業をとある目黒の会社でしていて、いろいろあって遅れたけどなんかλで画像の変換と加工をすることをやったりしてた。 だいたいゼロからやってまぁなんとかかんとかあって、λのメモリ制限だったかに引っかかって問題になったことだけは把握してる。 あとはMoneyForwardを辞めて、なんやかんやでメルカリという会社に入った。

メルカリについて

メルカリではUSプロダクトのバックエンドエンジニアとして日々英語を勉強している。あとCRM周りのことをやっていた気がする。 10月から同じチームに非日本語話者の新卒が二人入り、ますます英語の勉強が加速し、ファンタスティック・ビーストの主人公の英語の聞き取りやすさに感動を覚えたりもした。 ちなみにTOEICをウケるだけウケるというOKRを設定したので(Objectiveとしては「fluent communication」だったかな)、9月に受けた。結果、530点くらいだった気がする。535だっけ? そんなもん。 社内英語会話力検定試験みたいなのでは、文法がだめ、流暢に話せてる雰囲気だけはある、みたいな感じだったので、案の定文法が駄目の駄目の駄目だった。

英語が話せないのにUSプロダクト?

いろいろあったんじゃ…… とはいえ、ソフトウェアエンジニアとして振る舞っていくのに英語はあったほうが良いのはそうっぽいし、いい感じに勉強にも身が入る(勉強しないと話せないというヤバさ)ので良い環境。まぁ特に新卒氏にはオレのつたない英語につきあってくれて(付き合ってくれてるのかな……)ありがたく思っておりですね。はい。 とはいえ、英語の文章に対する抵抗はかなり減ったし、Wikipediaの英語版もとりあえずはそれなりにMacの辞書機能を使いながら、知らない言い回しをGoogleに投げ込みながら読めるくらいにはなった。つまり誰でも読めるので英語にウッてならなければよい話だった。

USプロダクトを触っているのは、MoneyForwardの前、Sansan時代のチームメンバーがそこにいて、その縁でちょうどバックエンドエンジニアを採用するタイミングに現れたっぽく、キャッチしてもらった。 出張も行った。牛肉がうまかった

メルカリという会社は?

サービス開発に不必要なことはとことん省エネ化されてて良い。仕組みでカバー。自動化でカバーという風潮がある。僕が入社から半年でやった申請は 1. 出張の経費申請 2. ディスプレイの買換

くらい。どちらもノールック承認なので、特に手間でもない。採用会食を行ったりしたが、その経費申請も不要な素晴らしい取組により、ノー申請。最高。 事務手続きが少ないというだけで「サービス開発外の不満」がたまらないので楽ちん。

猫がかわいいのは引き続き真実。写真は様々なところに上がっているが、だいたい現物を見ると「イメージよりデカイw」という感じらしい。 確かにデカイ。大丈夫なのかな……

ゲームについて

LoL と Cities Skylines を軸として日々のゲーム人生を謳歌している。 LoLはメルカリに部活があったんだけど、NonActiveだったので、Activeにしたようなきがする。まだまだActiveメンバーは少ないけど、もっと増やしたい。

LoLと英語

相性が良い。そもそも、LoLは国外のほうが盛り上がっていることもあって、グローバルメンバーが増えている会社においてはいい感じに人がちょこちょこ増えている。 最近良くVCをしているメンバーは、僕以外がそもそも日本語が母国語ではないメンバーで、僕が頑張って英語を喋っているので、だいたいみんな英語でしゃべる。 何が良いかというと、ゲームを同じタイミングでやっていてコンテクストの共有が済んでいるので、話が通じやすいし相手の話もわかりやすい。いい勉強になっている。

おわりに

特になんでもなかった。特に面白い話もなかった。 あ、PHPとGoを書いてます。PHPは7.2だっけ? たしかそのへんで、いい感じです。 GoはMicroservicesの文脈で触っていて、なんとなくわかった。ちゃんとはかけない。

あと、なんかのタイミングでSwiftとKotlinを触ったので立派なアプリエンジニアです(楽しかった(小並感))

まぁ来年もがんばります。

Twigを外部Templateファイル無しで使う

この記事で分かること

Twigの亜空間殺法(嘘)

背景

自前でTemplateEngineみたいなことをしようとしたときに、そもそもPHPでTwig使ってるんだからそれ使えば良くない? ってなったので、調べた。結論を言うとやりたいことより複雑性が高かったので今回採用はしなかった。

Twig はこちら

Twig for Developers - Documentation - Twig - The flexible, fast, and secure PHP template engine

コード

なんかPHPシンタックスハイライト効いてるのか効いてないのかわからん……

use Twig_Loader_Array;
use Twig_Environment;

$targetTxt = "You are {{ replaceText }} ?";

$loader = new Twig_Loader_Array(['index.html' => $targetText]);
$twig = new Twig_Environment($loader);
try {
    $res = $twig->render('index.html', ['replaceText' => 'hogehogewww']);
} catch(Exception $e) {
    var_dump($e);
}

$res; 
// "You are hogehogewww ?"

Twig_Loader_Array でインメモリに index.html をKeyとしてTemplateを束縛。 束縛したTempleteをRenderするときに連想配列で置換したい内容を挿入。

なるほどなぁ。

マネーフォワードを退職した

この記事でわかること

とくにないです

背景

退職した。退職エントリだがレギュレーションとかを遵守した(つもりになった)やつは社内向けに書いた。 興味がある人はマネーフォワードへ入社してみてください。esaに多分残ってます。残ってるよね……?

内容

5/31 が退職日です。 5/25 が最終出社日です。

間がなかったのは有給がもうなかったからです。

6/1 からは新しい場所で働き始めます。

MFで学んだこと、知ったこと、なるほどなぁと思ったこと一覧です

  1. お金に対する忌避感を拭えた話
  2. 真っ向から技術力で殴ることの大切さと、難しさ
  3. 現金はある程度あればよくて、ほかはガンガン投資に回すとよさそうなこと(投機じゃないよ)
  4. 職業お金持ちは職業お金持ちなんだなぁということ
  5. 田町という街
  6. 事業の大切さ
  7. 本音と建前と社内と社外と……そんな様々な境界面にこそなにか大切なことがありそうだなぁという軽い気持ち
  8. 情報が閲覧できることの大切さ
  9. 自由と責任
  10. お金は大切

おわりに

おすすめのETF教えてください

回文かどうかを判定するやつを作ってた

この記事でわかること

github.com

背景

github.com

↑がすごいいいなーってなった。 回文についてもなんか雰囲気判定したくなった。

なので作った。

まとめ

よかった!おわり!きもちぃー!ふぅー!

zshrcに書いてある適当な関数を紹介する

この記事でわかること

特になし

git で upstream と merge する関数

なんで?

fork branch 運用をしていると originhaito/repo で、 upstreamoriginal/repo になる。 定期的に original/repo から持ってこないとたいへんなことになる。

関数

function upstream_update() {
    branch=$1
    git add . || git commit -m "upstream update cunked!" > /dev/null
    git checkout $branch > /dev/null
    echo "fetching upstream..."
    git fetch upstream
    echo "merging upstream..."
    git merge "upstream/${branch}"
}
alias uu="upstream_update"

これを uu develop とすると、 current branch は適当に commit して、 develop に checkout して、upstream の develop と merge する。

小文字英数字の uuid を発行する

なんで?

rubySecureRandom.uuid と振る舞いを合わせた uuidgen

関数

function lower_uuid_gen() {
    tr '[A-Z]' '[a-z]' <<< `uuidgen`
}

このまま実行するだけ

雑にメモ帳を開く

なんで?

わからない……俺たちは terminal の呪縛から解き放たれた……はずなんだ……

関数

function je() {
    filename=$1

    e /tmp/$filename
}

junk emacs の略だと思う。 いろんなOneNoteとか、BearとかEverNoteとかいろいろ試したけど、何故か無理なのでこうなった。

Vue + slick (jquery plugin) でハマった話

この記事でわかること

背景

気持ちとしてはこういうことをしたかった。

vueシンタックスハイライトが効かなかったので別々で……

<template>
  <div class='cards is-slick'>
    <div class='card' v-for="element in elements">
      <element></element>
    </div>
  </div>
</template>
export default Vue.extend({
  data() {
    return {
      elements: []
    }
  },
  methods: {
    async getElements(){ HTTP.get('/api/elements').map(x => Element.new(x)) }
  },
  // mounted か created かはともかく
  async created() {
    this.elements = await getElements()
  }
})

雰囲気はわかってくれると思う。雰囲気はね。 で、問題は <div class='cards is-slick'>slick でカルーセルにしたい。

なので、とりあえずこうやって見る

// ...
mounted() {
  $('.is-slick').slick()
}

これは動かない。何故かと言うと、 slick はそのイベントが発火した時点のdivしか補足できず、新たに読み込まれた div はカルーセルの対象外になってしまうためだ。 つまり

  <div class='cards is-slick'>
    <div class='card' v-for="element in elements">
    </div>
  </div>

この状態で slick() を発火しても意味がないということだ。 これをどうにかしたい。

ずさんな解決方法

つまり、全てのElementが用意された時点で slick() を発火してやれば良い。

export default Vue.extend({
  data() {
    return {
      elements: [],
      isSlicked: false
    }
  },
  methods: {
    async getElements(){ 
      await HTTP.get('/api/elements').map(x => Element.new(x)) 
    }
  },
  async created() {
    this.elements = await getElements()
  },
  watch: {
    elements(vals, oldVals) {
      if (!this.isSlicked) return;

      $('.is-slick').slick('unslick')
      this.isSlicked = false
    }
  }
  updated() {
    if(!this.isSliced) {
      $('.is-slick').slick()
    }
/**  2018/02/16 11時05分 更新
    this.$nextTick(() => {
      try {
        // 既に slick() してあるElementに対してもう一度発火すると落ちる。
        // 逆に、まだ slick() していないElementに対して `unslick` を発火しても落ちる。
        // ほんに〜〜〜〜〜?????????
        $('.is-slick').slick('unslick')
      } catch(_){ 
      } finally {
        $('.is-slick').slick()
      }
*/
    })
  }
})

updated$nextTick() を使うと全てのRenderが完了してから呼ばれる。 これで解決したけど、ずさんすぎる気がする。 たぶんComponentのライフサイクルの設計が間違っているんだと思う。

誰か良さげな回避策を教えて……ください……

  • そもそも slick を利用しないという選択
  • ルーセルくらい自前で作れるだろ感
  • etc...