技術メモなど

業務や日々のプログラミングのなかで気になったことをメモしています。PHP 成分多め。

PHPで考える単一責任の原則

単一責任の原則(SRP:Single responsibility principle)とは、Robert C.Martin 「アジャイルソフトウェア開発の奥義」*1にまとめられたソフトウェア開発における原則のひとつです。
後年になって、同著者の「Clean Architecture」*2によって以下のように再定義されました。

モジュールは、たったひとつのアクターに対して責務を追うべきである。

本稿では、PHPソースコードを用いながら、この原則(以下SRP)が意味するところについて考えます。

SRP違反の例

 架空の社内アプリケーションを例に考えてみましょう。 以下の Employee は、社員の情報を表すクラスです。

<?php
class Employee
{
    private $code;
    private $name;
    private $email;
    private $privateTelNumber;
    
    public function __construct($code, $name, $email , $privateTelNumber)
    {
        $this->code  = $code;
        $this->name  = $name;
        $this->email = $email;
        $this->privateTelNumber = $privateTelNumber;
    }
    
    public function code(){ return $this->code; }
    public function name(){ return $this->name; }
    public function email(){ return $this->email; }
    public function privateTelNumber(){ return $this->privateTelNumber; }

    public function export(){
        $filename = $this->code . '_' . $this->name . '.csv';
        $arr = $this->profile();
        return File::Csv($filename, $arr);
    }

    public function profile(){
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }    
}

メソッド exportCsv は人事担当者がCSV出力する際に使われます。 メソッド profile は、一般社員も利用するアプリ内で、指定した社員のプロフィール画面を出力する際に使われます。 また、出力項目が同じであることから、exportCsv内でも使われています。

上記のモジュール(Employeeクラス)は、ふたつのアクター(一般社員・人事担当者)が利用しているという点において、SRPに違反しています。

モジュールは、たったひとつのアクターに対して責務を追うべきである。

ソフトウェアにおける責務とは、ソフトウェアの利用者=アクターの要望に対応する責務*3です。アクターの要望に変化があれば、そこにあるソースコードも変更しなればなりません。 その際、SRPが守られず複数のアクターに対し責務を持つモジュールは、変更に対しもろさのあるモジュールということが言えます。

現在CSV出力は社員コードと氏名、メールアドレスが対象となっていますが、さらに個人用電話番号を追加して欲しいという人事の要求がありました。 要求を受けた開発者が以下のように profile を改修しました。

<?php
// 略

    public function profile(){
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
            'privateTelNumber' => $privateTelNumber,
        ];
    }    
}

CSV出力には個人用電話番号が表示されるようになり、当初の要求は満たしています。
一方で、profile は、一般社員も閲覧できるプロフィール画面に利用されているため、一般社員にも見られてしまうおそれが生まれています。「一般社員は個人用電話番号を閲覧してはならない」といった要件が場合、これはバグとなるでしょう。 このように、あるアクターの要求からくる変更が、他の無関係なアクターへと影響を及ぼすことをSRPは警告しています。

SRPに沿った改修

Employeeクラスの役割を、各アクターに対応するように分離します。

<?php
class Employee
{
    private $code;
    private $name;
    private $email;
    private $privateTelNumber;
    
    public function __construct($code, $name, $email , $privateTelNumber)
    {
        $this->code  = $code;
        $this->name  = $name;
        $this->email = $email;
        $this->privateTelNumber = $privateTelNumber;
    }
    
    public function code(){ return $this->code; }
    public function name(){ return $this->name; }
    public function email(){ return $this->email; }
    public function privateTelNumber(){ return $this->privateTelNumber; }
}

class EmployeeCsvPresenter
{
    private $employee;
    
    public function __construct($employee)
    {
        $this->employee = $employee;
    }
    
    public function export()
    {
        $filename = $this->code . '_' . $this->name . '.csv';
        $body =  [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
            'privateTelNumber' => $this->privateTelNumber,
        ];
        return File::Csv($filename, $body);
    }

}

class EmployeeProfilePresenter
{
    private $employee;
    
    public function __construct($employee)
    {
        $this->employee = $employee;
    }
    
    public function profile()
    {
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
        ];
    }    
}

プロフィール画面の処理は EmployeeProfilePresenter に、CSV出力は EmployeeCsvPresenter にそれぞれ分離しました。 このソースコードは、最初のものよりも複雑さが増してします。しかし、アクター1つにつき1つのクラスが対応することによって以下のようなメリットが生まれています。

  • 影響範囲の限定
    ひとつのクラスが複数の役割を持っていると、そのクラスで作業する開発者にとっては改修による影響範囲の境界が見えづらくなります。SRPは、モジュールの役割を分離しその境界を明確にすることによってその影響範囲を限定します。分離された EmployeeCsvPresenter は、仮に改修によるエンバグがあったとしてもプロフィール画面機能に影響を及ぼすことはありません。 また、初めの改修について「そもそもprofileメソッドに手を入れなければよかったのではないか」という意見がありえますが、開発する上でのこういった「どこに手を入れるべきか/入れないべきか」という考慮事項そのものを減らす効果があります。

  • コンフリクトの回避
    もしCSV出力機能、プロフィール画面機能にそれぞれ同時に改修が必要になった場合、複数の開発者がコンフリクトを意識せずそれぞれ独立してソースコードを改修できます。これにより、並行開発における開発者間のコミュニケーションコストを減らします。

モジュール分割の指針

一方で、いざモジュールを無制限に分割しはじめると膨大なクラス群が出来上がり、複雑になります。 SRPは、分割の基準をアクターの存在に求めます。アクターが存在しないことは即ち、変更も影響範囲も存在しないからです。
SRPに即しているかは、ソースコード単体で決まるものではなく、その外部にあるコンテクストによって決まります。もし仮にアクターが人事担当者しかおらず、EmployeeクラスがCSV出力にしか使われないのであれば以下のコードでも問題ないということになります。

<?php
class Employee
{
    private $code;
    private $name;
    private $email;
    private $privateTelNumber;
    
    public function __construct($code, $name, $email , $privateTelNumber)
    {
        $this->code  = $code;
        $this->name  = $name;
        $this->email = $email;
        $this->privateTelNumber = $privateTelNumber;
    }
    
    public function code(){ return $this->code; }
    public function name(){ return $this->name; }
    public function email(){ return $this->email; }
    public function privateTelNumber(){ return $this->privateTelNumber; }

    public function export(){
        $filename = $this->code . '_' . $this->name . '.csv';
        $arr = $this->profile();
        return File::Csv($filename, $arr);
    }

    private function profile(){
        return [
            'code' => $this->code,
            'name' => $this->name,
            'email' => $this->email,
            'privateTelNumber' => $this->privateTelNumber,
        ];
    }    
}

まとめ

  • SRPは、ソフトウェアの複雑さを増す代わりに、モジュールの責務の境界を明確にする。
  • モジュールの責務は、それに対応するアクターの存在によって決まる。
  • モジュールは、たったひとつのアクターに対して責務を追うべきである。

AWS ソリューションアーキテクト-アソシエイトに合格した話

 昨日、AWSソリューションアーキテクト-アソシエイト(SAA)に合格しました!
ので、記念に顛末をざっくりとまとめておきます。

受験前

AWSは、S3、SQS、Lamda、CloudSearchをちょっと触ったことがある程度でした。 試験2週間前の社内勉強会で?マークを連発し、講師に「流石にちょっとマズイんじゃないですかね」と白い目を向けられる程度の能力。 なんとかなってよかったですが、今思ってもヤバイレベルでした。

準備

攻略本として今年1月に出た黒本を購入しました。

www.amazon.co.jp

目次
- 第1章 AWSサービス全体の概要
- 第2章 AWSにおける高可用アーキテクチャ
- 第3章 AWSにおけるパフォーマンス
- 第4章 AWSにおけるセキュリティ設計
- 第5章 AWSにおけるコスト最適化
- 第6章 AWSにおける運用管理

第1章で各種サービスの簡単な解説が一通りあり、第2章以降で分野ごとに深掘りしていく構成です。 同じサービスでも解説が各章に散らばっており、ひとつのサービスを深掘りしたいときにはやや読みづらい印象がありました。 試験が終わった後の感想としては、出題範囲が全体的にきちんと網羅されており、かなりの良書だと思います(本番の試験会場でも他の受験者の皆さんも多くがこの本を片手に試験に臨んでいました)。

わからないところは読み飛ばしつつ一通り目を通し、各章最後の問題集と購入者特典のサンプル模擬試験の問題集をAnkiにつっこんでひたすら解く、わからないところがあったら公式のドキュメントを読むなどして勉強しました。

模擬試験

AWSの公式サイトで模擬試験を受けられるので利用しました。 f:id:shkn:20190329224550p:plain クレジットカードで支払い(税込2160円)を終えればその場で試験開始できます。
結果は正解率と各分野毎の得点割合のみが通知され、答え合わせ等はありません。 本試験とほぼ同じUIで問題文の雰囲気も近いので予行演習としておすすめです。

本試験

自分は銀座の歌舞伎座テストセンターで受験しました。平日午前の受験でしたがなかなか混雑してました。 f:id:shkn:20190329225431p:plain銀座駅直結で迷いにくく、同じ建物の地下階にはタリーズや喫煙所もあるので本番前にいろいろ整えるには良い環境です。 当日の持ち物には、本人確認として顔写真の公的身分証・署名入りのクレジットカードが必要になります。
公式案内にはそれぞれコピーが必要とありましたが、上記の会場では提示だけで大丈夫でした(会場や時期によって異なるようのでちゃんと準備はしておいた方がよさげです)。

本試験は選択形式(4択が多いです)で出題されます。問題毎にフラグチェックをつけられるので、迷った問題を後で見返す時に便利です。
問題の内容は単純にサービスに対する知識を問われるだけではなく、「ある○○の会社が××を導入しようとしています、...」といったような、実際の運用を想定しソリューションアーキテクトとして答えを提示する、という形の問題がおおく出題されます。参考書的な短文の出題に慣れていると初見はかなり面食らうので、問題の雰囲気に慣れるという意味でも事前に模擬試験は受けておいてよかったです。
またそういう文章であるので問題文自体・選択肢どちらも長文が多く、なかなか疲れます。一通り解いた後わからなかった問題を再度確認するにも時間がかかるので、紙とペンがある場合(銀座では最初からもらえます)は2周目以降にそなえ、ありえない選択肢をあらかじめメモっておいて読む時間を短縮した方がよいでしょう。

まとめ

というわけで無事合格することができました。
とはいえほぼ参考書オンリーで勉強したためAWSサービスそのものについてはさほど触っておらず、実践レベルにはまだまだ至ってないな、というのが正直な感想です。
一方で各種サービスの概要理解には役立ったため、これからAWSを本格的に学ぼうとするあたってSAAの受験は良いとっかかりになると思います。
ともあれ、本試験の提出ボタンを押すまでヒヤヒヤだったので正直ホッとしています。今日は久々にぐっすり眠れそうです。

ファイルを強制ダウンロードさせるヘッダについて調べた

一般的なブラウザは、拡張子が html だったり jpg だったり pdf だったりといった表示可能なファイルを受け取ると、 問題なければそのままタブ内に表示しようとします。これをなんとかやめさせたい。何卒名前をつけて保存してほしい。
というわけでファイルのダウンロードを強制するレスポンスヘッダについて調べたところ色んな書き方があったので整理してまとめておきます。

ダウンロード形式に関わるヘッダは、 Content-Type Content-Disposition の2つがあり、
調べていると以下の3つの指定がよく使われているのが見つかります。

  • Content-Type: application/force-download
  • Content-Type: application/octet-stream
  • Content-Disposition: attachment

Content-Type: application/force-download

Content-Type の MIMEタイプに application/force-download を指定すると、ファイル形式に関係なくダウンロードを開始させられます。

<?php

header('Content-Type: application/force-download');

$file_path = './test.txt';
readfile($file_path);

一方で、force-download というMIMEタイプは正式には存在せず、IANAのMIMEタイプ一覧にも載っていません。
https://www.iana.org/assignments/media-types/media-types.xhtml

force-downloadは慣習的に使われているMIMEタイプであり、公式に定義されたものではありません。 なぜ強制ダウンロードされるかというと、Content-Type に未知のMIMEタイプを指定すると、当該ファイルを実行しようとせずダウンロードさせるというブラウザ側の仕様によります。 実際 force-download を使わず、hogefoo など適当な文字列を指定しても強制ダウンロードになります。

<?php
// 適当なMIMEタイプを指定しても強制ダウンロードになる。
header('Content-Type: application/kinoko_vs_takenoko');

$file_path = './test.txt';
readfile($file_path);

当初の要件は満たせるものの、なんとなく気持ち悪さは残ります。

Content-Type: application/octet-stream

application/force-download の代わりに application/octet-stream を指定してもファイル形式に関係なくダウンロードを開始させられます。こちらは任意のバイナリデータを表す、公式なMIMEタイプです。

application/octet-stream
これは、バイナリファイルでは既定です。これは未知のバイナリ形式のファイルを表すものであり、ブラウザーはふつう実行したり、実行するべきか確認したりしません。

https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types

<?php
header('Content-Type: application/octet-stream');

$file_path = './test.txt';
readfile($file_path);

application/force-download よりはまだしもですが、本来通知すべきMIMEタイプを指定していないという意味ではハックな感じがして、 これまたなんとなく気持ち悪さは残ります。

Content-Disposition: attachment

Content-Dispositionは、コンテンツをwebページの一部として表示するかダウンロードするかを示します。inlineを指定すればwebページとして、attachmentを指定すればダウンロードファイルとして、を表します。また、filenameパラメータで、ファイルの初期名を指定できます。

<?php
header('Content-Disposition: attachment; filename="hoge.txt"');

$file_path = './test.txt';
readfile($file_path); // ファイル名 hoge.txt としてダウンロードされる。

なんとなくよさげな感じがしますね。しかしContent-DispositionはHTTP1.1標準のヘッダではありません。

HTTP/1.1: Appendices 19.5

現代のブラウザのほとんどは対応していますが、古いIEだとこのヘッダ自体を無視してしまいます。humm..

まとめ

どの方法もつつけばモヤモヤする部分はありますが、まとめると

  • 基本は、Content-Disposition: attachmentを指定する。
  • 古いブラウザに対応しなければいけないときは、Content-Type: application/octet-stream を合わせて指定する。

としておくのが良いかと思います。