こんにちは7月に入社した開発部の志賀です。
今回は前回の「【新入社員】カルテット流!OOP研修の全貌〜取り組み編〜」に引き続き、技術編です!
本記事では、研修で個人的に学んだこと(技術)をお届けします。
オブジェクト指向研修で学んだこと
- 「Tell, Don’t Ask.」の原則
- テストコード
- PhpStorm機能
1. 「Tell, Don’t Ask.」の原則
恥ずかしいことに、この原則を知りませんでした
OOPにおけるクラス設計の原則で、「オブジェクトに状態を尋ねて振る舞いを決定するのではなく、オブジェクト自身に振る舞いを決定させるべき」というものです。
具体的には、ある処理をする時に、「その処理に必要な情報をオブジェクトから取り出す(getter)ことはしないで、情報を持ったオブジェクト自身にその処理をさせる」ということになります。
この原則を使うメリットとして、以下のようなことがあると思います。
- getter/setterが減り、メンバ変数のアクセス制御ができる → 疎結合にできる
- データを持つオブジェクト内で処理が完結するので、使う側は何も知らなくていい → 変更時に影響を受けない
タピオカミルクティー屋さんで考える
あるところに、一種類のタピオカミルクティーのみを売っているタピオカミルクティー屋さんがありました。
もしも、突然タピオカを使わない、ただのミルクティー屋さんに方向転換する場合どうでしょうか?
(コードにしてみると、突っ込むところは色々あると思いますが見逃してください!!!)
「ask」なタピオカミルクティー屋さん
Shopクラス
がTapiocaMilkTeaオブジェクトよりタピオカ・紅茶・牛乳の金額を取り出し、Shopクラス
がその合計金額を計算しています。
class Shop
{
public function sell(Drink $drink): int
{
return $drink->getTapioca()->getPrice()
+ $drink->getTea()->getPrice()
+ $drink->getMilk()->getPrice();
}
}
class TapiocaMilkTea implements Drink
{
private $tapioca, $tea, $milk;
public function __construct(Tapioca $tapioca, Tea $tea, Milk $milk)
{
$this->tapioca = $tapioca;
$this->tea = $tea;
$this->milk = $milk;
}
public function getTapioca(): Tapioca{...}
public function getTea(): Tea{...}
public function getMilk(): Milk{...}
}
「tell」なタピオカミルクティー屋さん
Shop::sell()
で行なっていた加算処理が、TapiocaMilkTea::getTotalPrice()
に移植されただけですが、そのおかげでShopクラス
はTapiocaMilkTeaクラス
のそのメソッドを呼ぶだけで、
「データ(金額)」も「計算処理内容」も何も知らない状態にすることができます。
class Shop
{
public function sell(Drink $drink): int
{
return $drink->getTotalPrice();
}
}
class TapiocaMilkTea implements Drink
{
private $tapioca, $tea, $milk;
public function __construct(Tapioca $tapioca, Tea $tea, Milk $milk){...}
public function getTotalPrice(): int
{
return $this->tapioca->getPrice()
+ $this->tea->getPrice()
+ $this->milk->getPrice();
}
// getTapioca()・getMilk()・getTea()が不要になりました!
}
ミルクティー屋さんに方向転換したら、、、
「ask」なタピオカミルクティー屋さんは、Shopクラス
・TapiocaMilkTeaクラス
の両方の変更が必要。
「tell」なタピオカミルクティー屋さんは、TapiocaMilkTeaクラス
のみの変更でOK!
2. テストコード
カルテットではフロントエンドもバックエンドも必須でテストを書いています
私自身、業務ではテストコードを書いたことがなく、個人的に書いた経験も少ししかありませんでした。そのため、テストコードを書くことに苦戦しました。
しかし研修を通して、テストコードを書くことに少しは慣れることができたのと、そもそもテストコードがなぜ大事なのかといった部分も感じ取ることができました。
具体的には、例えば以下のようなことを学びました。
- データプロバイダーを見やすくする方法
- Prophecyを使ったモックの作成
- テストできないコードの問題点
データプロバイダーを見やすくする方法
データプロバイダーを使うと複数のテストデータが実行できるので便利ですよね〜
ただ、どういうことを意図したテストデータなのか分からなくなることがありました。
そんな時は、以下のことをすると見やすくなります!!!
- 配列のキーをコメント代わりに使う →
タピオカ抜き
、甘さ控えめ
、氷なし
- 変数名で具体的に明示する →
$expectedTotalPrice
、$optionPrice
/**
* @dataProvider dataProvider
*/
public function test(int $expectedTotalPrice, int $optionPrice): void
{
$actual = 500 + $optionPrice;
$this->assertEquals($expectedTotalPrice, $actual);
}
public function dataProvider(): array
{
return [
'タピオカ抜き' => [450, -50],
'甘さ控えめ' => [400, 0],
'氷なし' => [600, +100],
];
}
さらにこの方法を使うと、どこのテストが失敗したのか一目瞭然になります
今回の例では、甘さ控えめ
のテストが失敗したことがパッとわかりますね!
.F. 3 / 3 (100%)
Time: 26 ms, Memory: 4.00 MB
There was 1 failure:
1) path\to\path::test with data set "甘さ控えめ" (400, 0)
Failed asserting that 500 matches expected 400.
Prophecyを使ったモックの作成
私自身、createMock()
しか使ったことが無く、逆にProphecyを使ったテストコードを書いたことがなかったので、最初は違和感がありました。
しかし、慣れるとProphecy
の方が$prophecy->関数名(関数の引数)->willReturn(返り値)
という風に直感的にかけるので使いやすいです
createMock()
を使ったテストコード
public function test(): void
{
$drink = $this->createMock(Drink::class);
$drink->method('hasTapioca')->willReturn(true);
$SUT = new Shop($drink);
$this->assertTrue($SUT->hasFatStraw());
}
Prophecy
を使ったテストコード
public function test(): void
{
$drink = $this->prophesize(Drink::class);
$drink->hasTapioca()->willReturn(true);
$SUT = new Shop($drink->reveal());
$this->assertTrue($SUT->hasFatStraw());
}
テストできないコードの問題点
テストコードを書こうとしてみたら、依存クラスをモックすることができずユニットテストにならないということが何度かありました。
これは、「クラス内でインスタンスを生成している」ことが問題でした。
この場合、「依存を注入する」ように設計し直すと、依存クラスをモックしてちゃんとユニットテストができるということが分かりました
変更前(テストできない)
class Shop
{
private $drink;
public function __construct()
{
$this->drink = new TapiocaMilkTea();
}
public function sell(): int
{
return $this->drink->getTotalPrice();
}
}
class ShopTest
{
//TapiocaMilkTeaクラスに依存しまくりのテストしかかけない。
}
変更後(テストができる)
class Shop
{
private $drink;
public function __construct(Drink $drink)
{
$this->drink = $drink;
}
public function sell(): int
{
return $this->drink->getTotalPrice();
}
}
class ShopTest
{
public function test()
{
//Drinkをモックにできるので、他クラス(Drinkクラス)に依存せずShopクラスだけのテストができる。
$totalPrice = 500;
$drink = $this->prophesize(Drink::class);
$drink->getTotalPrice()->willReturn($totalPrice)->shouldBeCalled();
$SUT = new Shop($drink->reveal());
$this->assertSame($totalPrice, $SUT->sell());
}
}
前者のようなテストができないクラスは、依存クラスと密結合してしまっていて、先述の「ミルクティー屋に転身する」時はShopクラス
のコンストラクタ内のnew TapiocaMilkTea()
をnew MilkTea()
に変更する必要があります。
一方、変更後のコードでは、Shopクラス
の変更の必要がなく、仕様変更に強い設計になっています。
テストコードを書くことで、プロダクトコードの設計の悪いところを見つけることもできるんだなとしみじみと感じました
3. PhpStorm機能
カルテット開発部では、エディターにPhpStorm(IntelliJ IDEAも)を使用しています。
ライセンス手当があるので、PHPerにはとても良い環境です
(Visual Studio Codeを使用している方もいます!)
命名の変更
リファクタリングとなると、変数名や関数名の変更が結構あると思います。
そのとき、以前までの私は全検索(Command + Shift + F
)をして愚直に変更していました。この方法では、変更し忘れている箇所があるためにテストが通らなかったり、レビューの際に「ここ名前変わってないよー」とご指摘いただくことがありました。(こんなミスすんなよって感じですよね、メンターの方ごめんなさい)
そんなことで、みなさんご存知かもしれませんが、PhpStormの「Rename機能」を教えていただきました。
使い方
- 変更したい変数名(または関数名)を選択する
- 左クリック後、「Refactor > Rename…」( Shift + F6 )を選択する
- 変更後の変数名(または関数名)を入力する
該当する変数のgetter(getHogeのような関数)があると、その箇所も変更するかを聞いてくれます
また、関数名変更の場合、他ファイルでの該当箇所も書き換えてくれます。
さすがPhpStormですもっと頼ろう!!!
さいごに
タピオカミルクティーに関して、個人的にミルクティーだけでタピオカ入ってない方が好きなためこんな記事になってしまいました。
OOP研修後、リファクタリング前と後の差分を眺めていると、自身の成長を感じてなんだか誇らしげな気持ちになりました
研修では、個人のレベルに合わせて丁寧に研修をしていただきました。本当にありがとうございました
前回の「カルテット流!OOP研修の全貌〜取り組み編〜」含め本記事で、カルテット開発部について雰囲気だけでも感じていただけたでしょうか?
現在、カルテット開発部(フロントエンドエンジニア / バックエンドエンジニア問わず)積極募集中です
カルテット開発部について、「ちょっとでも気になった」という方!Slackにて気軽に質問することができます!!(質問だけでもOKです)
本記事を読んで、弊社エンジニアのお仕事に興味を持っていただけたら幸いです。