アーキテクチャへの扉

名誉部外部員 むら(高2)

はじめに(免責)

この記事はネットや本などで知識だけを身に着けた経験の浅い人が書いています。この人はこんな考え方をしているんだなぁ位にこの記事を読み流してきちんとした本を読むことをお勧めします。本記事の考えのもとになっている本はを最後に紹介します。

本記事の目標

本記事ではプログラムとはどんなものか、またオブジェクト指向とは何かを説明しながらプログラムの大まかな構造を考えていきます。最終的に、良いと言われているようなプログラムの構造を考えられればなと思います。

プログラムの評価は使う人が直接触るわけではないので、いわゆる絵や音楽などの受け手からの評価とは違ってきます。プログラムに触れるのは仕様を変更したり追加したりする同業者または自分です。つまり、プログラムはそれを作り替える人から評価を受けるわけです。よって良いプログラムとは仕様を変更したり追加したりしやすいプログラムということになります。

プログラムを評価する指標は大まかに三つほどあります。一つ目は読みやすさです。読みづらければプログラムを理解することが大変になってしまいます。二つ目は再利用のしやすさです。同じようなものを何度も書いているとプログラムが無駄に大きくなってしまいますし、それだけ労力がかかってしまいます。三つめは正しさです。当然ですがバグが多いプログラムはプログラマに対してだけでなくユーザーに対しても被害が出ます。

プログラム

コンピュータプログラム(英:computer program)とは、コンピュータに対する命令(処理)を記述したものである。

Wikipediaからプログラムの説明を引用してきました。プログラムは仕事の手順を示した静的なデータです。

https://cookpad.com/recipe/2477629

これは生チョコのレシピの一部です。プログラムはこのような料理のレシピと同じように「最初にこれをやる、次にこれをやる……」ということが書かれたデータです。

料理をするときには材料が必要となり、その材料をもとに食べ物を作ります。プログラムも同じく、あるデータを使って決まった仕事を行い、得られたデータを出力します。つまり、プログラムは入力、処理、出力で構成されています。(このうちのどれかが欠けることはあります)関数について考えてみても、この構成要素がわかります。関数は引数が入力され、中である決まった処理をして、戻り値を出力します。関数はそれ自体が一つのプログラムになります。

入力の段階で処理しやすいデータになっていると関数などで中に書くコードが少なくなるのでプログラムが単純になります。料理でいうところの下ごしらえです。3分クッキングでは下ごしらえを完璧にすましてあるので実際の処理内容(レシピ)がとても分かりやすくなっています。関数の中でデータを動的に取得して(現在時刻の取得など)から、そのデータと引数を使って処理をしたいことがあると思いますが、やってはいけません。これは処理の部分でデータを用意していることになります。また、ある引数を入れたときに常に同じ戻り値が返ってこないことになります。これは、入力と出力を見てプログラムが正しく動いているかどうか判断することができなくなるのでやってはいけません。現在時刻なども外で取得してから関数の引数などに渡しましょう。

プログラムの依存関係

プログラムはプログラムの中で使うことができます。関数の場合関数の呼び出しと言ったりします。依存関係は使うものと使われるものの関係です。使うものは使われるものに依存しているといいます。車で考えてみましょう。車はタイヤを使います。タイヤは車に使われます。車がなくてもタイヤの機能は成り立ちますが、タイヤがないと車の機能は成り立ちません。つまり、タイヤは車に依存しておらず、車はタイヤに依存していることがわかります。AプログラムがBプログラムを使っているとすると、BプログラムがないとAプログラムを使うことができませんが、AプログラムがなくてもBプログラムを使うことができます。なので、AプログラムはBプログラムに依存していることとなります。

さて、車はタイヤに依存していると話しましたが一般的に物が製品を使う、つまり製品が部品に依存するという関係が成り立っています。しかし、プログラムの中では部品と製品との違いがあいまいなことがよくあります。ではどのようにプログラムの依存関係を考えるかというと、より根本的かつ汎用的なプログラムのほうに依存します。製品と部品では部品のほうが汎用的です。汎用的な部品は一つの製品だけでなく、様々なところで使うことができます。タイヤはネジに依存していますが、ネジを使っている製品はとてもたくさんあります。また、AプログラムがBプログラムに依存しているとBプログラムの仕様が変わったときにAプログラムの仕様を変えなければならない可能性があります。しかし、その逆はありません。ネジの太さが変わってしまうとそのネジを使っているタイヤは使えなくなってしまいますが、タイヤのネジ穴が太くなってもネジが使えなくなることはありません。

アプリケーション

アプリケーションはプログラムその物ではありません。入力、処理、出力では説明できないからです。ただ、アプリケーションの仕様を分解するとそれぞれを入力、処理、出力で説明できるようになります。ここからアプリケーションは複数のプログラムが集まって出来ていることがわかります。

アプリケーションのユーザはパソコンやスマホなどのデバイスを操作して画面に何かが表示されたり音が鳴ったりすることを期待します。ユーザの操作がプログラムの手順を始めるトリガーとなり、処理に必要なデータを作ることがあります

ユーザの操作から得られるデータ以外にアプリケーションやサーバ上にあるデータが必要となるときがあります。これらのデータを得るためのデータアクセサが必要になります。ユーザの操作によって得られるデータや、データベース、ファイルなどのデータはそのままでは処理に向いていないことがよくあります。よって、処理に必要なデータを処理しやすいデータに直すプログラムが必要になります

ロジックに必要なデータは様々なところから持ってくるので、それらを一括にまとめてロジックに渡せると便利です。様々なデータとプログラムを繋ぐことからこのプログラムをロジックと名付けます。

出力されるデータもただのデータでしかないので、出力されたデータを解釈して画面に表示したり音を鳴らしたりなどのユーザの体験を提供するプログラムが必要です。

これらを踏まえてアプリケーションの中のプログラムの基本的な構造を考えてみます。

  • ユーザの操作で処理がスタートします
  • ユーザの操作で得た入力データを処理しやすいデータに変換し、「ジョイント」に入力します。
  • 「ジョイント」で「データアクセサ」からファイルやデータベースのデータを持ってきて「ロジック」に入力します
  • 「ロジック」で入力されたデータに対してある決まった処理を行い、得たデータを「ジョイント」に返します
  • 「ジョイント」で帰ってきたデータを複数の「出力」に渡します。
  • 「出力」で渡されたデータを解釈して画面に表示したり、音を鳴らしたりと様々なことをします

これら一つ一つのプログラムも入力、処理、出力で出来ていることがわかります。

では、依存関係の話をアプリケーションのプログラムに応用してみましょう。アプリケーションのプログラムは基本的に入力、ロジック、出力、データアクセサ、ジョイントでできています。アプリケーションにとって根本的なのはロジックです。ロジックには仕様その物が書かれています。また、ロジックは同じ仕様の別アプリケーションでも使うことができます。

一方、入力、出力はアプリケーションごとにUIが異なり、UIが変わってもアプリケーションが成り立つ(パズドラは何回かUIが変わっています)ことから根本的ではないことがわかります。また、データアクセサもファイルの拡張子やデータベースの種類などによってアプリケーションの根本的な仕様は変わらないので根本的ではありません。

そして、ジョイントは入力、出力、データアクセサとロジックの間を取り持つので根本度も中間になります。よって、ジョイントがロジックに依存して入力、出力、データアクセサがジョイントに依存することになります。しかし、ジョイントはデータアクセサや出力を参照する必要があるのでこれらの依存関係を守ることは難しくなってしまいます。

アプリケーション構造_手続き

オブジェクト指向

これまで話した構造がBasicやC++などの手続き型言語で出来る構造です。この構造をC#やJavaなどのオブジェクト指向言語を使うことでさらに強化していきます。(PythonやC++などのオブジェクト指向言語では対応していない機能がありますが、代替手段があります。多分)ただ、その前にオブジェクト指向の機能はどんなものがあるか確認していきましょう。

クラス

クラスは型と呼ばれることもあります。型、つまり物を作るときに元となるものです。クラス自体は使うことができません。クラスを使って物を作るのですが、その物をインスタンスと呼びます。クラスには主にフィールドと呼ばれるデータ(変数)とメソッドと呼ばれるフィールドを使ったプログラムを定義することができます。クラスには二つの使い道があり、一つ目はインスタンスを生成することで、二つ目はインスタンスを入れる変数にすることです。

インスタンス

インスタンスの中にはクラスで定義されたデータとプログラムが入っています。このデータやプログラムは公開するか非公開にするかクラスで決めることができます。公開するとインスタンスの外で使うことができて、非公開にするとインスタンスの内側でしか使うことができません。

インスタンスには二種類の使い方があります。データのまとまりと共通のデータに対するプログラム群です。これは二種類の使い方どちらでも使えるわけではなく、どちらか一つの使い方に絞る必要があります。データのまとまりとして使うインスタンスをデータ構造、共通のデータに対するプログラム群として使うインスタンスをオブジェクトといいます。データ構造もオブジェクトもデータが主となっていることがわかります。よって、クラスはデータのまとまりとして名前を付けるべきです。

データ構造はデータのまとまりです。それ以上でもそれ以下でもありません。プログラムにデータを渡したりデータを出力したりするときに使います。データ構造はただのデータなのでクラス内には公開フィールドしか定義してはいけません。

プログラムは入力、処理、出力で出来ていると話しました。当然メソッドもそれに従わなければなりません。メソッドの入力は引数のほかにフィールドがあります。(メソッドにフィールドを入力しない場合、そのプログラムはメソッドではなく関数で実装した方がよいことになります)引数はメソッド内の処理を実行するときに入力するデータなのに対し、フィールドはあらかじめ決めておくデータになります。メソッドを使うときには引数しか設定しないのでフィールドをころころ変えてしまうとある引数に対して常に同じ戻り値が返ってこないことになります。よって、オブジェクトのフィールドは外からも(できれば中からも)変更できないようにするのが好ましいです。ここでフィールドを非公開にする必要が出てきます。オブジェクト内のデータが欲しいときには公開したいデータだけをまとめた専用のデータ構造のクラスを作り、そのインスタンスを返すメソッド(またはプロパティ)を返します。

インターフェース

インターフェースは各プログラムのつなぎ目です。インターフェースには実装されていないメソッド(またはプロパティ)を定義することができ、それらをクラスに実装させることで間接的にクラスのメソッド(またはプロパティ)を使うことができます。インターフェースは複数のクラスを「同じ機能を持つもの」として同じように扱えます。インターフェースを変数にすることによってその変数にはインターフェースが実装されているクラスだったらなんでも入れることができます。

また、インターフェースを介してクラスをつなぐことによって依存関係を逆転することができます。例えば、根本的なロジックを持つクラスAが詳細的なロジックを持つクラスBを参照したいとします。

依存関係逆転の問題

しかし、これだと根本的なクラスが詳細的なクラスに依存していることになってしまいます。そこでクラスA専用のインターフェースBを用意してクラスBがそれを実装します。

依存関係逆転の解決

インターフェースBはクラスA専用なのでクラスAが欲しい機能だけが定義されています。クラスAの仕様が変わりインターフェースBに対して必要な機能が変わるとインターフェースBは中身の処理を定義していないので簡単に機能を変えることができます。インターフェースBの機能を変えるとクラスBを変更するかインターフェースBの機能を持った新しいクラスを作る必要があります。これで詳細的なクラスBが根本的なクラスAに実質依存することができます。

抽象クラス

抽象クラスはインスタンスを生成することができず、型として使います。その代わり抽象クラスを基に派生クラスをつくることができ、派生クラスは抽象クラスとして扱うこともできます。派生クラスはその名の通り抽象クラスから派生したものです。よって派生クラスは抽象クラスとして扱える必要があります。イメージとしては抽象クラスが種類、派生クラスが物となります。抽象クラスは厄介な機能なのであまり使う頻度は高くありません。というか安易に使ってはいけません。機能をまとめたいだけならインターフェースを使いましょう。

オブジェクト指向を使ったプログラムの構造

オブジェクト指向の主な機能を確認したところでこれをアプリケーションのプログラムに適用してみましょう。オブジェクト指向は再利用のしやすさに特化しています。

ソフトウェアのプログラムは入力、出力、ロジック、ジョイント、データアクセサで出来ていること、そして入力と出力がジョイントに依存していてジョイントがロジックに依存しているべきだということをプログラムの章で話しました。関数だけではこの依存関係にすることが難しいですが、オブジェクト指向の機能であるインターフェースによって依存関係を逆転させることで可能になります。

インスタンスを使うことによってデータとプログラムが一体となり、処理プログラムを再利用できるようになりました。そこで、様々なデータに対する処理をそれぞれクラスで定義し、それらを使って一つの大きな処理をこなすプログラムを作ります。このような小さなロジックを複数フィールドに持ち、それらをつかって大きな処理をするメソッドを持つオブジェクトを制御フロー(またはトランザクション)と名付けます。

ジョイントがデータアクセサのインターフェースに依存することでファイルの拡張子やデータベースのツールが変わったときもジョイント部分を一切変更せずにデータアクセサを付け替えるだけでよくなります。

これらの変更を加えたプログラムの構造を図にしてみます。

アプリケーション構造

この構造がアプリケーションの中に複数できることになります。ファイルやデータベース、画面表示やサーバ通信はそれぞれ例です。実際はこの構造からクラスを付け足したり減らしたりして仕様にあった構造を作ることになりますが基本的にこの構造を維持することでそれぞれの機能を再利用しやすくなると思います。また、使うツールによっては変換が必要ないデータが出てくる可能性があります。しかし、必ず別の処理専用のデータ構造にデータを移す必要があります。なぜなら、処理で扱うデータ構造と入力、出力で扱うデータ構造を一緒にすると入力、出力の都合でデータ構造を変更しなければいけなくなったときに処理もその影響を受けてしまうからです。

先人の知恵

今回のプログラムの構造に使われている知恵

  • DRY(Don’t repeat yourself)
  • OAOO(Once and only once)
  • SRP(Single Responsible Principle, 単一責務の原則) 注:名前に惑わされて間違った説明をしていることがあります
  • OCP(Open Closed Principle, オープンクローズドの原則)
  • ISP(Interface segregation principle, インターフェース分離の原則)
  • DIP(Dependency inversion principle, 依存性逆転の法則)
  • SDP(Stable-dependencies principle, 安定依存の原則)

プログラムの構造を考えるうえでの知恵

  • GOFのデザインパターン 注:目的ではなく手段です
  • DDD(Domain-driven design, ドメイン駆動設計)
  • LSP(Liskov substitution principle, リスコフの置換原則)
  • 求めるな、命じよ
  • デルメルの法則

読みやすいコードにするための知恵

  • PLS(Principle of least surprise, 驚き最小の原則)
  • ループバックチェック

正しいプログラムを作る上での知恵

  • テスト駆動設計
  • ハンブルオブジェクトパターン

おすすめの本

  • リーダブルコード(清和書林にあります!!)
  • Clean Architecture
  • オブジェクト指向でなぜ作るのか(清和書林にあります!!)

さいごに

これまでつらつらと書いてきましたが、小さいアプリケーションではこの構造を完璧に守らなくてもあまり問題にならずにかけちゃいます。しかし、プログラムの構造を適切に作ることによって機能を拡張しやすくなるのは事実です。この記事でプログラムの構造を作る上で様々な考え方があることを知ってもらえたら幸いです。

次へAMDのZen3について考える>
前へ深層強化学習によるリバーシAI>