プログラミングにおける抽象度とレイヤー

323, 2020-05-15

目次

抽象度とレイヤー

抽象化とは、何かの手段や構造を抽象的に表現することだ。
たとえば関数の引数。
複数ある引数を一つの構造体にまとめる。

void func(int x, int y, int w, int h);

void abs_func(rect_t rect);

こうすることで剥き出しのプリミティブな変数の抽象度が深くなり、レイヤーが一段増えることになる。

layer 0: x, y, w, h
layer 1: rect

このレイヤーは抽象度の深度を表している。このレイヤーが多ければ抽象度が高いことになる。

さて、抽象度が深くなると何が嬉しいのか?
これは経験則と一般則から言えるが、普通は抽象度を深くしたほうが実装はしやすくなる。
たとえば↓のような処理。

int vx = 1;
int vy = 1;
player.x += vx;
player.y += vy;

これは構造体が剥き出しになっているので、関数でラップすることで抽象度を深くすることが出来る。

void player_move(player_t *player, int vx, int vy) {
    player->x += vx;
    player->y += vy;
}

player_move(&player,  1, 1);

さらに vector2_t を定義してそれを渡すようにすれば、抽象度はさらに深くなる。

void player_move(player_t *player, vector2_t v) {
    player->x += v.x;
    player->y += v.y;
}

vector2_t v = {1, 1};
player_move(&player,  v);

player.x に直接アクセスしていたコードが、ずいぶんと回り道に見える実装になった。
プレイヤーの移動処理は player_move に抽象化され、座標などのデータも vector2_t に抽象化された。
注意したいのは、抽象化することで、直接的な演算が見えなくなっていることだ。
今までは player.x に直接代入するコードがあったが、これが見えなくなっている。代わりに、開発者に必要とされるのは player_move 関数と vector2_t についての理解だ。座標の演算は player_move が、座標の管理は vector2_t がやってくれるので、開発者は理解しなくてもいい。

このように、抽象度を深くすると、開発者の理解すべき実装も抽象化される。つまり、↓のレイヤーで言う所の

layer 0: 座標の演算、座標の管理
layer 1: player_move, vector2_t の使い方
layer x: 開発者

layer x に接しているレイヤーだけの理解で済むことになる。
さて、もう一層抽象度を深くしてみよう。問題にしたいのが↓のコードだ。

vector2_t v = {1, 1};
player_move(&player,  v);

このコードから、開発者は vector2_tplayer_move について理解しなくてはいけないことがわかる。
しかし、まだ抽象化出来る。
たとえば↓のように。

player_advance(&player);

player_advance 関数はプレイヤーに加算するベクトルも抽象化して、開発者には見えなくしている。
これによって開発者は、player_advance の使い方だけを理解すれば良いことになる。

layer 0: 座標の演算、座標の管理
layer 1: player_move, vector2_t の使い方
layer 2: player_advance の使い方
layer x: 開発者

抽象度を深くすることによって、二つ合ったものが一つになった。よって開発者は理解すべきものが減った。

ここまでの話は、ライブラリやフレームワークの実装で一般に行われている抽象化の話だ。
ライブラリを使ったことがある人なら見慣れた話だったと思う。

抽象化の副作用

もちろん抽象化は万能ではなく、副作用も含んでいる。
さきほどの player_advance 関数を使用していた開発者はこう思った。

「移動量を変えたい」

するとどうだろう。player_advance は途端に邪魔になる。開発者は移動量を変えたいのに、player_advance にはその手段が見えない。関数には引数も渡せない。これは困る。
ライブラリ開発者は、そんな開発者のために次のような関数を提供する。

void player_advance_by(player_t *player, int v);

このように抽象化された関数は、小回りが効かなくなる。つまり細かい融通が効かないのだ。

抽象化と小回りの両立

抽象度を深くしてかつ小回りも使えるようにしたい。
そんなことが可能だろうか?
実は身近なアプリにそれを実現しているものがある。それは UNIX 系のコマンド群だ。
コマンドは main 関数のインターフェースを持っている。それは↓のようなものだ。

int main(int argc, char *argv[]) {
    return 0;
}

引数は argcargv だが、argv には実行ファイル名とコマンドライン引数が格納される。
そして、コマンドライン引数にはオプションを渡すことが出来る。オプションはショートオプションやロングオプションなどだ。このオプションによってコマンドの振る舞いを変える。

この main 関数のインターフェースは抽象度が非常に深い。

layer 0: なにかの処理 
...
layer n: オプションの使い方
layer x: 開発者

開発者はオプションの使い方さえ学べば、コマンドに色んな振る舞いをさせることができる。
これは抽象化と小回りの両立と言える。

抽象化の目安

では先ほどのコマンドの話で、player_advance にも argcargv を渡せばいいのではないか? という疑問が生まれる。
つまり↓のようなことだ。

void player_advance(player_t *player, int argc, char *argv[]);

この関数の振る舞いを変えるにはオプションを使う。

int argc = 2;
char *argv[] = {
    "--amount",
    "4",
    NULL,
};
player_advance(&player, argc, argv);

これを見てどう思うだろうか。使いやすいだろうか? 私は使いやすいとは思えない。抽象度は深くなっているはずなのに、使いづらい。
それに関数の実装を考えると、オプションの解析もやらなくてはいけない。関数を呼び出すごとに解析をやっていたら関数の呼び出しも遅くなる。

このようにモノゴトには適切な抽象度の深さというものがあるのがわかる。
この適切な深さを探すのがライブラリ開発者の仕事だ。
浅くては使いづらいし、深すぎても使いづらい。丁度いいところを見極める必要がある。それこそ職人芸のように。

Webアプリケーションの制作ならNARUPORT

Webアプリケーションの制作ならNARUPORTにお任せください。
Webアプリの他にもGUIアプリやChromeExtension, スクリプトの制作など可能です。
以下のお問い合わせフォームからご依頼ください!

投稿者名です。64字以内で入力してください。

必要な場合はEメールアドレスを入力してください(全体に公開されます)。

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク