みなさんこんにちは! Server Team Badooの開発者であるAlexey Grezovです。 Badooでは、コードを維持、開発、および再利用しやすくするように常に努力しています。これらのパラメーターに依存して、どの機能を迅速かつ効率的に実装できるかが決まるからです。 この目標を達成する1つの方法は、間違いを許さないコードを書くことです。 最も厳密なインターフェイスは、呼び出しの順序を間違えません。 内部状態の最小数は、期待される結果を保証します。 先日、これらのメソッドのアプリケーションが開発者の生活を簡素化する方法を説明した記事を見ました。 それで、「ポカヨケ」の原理に関する記事の翻訳をあなたの注意にもたらします。
中規模または大規模のチームでコードを操作する場合、他の人のコードを理解して使用するのが困難になることがあります。 この問題にはさまざまな解決策があります。 たとえば、特定のコーディング標準に従うことに同意したり、チーム全体が知っているフレームワークを使用したりできます。 ただし、多くの場合、これは十分ではありません。特に、バグを修正したり、古いコードに新しい関数を追加する必要がある場合はなおさらです。 特定のクラスが何のために意図されていたのか、それらがどのように個別にそして共同で動作するべきかを思い出すことは困難です。 このような場合、気付かないうちに誤って副作用や間違いを追加する可能性があります。
これらのエラーはテスト中に検出できますが、実際に運用に移行する可能性があります。 そして、それらが検出されたとしても、コードをロールバックして修正するにはかなり時間がかかる場合があります。
それでは、どうすればこれを防ぐことができますか? ポカヨケ原理を使用します。
ポカヨケとは?
ポカヨケは、日本語では「ミスプロテクション」(エラー保護)と略される英語の用語であり、ロシア語版では「愚か者に対する保護」としてよく知られています。 この概念はリーン製造で生まれました。ここでは、機器のオペレーターがミスを回避するのに役立つメカニズムを指します。
製造以外にも、ポカヨケは家電製品でよく使用されます。 たとえば、SIMカードの場合、非対称形状のため、右側のアダプターにしか挿入できません。
反対の例(ポカヨケの原理を使用しない)はPS / 2ポートで、キーボードとマウスの両方で同じコネクタ形状をしています。 それらは色によってのみ区別できるため、混同しやすいです。
ポカヨケの別の概念は、プログラミングで使用できます。 アイデアは、コードのパブリックインターフェイスをできる限りシンプルかつ簡単にし、コードが誤って使用されるとすぐにエラーを生成することです。 これは明らかなように思えるかもしれませんが、実際には、そうではないコードに遭遇することがよくあります。
ポカヨケは意図的な虐待を防ぐためのものではないことに注意してください。 目標は、偶発的なエラーを回避することのみであり、悪意のある使用からコードを保護することではありません。 何らかの方法で、誰かがあなたのコードにアクセスできる限り、本当に望むならいつでもヒューズをバイパスできます。
コードのエラー耐性を高めるための具体的な対策について説明する前に、ポカヨケメカニズムを2つのカテゴリに分類できることを知っておくことが重要です。
- エラー防止
- エラー検出。
エラー防止メカニズムは、エラーを早期に排除するのに役立ちます。 インターフェイスと動作を可能な限り簡素化することにより、誤ってコードを誤って使用することがないようにします(SIMカードを使用した例を思い出してください)。
一方、エラー検出メカニズムはコードの外部にあります。 彼らは私たちのアプリケーションを監視して、起こりうるエラーを追跡し、それらについて警告します。 例としては、PS / 2ポートに接続されたデバイスが正しいタイプかどうかを判断し、そうでない場合は、なぜ動作していないのかをユーザーに伝えるソフトウェアがあります。 そのようなソフトウェアは、コネクタが同じであるため、エラーを防ぐことはできませんでしたが、それを検出して報告することができます 。
次に、アプリケーションのエラーの防止と検出の両方に使用できるいくつかの方法を見ていきます。 ただし、このリストは出発点にすぎないことに注意してください。 特定のアプリケーションによっては、コードのエラー防止を強化するために追加の対策が講じられる場合があります。 さらに、プロジェクトにpoka-yokeを実装することも重要です。アプリケーションの複雑さとサイズによっては、潜在的なエラーのコストと比較して高すぎる場合があります。 したがって、どの手段が最適であるかを決定するのは、ユーザーとチーム次第です。
エラー防止の例
型宣言
以前はPHP 5で型ヒントとして知られていましたが、型宣言はPHP 7で関数やメソッドを呼び出すときのエラーから保護する簡単な方法です。関数の引数に特定の型を割り当てることで、この関数を呼び出すときに引数を混乱させることが難しくなります。
たとえば、ユーザーに送信できる通知を見てみましょう。
<?php class Notification { private $userId; private $subject; private $message; public function __construct( $userId, $subject, $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() { return $this->userId; } public function getSubject() { return $this->subject; } public function getMessage() { return $this->message; } }
型宣言がないと、誤って誤った型の変数を渡す可能性があり、アプリケーションを混乱させる可能性があります。 たとえば、 $userId
はstring
であると仮定できstring
、実際にはint
である可能性があります。
コンストラクタに間違った型を渡すと、アプリケーションがこの通知で何かを実行しようとするまで、エラーはおそらく気付かれません。 そして、現時点では、おそらくint
代わりにstring
を渡すコードを指すものが何もないという不可解なエラーメッセージが表示されます。 したがって、開発中にできるだけ早くこのようなエラーを検出するために、通常はできるだけ早くアプリケーションをクラッシュさせることが推奨されます。
この特定のケースでは、型宣言を追加するだけで済みます。間違った型のパラメーターを渡そうとすると、PHPは停止し、致命的なエラーをすぐに警告します。
<?php declare(strict_types=1); class Notification { private $userId; private $subject; private $message; public function __construct( int $userId, string $subject, string $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : int { return $this->userId; } public function getSubject() : string { return $this->subject; } public function getMessage() : string { return $this->message; } }
デフォルトでは、PHPは無効な引数を期待される型にキャストしようとすることに注意してください。 これを防ぎ、致命的なエラーが生成されるのを防ぐには、厳密な型指定( strict_types
)を有効にすることが重要です。 このため、スカラー型を宣言することはポカヨケの理想的な形ではありませんが、エラーを減らすための良い出発点として役立ちます。 厳密な型指定が無効になっている場合でも、型宣言は、引数にどの型が期待されるかについての手がかりになる可能性があります。
さらに、メソッドの戻り値の型を宣言しました。 これにより、特定の関数を呼び出すときに期待できる値を簡単に判断できます。
明確に定義された戻り値の型は、戻り値を操作するときに多くのswitchステートメントを避けるためにも役立ちます。明示的に戻り値の型を宣言しなくても、メソッドは異なる型を返すことができます。 したがって、私たちのメソッドを使用している人は、特定のケースでどのタイプが返されたかを確認する必要があります。 明らかに、 switch
を忘れることができますが、これは検出が困難なエラーにつながります。 ただし、関数の戻り値の型を宣言すると、これらはあまり一般的ではなくなります。
値オブジェクト
型宣言では解決できない問題は、複数の関数引数があると、呼び出されたときにそれらの順序が混乱する可能性があることです。
引数の型が異なる場合、PHPは引数の順序の違反について警告することができますが、同じ型の引数が複数ある場合は機能しません。
この場合のエラーを回避するために、 値オブジェクトで引数をラップできます 。
class UserId { private $userId; public function __construct(int $userId) { $this->userId = $userId; } public function getValue() : int { return $this->userId; } } class Subject { private $subject; public function __construct(string $subject) { $this->subject = $subject; } public function getValue() : string { return $this->subject; } } class Message { private $message; public function __construct(string $message) { $this->message = $message; } public function getMessage() : string { return $this->message; } } class Notification { /* ... */ public function __construct( UserId $userId, Subject $subject, Message $message ) { $this->userId = $userId; $this->subject = $subject; $this->message = $message; } public function getUserId() : UserId { /* ... */ } public function getSubject() : Subject { /* ... */ } public function getMessage() : Message { /* ... */ } }
引数は非常に特殊なタイプであるため、混同することはほぼ不可能です。
スカラー型の宣言よりも値オブジェクトを使用することのもう1つの利点は、各ファイルで厳密な型指定を有効にする必要がなくなったことです。 そして、これを覚える必要がなければ、それを忘れることはできません。
検証
値オブジェクトを使用する場合、オブジェクト自体の内部でデータをチェックするロジックをカプセル化できます。 したがって、無効な状態の値オブジェクトの作成を防ぐことができます。これは、アプリケーションの他のレイヤーで将来問題を引き起こす可能性があります。
たとえば、 UserId
が常に正であるというルールがあります。 入力としてUserId
を取得するたびに確認できますが、一方で、 UserId
かで簡単に忘れることもあります。 また、この忘れっぽさがアプリケーションの別のレイヤーで実際のエラーにつながる場合でも、エラーメッセージから実際に何が悪かったのかを理解することは難しく、デバッグが複雑になります。
このようなエラーを防ぐために、 UserId
コンストラクターに検証を追加できUserId
。
class UserId { private $userId; public function __construct($userId) { if (!is_int($userId) || $userId < 0) { throw new \InvalidArgumentException( 'UserId should be a positive integer.' ); } $this->userId = $userId; } public function getValue() : int { return $this->userId; } }
したがって、 UserId
オブジェクトUserId
するときに、正しい状態になっていることを常に確認できます。 これにより、アプリケーションのさまざまなレベルでデータを常にチェックする必要がなくなります。
ここでは、 is_int
を使用する代わりにスカラー型宣言を追加できますが、これis_int
、 UserId
使用されている場合UserId
is_int
強い型指定を有効にすることに注意してください。 これが行われない場合、PHPは他の型がUserId
として渡されるたびにint
にキャストしようとしUserId
。 これは問題になる可能性があります。たとえば、ユーザーIDは通常float
はないため、 float
渡すと誤った変数になる可能性があるためです。 他の場合、たとえばPrice
オブジェクトを操作できる場合、PHPはfloat変数をint
自動的に変換するため、厳密な型指定を無効にすると丸めエラーが発生する可能性があります。
不変性
デフォルトでは、PHPのオブジェクトは参照渡しされます。 つまり、オブジェクトに変更を加えると、アプリケーション全体で即座に変更されます。
このアプローチには利点がありますが、いくつかの欠点があります。 SMSと電子メールを介してユーザーに送信される通知の例を考えてみましょう。
interface NotificationSenderInterface { public function send(Notification $notification); } class SMSNotificationSender implements NotificationSenderInterface { public function send(Notification $notification) { $this->cutNotificationLength($notification); // Send an SMS... } /** * Makes sure the notification does not exceed the length of an SMS. */ private function cutNotificationLength(Notification $notification) { $message = $notification->getMessage(); $messageString = substr($message->getValue(), 160); $notification->setMessage(new Message($messageString)); } } class EmailNotificationSender implements NotificationSenderInterface { public function send(Notification $notification) { // Send an e-mail ... } } $smsNotificationSender = new SMSNotificationSender(); $emailNotificationSender = new EmailNotificationSender(); $notification = new Notification( new UserId(17466), new Subject('Demo notification'), new Message('Very long message ... over 160 characters.') ); $smsNotificationSender->send($notification); $emailNotificationSender->send($notification);
Notification
オブジェクトは参照によって渡されるため、意図しない副作用が発生しました。 SMSNotificationSender
メッセージの長さをSMSNotificationSender
関連するNotification
オブジェクトがアプリケーション全体で更新されたため、後でEmailNotificationSender
に送信されたときにメッセージも切り捨てられました。
これを修正するには、 Notification
オブジェクトを不変にします。 変更を行うためのsetメソッドを提供する代わりに、これらの変更を行う前に元のNotification
コピーを作成するメソッドを追加します。
class Notification { public function __construct( ... ) { /* ... */ } public function getUserId() : UserId { /* ... */ } public function withUserId(UserId $userId) : Notification { $c = clone $this; $c->userId = clone $userId; return $c; } public function getSubject() : Subject { /* ... */ } public function withSubject(Subject $subject) : Notification { $c = clone $this; $c->subject = clone $subject; return $c; } public function getMessage() : Message { /* ... */ } public function withMessage(Message $message) : Notification { $c = clone $this; $c->message = clone $message; return $c; } }
これで、たとえばメッセージの長さを短くするなど、 Notification
クラスを変更すると、アプリケーション全体に適用されなくなり、さまざまな副作用を防ぐことができます。
ただし、PHPでは、オブジェクトを真に不変にすることは(不可能ではないにしても)非常に難しいことに注意してください。 しかし、コードをよりエラー耐性にするためには、クラスのユーザーが変更を行う前にオブジェクトのクローンを作成する必要性を覚える必要がなくなるため、setメソッドの代わりに「不変の」withメソッドを追加するだけで十分です。
nullオブジェクトを返す
時々、何らかの値またはnull
返すことができる関数やメソッドに出くわしnull
。 そして、これらのnull戻り値は問題になる可能性があります。ほとんどの場合、null値を使用して処理する前にnull値をチェックする必要があるからです。 これも忘れがちです。
戻り値を確認する必要をなくすために、代わりにnullオブジェクトを返すことができます。 たとえば、割引付きまたは割引なしのShoppingCart
がある場合があります。
interface Discount { public function applyTo(int $total); } interface ShoppingCart { public function calculateTotal() : int; public function getDiscount() : ?Discount; }
applyTo
メソッドを呼び出す前にShoppingCartの最終値を計算する場合、 getDiscount(): null
関数がgetDiscount(): null
または割引を返したことを常に確認する必要があります。
$total = $shoppingCart->calculateTotal(); if ($shoppingCart->getDiscount()) { $total = $shoppingCart->getDiscount()->applyTo($total); }
このチェックが実行されない場合、 getDiscount()
がnull
返すときにPHP警告やその他の副作用がgetDiscount()
null
。
一方、割引が提供されていないときにnullオブジェクトを返すと、これらのチェックを回避できます。
class ShoppingCart { public function getDiscount() : Discount { return !is_null($this->discount) ? $this->discount : new NoDiscount(); } } class NoDiscount implements Discount { public function applyTo(int $total) { return $total; } }
さて、 getDiscount()
を呼び出すと、たとえ割引がなくても、常にDiscount
オブジェクトを取得します。 したがって、割引が適用されない場合でも、合計金額に割引を適用でき、 if
は不要になります。
$total = $shoppingCart->calculateTotal(); $totalWithDiscountApplied = $shoppingCart->getDiscount()->applyTo($total);
オプションの依存関係
nullの戻り値を避けたいのと同じ理由で、すべての依存関係を単に必須にすることで、オプションの依存関係も削除したいと考えています。
たとえば、次のクラスを考えます。
class SomeService implements LoggerAwareInterface { public function setLogger(LoggerInterface $logger) { /* ... */ } public function doSomething() { if ($this->logger) { $this->logger->debug('...'); } // do something if ($this->logger) { $this->logger->warning('...'); } // etc... } }
2つの問題があります。
-
doSomething()
メソッドでロガーを常にチェックする必要があります。 - サービスコンテナでSomeServiceクラスを構成するときに、ロガーの構成を忘れたり、クラスがこれを実行できることをまったく知らない場合があります。
LoggerInterface
必須の依存関係にすることで、コードを簡素化できます。
class SomeService { public function __construct(LoggerInterface $logger) { /* ... */ } public function doSomething() { $this->logger->debug('...'); // do something $this->logger->warning('...'); // etc... } }
これで、パブリックインターフェイスの煩雑さがSomeService
、誰かがSomeService
新しいインスタンスを作成するたびに、クラスがLoggerInterface
インスタンスを必要とすることがLoggerInterface
、指定を忘れることができなくなります。
さらに、ロガーの存在を常に確認する必要性をなくしました。これにより、 doSomething()
が理解しやすくなり、誰かが変更を行うたびにエラーの影響を受けにくくなります。
ロガーなしでSomeService
を使用したい場合、nullオブジェクトを返す場合と同じロジックを適用できます。
$service = new SomeService(new NullLogger());
結果として、このアプローチはオプションのsetLogger()
メソッドを使用するのと同じ効果がありますが、コードを簡素化し、依存性注入コンテナーでのエラーの可能性を減らします。
パブリックメソッド
コードを使いやすくするには、クラス内のパブリックメソッドの数を制限することをお勧めします。 その後、コードの混乱が少なくなり、リファクタリング時に下位互換性を拒否する可能性が低くなります。
トランザクションのアナロジーは、パブリックメソッドの数を最小限に抑えるのに役立ちます。 たとえば、2つの銀行口座間の送金を考えてみましょう。
$account1->withdraw(100); $account2->deposit(100);
トランザクションを使用するデータベースは、補充ができない場合(またはその逆)に引き出しをキャンセルできますが、 $account1->withdraw()
または$account2->deposit()
呼び出しを忘れないようにすることはできません。誤った操作へ。
幸いなことに、2つの個別のメソッドを1つのトランザクションに置き換えることで、これを簡単に修正できます。
$account1->transfer(100, $account2);
その結果、トランザクションを部分的に完了することでミスを犯すことがより困難になるため、コードの信頼性が高まります。
エラー検出の例
エラー検出メカニズムは、それらを防止するようには設計されていません。 問題が発見された場合にのみ問題を警告する必要があります。 ほとんどの場合、アプリケーションの外部にあり、特定の間隔で、または特定の変更後にコードをチェックします。
単体テスト
ユニットテストは、新しいコードが正しく機能することを確認するための優れた方法です。 また、誰かがシステムの一部を再編成した後でも、コードが引き続き正しく機能することを確認するのに役立ちます。
ユニットテストの実施を忘れる可能性があるため、 Travis CIやGitLab CIなどのサービスを使用して変更を行う場合は、自動的にテストを実行することをお勧めします。 彼らのおかげで、開発者は何かが壊れたときに通知を受け取ります。これは、変更が意図したとおりに機能することを確認するのにも役立ちます。
エラーの検出に加えて、単体テストは特定のコードを使用する優れた例であり、他の誰かがコードを使用するときにエラーを防止します。
テストカバレッジレポートと突然変異テスト
十分なテストを書くことを忘れてしまう可能性があるため、テスト時にCoverallsなどのサービスを使用して、コードカバレッジに関するレポートを自動的に生成すると便利です。 コードカバレッジが低下するたびに、Coverallsから通知が送信され、欠落しているテストを追加できます。 Coverallsのおかげで、コードカバレッジが時間とともにどのように変化するかを理解することもできます。
十分な単体テストがあることを確認する別の方法は、たとえばHumbugを使用して、突然変異テストを使用することです 。 名前が示すように、彼らは私たちのコードがテストで十分にカバーされているかどうかを確認し、ソースコードをわずかに変更してから、ユニットテストを実行します。
コードカバレッジレポートと突然変異テストを使用して、ユニットテストがエラーを防ぐのに十分であることを確認できます。
静的コードアナライザー
コードアナライザーは、開発プロセスの開始時にアプリケーションのエラーを検出できます。 たとえば、 PhpStormなどのIDEは、コードアナライザーを使用してエラーを警告し、コードを記述するときにヒントを提供します。 エラーは、単純な構文から反復的なコードまでさまざまです。
ほとんどのIDEに組み込まれているアナライザーに加えて、特定の問題を特定するために、サードパーティのアナライザーやカスタムアナライザーをアプリケーションのビルドプロセスに含めることができます。 PHPプロジェクトに適したアナライザーの部分的なリストは、 GitHubにあります 。
SensioLabs Insightsなどのオンラインソリューションもあります。
ロギング
他のほとんどのエラー検出メカニズムとは異なり、ロギングは実稼働環境で実行中のアプリケーションのエラーを検出するのに役立ちます。
もちろん、これには、予期しないことが発生したときにコードがログに書き込むことが必要です。 コードがロガーをサポートしている場合でも、アプリケーションのセットアップ時にロガーを忘れることができます。 したがって、オプションの依存関係は避けてください(上記を参照)。
ほとんどのアプリケーションは少なくとも部分的にログを記録しますが、そこに書き込まれる情報は、 KibanaやNagiosなどのツールを使用して分析および制御すると、非常に興味深いものになります。 彼らは、人々がアプリケーションをテストするときではなく積極的に使用するときに、アプリケーションでどのようなエラーや警告が発生するかを知ることができます。
エラーを抑制しない
エラーを記録する場合でも、エラーの一部が抑制されることがあります。 「回復」エラーが発生した場合、PHPは動作し続ける傾向があります。 ただし、バグはコード内のバグを示す可能性があるため、新機能を開発またはテストするときに役立ちます。 @を使用してエラーを抑制すると 、ほとんどのコードアナライザーが警告を表示するのはこのためです。これにより、アプリケーションの使用後に必然的に再表示されるエラーが隠される可能性があります。
原則として、わずかな警告でもメッセージを受信するには、 error_reporting PHP E_ALL
レベルerror_reporting PHP E_ALL
に設定することerror_reporting PHP E_ALL
します。 ただし、これらのメッセージをどこかに記録し、ユーザーから隠すことを忘れないでください。これにより、アプリケーションアーキテクチャまたは潜在的な脆弱性に関する機密情報にエンドユーザーがアクセスできなくなります。
error_reporting
に加えて、常にstrict_types
を含めることが重要strict_types
これにより、PHPが関数引数を期待される型に自動的にstrict_types
しようとしないため、検出が困難なエラー(たとえば、 float
をint
にキャストする際の丸めエラー)につながる可能性があります。
PHPの外部で使用する
ポカヨケは特定の手法よりも概念であるため、PHPに関係のない分野にも適用できます。
インフラ
インフラストラクチャレベルでは、 Vagrantなどのツールを使用して、 運用環境と同一の共通開発環境を作成することにより、多くのエラーを防ぐことができます。
JenkinsやGoCDなどのビルドサーバーを使用してアプリケーションの展開を自動化すると、変更をアプリケーションに展開する際のエラーを防ぐことができます。
REST API
REST API
を作成するとき、APIの使用を簡素化するためにpoka-yokeを実装できます。 たとえば、URLまたはリクエストの本文で不明なパラメーターが渡されるたびにエラーを返すようにできます。 APIクライアントの「破損」を避けたいので、これは奇妙に思えるかもしれませんが、原則として、開発プロセスの初期段階でエラーが修正されるように、APIを使用している開発者に誤って使用されていることをできるだけ早く警告することをお勧めします
, API color
, -, API, colour
. - , .
, API, , Building APIs You Won't Hate .
- . , . , color
colour
, , .
, . – , .
poka-yoke . , , , . .
おわりに
poka-yoke , , , , . -, , , .
– , , , , , . , , .
, , public- .