概要
新卒時代の私は、「設計」という言葉の意味がうまく飲み込めませんでした。
ググって出てくる辞書的な説明は以下のようなものです。
建築物や工業製品等といったシステムの具現化のため、必要とする機能を検討するなどの準備であり、その成果物としては仕様書や設計図・設計書等、場合によっては模型などを作ることもある。
今見れば、そりゃそうだ、という感想になるのですが、当時の私にはプログラムを書いたり、そのための検討をすることが設計であるようにはどうも思えませんでした。
競プロを見ていても設計をしている感じはないですし、「ソフトウェアを作る」ということを一通り経験してみるまでは意外と理解しづらいものなのではないかと思います。
そんな中で、「こういうことだったのか」と腑に落ちる機会の一つがの仕組みを理解したときでした。今回はこれについて話します。
結論
- 閉じたモジュールに触れると設計という概念が腑に落ちる。
- 「疎結合、高凝集」の考え方も腑に落ちる。
前置き
これ以降、以前の記事で作成したプログラムを例に説明します。
対象にするソースはheaders/table_index.h、main_table.c、index_table.c、Pool.cです。
モジュール構造とは何か
プログラムのある機能を提供するまとまりをモジュールといいます。1やりたいことに対して必要な処理をいくつかの塊に分けて実装することをモジュール分割といい、モジュールの構造(モジュール間の関係が密か疎か、上下関係など)をモジュール構造といいます。
今回やりたいことは書籍データを登録したり検索することです。
検討の結果、今回はメモリの領域を管理するモジュールと、インデックスを管理するモジュール、書籍データを登録するモジュールの3つに分割しました。
比較のため、モジュール分割せずにソースを書いたらどうなるかを示しておきます。
ソース
#include <stdio.h>
#define TITLE_MAX 150
#define AUTHOR_MAX 50
#define MAX_RECORDS 1024
#define DATA_SIZE 256
typedef struct Record
{
char Title[TITLE_MAX];
char Author[AUTHOR_MAX];
int ISBN_bottomseven;
int Year;
} Record;
typedef struct Table
{
int key_count;
int keys[MAX_RECORDS];
Record pool[MAX_RECORDS];
} Table;
void table_insert(
Table *t,
const char *title,
const char *author,
int isbn_topsix,
int isbn_bottomseven,
int year)
{
if (t->key_count >= MAX_RECORDS)
return;
int i = t->key_count;
t->keys[i] = isbn_topsix;
strncpy(&(t->pool[i].Title), title, TITLE_MAX);
strncpy(&(t->pool[i].Author), author, AUTHOR_MAX);
t->pool[i].ISBN_bottomseven = isbn_bottomseven;
t->pool[i].Year = year;
t->key_count++;
}
char *table_search(Table *t, int key)
{
for (int i = 0; i < t->key_count; i++)
if (t->keys[i] == key)
return t->pool[i].Title;
return NULL;
}
int main()
{
Table t;
t.key_count = 0;
table_insert(&t, "Harry Potter", "J.K.Rowling", 978491, 5512377, 1997);
table_insert(&t, "Capital", "Karl Marx", 978448, 511904, 1867);
table_insert(&t, "Factfulness", "Hans Rosling", 978482, 2289607, 2018);
char *v = table_search(&t, 978482);
if (v)
printf("search 20 -> %s\n", v);
return 0;
}
モジュール分割することによって、以下のようなメリットがあります。
私は以下の2点を理解したあたりで設計の概念が腑に落ちた感じがしました。
自分のモジュールに閉じて考えれば良くなる
まずインデックスを管理するモジュール(index_table.c)について考えます。
これはインデックス構造体を管理しています。
各APIはインデックス構造体の領域確保は上位に任せたうえで、(各APIを使って操作された)どのようなテーブルを引数に受け取っても正常に動くよう設計してあります。
このとき(=index_table.cをプログラミングするとき)、各APIが実際どのように使われるかは強く意識する必要はありません。「どう使われても大丈夫なように」作るだけです。
この考え方が「設計」を感覚的に理解するいい例になったように感じました。
一部を変更/修正する際、影響範囲を抑えられる
また、各モジュールが意味的に分離しているので、ロジックの一部を変更/修正する際に影響範囲が閉じることがあります。
例はリポジトリの方を見てもらえば一目瞭然で、テーブルをソートするように修正したのがindex_sortedtable.cです。コンパイルするソースをすり替えるだけで動くようになります。
モジュール分割の仕方や修正の内容によってはそううまくいかないこともあります。
将来の変更を見越して修正量が少なく/修正内容がわかりやすくなるようにできているか、というのがモジュール分割の優秀性の一つかと思います。
以上がメリットです。
疎結合、高凝集について
また、別々のモジュール同士が依存関係を持たないよう(=結合が疎であるように)設計するのが好ましいとされます。上記のメリットの2つ目「一部を変更/修正する際、影響範囲を抑えられる」に照らして考えると、疎結合である方が影響範囲が小さくなることがわかり納得できると思います。
今回の例でいうと、メモリ管理をするモジュールはレコードのデータ形式に依存しないよう、1レコード256バイトまでなら自由に使える設計にしました。
ただし、256バイトを超える場合はうまく動かないので、「pool_initで1レコードあたりのサイズを指定できるようにする」設計よりは密結合になっているといえます。
最後までお読みいただきありがとうございました。
-
コンポーネントとか呼んだりしますが使い分けは曖昧です。縦の関係を意識するときはレイヤーとか処理層と言ったりします。 ↩