SOLID原則

概要 プログラム設計上の原則である SOLID原則 に関する記事です。個々の原則は何となくわかるものの、それぞれが目指す目的は何か、その手段として何を試みているのか、それによってこの原則ができたのか、といったポイントは聞かれてもサッと出てこなかったりします。それは多分、まだ理解しきれていない部分があるということかなと思います。 今回はより俯瞰的な観点から、それぞれの原則の特徴や立ち位置を分析してみたいと思います。 各技法 Single Responsibility Principle (単一責任の原則) 「これはね、Webも DBも メールも 何でもこなしている便利なサーバーなんだよ。」 「それって下手に触ると全部止まってしまうってことかい?」 SRPはクラスの責務を限定することで、保守の範囲を限定化するための原則です。 SOLID原則 - SRP Open Closed Principle (開放閉鎖原則) 「このマシンは CPUもメモリもディスクも拡張可能にできているのさ。 それに変更がケースの内側だけなら、ユーザーや外部デバイスも気づかないだろ?」 OCPは クラスの拡張性を最大化し、デグレードのリスクを最小化するための原則です。 Liskov Substitution Principle (リスコフの置換原則) 「こちらが私の業務を引き継ぐ方です。 皆さんからの指示は従来どおりで大丈夫です。もちろん同じ結果も保証しますよ。」 LSPは 継承したクラスに対し互換性を保証させるための原則です。 Interface Segregation Principle (インターフェイス分離の原則) 「弁護士の資格と医師の資格、一緒にしたら便利な資格にならないか?」 「おいおい、取得する側の立場にもなってみろよ」 ISPは インターフェイスを適切に分離し実現の負荷を最小化する原則です。 Dependency Injection Principle (依存性逆転の原則) 「明日の会議には翻訳担当と議事担当が必要なんだが、誰に頼めばいい?」 「心配しなくても誰かを着席して待たせておきますから、本業に集中して大丈夫ですよ」 DIPは インターフェイス間の結合を完全に疎にするための原則です。 一覧 実際に一覧にしてみましたが、改めて見るとそれなりに情報量があります。一気に理解しようと思うと厳しいので、徐々に消化してください。 最初に表の項目に関して簡単に説明します。 英名・和名:原則の英名と和名を提示します 原則と実現手段:基本的な原則を太字で記載し、その下に実際にどう実現するかという手法を提示します クラス・継承・I/F:原則を作り込む際に使用する抽象度を(具象クラス・継承関係・I/F実現)の3種に分類して提示します 縦軸の作り込み・横軸の作り込み:原則を作り込む箇所を縦軸(クラスの継承関係)と横軸(クラスの利用関係)で提示します 縦軸の効果・横軸の効果:原則の効果を縦軸(クラスの継承関係)と横軸(クラスの利用関係)で提示します 最終目的:技法は手段ですが その目的は何なのかを改めて振り返ります # 英名 和名 原則と実現手段 具象 継承 実現(I/F) 縦軸の作り込み 横軸の作り込み 縦軸の効果 横軸の効果 最終目的 1 Single

ICONIX

ICONIX概要 ICONIXとは、ユースケース駆動開発の手法の一つです。決して最近の技法ではなくUMLが登場した頃に誕生した手法ですが、本質的でスモールセットな側面も相まって、今日でも依然として有用な手法となっています。 ICONIXの特徴は、ざっと纏めると以下の様になります。 ユースケース駆動で、ロバストネス図、シーケンス図へと動的モデルを詳細化すること 動的モデル導出の過程でクラス図の静的モデルを導出すること 分析過程でこれら動的側面と静的側面を突き合わせ検証と洗練を繰り返すこと 純粋なUMLに加えロバストネス図を活用し動的モデルのミッシング・リンクを補完すること ICONIXの利点 厳密な用語定義 ICONIXを適用すると何が良いのでしょうか。まず第一に用語が洗練されます。用語の整理は一般的には用語集等を作成しますが、ここでの付加価値は用語の定義の厳密化だけです。多くの場合、用語自体には何の検証も入りません。ICONIXの場合は、プロセス全体を通じて用語を検証します。ここで検証された用語は以下に提示される厳密さを持ちます。 # 項目 説明 1 名称 用語の名称です。利用局面の想定して検証し抽象度や観点を検討しています 2 関連 用語間の関連です。用語同士の関連が明確になります 3 数的関連 用語間の数的関連です。1:1の関係にあるのか1:Nの関係にあるのかが瞭然です 4 関連の方向 用語の関連の方向性です。一方的な関連なのか双方的な関連なのかも見分けられます 5 関連の抽象関係 用語同士の抽象関係です。抽象化した用語や具象化した用語を確認できます 6 関連の保有関係 用語同士の保有・包含関係です。用語の構造が明確化されます 7 属性 用語が含む属性情報です。用語一つでどれだけの情報を保持しているかが明確になります 8 操作 用語が実行可能な操作です。用語の振る舞いを明示し責務を確認することができます   これだけ分析されているのであれば、一般的な用語集よりはるかに明確に分析できている分かると思います。一方で、この全て分析するのは相当骨が折れるのではという懸念も生じるかもしれません。確かに労力は掛かりますが、この分析は繰り返して行うため負荷は分散されます。特に最初の分析はラフなものでも構いません。むしろ最初から詳細な分析は不可能で、動的モデルの分析が進むのにつれて徐々に詳細化できてくる、というのが自然な流れです。最終的にはプログラムレベルのクラス図に近いレベルの詳細な概念モデルが醸成されます。 詳細化可能な動的モデル 動的モデルの分析はユースケース図・ロバストネス図・シーケンス図と詳細化されます。またそれぞれの図には明確な役割があります。簡単にまとめてみましょう。併せて、各分析過程でドメイン用語がどう洗練されるのかも提示します。 # 項目 説明 ドメイン用語 1 ユースケース図 利用者がシステムを利用する際のユースケースを明示します。機能要件の定義に相当します 名称と各種関連 2 ロバストネス図 各ユースケースの動きを概念レベルで捉えます。画面・処理・データの相互作用を定義します。機能設計に相当します エンティティとしての属性情報 3 シーケンス図 各ロバストネス分析のコントロール処理を詳細化します。クラス毎の責務を明示化するプログラム設計に相当します 各種クラスとしての操作 (振る舞い)  こうしてみると、ラフな仮説から整合性を担保しながら詳細化してゆく、とても理に適った分析手法だということが分かるのではないかと思います。また、無駄に多くの設計書を書き上げるのではなく、最小限のUML(ロバストネス図も含む)のみで分析を進められるというのも工数的に魅力ですし、そのUMLの力を最大限に引き出して、無駄なくプログラムに落としやすい分析・設計工程が踏めるというのも魅力です。

Solid原則 - SRP

Single Responsibility Principle (単一責任の原則)  “クラス変更する理由は1つ以上存在してはならない” 「これはね、Webも DBも メールも 何でもこなしている便利なサーバーなんだよ。」 「それって下手に触ると全部止まってしまうってことかい?」 SRPはクラスの責務を限定することで、保守の範囲を限定化するための原則です。 概要 -- 冒頭の会話の例だと、最近ではコンテナを活用して環境を分離するのではないでしょうか。コンテナは一つのプロセスに責任を持つ存在です。一つの目的のために用意された環境なので、その目的に沿わない部品は気軽に変更・削除ができます。同じ考え方はプログラムにも適用可能です。プログラムはプロセスの構成要素ですから責務は更に細分化されます。 細分化はアプリケーションが提供する機能による細分化、または1つの機能を実現するための役割による細分化、といった複数の切り口から行うことができます。機能による細分化は作るアプリケーションに依って異なりますが、役割による細分化の具体例としては、次のようになります。 [情報系] メモリ上の情報保持に責任を持つモデル [処理系-Biz] アクターとのユースケースを制御するコントローラ [処理系-Biz] アトミックなロジックに責任を持つサービス [処理系-Biz] 永続データのやり取りに責任を持つリポジトリ [処理系] ビジネスを意識しない汎用的なユーティリティ *1) Biz = ビジネスロジック系 こうして責務を細分化してゆくと、クラスが負う責務は1つになり、責務が絞られるとそのクラスを変更する理由も1つに絞られます。つまり単一の責任しか負わないわけです。単一の責任しか負わないクラスでは次のような変化が起こります。 コード量減とノイズ減を実現することができ 読み込む際の理解容易性が向上し 更新する際の副作用が低下する これが保守をする際の安全性に繋がります。この様に、単一の責任だけを持ったクラスが協調して全体を構築するべきだという原則が、単一責任の原則となります。 特徴 単一責任の原則には幾つかの特徴的な部分があります。 1つ目はクラスの抽象化観点(縦軸)における継承やポリモーフィズムとの関連の薄さです。リスコフの置換原則やインターフェイス分離の法則、依存性逆転の法則は継承やポリモーフィズムといった言語機能の活用を前提としたパターンですが、単一責任の原則にはその前提がありません。単なるクラスにも適用可能です。単一責任の原則は純粋に責任を分割するだけの原則なので、特に言語機能には依存しないのです。しかし、依存しないとは言え無関係な訳ではありません。継承時には追加した実装分に単一の責任をもたせ、インターフェイス実現時にはその実装に対して単一の責任を持たせる…というデザインで付加価値を与えることは可能だからです。 2つ目はクラス間連携(横軸)における適用局面の広さです。単なるクラス間の関係にも、継承やインターフェイスの実現といった"is-a"関係にも関連する原則だからです。特定の言語機能に依存しない考え方の原則というのはこういう点で強力です。更にはサブシステム間、マイクロサービス間、システム間、といったより大きな粒度においても適用可能ですし、逆にクラスより粒度の細かいメソッドの責務設計にも適用できます。具体的には「ビジネストランザクション全体」「再利用可能なビジネスフロー」「非ビジネスユーティリティ」といった粒度での責務分割がこれに相当します。こういった適用範囲の広さも特徴的なわけです。 3つ目は適用局面の広さに関連しますが、初期フェーズにおける適用の重要さ(時間軸)です。デザイン全体に適用可能なものですから、設計初期に単一責任の原則を理解した設計者が居るか居ないかで、その後のシステムにおける保守性は大きく変わります。それは原野を前に都市計画を用意する様なもので、その有無で区画に用途を定めた整然とした街ができるのか、または予測不可能な蚤の市の様になるのかが分かれます。 以上の様に、非常に大きな効果を得られるので、最初に覚える原則としては最もおすすめできるもの、それが単一責任の原則となります。 コードサンプル (JVM系の言語のほうが得意ですが、勉強がてらGolangで書いてみました) Before 以下のコードはサーバーを表現しています。このサーバーは Webサーバ・DBサーバ・Mailサーバの3種類の役割を果たしています。昔はこの様なサーバも多く見かけましたが、1つのサーバー内に複数の責務をもたせると、このサーバーをメンテナンスする理由が増えます。しかも、メンテナンス時には他の機能に影響が出ないように慎重にならなければなりません。これは現実の世界でもコードの世界でも同じです。単一の責任をもたせるようにリファクタリングしなければなりません。 // 複数の責務を負ってしまっている状態 type Server struct { HTMLs []*web.HTML DBConn *db.Conn Mails []*mail.Mail } func (s Server) ServeWeb() error { } func (s Server) ServeDB() error { } func (s Server) ServeMail() error { } After 以下のコードはサーバーの問題点を修正したコンテナを表現しています。コンテナはインターフェイスで表現され、実体はWebコンテナ・DBコンテナ・Mailコンテナに分かれ、それぞれが独自の役割を果たします。つまり、各コンテナは単一の責任しか負いませんので、修正の理由もその責務の範囲内のものに限定されます。こうすると修正対象のコードが限定され、保守しやすくなります。さらに、修正の影響もコンテナ内に閉じられますから、他の機能に変更の副作用が出ることはありません。これもまた現実の世界でもコードの世界でも同じです。

ICONIX - 1.Domain分析

ICONIXの分析工程 ここでは、ICONIXの分析工程を見てゆきます。最初に全てのベースとなる静的モデルのドメイン分析、続けて動的モデルのユースケース分析、ロバストネス分析、そしてシーケンス分析と進みます。この過程で、全ての主要な用語はドメイン分析で作成したドメインモデル(用語集)を元に行わなければなりません。 例えば、ドメインモデルに「ユーザー」と定義したならば、以降の分析に現れる用語は「利用者」ではなく「ユーザー」です。用語のブレを許さずに「ユーザー」に統一しなければなりません。逆に「利用者」の方が適切だと思ったならば、以降の分析で使う文書の訂正より先にドメインモデルを書き直す必要があります。それほどドメインモデルは影響力の高いものなわけです。 では、そのドメインの分析から始めたいと思います。 ドメイン分析 ドメインモデルは全ての工程の基本となります。何故ならそれが正規の用語集となるからです。フワフワとした曖昧な言葉を棄て、正確で本質的な用語を捉えてゆくと、思考がクリアになります。また、ドメイン用語は属性も定義しますので、誰かが「ユーザー」と記述 (会話) した際、そこに「ユーザーID」「氏名」「住所」等の関連属性が含まれているか否かという点も明確になります。 ですから、仮に「ユーザーID」や「氏名」「住所」等が含まれていた場合は、記述や会話の中で「ユーザー」という表現が出た場合にこれらの情報も含むことが分かるわけです。厳密さだけではなく簡潔さも手に入れる事ができます。会話と文書が徐々に変わってゆくのが体験できるかも知れません。 Before: システムはユーザID, 氏名, 住所を画面に表示する。 After : システムはユーザー情報をユーザー詳細画面に表示する。 ドメインモデルの抽出 ドメイン分析を行うには、最初にドメインモデルの候補を抽出します。これはドメインエキスパートが提供するユーザーストーリー等から導き出します。何やら難しそうに聞こえますが、ドメインエキスパートというのは業務に詳しい担当者、ユーザーストーリーとは業務の流れを説明した文章です。要は「業務担当者のお話を聞いたり文章を読んだりしてキーワードを見つける」という作業に過ぎません。 ユーザーストーリーが存在しない時は、関連がありそうな文書を漁ってみます。それは簡単なシステム化の企画書かも知れませんし、何らかのマニュアルや、時には誰かが残したメモかも知れません。文書化されたものがなければ業務担当者へのインタビューで代替するのも一案です。 主要なドメインモデルの選出 ドメインモデルの候補が抜き出せたら、候補の中から主要なドメインモデルを選出します。対象システムとの関連性が薄そうなモデルを一旦脇に寄せて主要なモデルだけを選りすぐります。 例えば営業支援システムを開発しているとします。“営業担当”, “売上実績” 等は関連が深そうですが、“社用車”, “出社時刻” 等は関連が薄そうです。こうして関連の深そうなモデルを主要なドメインモデルとします。 これが重要なのは、直前の工程で多くの候補が出た場合 その全てを分析するのは大変だからです。逆に利用頻度が高く重要なものに絞って分析すれば時間の節約になり効率の良い仕事ができます。 その他のモデルの分析も無意味ではありませんが、分析初期の貴重な時間が惜しいですし 分析初期の精度にもあまり期待できません。それよりかは重要なドメインモデルに絞って分析し、フィードバックを得ることで徐々に分析精度を高めた後に、その他のモデルを追加した方が効率的という考え方です。 スコープ外に区分けしたドメインモデルも、後から必要だと分かった時に分析することは可能なので、ここでは一旦スコープを限定してしまいます。 モデル名のチェック 抽出したドメインモデルには最初から名前が存在しますが、あくまで仮の名称として捉えてください。分析が進んでも名前が変更されない保証は何一つありません。「今までそう呼ばれてきたから」「その時はその名前が良さそうに見えたから」という程度の理由で名前付けされている可能性も高く本質的な名前とは限りません。勿論それが最適な名前である可能性もありますが、最初はそれを疑ってください。 例えば、以下に示すのはあまり良くない名前の例とその改善例です。 # 問題点 例 改善方針 改善例 1 抽象的すぎて他のモデルと衝突しがち 担当者 修飾子を付けて具体化 ○○社経理担当 2 名詞に見えるが実体は動詞 承認 別のモデルの「操作」に分類 「経理部長」内の「承認()」に移動 3 同じものに複数の名称が存在 - クレジットカード

ICONIX - 2.UseCase分析

ユースケース分析 静的分析であるドメイン分析が(たたき台ベースで)終わったら、次にユースケース分析に入ります。ユースケース分析とはシステムの利用者(アクター)とシステムとの利用局面を洗い出すことで、システムに求められる機能要件を洗い出すものです。 また、利用者とシステムとの接点が判明するので局面毎の非機能要件を定義することもできます。例えばアクター(システム管理者)の画面操作は日に何十回と行われ、秒単位で応答する必要があるが、アクター(バッチタイマー)からのデータ操作は日に1度しか行われず、1時間以内に終われば良い…といった形です。 アクター分析 アクターとは外部からユースケースに働きかける存在で ユースケース図の上では 人型(スティックマン) で表現されます。表現上では人の様ですが、先の例の様に必ずしも人とは限らず 関連システムである場合もあります。要するにアクターの本質は単に外部からシステムに働きかける 役割 に過ぎません。 アクターの観点と抽象度 アクター分析で難しいのは、同じ人やシステムをどの観点から捉えるかという点です。同じ人間でも、男性、会社員、営業部長、人事評価者、スポーツクラブ会員、有権者、普通免許所持者、等の色々な側面(役割)を持つからです。大事なのは次の2点です。 どの観点から アクターを見るか どの抽象度で アクターを表現するか [シナリオ] 例えば、100を超える各種APIを管理するシステムを運用している部署「API管理部」があるとします。この部では社内外に公開されるAPIを管理しており、マイクロサービス毎にグルーピングされたAPIを登録・管理しています。管理下に置かれたAPIは、API毎のアクセス量やサービスレベルの計測、認可機能の付与やAPI仕様書の公開、といった汎用サービスを受けられるものとします。 さて、このシナリオのアクターとして「API管理部長」が登場したとします。これは特に不思議ではありません。しかし、この部長が実際に行っているのが「API登録時の承認作業」だけだとしたら「API登録承認者」の方が適切な名前かも知れません。(観点の調整) ところが別の事実が判明し、現時点での実業務はAPI登録時の承認作業だけですが、本来任されているのはAPIの品質管理全般だとしたら「API品質責任者」が妥当な名前でしょう。(抽象度の調整) ここでは「大まかな観点の抽出」→「システム視点での具象化」→「役割視点での抽象化」」の順にアクターを分析してゆきました。実際にどのような順番で分析するかは分析者の自由です。大事なのはファーストアイデアに飛びつかず、常に改善の余地を探ることです。 [観点や抽象度の重要さ] 写真撮影や絵画の世界では、視点(構図)や焦点(ピント)が非常に重要です。アクターにおける観点と抽象度もそれと比較すると分かりやすいかも知れません。軽視しても一応の作品(分析)は出来るものの、名作には成り得ないという点も似ているような気がします。 自分で定義したアクターに満足いかない場合は、以下の質問に答えてみると何かが見えるかも知れません。 自分が分析対象のシステムになったつもりでアクターを見ると、アクターはどの役割に見えますか? アクターの抽象度を上げたり下げたりして名称を変えてください。どの名称が一番しっくりきますか? アクターの汎化 ドメイン分析には汎化という関連がありましたが、アクターの世界にも汎化は存在します。汎化は無理して抽出する必要もありませんが、汎化関係を明示したほうが役割の階層が理解しやすくなるケースもあり、その場合は汎化を表現することをおすすめします。 例えば、先のアクターとして登場した「API品質責任者」ですが、この役割の人でなくとも「API管理部」の人間であれば誰でもAPI管理システムにログインし、API稼働状況の確認が出来るとします。仮にこの役割を「API一般管理者」とします。更にAPI品質責任者がAPI一般管理者の役割を含むのであれば、両者には汎化(継承)関係がありそうです。 これを検証するには「API品質責任者がAPI一般管理者として振る舞うことが可能か」を確かめてみて下さい。可能であれば関係の関係が成立していると言えそうです。 ユースケース分析 アクターの分析が完了したらユースケースの分析を行います。ユースケースは楕円の中にアクターの行動を記述して表現します。アクターを(主語)として「(目的語)を(動詞)する」という書き方にすると描写が明確になります(*1)。 表現方法として「〜することができる (Can)」「〜する可能性がある (May)」といった助動詞を含む書きっぷりもありますが、こうすると一見 正確なようで助動詞部分に曖昧さが残ります。これが結構なデメリットとなるのでこの様な表現は控えた方が良いです。ユースケースとして存在するのかしないのかを確認し、存在するならはっきりと言い切る様にしましょう。 *1) 目的語を明示すると後の工程で処理すべき情報(エンティティ)を明確にする効果があります。エンティティの詳細はロバストネス分析の際に説明しますが、ここでは処理対象の情報 (エンティティ) を明確にしておくと後の分析が楽になる」くらいに捉えてください。 目的を書くか手段を書くか ユースケースの書きっぷりには大きく2種類あります。それは目的でユースケースを書くか、手段でユースケースを書くかです。 1.目的 例えば、先のユースケース「APIの稼働を確認する」を例にとります。APIの稼働を確認するというのは最終的な目的です。最終的な目的は明白ですが、一方で具体的な作業は不明です。「実装に引きずられない抽象化されたモデルは良いものだ」という声も存在しその点はその通りなのですが、「システムとの具体的な対話が分からなければシステム設計も難しい」というのもまた事実です。 2.手段 一方で「APIのモニタリングページを閲覧する」というユースケースであれば、具体的な作業は明白です。しかし今度は目的が曖昧になってしまいます。モニタリングページを閲覧する目的はAPIの稼働確認ではなく性能確認かも知れません。目的が分からなければシステム化により何をしたかったのかが曖昧になり、その達成度も測れないでしょう。 3.両方 では一体どうすれば良いのか。一つの答えは両方を書くことです。両方を書くとゴチャゴチャしそうですが、整理して書けば決してそうはなりません。整理して可視化できるところがUMLの長所ですので、これを活用しない手はありません。ユースケースの部分を2層に分け、目的の層 (第一層)と手段の層 (第二層以降) で階層的に記述すれば、それぞれの長所を競合なく取り込むことができます。 こんな感じですね。 システム境界の明示 次に、システムとの境界を明示します。境界を明示することでどのユースケースがどのシステムに所属するのかを明示できます。今回は目的と手段を分けて記述しているので、システム化された手段の部分だけを境界内に含めています。こうすることで、ある目的のため2系統のシステムを操作しなければならないユースケースも表現できますし(あまり見かけませんが)、何より表現として正確です。 関連線の詳細化 (Option) ドメインモデル程ではありませんが、ユースケース図上でも関連線の表現が幾つかありますので、余力があったら詳細化しても良いでしょう。 一方で、読み手が詳細な関連線に混乱してしまい、却って理解を妨げるようなケースもあります。その様な場合であれば無闇な詳細化は避けるべきです。正確な図を書くのは単なる手段であって、理解を共有するのが本当の目的ですから。

Solid原則 - OCP

Open Closed Principle (開放閉鎖原則)  “クラスは拡張に対して開いていなければならず 修正に対して閉じていなければならない” 「このマシンは CPUもメモリもディスクも拡張可能にできているのさ。 それに変更がケースの内側だけなら、ユーザーや外部デバイスも気づかないだろ?」 OCPは クラスの拡張性を最大化し、デグレードのリスクを最小化するための原則です。修正が外部に影響しないようにクラス外との窓口を限定し(Close)、修正自体を阻害しないようにクラス内の拡張ポイントを意識して設計します(Open)。 概要 冒頭の例では、マシン(コンピュータ)を引き合いに出して説明しました。マシンは内部に多くの拡張ポイントを用意している一方で、外部との接触はPCケースによって限定され一部のボタンや端子からしかやり取りできません。 例えば、マシンのケースを開ければ HDDやメモリのスロットによりパーツの追加・交換を簡単に行うことができます。これが「拡張に対して開かれている」部分です。逆に、専用メモリが基盤に埋め込まれており追加・交換ができなければ「拡張に対して開かれていない」ことになります。 一方で、マシンのケース自体は外部との接触を限定する役割を持ちます。ケースはマザーボード等の脆弱な内部基盤を隠蔽し、最小限のインターフェイスを外部に公開します(カプセル化)。具体的には「電源ボタン・ディスクドライブ・USB端子・電源ソケット」という最小限の窓口のみ露出され残りはケースで覆われます。露出部分の挙動さえ担保できれば、それを利用する側に修正の影響は及びません。これが「修正に対して閉じられている」部分です。修正に対して閉じられていれば、デグレードの危険性や再テストの必要性を最小化できます。 修正に対して閉じられている構造は、何もマシンに限った構造ではありません。自動車や船舶等の機械製品はもちろんのこと、人間や動物の体だって修正に対しては閉じられています。ですから運転手が直接エンジンを触って始動することは不可能ですし、動物が食べ物を口を介さずに胃に押し込めることも不可能です。イグニッションキーによる始動や口で咀嚼してからの嚥下で問題ありませんし、むしろその方が便利で安全にできています。例えば、ブレーキが踏まれていなければ始動できませんし、毒草を口にした際には味覚が異常を検知してくれます。なお、これをプログラムで表現すると Setter内に Validation機能を付加した状態に相当します。 特徴 OCPの特徴は、スコープ視点では最適化の範囲がクラスに特化している点です。また、この最適化が、クラスには最適な責務が割り当てられていることが前提で、その責務における変更には継承や委譲を用いて開放し、他クラスからの干渉にはカプセル化を用いて閉鎖するという手段をとります。 コードサンプル 実装に話を移します。コード上での Open-Closed-Principle(開放閉鎖原則) は継承と委譲による縦方向(自身の関連)の開放と、カプセル化による横方向(他者との関連)の閉鎖によって実現します。縦方向と横方向と表現しているのは、一般的にクラス図上では自身の継承関係は親クラスを上方に、子クラスを下方に配置して表現するからです。 閉鎖 閉鎖部分は変数やメソッドの可視性で制御します。これらの可視性を private (Javaなら protectedも可) にすることで外部アクセスを遮断します。こうすれば外部のクラスから変数やメソッドにアクセスされることはありません。 public class Machine { private boolean powered; private CPU cpu; private Memory memory; private HardDisk hardDisk; private Set<USBDevice> usbDevices = new HashSet(); private Set<HDMIDevice> hdmiDevices = new HashSet(); // 振る舞いを省略 } (Javaによる閉鎖) 外部から変数へのアクセスが必要な場合は、Getter / Setter等の publicなアクセッサメソッドを通じてアクセスを許可します。結局はアクセスさせてしまうのであれば最初から変数を publicにするのと同じだと思われるかも知れませんが、以下の点が異なります。

Factory Method

目的 FactoryMethod はインスタンスの生成に関するパターンです。 このパターンの目的は、インターフェイスに対する実装の生成・代入局面を隠し、疎結合を完成させることです。 概要 インターフェイスを用いてポリモーフィズム(多様性)を実現したいとします。また、利用側にインターフェイスだけを見せることで疎結合も実現したいとします。こうすることで、多様な実装の切り替えに対応することができ保守性を高められるからです。さて、この時気をつけなければならないのが、ポリモーフィズム適用時の隙(仮称)です。 ポリモーフィズム適用時の隙 あるサスペンスドラマで犯人が怪人の仮面をかぶっていたとします。犯人はAさんかも知れない。しかしBさんかも知れない。AさんにもBさんにも第1の犯行が可能だ。そして難解なことに第2の犯行も可能だ。 ポリモーフィズムとは「多様性」です。仕事を決めてやり方を決めない「インターフェイス」によってその多様性を担保します。多様性はインターフェイスを満足させられる実装であれば、どれでも実現可能…という仕組みで担保します。この例で説明するならば、犯人はAさんでもあり得るしBさんでもあり得るのという部分です。そして「疎結合」というのは、利用者は何を使っているのかを感知していないという構造で担保します。この例で説明するならば、目撃者は仮面をつけて行った2つの犯行しか見ていないという部分です しかし、AさんかBさんの何れかが仮面を被るその瞬間を見られていたら? 多様性はいとも簡単に崩れ去ります。ですから犯人は仮面を被る瞬間を見られてはいけません。仮面を付けた状態で物陰から登場するのです。この、物陰の役割を果たす存在こそが Factory Method、物語や設計をミステリアスにさせるエッセンスです。 以上の例からもわかるように、Factoryと聞くと生成プロセスに注目してしまいがちですが、FactoryMethodの真の目的は生成物をインターフェイスの型で提供することで、その実体を隠しきることなのです。 例えば、良く見かける生成プロセスを隠しきれていないコードを提示します。このコードは生成のプロセスを完全に隠蔽してはいません。そのため当該スコープにおける多様性は担保出来ていません。これが Factoryを使わないコードの限界です。 var e tech.Engineer = &impl.Onda{} e.Program() e.Test() e.Publish()  ではこのコードが問題であるかというと、一概にそうとも言えません。何故なら代入の瞬間以外はインターフェイスに依存していないコードであることは担保できているので、本当にポリモーフィズムが必要になったその後に、簡単にポリモーフィズムを完成させることができるからです。 [余談] ポリモーフィズムとインターフェイス分離の原則 先の例に関してですが、名探偵が、様々な状況から推測して犯人を絞り込んでゆく様は、インターフェイスの責務を増やしてゆく過程に相当します。犯行日の深夜に動けた人、凶器を調達できた人、そして被害者の行動を知っていた人。最終的にインターフェイスを満足させ続けた人が犯人です。この様にして重すぎるインターフェイスの責務は実現クラスの枯渇を招きます。謎を解き明かしたい探偵の技術としては良いものですが、抽象化の力を振るいたい設計者として悪しきものです。この件に関する詳細は「インターフェイス分離の原則」で説明したいと思います 実現 次のシナリオでは、"Hello"という文字列を格納するため、Storageを生成しています。最初のテストケースでは factoryを用いてその実体を隠蔽できていますが、二番目のテストケースでは factoryを用いないため、その実体を隠しきれていません。この瞬間に疎結合という価値は失われてしまいます。 package factory_method_pattern import ( "testing" "github.com/stretchr/testify/assert" "github.com/koooyooo/go-design-pattern/factory_method/factory" ) // FactoryMethodを適用した場合 // Storageの実体を知ることなく利用できる (疎結合) func TestFactoryMethod(t *testing.T) { // Factory経由で実体を隠蔽 s := factory.CreateStorage() err := s.Store([]byte("Hello")) assert.NoError(t, err) } // FactoryMethodを適用しない場合 // Storageの実体を知ってしまう (密結合) func TestNonFactory(t *testing.

Singleton

目的 Singletonの目的は一つだけインスタンスを生成し、それを使い回すことです。 パターンの実装としては「一つしか生成できなくする」ための制約を作り込む形となります。 OOPとインスタンス数 オブジェクト指向(OOP)の世界では、特別な理由がない限り現実世界のモノの数と、インスタンスの数を一致させます。豚が3匹いるなら Pigインスタンスを 3つ生成します。同様に車が1000台なら Carインスタンスを1000個生成します。この理由は、複製したインスタンスの数だけ固有の情報を格納できるからです。現実世界のものが増えたのならば、それを管理するオブジェクトも同じだけ増やすのが基本です。オブジェクト指向言語が現実の世界や概念の構造を雄弁に表現できるのは、この状態の複製が簡単に(new Car() or Car{})実現できるからという点が大きいのです。 さて、オブジェクトと言えば状態と振る舞いを持ちます。この両者で特に大きな進化をもたらしたのはどちらでしょうか?実は、インスタンスの増加で増えるのは状態だけです。状態はクラス(struct)定義時のメソッドの宣言から変わることはありません。インスタンスが複製されてもその振る舞いが同じ様にコピーされるだけです。唯一の振る舞いの変化が許される部分は、そのインスタンスの状態に依って振る舞いが変わるロジックが定義されている時です。しかし、オブジェクト指向言語によって主役の座に位置するのが状態であることに変わりはありません。 この常識を根底から覆すのが Singletonです。何故ならその状態を1つで良い…と言い張るのですから。しかし Singletonについて深く考察してゆくと、そこにもまた OOPの本質に迫る側面があることに気付くはずです。 一つだけインスタンスを生成してそれを使い回すと何が良いのでしょうか。すぐには思い浮かばないかも知れませんが、これが活きる局面は数多くあります。 1. 数の限定 最初に紹介するのは、本当に数が限られているものをそのとおりに表現するものです。1つしかない物を厳格に表現するなら Singletonは良い選択です。例えば日本に政府は一つしかありません。つまり以下のコードが成立します。 gov := jpgov.Instance() ディズニーランド内にミッキーマウスは複数存在しないという話を聞いたことがあります。それが本当なら次のコードも成立します。 m := disney.MickyInstance() 次にシステム的な面で考えてみます。データベースを管理するマネージャが1つしかないのであれば、以下の表現が可能です。インスタンスは関数経由で取得しても良いですし、初期ロード時に生成しても構わないなら変数として用意しておいても良いでしょう。 // 関数で取得(実装上の選択肢は広がる) dbMgr := database.ManagerInstance() // 変数で取得 dbMgr := database.ManagerInstance 2. 状態の共有 Singletonが公開されるということは、単一の状態も公開されるということです。一般的にSingletonの参照はグローバルに提供されることが多いため、どこからでもアクセス可能な単一の状態という便利なものが手に入ります。例えば、Util系のクラス(package)は振る舞いという観点で単一のものを提供しますが、Singletonでは状態という観点で単一のものが提供出来るわけです。 次の例は、Observerパターンで通知を行う局面です。システムの各所からこのObserverに通知が入ります。このObserverは Singletonとして常に単一のインスタンスが参照されるので情報を集中的に管理できています。各所で別々のインスタンスを生成してしまったのでは集中管理になりません。 func (s Service) report() { o := observer.Instance() o.reportAccess(1) } 一つしか無いというのは、一箇所で情報を管理できるという強みがあるのです。総理大臣や大統領はいくら忙しくても2名以上にしません。責務あるインスタンスに情報を集約させるための手段としても Singletonは有効なのです。 3. 参照の取得 上の例をもう少し使います。通常、Observerのインスタンスが必要なら上位の生成元から延々と引数で引き渡す必要があります。しかし、Singletonではその必要がありません。Singletonの生成部分はグローバルな関数(Javaなら staticメソッド, Golangなら グローバル関数)ですので、調達は簡単です。引数をシンプルに保てますのでその点では設計上の長所となります。 一方で、グローバルな関数に触れるというのは、その処理が副作用を持つということです。単純な長所だけではないので、この部分には気をつける必要があります。余談ですが、この解決策としてDI(Dependency Injection: 依存性の注入)という仕組みが存在し、DIが導入の際には Singletonは DIコンテナ側で実現されます。これがSingletonの完成形かもしれません。 type Service struct { // 注入される依存性 o *observer.

TCP BBR

偶然聞き慣れない単語を目にしたので、TCP BBRについて調べてみました。主に動画とブログの内容をまとめたものとなります。 概要 TCP BBRは2016年に論文が公開されたTCPの問題点を解消するためのアルゴリズムです。主に回線の状況が悪い時に効果を発揮するものです。実は、1974年に最初の仕様が公開されたTCPですが、現代の様な大量のトラフィックを想定しておらず、幾つか使用上の問題を抱えています。 TCPの課題 1. 検知の遅延 ネットワークの混雑を検知にパケットロスを利用しています。このためネットワーク混雑の結果としてパケットロスが発生しこれを検知して初めて対応する形式でした。既に実害が発生してからの対応となるため、その間の利用効率は高くありません。 2. 非効率な輻輳制御 また、TCPは複数ユーザの利用を念頭におき指数バックオフ(再送タイムアウトが連続する場合、タイムアウト値を2倍にする機能)を実装していますが、仮に単一ユーザからのアクセスが全てだとすると、この影響で帯域の利用効率は半減してしまいます。 TCP BBRによる解決 検知の遅延に対し、TCP BBRはネットワークの混雑を直接監視します。これにより対応に至るまでのタイムロスを低減します。これによりTCPで低減していた利用効率は改善されます。 また、TCP BBR上ではアルゴリズムもより正確に調整され、結果的に同じネットワークをより効率的に利用できます。 参考欄に提示したブログ内で紹介されたテストにおいては、同一ネットワークで従来の2倍以上の帯域が出ていました。 そして嬉しいことに TCP BBRはクライアントサイドに依存したテクノロジーではなく、サーバーサイドの対応のみで実現することができます。OSのカーネルがサポートされているのであれば、BBRをサポートするようフラグを設定するだけです。 更にGoogle Front End (GFE)は TCP BBRに対応しているため、この背後に存在するサービス(Cloud LB等)は既に対応済みです。 まとめ 現在、HTTPの通信内容をバイナリベースとした HTTP/2や HTTP/2を基盤としてBody部分(従来踏襲)に軽量な ProtocolBuffersを適用した gRPCの採用が進んでいますが、そのベースとなるL4のプロトコルは依然としてTCPです。例えば、HTTP/2は平常時では高速ですが、一定のパケットロスを発生させるとHTTP/1と遜色ない性能に低下してしまうというシミュレーション結果もあるそうです。これは依然としてTCP上の問題が残っていることを意味します。 TCPの枠組みから外れた QUICベースの HTTP/3が普及すれば TCPの諸問題は消えてなくなるでしょうが、それまでの間、TCP BBRを意識して設計するのも悪くないかも知れません。 参考 [Blog] https://medium.com/google-cloud/tcp-bbr-magic-dust-for-network-performance-57a5f1ccf437 [Paper] https://research.google/pubs/pub45646/ https://www.youtube.com/watch?v=2nefATBHs1o

Hugo Cloud Run

Cloud Load Balancing x GCS の課題 GCS x Cloud LB によるHugoのホスティングは問題なく稼働していたものの、LBの常時稼働によるコストは日額で63円程掛かります。月額に換算すると1830円、静的サイトをホスティングするにしては安くはない金額です。WordPressのホスティングなら、多少サービスの良い業者でも月額1000円程度、その2倍近く掛かってしまうのは設計的に良かったとしても、あまり人に勧められる選択ではありません。 もちろん、HTTPS化を諦めてGCS単体でホスティングすれば十分に低コストになるでしょう。ただ全てがSSL/TLS化されてきている現状で、暗号化なしという選択はそれはそれで違和感が拭えません。 GCSの提供するWebサイトホスティングの機能(index.html等のコール)はCloudLoadBalancing経由かDNS経由でコールしない限り効果を発揮しません。LBは高コスト・DNS経由は非暗号、となると GCSを離れた第3の選択肢を考えなければなりません。 Cloud Run の適性 そこで思いついたのが、Cloud Runによるホスティングです。Cloud Runは HTTPSに対応しており、且つ低価格です。低価格な理由はアクセスが少ない時にはコンテナを停止できるからです。またCPUなら毎月 180,000vCPU秒, メモリは 360,000GiB秒, リクエストは 2,000,000リクエストまで無料という無料枠までついてきます。(最新の情報は Cloud Run 料金を参照) また利用も非常に簡単で、基本的にはコンテナをレジストリに登録し、それを起動コンテナとして指定するだけです。一般的なWebアプリケーションやAPIであれば、ストレージとの繋ぎ込みくらいの労力は必要ですが、今回の場合はそれすら不要です。一度起動したコンテナのコンテンツは更新されませんし、負荷が掛かって10台・100台とスケールアウトしようと、コンテナ間の内容に齟齬がでることはありません。 ではコンテンツを更新したくなったらどうするのか、その場合は内容が異なる別バージョンのイメージを用意しそれを指定するだけです。しかし、理論通りに動くか、本当に課題が無いのかは分かりません。そこで、実際に構築してみることにしました。 Cloud Runの設定 Dockerfile イメージを作成する Dockerfileは非常に簡単です。Hugoが publicディレクトリ内に出力した静的コンテンツをドキュメントルートである/usr/share/nginx/htmlに配置して、通常どおりに起動するだけです。 $ hugo Start building sites … | EN -------------------+----- Pages | 26 Paginator pages | 0 Non-page files | 6 Static files | 35 Processed images | 0 Aliases | 9 Sitemaps | 1 Cleaned | 0 Total in 280 ms FROMnginx:1.

Hugo Gcs

ローカル起動 Hugo ローカルサーバの起動は非常に簡単で、以下のコマンドを実行するだけです。規定のポートは1313であるため http://localhost:1313にアクセスすれば編集中のサイトをリアルタイムに閲覧することができます。エディタで編集中の文書を保存すれば閲覧中のページがリロードなしで更新されます (データバインディング)。 $ hugo serve -D 静的コンテンツ生成 生成 また、サーバー機能は不要なので生成されたWebコンテンツだけが欲しいという場合もあると思います。その場合は以下のコマンドを入力することで、直下の publicディレクトリ配下にHTML等のWebコンテンツが出力されます。 $ hugo 提供 例えば、この状態でpublicディレクトリをマウントさせた Nginxを起動させれば http://localhost:80 でコンテンツを提供することができます。 $ docker run -p 80:80 -v $(pwd)/public:/usr/share/nginx/html nginx これを利用して、以下の様な Dockerfileを用意し DockerImage内にコンテンツを固めたものをDockerベースで配信というのも一つの考えです。 FROMnginx:1.19-alpineCOPY ./public /usr/share/nginx/htmlEXPOSE80CMD ["nginx", "-g", "daemon off;"]$ docker build -t nginx-hugo . $ docker container run --rm --name nginx-hugo -p 80:80 nginx-hugo ただ今回は、配信の度に以下の作業を行うのが少々面倒かも知れないと考え、Dockerベースの配信(当初CloudRunを想定)を止めました。とはいえ構成はシンプルなのでアイデアとしては悪くないと思います。 コンテンツの生成 イメージのビルド イメージのレジストリ登録 サービスの参照先イメージの変更 実は、Hugoのデプロイ先は、GitHubPagesが有名なのですが、今回試したら生成されたリンクのURLに問題があり(一部URL要素が重複生成された)上手くページ遷移できないという問題があったため、その他の手段を探したという経緯があります。 GCS 今回着目したのは GCS (GoogleCloudStorage) による提供です。GCSは Amazon S3の様なオブジェクトストレージで、S3同様に静的コンテンツをインターネット公開する機能も備わっています。またS3同様にランニングコストが非常に低価格です。長期的な視野で考えるとこの点は魅力です。 プロジェクト作成 GCSでのサイト管理をすると決めたら、GCPプロジェクトを作成します。GCSの世界ではプロダクト毎にプロジェクトを作成するというのがベストプラクティスになっているため、静的サイトの構築程度でも1プロジェクトを用意します。一度GCPを使った事があるのであれば決済手段の登録等が不要ですので簡易的な手続きのみで完了します。

Hugo

Intro Hugoでサイトを作成してみました。実際に作ってみるととても軽量なサイトになっているので驚きです。元々はWordPressでサイトを作成しようと考えていたのですが、WordPressの場合コンテンツの管理にMySQLを利用するため、維持費の面で年に1万円前後かかってしまうというのがネックでした。 また、はてなブログPro等も検討し、こちらのほうがトータルでは安価で手軽なのですが、やはり自由度の面で一定の制限が掛かってしまいます (大抵の人には十分な自由度とは思いますが) 基本的には静的コンテンツを配信するだけなので、特段リッチな基盤は必要ありません。Webサーバ上にHTMLを置くだけでも良いわけです。特に、昨今はApache,NginxベースのWebサーバを立てないでも配信する手段があります。GitHubPagesを利用しても良いですし、Amazon S3 や Google Cloud Storageといった Cloud上のストレージサービスに配信機能がありますので、その中にコンテンツを配備し設定を公開とするだけで安価に配信可能な訳です。 Hugo Hugo自体は、静的サイトジェネレーターと呼ばれるものです。静的サイトジェネレータの有名どころは Site Generators で確認できますが、Hugoは Golangでこれを実現したものとなります。多くの静的サイトジェネレータがJavaScriptを採用していますが、Golang製であるHugoは高速な処理に定評があります。 Markdown Hugoではコンテンツ(文章)をMarkdownとして記述します。 平易な記法でリッチなコンテンツを表現できるため、最近では多くのBlogサービスもMarkdown記法に対応してきていますが、Hugoを始めとした静的サイトジェネレータはMarkdownを主たる記法としてコンテンツを生成しています。 Static Site Generator 静的サイトジェネレータは「サイトは更新される回数より閲覧される回数の方が多いのだから、閲覧の度に動的なサイト生成する従来のツールは非効率だ、その代わりに更新時にサイトを生成して閲覧時には同じものを提示しよう」という発想によるものです。確かに閲覧時の処理負荷はかなり低い様で、各ページを高速に閲覧することができます。逆に、何らかの計算処理が必要な動的なページを生成しようと思ったら、静的サイトジェネレータには荷が重いということになります。