プログラミングの設計における抽象化の方法

289, 2020-02-10

目次

プログラミングにおける抽象化とは?

「抽象化(ちゅうしょうか)」……。
その魅力的で甘美な響きは数々の開発者を魅了してきた。
私も例外ではない。

プログラミングにおける抽象化とは、設計における問題解決の手法である。
複雑で具体的な手続きを、シンプルで抽象的なものに変形する。

すると何が起こるか?

生産性が上がるのである。それも劇的に。

しかし、プログラミングにおける抽象化は、開発者のセンスが求められる。
そのため、抽象化の前に散っていった開発者は多い。
その屍の積み重ねは、スタックオーバーフローを起こすほどだ。
私も例外ではない。

この記事では抽象化の手法について具体的に見ていきたいと思う。

具体的な抽象化の例

神は言われた。
得たければまずコードを与えよと。
コードより明快で直結した提示はない。
私もコードを示そうと思う。

さて、まず見てほしいのは↓のコードだ。

# obj1はobj2に衝突しているか?
collided = False
if abs((obj1.x - obj2.x)) <= obj1.width and abs((obj1.y - obj2.y)) <= obj1.height:
    collided = True

if collided:
    print('obj1 collided to obj2')

このコードはobj1obj2が衝突しているか矩形判定をしているコードだ。
なんて複雑なコードだろう。私はめまいがする。
とてもじゃないが、また同じコードを書こうなどという気分にはなれない。
皆さんはどうだろうか?

こういう時に抽象化を行う。
抽象化するとさっきのコードはこうなる。

# obj1はobj2に衝突しているか?
if obj1.collision_to(obj2):
    print('obj1 collided to obj2')

ずいぶんすっきりしたが、お分かりいただけただろうか。
先ほどのabsや比較演算やらandやらの集まりが、シンプルなメソッドにまとめられている。
そしてメソッドにはわかりやすい名前が付けられ、開発者に微笑みを返している。

これぐらいのことは皆さんは常日頃からやっていることで、大したことではないが、抽象化の威力がよくわかる例だと思う。
さっきのコードに比べると、抽象化したコードはまた使ってみようという気持ちになれる。

ファクトリーパターン

さらに例を挙げよう。
我々はゲームを作っている。
そしてゲームには無数の敵オブジェクトが存在する。
この敵オブジェクトを生成し、使いたい。

私が最初に書くコードはこうだ。

hp = Hp(10)
weight = Weight(80)
fat_cat_enemy = CatEnemy(hp, weight)

hp = Hp(4)
weight = Weight(40)
poor_cat_enemy = CatEnemy(hp, weight)

太った猫、fat_cat_enemyや貧しい猫、poor_cat_enemyを部品を詰め込んで生成している。
これもまた、なんて複雑なコードだろう。私のため息は沸騰したヤカンのように止まらない。

このコードを抽象化するとこうなる。

fat_cat_enemy = enemy_factory.create('fat cat')
poor_cat_enemy = enemy_factory.create('poor cat')

おわかりいただけただろうか。太った猫や貧しい猫という一連の構築処理が、fat catpoor catという文字列に抽象化されている。
これは有名なデザインパターンのひとつだ。
このコードでは、また太った猫を何体も作ってみようという気になれるだろう。

抽象化による生産性の向上

このように抽象化を行うと、生産性を上げることが出来る。
その効果は積み重ねによるものだが、気が付くととてつもなく大きな効果を生むことになる。

さらに抽象化はコードを節約するので、設計もしやすくなる。
はっきり言って、設計を成功させたいなら抽象化を考えるべきである。
念入りに議論し、トライ&エラーを繰り返し、これぞ求めていた抽象化だヒャッハー! と言えるようになるまで心血を注ぐべきである。

ソフトウェアの設計は困難を極めるため、それぐらいやってやっと成功の兆しが見えてくると言える。
これを忘れると過去の私のようにスタックに積まれ、無数の剣を突き立てられ、火あぶりにされることだろう。

抽象化と文字列

抽象化は、とことんまで突き詰めると文字列になる。
たとえばC言語におけるfopen関数を考えてみよう。

FILE fin = fopen(fname, "rb");

fopen関数はファイルを開く関数だ。第1引数に開きたいファイル名を、第2引数にモードを指定する。
このfopen関数は抽象化されている。
特にモードに注目してほしい。モードは文字列だ。そしてrbやらwbやらabやら沢山のオプションを書くことが出来る。
複雑で大量にあるモードを文字列に抽象化することで、開発者の使い勝手を向上し、コードを書きやすくしている。

このように、抽象化を行っていくと、最終的には文字列に行きつくことが多い。
先ほどのファクトリーパターンを思い出してみて欲しい、あれも敵オブジェクトの構築方法を文字列に抽象化していただろう。

抽象化の程度

しかし、何でもかんでも文字列にすればいいというわけではない。
例えばC言語のmain関数を考えてみよう。

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

注目してほしいのはmain関数の仮引数だ。
main関数はコマンドライン引数を引数の文字列の配列に取る。
この文字列の配列がたとえばただの文字列だったらどうなるだろうか。
先ほどの論理だと配列より文字列にした方が抽象度は高い。

つまり、こうだ。

int main(const char *args) {
    return 0;
}

このmain関数を使って作ったコマンドは↓のように呼び出されることだろう。

$ cmd "this is argument"

複数の引数を渡したい時は、文字列のフォーマットを決めて統一する必要があるだろう。
たとえばCSVだ。

$ cmd "this, is, arguments"

main関数内ではCSVをパースしてただの文字列を引数のリストに変換する必要がある。

ここまでの設計はとても抽象化されているように見える。
が、しかし、果たして使いやすいだろうか?
答えはNOだ。

この抽象化によって開発者には2つの仕事が追加されてしまう。
ひとつは引数のフォーマットの決定、それからフォーマットに従った引数の文字列の解析だ。

抽象化は基本的には開発者の負担を減らすためにあるのだが、この例では逆に仕事が増えている。
コマンドライン引数の特性、つまり、コマンドに続いて引数が半角スペース区切りで渡されるという特性に対しては、単純な文字列より文字列の配列にした方が使い勝手が良いことになる。

このように抽象化は程度によっては、逆にコストを上げてしまうことがある。
この抽象化の程度を見極めて設計をするのは開発者のセンスによるところになる。

おわりに

抽象化はやり過ぎても駄目だし、やらな過ぎても駄目だ。
まるで倦怠期のパートナーのような付き合いが必要になる。
ここで挙げた抽象化の例はほんの一部分だったが、実際の開発ではこれより高度な抽象化が必要になるだろう。
コツとしては、文字列から逆算的に抽象度を下げていくことだ。そうしてどこら辺が丁度いいのかを探す。
以上だ。

おしまい

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

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

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

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

投稿する内容です。

スポンサーリンク

スポンサーリンク

スポンサーリンク

スポンサーリンク