
プログラム設計の本質|プログラム設計のコツや実践的な流れ
プログラムを作っていて何か困ったことが起きたとしたら、プログラムの設計が原因であることが多いものです。
例えば、プログラムを作っていてだんだん頭が混乱してきて訳が分からなくなったり、一つのプログラムの行数がものすごく長くなり困ってしまった経験がある方は多いでしょう。
また、他の人が作ったプログラムを読んでも、内容がすっと頭に入って来ない、同じような処理が至る所にあるけれど違いが分からない、そもそも何をやっているのかわからない、ということも日常茶飯事ではないでしょうか。
プログラムの設計は、そのようなことを起こさないために必要なのです。プログラムを作り始める「前」にしっかりと設計を行い、プログラムへ「意味」を与えることが、色々な失敗をしないために必要なのです。
この記事ではそんな「プログラムの設計」について、プログラムの設計の初心者向けに、特定のプログラミング言語や方法論に依存しない、一生通じる秘訣をお伝えします。
プログラムを作る前にこの記事の内容を思い返していただければ、きちんとしたプログラムを設計して作ることができる、周囲から信頼されるプログラマとしての第一歩を踏み出すことができるようになるでしょう。
関連記事1.プログラムの設計の本質
辞書で「設計」を調べると「機械類の製作や建築・土木工事に際して、仕上がりの形や構造を図面などによって表すこと。(大辞林 第三版)」とあります。
プログラムの設計も同じです。プログラムを実際に作り始める「前」に、何をどう作るのかしっかりと考えるべきなのです。
プログラミングが他のものづくりとは大きく異なるのは、「とりあえず作り始めてみよう」とできること、トライアンドエラーが簡単なことです。これはプログラミングの敷居の低さでもあり、大変良いところです。
ですが、良い品質のプログラムを作りたいなら、他のものづくりと同じような、作り始める前の設計が必要不可欠です。作りながら考えるのでは難しいこと、作った後ではもう遅いことを、作る前に考えておくのです。
ここでは、プログラムの設計がなぜ必要かの説明を通じて、プログラムの設計の本質をお伝えします。
1-1.プログラムの設計とはプログラムに意味を与えること
プログラムの設計の本質は、人間にとって明確な意味が感じられ、容易に理解できる程度の単位にプログラム全体を分解することです。
人間が自然にできて、コンピュータが未だにできないことは「意味付け」です。すなわち、モノに意味や理由を与えることです。人間はモノへの意味付けと、意味付けをすることで得られる原因と結果の因果性を、複雑な自然や状況を理解するための強力なツールとして長年利用してきました。
プログラムも意味付けをすべき対象です。個々のプログラムに明確な意味や役割を持たせると、プログラム自体の細かい内容・実装が分かっていなくても、人間はプログラム全体を理解し把握できるのです。
つまり、プログラムとは意味付けされたものの集合体であるべきなのです。多くのデータと処理から作られているプログラムに対し、データには構造で意味を与え、処理は意味付けした単位で分解することが、プログラムの設計です。
1-2.プログラムの意味は抽象化で発見する
プログラムの仕様書をただ眺めていても意味は掴めません。プログラムで扱っているモノは何か、それらのモノ同士がどう関連し合っているのかといった、問題領域の把握とその「抽象化」が、意味を見つけ出すのには必要だからです。
例えば、電話機が誰でも使えるのは、電話機の概念が抽象化され、使い方が広く共有されているからです。電話のシステムは、電話機そのものやバックボーンのネットワーク、各種制度が組み合わさった非常に巨大・高度・複雑な技術の集合体ですが、使う人はそれらを意識しません。プログラムも同じです。
そのようなモノを仕様・要求から見つけ出し、適切に作り込むことが、プログラムの設計だとも言えるでしょう。プログラミングする対象を抽象化して把握することで、個々のプログラムの詳細を意識せずに済むので、プログラム全体の見通しが良くなります。
抽象化はモデリングにも繋がります。つまり、現実のモノをプログラム上でどう表現するかです。現実の全てをプログラム上では表現できないので取捨選択が必要ですが、その際には問題領域の抽象化のセンスが求められます。モノを抽象的に捉えることが出来なければ、設計者としてのレベルは上がりません。
1-3.プログラムの設計は人間に向けて行うもの
プログラムの設計は、人間に向けて、つまり人間のために行うことを常に念頭に置かなければなりません。プログラムを動かすコンピュータは、プログラムの設計など何も気にしていないからです。
コンピュータは、プログラムの設計がどうであろうとお構いなしに動きます。コンピュータの本質は、プログラムに書かれた一つ一つの処理手順を、順番どおりに愚直に実行することだからです。
だから、プログラムがいくら長く、複雑で、大きくても、書いてあるとおりに動きます。何千何万行の関数でも、一つの関数に何百、何千の変数があっても、条件文がいくら複雑でも、分岐がいくら多くても、ループがいくら深くネストしても、関数への引数がいくら多くても、です。
このように、いくらプログラムが複雑になったとしても、コンピュータにとっては何も大変なことではないのです。コンピュータにとってはプログラムは意味があるものではなく、単にデータと処理手順の塊以上のものではないからです。
大変なのは、人間が複雑なプログラムを扱う時です。プログラムがごく小さいうちは設計をしなくても作れますが、ある程度以上に大きなプログラムは設計無しではきちんとしたものを作れませんし、メンテナンスもできません。
2.プログラムの設計の流れとコツ
ここではプログラムの設計をする際の流れを紹介しつつ、プログラムの設計に慣れている人が掴んでいる「コツ」をお伝えします。
プロフェッショナルなプログラマなら、プログラムで実現すべきことを正しく理解し、どうプログラムを組み上げるか全体視点で考え、作り始めて大丈夫か机上で確認します。そのように入念な準備をして、プログラムを実際に作り始めます。行き当たりばったりでいいものを作れるのは、よほどの天才です。
なお、この章の各節には作業のインプットとアウトプット例を記載しました。それらがいわゆるオブジェクト指向分析でのインプット・アウトプットであるのは、単に私の専門領域がオブジェクト指向プログラミング言語(主にJava)だからです。あらかじめご承知おきください。
2-1.プログラムの目的を理解して、モノと処理を抽出する
インプット:要件定義書、基本設計書(あれば)、詳細設計書(あれば)
アウトプット:ユースケース図、クラス図(概要レベル)、アクティビティ図(概要レベル)、状態遷移図(概要レベル)
個々のプログラムに持たせるべき意味は、プログラムでやろうとしていること、やるべきことを人間が理解して初めて明らかになります。ですから、プログラムの設計でまず最初に行うのは、プログラムの目的を正確に理解し、その目的をプログラム内で達成すべき複数の目標に分解することです。
この段階では、プログラムにおいてどのような概念のモノがあるのか、そのモノへどういう編集を行うのかを全体視点で把握し、リストアップします。普通はプログラムには何かしらの入出力とプログラム内部で保持すべき状態があるはずなので、そこに着目します。
つまり、個々のプログラム間で受け渡されるデータと、データを加工・編集するための条件をきちんと仕様から抽出して、データ入出力と処理の流れを明確に見極めることが、全てのプログラムの設計のスタート地点です。
2-1-1.プログラムの仕様書はプログラムの設計書ではない
設計フェーズを経て、プログラマに与えられる仕様書は、実は「プログラムの設計書」では必ずしもないのです。仕様書は「こういう入力があった場合はこういう出力・動作をしなさい」と言う、単なる入出力や動作の仕様であり、プログラムで実現すべきことをプログラマに伝える以上の意味を持ちません。
業務や処理の設計者でも分かる言葉で書かれた仕様書と、プログラミング言語で書かれたプログラムは、本来は全く異なるものです。仕様書を基にしてプログラムの設計を行うのは、プログラミング言語のスペシャリストとしてのプログラマの役割・責任であり、どう実現するかの手腕が問われるのです。
仕様書の文面どおりにプログラムを作ると、一つのメソッドや関数で全ての処理を行うプログラムになりがちです。確かに仕様書どおりですが、人間が理解しやすかったり、効率的なプログラムにはならないことが多いです。個々のモノや処理への分解、何より意味付けがされていないからです。
2-2.プログラムをデータ中心に眺めて分解する
インプット:ユースケース図、クラス図(概要レベル)、状態遷移図(概要レベル)
アウトプット:クラス図(データ)、状態遷移図(詳細レベル)
プログラムを分解する際のコツの一つは、データを中心にして考えることです。プログラムの主役はデータであり、処理はデータを加工・編集する脇役であることを強く意識する必要があります。プログラミングとはデータに対して行うものだからです。
データは構造を持ちます。データを中心に考えるということは、プログラムで出現するモノ、すなわち個々のデータがどのような単位で分割されるのか、どのような値や属性を持つものであるか整理し、プログラミング言語で表現することです。この作業はモデリングとも呼ばれます。
この段階では、プログラム上でデータを表現する構造、すなわちクラスや構造体などの設計を行います。これらは、プログラムで使うプログラミング言語にある機能を使いましょう。現代のプログラミング言語であれば、大抵はデータを構造化するための機能があるはずです。
データは、結局はたくさんの変数の集まりです。それらの変数の群れを、プログラムからは一つの意味を持つ一つのモノとして、一括して取り扱うことが肝心です。場合により、モノの中にもモノがあるという階層的な構造を持つこともありますが、関連性があるものは近くに置くべきだからです。
2-2-1.例えば伝票処理のシステムなら
例えば、伝票処理をするプログラムを作っているなら、データとしての主役は個々の伝票です。伝票をどう処理するかはプログラム上では脇役です。伝票へどういう操作が行われるか、操作により伝票の状態がどう変わっていくか、全て伝票というモノを中心にして分解・整理します。
プログラム全体では複数の処理が組み合わさっている場合でも、伝票というモノを媒介にして処理を繋げます。重要なのは、伝票というモノ自体が処理の間の媒介になることです。それにより、プログラム上での考えがシンプルになります。
2-3.一つ一つの処理をなるべく小さく単純なものに分割する
インプット:ユースケース図、クラス図(データ)、アクティビティ図(概要レベル)
アウトプット:クラス図(データ+処理)、アクティビティ図、シーケンス図
プログラムの主役はデータですが、脇役である処理もきちんと整理して考えなければなりません。処理を実行する順番には業務上の意味がありますし、処理をどういう単位で分割するかがプログラムの品質に直結するからです。
この段階では、具体的な処理、すなわちメソッドや関数の仕様を考えます。大事なのは、その処理で何を行わせるか、入力・出力が何であるかの人間にとって分かりやすい意味付けです。
処理の分解を行う際のポイントの一つは、データと処理を明確に分けることです。すなわち、一つの処理は外部から与えられたデータの操作やデータの状態変化に徹することです。それにより初めて、処理全体を業務視点で、人間視点で自然に「読む」ことができるようになるのです。
処理の分解が出来ていないと、一つの処理が簡単に大きく、複雑になります。一つの処理でやるべきことが多くなるからです。そして、大きく複雑な処理は人間が作りづらく、テストをしづらく、メンテナンスをしづらいのです。
2-3-1.何でもできるプログラムは、何もできないプログラム
いくら大きく長く複雑な処理であっても、コンピュータにとっては何も不都合はありません。人間がプログラムに取り組む時の難易度にとても大きく影響するだけです。
例えば、10行と1,000行のプログラムでは行数が100倍違います。ですが、プログラムの複雑さは100倍をはるかに超えます。10行のプログラムならばきっと一目で内容が分かりますし、テストもきっと簡単でしょう。ですが、1,000行のプログラムは相手にするのがとても大変です。
つまり、何でもできるプログラムは、何もできないプログラムと同じなのです。ですから、データの状態を変化させることを中心にして整理し、それぞれの処理をなるべく小さく単純なものに分けていきます。プログラム全体を、小さな処理で意味のある順番通りにつなげて作るのです。
2-4.プログラムが動くかを作る前に机上で確認する
インプット:クラス図、アクティビティ図、シーケンス図、状態遷移図
アウトプット:プログラム(データ・処理のインターフェイス部分のみの実装)
プログラムの仕様・要求からデータと処理を抽出し、データを適切に考え、処理を適切に分けられたとしても、すぐにプログラムを作り始めてはいけません。プロフェッショナルなプログラマが、プログラムを実際に作り始めるのは、もう少し先です。
プログラム全体視点でデータと処理を俯瞰して、全体がきちんと動くであろうことを机上で確認し、これでいけると心から確信してから作り始めるべきです。なぜかというと、この時点ならまだやり直しができるからです。これがプログラムの設計の最終フェーズです。
さらに、プログラム全体の机上確認後でも、各処理の内容を作り始めるのは、プログラム上で全体の処理がつながることを実際に確認してからの方が良いでしょう。つまり、データ定義と処理のメソッドや関数定義とその流れだけを仮にプログラミングし、確かにこれで大丈夫だと確認してからです。
ここまで来て、ようやくこの後に続くプログラムの実装を「ただの作業」にまで落とし込めた、ということになります。そして、この状態まで持ってこられれば、プログラミングを他の人に任せることも安心してできるのです。
2-4-1.もったいない精神はプログラムの設計の敵
プログラミングでは「もったいない精神」が大事です。プログラムを流用できるように作るのは、同じようなプログラムを作る時間がもったいないからです。でもある意味では、そういうもったいない精神はプログラムの設計の敵なのです。
プログラムを作り始めてある程度形になると、もったいない精神が出て来きます。すると、設計が駄目なプログラムをなかなか捨てられません。作業の納期が近ければなおさらです。このようにして、世の中には設計が駄目なプログラムが量産・蓄積されていきます。
プログラミングをしていて問題が発生した場合にも、もったいない精神が邪魔をします。作ったプログラムを無駄にしないために、他のプログラムをいびつに変えていく誘惑に抗うのはなかなか難しいものです。でも、それはプログラムの設計が目指すところとは全く違うのです。
だからこそ、プログラムを本格的に作る前にしっかりと設計を行い、作業が戻らないように準備をするのです。
3.プログラムを設計する時に守るべき指針
ここまでで、プログラムの設計についての意義と大まかな流れをお伝えしてきました。ここでは、もう少し具体的な、プログラムの設計で守るべき指針についてお伝えします。
プログラムを設計する時に守るべき指針はいくつかありますが、共通しているのはプログラムの構成要素の意味と役割を明確にするということです。
3-1.データや処理にはピッタリな名前を付ける
名前とは人間が何かに与えた意味です。適切な名前を与えるということは、その意味を通じて適切な役割を与えるということです。プログラムの設計においては、これが最も重要な指針です。プログラムの設計時に名前付けに悩めば悩むだけ、後から苦しむことがなくなります。
適切な名前付けは全てに優先されます。「名は体を表す」はプログラムの設計では絶対的な正義です。プログラマがデータや処理に適切な名前を付けられるのなら、これ以外の指針は理解・実践できていなくてもまだ許容できます。それくらい重要なことです。
3-1-1.適切な名前とは意味のある名前
データや処理にどういう名前を付けようか悩む時は、そのデータなり処理の役割を明確にできていなかったり、決められていないということです。特に、二つ以上の意味を持った名前を付けたくなった時は黄色信号ですから、プログラムの全体構成から少し考え直しましょう。
例えば、関数の名前が「process_012_345」だと何が何だかわかりません。引数が「param987」でも同じです。人間にとって意味がないからで、このような連番ベースの名前は避けるべきです。管理上で連番が必要なのは分かりますが、プログラム上では意味のある名前を統一的に持たせるべきです。
プログラムをざっと眺めた時に、解読・解析をせずに自然に「読める」かは、名前の付け方に大きく依存します。ここが、良い設計か否かの最初の分かれ目です。プログラミング言語は英語ベースなので、色々な名前もやはり英語が自然ですが、ここで英語を恐れたり、考えるのを面倒くさがってはいけません。
3-1-2.プログラムに出てくる全ての名前を意識する
名前を付ける対象はデータや処理だけではありません。変数も同じですし、Javaのパッケージ名などの名前空間や状態の名前なども同じです。全ては人間にとってわかりやすいかが基準です。それぞれのプログラミング言語で自然な、名前の付け方のベストプラクティスもありますので、ぜひ参考にしましょう。
3-2.同じことをするプログラムを作らない
プログラムの設計では「同じことをするプログラムを複数作らない」ことが重要です。そのために、プログラムの設計の一番最初の手順としてプログラム中に出てくると思われるデータと処理をリストアップし、一つの資料にまとめ、全体を俯瞰するのです。
これはプログラム全体の見通しをよくするためと、無駄なものを作らないために行います。この処理はここだけで行う…と決まっていれば、そこだけしっかりと作ればよいのですし、処理の詳細を知りたければそこだけ参照すればいいからです。これは、プログラマにとって非常に大きなメリットです。
ちなみに、共通する処理を一つにまとめたいがために、プログラミング言語の様々な概念や機能が生まれました。その例が古くは関数であり、オブジェクト指向での継承であり、アスペクト指向でのアスペクトです。それらの機能を知り、上手に使うことが、上手に設計をするためのコツです。
3-2-1.コピペをしたくなったら考え直す
プログラム開発の現場でよく見るのはコピーアンドペースト(以下、コピペ)です。内容がほぼ同じで、一部がほんの少しだけ違う、というものです。これはプログラムの作り方においては失格で、プログラムの設計の良し悪しを評価をする以前の、様々な問題を抱えた危険な状態です。
きちんと設計が出来ていれば、処理に何か問題があった時は一ヵ所だけ直せば済むのです。コピペされた部分を全て同時に直すのは時間と労力の無駄です。複数の同じようなプログラムがあった時に、何が違うのかを調べるにも時間がかかりますし、調べた結果、全く違いがなかった時は実に落胆します。
コピペは短期的には省力化になりますが、長期的には無駄なだけです。プログラムを作っていてコピペをしたくなったのなら、プログラムの設計が何か間違っていると思いましょう。
3-3.プログラム同士を独立させる
プログラムはそれ自身で独立しているように設計すべきです。すなわち、プログラム同士の依存関係を弱くするということです。
プログラム同士のつながりは、処理の入出力であるデータにだけ担わせるべきです。もちろん、処理全体の流れを実行し制御する、いわゆるまとめ役となるプログラムは設計上でも必要ですが、それ以外のプログラム上の処理がお互いを過剰に知り、依存し合っているのは良くないプログラムの設計の典型です。
ここで述べることは、プログラミングの専門用語での、いわゆる「結合度を弱くする」ということに関連しています。
3-3-1.依存関係の強さは変更への弱さを生む
プログラム同士の依存関係が強いと、プログラムの改修をする時などに影響範囲が大きくなり、変更に弱くなります。途中に別の処理を割り込ませることも難しくなります。さらにそういう状態では、プログラムのコピペをして楽をしようとする人が必ず出てきます。
そして、プログラム間の依存の強さは、テストのしやすさにも大きく影響します。何かのテストをする時に、たくさんの複雑に絡み合ったプログラム群をテストするのと、簡単な入出力しかない一つのプログラムだけに集中してテストするのとでは、テストにかかる手間が圧倒的に違うのです。
3-4.一つのプログラムには与えた役割だけをやらせる
この記事の中でずっとお伝えしようとしているのは、プログラムの意味の明確化であり、すなわち役割の明確化です。ですから、一つのデータ・処理には意味・役割を一つだけ与え、それだけに徹させることが絶対に必要なのです。
ここで述べることは、プログラミングの専門用語での、いわゆる「粒度を適切にする」「凝集度を高くする」ということに関連しています。
3-4-1.入力×処理=出力
プログラムとは入力・処理・出力が連続したものであり、さらに「入力×処理=出力」です。足し算ではなく掛け算です。つまり、入力と処理のどちらかでも少しでも多く、大きく、複雑になると、プログラムは簡単に複雑になります。この掛け算は、場合によってべき乗になり、さらに簡単に複雑になります。
これはプログラム全体でも同じです。何かの処理が他の何かの処理に強く依存したり、処理内容が大きく影響されるようだと、組み合わせのパターンが一気に増大し、複雑さも同様に増加します。それをプログラムの設計の時点で防止しなければならないのです。
だから、データや処理には、設計で与えた役割だけに集中させます。データの値や状態変更だけをやる処理、データのストレージとのI/Oだけをやる処理、データを画面に表示しユーザ操作の結果を受け取るだけの処理など、設計で与えた役割以上のことを行わせてはいけないのです。
3-4-2.引数が増えてきたら危険信号
ちなみに、プログラムの設計をしていて、処理を行う関数やメソッドの引数が増えてきた時は危険信号です。引数が増えているということはその処理でやりたいことが増えているということであり、処理でやりたいことが増えているということは、役割を与えすぎているということです。
3-5.絵や図や表で考える:複雑さの見える化
言葉だけでロジカルに物事を考えることは、人間にとっては本質的に難しいことです。人間は、周囲の情報のほとんどを視覚から得ているからです。ですから、プログラムの設計をする時も、視覚に訴えるもの、つまり絵や図や表を使って「見える化」することが大変効果的です。
言葉だけではプログラムの複雑さが「見えません」。それに、言葉では一言で済むことが、絵や図や表にすることで初めて、設計対象の複雑さが見えてくることも珍しくはありません。それらの視覚的ツールを使うことで、ようやく設計上の抜け、漏れが見えてくるのです。
3-5-1.見た目でスッキリさせることを目指す
例えば、図などでプログラム同士の関係を見える化すると、プログラム間の関係を表す「線」がすごく入り混じることが多いものです。その状態はプログラムの設計がまだ上手にできていないことを表しています。このように、見た目でスッキリしているものは、設計上でもスッキリしているのです。
3-5-2.デジタル・アナログのツールを使い分ける
プログラムの設計を見える化するためのツールや方法論は様々です。いわゆるデータフロー図や、オブジェクト指向分析で使われるUMLなども、そのためのツールの一つです。ツールとして設計の表現方法が全世界で統一されていますので、他の人の考えがよくわかりますし、伝わります。
そのような専用のツールを使わなくても、例えばExcelやPower Pointで表や図を書くことは有用です。さらにノートと鉛筆、ホワイトボードとマーカー、付箋紙などのアナログなツールも大変有用ですし、デジタルなツールよりもお手軽でお金もかかりません。私はこれらのアナログなツールを愛用しています。
プログラムの設計を、手書きで簡単な図や表や箇条書きにするだけでも見えてくるものがあります。考えをまとめている状態では、まだデジタルなツールを使うべきではないことも多いものです。正式な設計文書を作る時はデジタルなツールを使うべきですが、タイミングを計りましょう。
3-6.プログラミング言語と少し距離を置く:高い抽象度で考えた後に具体化する
プログラムは必ず何かのプログラミング言語で書き下します。ですから、プログラムの設計は特定のプログラミング言語やフレームワーク(以下、FW)を前提としますが、場合によっては少しそれらと距離を置く必要があります。
3-6-1.プログラミング言語・FWの機能に固執しない
プログラミング言語やFWが持つ各種機能は、プログラムの設計を行う初期の段階では、影響度はあまり大きくありません。むしろ、プログラミング言語やFWの特定機能を使うことに意識が行くあまり、いびつな設計になることがあります。
例えば、何かのFWの機能を使うことに固執するあまり、実はプログラミング言語の標準機能でとても簡単にできることをやたらと難しく考えてしまったり、FWにその特定の機能がないから実装できないと考えてしまうのは、プログラミングの初級者によくありがちなことです。
3-6-2.高い抽象度で上手くいくから、具体的なレベルでも上手くいく
先にお伝えしたとおり、プログラムとは「入力×処理=出力」です。どのようなプログラミング言語やFWであっても、少し高い視点から眺めれば、やっていることに本質的な違いはないことを知りましょう。やり方やお作法が少し違うだけなのです。
プログラムの設計では、一番初めはデータ・処理がどう流れる…という高い抽象度で考え、特定のプログラミング言語やFWは意識しません。高い抽象度で上手くいくことが確認出来て初めて、より具体的なプログラミング言語やFWの世界に降りて、実装の仕方を考えるべきなのです。
つまり、処理の詳細を省ける、抽象度が高いレベルですら上手く作れる見込みがないものが、より具体的なレベルで上手く作れるはずがないのです。
4.プログラムの設計の例
この章では、プログラムの設計のやり方を、例を使って説明してみます。この例で使うプログラミング言語はJavaですが、他でも似た感じになると思います。
4-1.プログラムするものの説明
Aさんはプログラミングを勉強中のプログラマです。会社では、WordPressのような記事を管理するシステムの開発プロジェクトに所属しています。
そのシステムに、いわゆる「いいね!」の機能を追加することになりました。
いいねボタンは、以下のようなユースケースで使われます。記事IDはこのシステム上で記事を識別するための数字です。
ユースケース(記事のいいねボタン押下)
- ユーザは、記事画面にある「いいね」ボタンをクリックあるいはタップする。
- 記事画面は、いいねAPIを記事の記事IDをパラメータにして呼び出す。
- いいねAPIは、データベースの記事テーブル上の、記事IDに紐付くいいねの数を1加算し、現在のいいねの数を戻す。
- 記事画面は、いいねAPIが戻したいいねの数で、画面に表示されているいいねの数を書き換える。
- ユーザは、いいね数が変更されたのを記事画面で確認する。
Aさんは、3の「いいねAPI」を作ってほしいと伝えられました。この作業はAさんのプログラミングの勉強も兼ねているので、プログラムの作り方の具体的な指示はあえてされていません。
しばらくして、Aさんはプログラムを作り上げました。でも、ソースコードレビューをした先輩のBさんからは「確かに動きはするけれど、プログラムの設計が全然できていないね。他のメンバーが作ったソースコードも参考にして、プログラムの設計から考え直してよ」と言われてしまいました。
4-2.見直し前のプログラム
Aさんが作ったプログラムは以下のものです。これはプログラミングの初心者が作りがちな、典型的なプログラムです。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class IINE_KOSIN_API { public static String shori(String kiji) throws Exception { String setuzoku = System.getenv("CONNECT_STRING"); Connection db = DriverManager.getConnection(setuzoku); Statement SQL1 = db.createStatement(); ResultSet kekka = SQL1.executeQuery("SELECT FAVORITE FROM ARTICLE WHERE ARTICLE_ID = " + kiji); String iine = "-1"; boolean aru = kekka.next(); if (aru) { iine = kekka.getString(1); iine = Integer.parseInt(iine) + 1 + ""; SQL1.executeUpdate("UPDATE ARTICLE SET FAVORITE = " + iine + " WHEER ARTICLE_ID = " + kiji); SQL1.close(); } db.close(); return iine; } }
ータベース上にあるテーブルから現在のいいねの数を取得し、+1した後に書き戻し、呼び出し元に値を返す、それだけのシンプルなプログラムです。Javaでデータベースを扱う際の基本どおり、教科書どおりの書き方です。
確かに、ユースケースにあるとおりには作られています。ですから、プログラムの動作の観点では問題はありません。でも、B先輩はプログラムの設計の観点で問題があると言っています。
このプログラムの特徴は以下のとおり。以降では、これがプログラムの設計の観点から見直すとどう変わるか、順を追って見てみます。
- クラスは1つ
- メソッドもstaticなもの1つで、全ての処理がメソッド中に書かれている
- データの型を意識できておらず、本来は数値である記事IDやいいねの数を、文字列として扱っている
- クラス、メソッド名、変数名のルールがばらばら、かつJavaとしては不自然
- データベースを扱うためのJavaのAPIを、処理内で直接利用している
4-3.プログラムの設計
4-3-1.モノと処理を抽出する
まずは、ユースケースの文章をじっくりと眺めてみます。プログラムの問題領域に、どういうモノや処理があるのかを知るためです。
ユースケースの文章中の名詞に注目してみると、「ユーザ」「記事画面」「いいねボタン」「いいねAPI」「記事」「記事ID」「いいねの数」「データベース」「記事テーブル」といったモノがあるようです。
次にどういう処理がありそうか、目的語と動詞に注目しましょう。「ボタンをクリックまたはタップする」「いいねAPIを呼び出す」「いいねの数を1加算する」「いいねの数を書き換える」「いいねの数を戻す」というものですね。
こういうモノや処理をノートやホワイトボード、大きめの付箋紙などに、以下のように書き出してみます。
4-3-2.データ中心に眺めて分解する
では、これらのモノや処理がどうまとめられるべきか、どう関連しているかを考えてみます。
記事 | このユースケースの中心だと思われます。このユースケース内では、記事は記事IDといいねの数という値を持っていて、いいねの値を加工して保存するのが目的だからです。 |
いいねAPI | いいね加算の処理を受け付け、データベースにアクセスして、呼び出し元に結果を戻す、という役割を持っていそうです。 |
データベース | いいねAPIからの要求に対して、記事の情報を保存する役割のようです。それに記事を画面を表示しているのですから、記事を検索する処理も多分あるはずですよね。 |
記事テーブル | データベースの中にあるモノの一つで、ここに記事の内容が保存されます。記事テーブルにはデータベースを経由しなければアクセスできません。 |
記事画面 | ユーザの画面操作をきっかけにしていいねAPIを呼び出し、その結果を自分自身に反映して、情報をユーザに伝えるのが役割です。 |
ユーザ | 記事画面で記事を読んだり、操作する人のことですね。ユーザは記事画面しか使えず、そこから先にあるシステムの内部には手を出せません。 |
これらのモノと処理、それらの繋がりを大雑把にまとめると、以下のようになります。クラス図に似た形式で書いてありますが、考えがまとまりさえすれば、どんな形式でもよいです。
これで、今回のユースケースの中にあるモノ同士の関係性や、お互いが呼び出す側・呼び出される側のどちらであるかというようなものが、全体視点で整理されたのが分かるでしょうか。以下では、それぞれのモノをクラス、処理をメソッドとして扱います。
4-3-3.処理を小さなものに分解する
問題領域のモノや処理を、大まかにですがクラスやメソッドへ分解できたので、それぞれのクラスが持っているメソッドではどういうことをやるのかに分解してみます。
そして、これらのモノがどういう順番でどう呼び出されるかを考えてみました。以下はUMLのシーケンス図のようなものです。上から下に処理が流れ、左から右に呼び出しが行われます。
一番内容が多いのは、きっとArticleApiの処理でしょう。ArticleApiが一番関連するクラスが多い(=繋がっている線が多い)ですよね。
4-3-4.机上での確認
ここまでの作業で、プログラムの大枠と流れはできた感じがしてきました。次は実際にプログラムを作ってみます。
でも、この段階ではまだガワだけです。つまり、クラスとメソッドの宣言、必須と思われるフィールドや変数のみの状態で、作れそうか・動きそうかを確認するだけです。それでもコンパイルエラーが出ると気持ち悪いので、nullや0などをとりあえずreturnしているところもあります。
あと、それぞれのメソッドでここでは何をやるというメモが、コメントで書かれています。このメモが書けるのは、きちんと設計をしているからこそです。
public class Article { long articleId; int favorite; public int getFavorite() { // 自分のいいねの数を戻す return favorite; } public void addFavorite() { // 自分のいいねの数を+1する } }
public class ArticleApi { public int addFavorite(long articleId) { // 記事リポジトリを作る ArticleRepository repository = new ArticleRepository(); // 記事リポジトリから記事を持ってくる Article article = repository.load(articleId); // 記事のいいねを+1する article.addFavorite(); // 記事リポジトリが記事を保存する repository.store(article); // 記事のいいねの数を戻す return article.getFavorite(); } }
import java.sql.Connection; public class ArticleRepository { // データベース接続、どうやってオープンとクローズをしようか? Connection conn; public Article load(long articleId) { // データベース内の記事テーブルから引数の記事IDを検索する // 検索結果からArticleを作って戻す return null; } public void store(Article article) { // データベース内の記事テーブルへ引数のArticleを保存する } }
まあ、なんとかなりそうですね。そして何より、ArticleApiでは、この時点でも処理の大事なところが形にできているのは注目すべきことです。
この状態でプログラムの設計は仮決定にして、さっそくプログラムの詳細を作り始めましょう。
4-4.見直し後のプログラム
さて、プログラムを設計から見直した結果、以下のようになりました。
public class Article { private final long articleId; private int favorite; public Article(long articleId) { this.articleId = articleId; } public long getArticleId() { return articleId; } public int getFavorite() { return favorite; } public void setFavorite(int favorite) { this.favorite = favorite; } public void addFavorite() { favorite++; } }
import java.sql.SQLException; public class ArticleApi { public int addFavorite(long articleId) throws SQLException { Article article; try (ArticleRepository repository = new ArticleRepository(System.getenv("CONNECT_STRING"))) { article = repository.load(articleId); if (article == null) { return -1; } article.addFavorite(); repository.store(article); } return article.getFavorite(); } }
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class ArticleRepository implements AutoCloseable { private final Connection conn; public ArticleRepository(String connStr) throws SQLException { conn = DriverManager.getConnection(connStr); conn.setAutoCommit(false); } public Article load(long articleId) throws SQLException { Article article; try (PreparedStatement pstmt = conn .prepareStatement("SELECT ARTICLE_ID, FAVORITE FROM ARTICLE WHERE ARTICLE_ID = ?")) { pstmt.setLong(1, articleId); ResultSet rset = pstmt.executeQuery(); if (!rset.next()) { return null; } article = new Article(rset.getLong("ARTICLE_ID")); article.setFavorite(rset.getInt("FAVORITE")); } return article; } public void store(Article article) throws SQLException { try (PreparedStatement pstmt = conn.prepareStatement("UPDATE ARTICLE SET FAVORITE = ? WHERE ARTICLE_ID = ?")) { pstmt.setInt(1, article.getFavorite()); pstmt.setLong(2, article.getArticleId()); pstmt.executeUpdate(); } } public void close() throws SQLException { conn.commit(); conn.close(); } }
4-5.見直し後のプログラムはこんな感じ
4-5-1.意味付けが明確になった
プログラム全体の行数は数倍に増えました。クラスやメソッドの数も増えて、パッと見は複雑そうです。でも、しっかりとした「意味」が、見かけの複雑さと引き換えに得られました。
役割や機能を意識したクラスやメソッドにしたので、それぞれの意味が明確になりました。一つのクラスは一つの役割、一つのメソッドは一つの機能だけ持っていて、メソッドの行数も少なく抑えられています。クラスやメソッド、変数に適切な名前を付けたことで、意味が通じやすくもなっています。
4-5-2.再利用性が向上し、依存関係が減少した
各クラスの再利用性も高まりました。これはクラスが役割で分割され、お互いの依存関係も最小限だからです。ArticleApiはそのままでシステム内で使えますし、ArticleやArticleRepositoryも、それらが持つ機能を他のプログラムから簡単に使えます。
4-5-3.人が「読める」プログラムに近づいた
見直し後のArticleApiは、Javaやプログラミングを良く知らない人でも、何をしているのか分かりそうな気がしませんか? それに、メソッドの内容、特にArticleService.addFavoriteが、ユースケースの記述レベルに近づいたことが分かるでしょうか。
これは高い抽象度でプログラミングできているからです。以前の処理だと、データベースの構造、JavaのJDBCのAPIやSQLの専門知識を知らないと、何をしているのか分かりません。
ユースケースは、普通はシステムを使うユーザでも読めるレベルで書くものですが、プログラムの設計をきちんとすれば、プログラムもユースケースと同じように「読める」ようになるのです。
5.プログラムの設計で周囲の一歩先を行くための秘訣
ここまでで、プログラムの設計で意識してほしいことをお伝えしてきました。この章ではプログラムの設計の熟練者がいつも意識していることをお伝えします。
ここで述べる内容はプログラミングの初級者には難しいと思います。それでも全てのプログラマに心構えとして知っておいて欲しいことばかりです。そして、プログラマとして仕事をしていれば、いずれ直面する機会があるものばかりです。
それに、これらを知っていれば、周囲のプログラマの一歩先を行き、良い品質のプログラムを作るためのきっかけとできるのです。
5-1.プログラムの設計では分かりやすさを最優先する
プログラムの設計では、プログラムの分かりやすさが最優先事項です。周囲のプログラマがついてこれないような高度なテクニックを縦横無尽に使うことと、良いプログラムの設計はイコールではありません。
プログラムの分かりやすさと良く比較されるのは性能ですが、プログラムの仕様として、よほど極端な性能(例えばリアルタイム処理での処理時間要件など)を求められていない限りは、設計時はプログラムとしての明確さと意味付けの分かりやすさを優先します。
5-1-1.プログラムの設計が出来ていれば性能は出せる
確かに、プログラムの性能はプログラムの設計に負う部分が大きいものです。しかし、適切なプログラムの設計が出来てさえいれば、普通の業務向けシステムなら大体の場合で十分な性能は出るものですし、性能改善をすべき個別のプログラムの絞込みも行いやすくなるものです。
逆に言えば、性能改善をする時の対象や影響範囲を絞り込み、限定するためにも、そもそもプログラムの設計が出来ていなければなりません。プログラムをお互いに関係のない部分に設計レベルで分割しているからこそ、問題の箇所を明確にできるのです。
なお、スーパーコンピュータ向けの大規模科学技術計算のプログラム設計と、業務システムのプログラム設計は大きく異なります。これはプログラムの目的が違うからです。科学技術計算向けのプログラムは性能が最優先なので、読みづらくても許されます。でも、業務プログラムはそうではないのです。
5-2.プログラムの設計のパターンを学ぶ
プログラムの設計にはいわゆる「よくあるパターン」が頻出します。個別のプログラムの仕様はシチュエーションごとに全く違いますが、何かを実現するための方法はそう多くはないのです。
あなたが今作ろうとしているプログラムは、恐らく同じようなやり方で、どこかの誰かが以前作った類のプログラムです。その時に上手くいったこと、失敗したことを学ぶことが必要です。上手くいったことは今回も改良しながら使えばいいですし、そして何より、失敗を繰り返してはならないのです。
プログラムの設計においては、自己流では失敗するだけです。先人の知恵をあなたのプログラムで生かすべきです。その知識を得られる書籍も数多く出版されています。視野を広く持って、色々な知識を身に着けるための努力を続けましょう。
5-2-1.ベストプラクティスを学ぶ
このプログラムを設計するためのパターンは、プログラムの設計を行う上でのベストプラクティスとして、プログラマの間で過去から多くが蓄積されているのです。
例えば、上手なプログラムの設計のパターンの一つは、データのプレゼンテーション部分とロジック部分を設計レベルで明確に分離することです。現在のプログラムの設計における主要パターンの一つであるMVC(Model View Controller)は、これを強く意識しています。
いわゆるGang of Four(GoF)のデザインパターンも、プログラムの設計のパターン集の一つです。オブジェクト指向プログラミング言語向けのパターンではありますが、プログラムを設計する上では非常に参考になるものであり、プログラマが身に着けるべき、プログラムの設計のイディオムの一つです。
これらのプログラムの設計の方法は、パターンとして学習できます。パターンを学ぶには学習コストがかかるので、短期的には無意味・無駄に思えるかもしれません。でも、長期的にはとても重要です。どれだけ設計のパターンを知っているかが、プログラマとしての引き出しの多さに繋がります。
5-3.プログラムの設計どおりに作られているか、常に目を光らせる
特に仕事でのプログラミングでは、プログラムの設計通りに実際にプログラムが作られているか、つねに見守る必要があります。場合によっては、見守るというよりも監視をするくらいでないと駄目なケースもあります。
プログラミングは、本質的には個人の作業です。プログラミングには個人が良いと信じるスタイルや流儀、信念のようなものがあります。ですが、プログラムの設計は、それらの個人の信念よりも守るべき優先度が高いのです。
5-3-1.プログラムはチームで作るもの
プログラムの設計を最優先としないと、プログラム全体がいびつになり、統一感がなくなります。仕事で行うプログラミングは個人の作業ではなく、チームの作業です。チームの成果物として、一つのプログラム群を作るのです。
ですから、チームに参加する全てのプログラマの間でソースコードを共有することが、全てのスタート地点になります。個々人にそれぞれの流儀で好き勝手にプログラムを作ることを許してはいけません。ソースコードは隠すものではありません。チームでプログラムを作るとは、そういうことです。
5-3-2.プログラムはレビューで良くする
プログラムが設計どおりに作られているか、プログラムをお互いにレビューしてチェックします。危険な状態に進みかけていることを感知したらすぐ修正します。レビューでは例外を許さない強い意志と、直すべき点を説得力を持って伝える伝達力が必要です。場合により、ヒューマンスキルも必要です。
ただ、プログラムの設計が駄目な可能性もあるので、プログラマからの意見はよく聞き、判断しなければなりません。駄目なプログラムの設計だと判明したら、すぐに方針変更すべきです。方針変更できる時期を過ぎていたら、少なくとも課題だということを明確に残し、近い将来に必ず直しましょう。
5-4.プログラマ全員が自分はアーキテクトであると意識する
本来は、プログラムの設計とシステムの設計は一体であるべきものです。システムを作る作業において、要件定義・設計・実装の担当者が通常は別の人間であることが、問題のあるプログラムを設計し、作ってしまう原因の一つです。
システムを設計する責任を負うのはアーキテクトです。上流工程でアーキテクトが行うべき仕事は、業務を実現するシステムを、統一された視点で設計することです。言葉を代えれば、システムを抽象化した「モデリング」をすることです。ですから、アーキテクトには技術だけでなく業務の理解も必要です。
プログラムの設計は、本来はそのモデリングの結果を基にして始めるべきなのです。それにより、初めてシステム全体として統一された視点や方向性が確保できるのです。すなわち、個々の設計者やプログラマに、システムの設計を好き勝手に行わせてはならないのです。
5-4-1.アーキテクトがいなくてもプログラムの設計はできる
このように、アーキテクトの責任は重大ではありますが、専任のアーキテクトがいないプロジェクトは非常に多いでしょう。そのようなプロジェクトでは、個々のプログラマがアーキテクトの役割を果たす必要があるのです。
アーキテクトの役割は、この記事内でお伝えしてきたことを考え、プロジェクト内へ広めて徹底させることです。アーキテクトは自身のプログラマとしての経験からこれらのことに熟達しています。ですが、普通のプログラマでも彼らの視点を持ち、彼らの役割を担うことは十分にできるのです。
5-5.駄目な設計のプログラムを捨てる勇気を持つ
駄目な設計のプログラムは、直近では問題ないかもしれませんが、将来へは必ず禍根を残します。納期や要員の都合など、プログラミングの外側にある事情は察せられますし理解もできます。それでも、勇気を持ってプログラムを作り直す決断をすべき時があります。
良いプログラムの設計の裏返しが駄目なプログラムの設計であり、百害あって一利なしです。プログラムが複雑すぎて手を付けられない、テストが非常に困難でいつも同じ箇所で違った問題が出る、職人芸的で他では何の役にも立たないバッドノウハウが必要になる…など、悪いところだらけです。
駄目なプログラムの設計で作られたモノには、要員の作業負荷も、コスト的な負荷もとても余計に必要です。ゆくゆくはその駄目なプログラムがあるプロジェクトには悪い評判が立ち、誰も寄り付きません。結局、何かのタイミングで余計なコストを払って、最初から作り直す羽目になるのです。
5-5-1.リファクタリングのすゝめ
今まで実務で多くのプログラムを見てきました。たまたまかもしれませんが、洗練された、良いプログラムの設計が最初から行われている例はほとんどありませんでした。それでも、良いプログラムの設計を目指さなければならないのです。良いプログラムの設計をできる人を育てていかなければならないのです。
プログラムの表向きの機能や動きを変えずに内部の作りを変えることを「リファクタリング」と呼びます。プログラムの全体を一度に作り直すのは事実上不可能なので、少しずつ良い設計のプログラムに入れ替えていくのです。その作業を勇気を持って始め、粘り強く、根気強く続けていくのです。
リファクタリングは、データや処理の名前を適切にするところから始まります。そのような小さな改善を積み重ね、良いプログラムの設計を当たり前にしていかなければならないのです。今ではリファクタリングに使える様々なツールがあり、優れたリファクタリングの書籍もあります。ぜひ参考にしましょう。
6.さいごに
この記事をお読みになったことで、プログラムの設計の重要さや、良いプログラムの設計をするためのヒントを何かしら掴んでいただけたのであれば幸いです。
私が考えるに、プログラマへ最終的に求められるのは、実装能力ではなく、設計能力です。実際のところ、上手にプログラムを設計できるかが、プログラムを作る仕事がクリエイティビティを感じられる作業になるか、ただの苦行になるかの分かれ目です。
今はプログラムの設計をしない立場の人でも、プログラムの設計を常に意識しながらプログラムを作って欲しいのです。今作っているプログラムは設計の観点からは良いか悪いか、本来どう設計されているべきか。それを自分自身の言葉で、自分自身の意見・意思として表現できるようになって欲しいのです。
良いプログラムの設計は、皆を幸せにします。いずれあなたがプログラムの設計をする立場になった時、良いプログラムの設計ができるかどうかは、過去から、普段からどれだけ意識してきたかに大きく影響されます。全てはあなたの心がけと、これからの努力次第です。
そして、自分がいる環境が世間一般から見てどういうレベルかは、その環境の一歩外に出てみないと分かりません。今の現場で行っているプログラムの設計「もどき」の作業が、実は全くのでたらめ、かつ意味のない作業である…ということも十分ありえます。
この記事でお伝えしたプログラムの設計の観点は、時代により大きく変わることはないでしょう。ずっと昔から同じことが言われ続けているからです。ですが、個々の設計テクニックやベストプラクティスは、少しずつですが進歩や変化を続けています。
ですから、あなたがプロフェッショナルなプログラマでありたいならば、良いプログラムの設計とは何か、さらに良いプログラムとは何かを常に自ら問い続け、勉び続け、実践し続ける必要があるのです。これがプログラマとして成長するための王道なのです。
関連記事
コメント