2015年08月06日

プログラムのソースコードを抽象化するということについて

プログラミングの「抽象化」ってどういう意味で、なぜ必要なのか - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く

という記事を読みました。
説明が非常にわかりやすく、納得のいく点もあり、概ね良い記事だと思います。
が、個人的にはどうしても「それは間違ってないか?」と思う点もいくつかありました。

そこで、そういうとこも含めて、
「プログラムのソースコードを抽象化するということ」
について、本記事で自分なりにまとめてみようと思います。

予め言っておくと、あくまで自分の考えをまとめるだけなのです。
この記事が完全に正しくて、上記の記事の内容が間違っていると主張するものではありません。
「自分はこう考えてるんだけど、これは自分の勘違い?それともあってる?」
これぐらいびくびくしながら書いてます。まさかりください。

もう一つ予め言っておくと、説明に使用する言語はJavaScriptです。
それも伝わりやすいようあえてレガシー(別名:化石)な書き方をします。


さて、ここから本題ですが、抽象化とは一体どういう操作のことを指すのでしょうか。
それを説明するために、例を出していきたいと思います。
ここでは、「入力に『50』を渡して、それを2倍して返す」みたいな処理をしたいとします。
これをプログラムに記述すると以下のようになります。

var input = Number(window.prompt("50を入れてください"));
if(input !== 50) throw new Error("50いれろっつってんだろ!w");
window.alert(100);


おいお前2倍つってるのにしょっぱなから定数で100をalertしてんじゃねえよ!!www



と、思うかもしれませんが、一応これでも仕様は満たしますよね?
「50*2」を暗算すると「100」だし、何も問題ないはずです。

ただ、これぐらいガチガチなコード(※1)をすると、厄介なことが起こります。

例えば、急遽問題が
「あっ、さっき50言ったけど間違いだったわ20ね^^」
と言われた時に

var input = Number(window.prompt("20を入れてください"));
if(input !== 20) throw new Error("20いれろっつってんだろ!w");
window.alert(40);
はい、全ての「50」と書かれた部分を「20」に直して、「100」の部分を「40」にしました。

今回のようなたった3行で記述できるプログラムあれば割りとすぐ対応できますね。
しかしこれが1000行10000行と増えた時に、該当するすべての行を書き換えるのは流石に死にます

そこで人類は抽象化するのです。抽象化の第一歩は「変数化」です。

var requiredNumber = 20; //←要求される数値を変数に格納
var input = Number(window.prompt(requiredNumber+"を入れてください"));
if(input !== requiredNumber) throw new Error(requiredNumber+"いれろっつってんだろ!w");
var output = input * 2;
window.alert(output);
はい、これで「20」を書く場所が1箇所になりましたね。(ついでにalertもinputに書き換えました)

こういう風に変数化することで、ここでもし仮に
「あー20もやっぱ嘘だったわw80なw」
みたいな、殺意が湧く仕様変更がきても

var requiredNumber = 80; //←この一行だけを書き換えるだけ!!!!
var input = Number(window.prompt(requiredNumber+"を入れてください"));
if(input !== requiredNumber) throw new Error(requiredNumber+"いれろっつってんだろ!w");
var output = input * 2;
window.alert(output);
たった1行変更するだけで問題なく動作するようになりました。変数化偉いぞ!!

さて、変数化で世界に平和が訪れた、めでたしめでたし。
…だったら良かったのですが、残念ながらそういうわけにはいきません。

例えばここで
「入力は1~50の範囲の整数が3回重複を認めて与えられるので、それの和を求めてください」
に問題が変わったとしましょう。書くとこうです

var min = 1;
var max = 50;
var a = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください1回目"));
if((a<min)||(a>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(a!==Math.floor(a)) throw new Error("整数よこせっていっただろ!!!");
var b = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください2回目"));
if((b<min)||(b>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(b!==Math.floor(b)) throw new Error("整数よこせっていっただろ!!!");
var c = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください3回目"));
if((c<min)||(c>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(c!==Math.floor(c)) throw new Error("整数よこせっていっただろ!!!");
var output = a + b + c;
window.alert(output);
はい、一応動きますね。
ただ、これもやはり問題を抱えています。なんとなく想像付くと思いますが
「ごめん3個の和じゃなくて4個の和だったわw」
だとどうなるか。

var min = 1;
var max = 50;
var a = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください1回目"));
if((a<min)||(a>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(a!==Math.floor(a)) throw new Error("整数よこせっていっただろ!!!");
var b = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください2回目"));
if((b<min)||(b>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(b!==Math.floor(b)) throw new Error("整数よこせっていっただろ!!!");
var c = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください3回目"));
if((c<min)||(c>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(c!==Math.floor(c)) throw new Error("整数よこせっていっただろ!!!");
var d = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください4回目"));
if((d<min)||(d>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(d!==Math.floor(d)) throw new Error("整数よこせっていっただろ!!!");
var output = a + b + c + d;
window.alert(output);
はい。
これ、100個とか1000個とか10000個とかになったら死にます

そこで人類は抽象化するのです。抽象化の第二歩は「制御構造による抽象化」です。

var min = 1;
var max = 50;
var length = 4; //入力の個数を変数化

var input = []; //入力を配列にする
for(var i=0;i<length;++i){ //for文を使って入力処理を抽象化した
input[i] = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください"+(i+1)+"回目"));
if((input[i]<min)||(input[i]>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(input[i]!==Math.floor(input[i])) throw new Error("整数よこせっていっただろ!!!");
}

var output = 0;
for(var i=0;i<length;++i){ //for文を使って出力計算を抽象化した
output += input[i];
}
window.alert(output);
繰り返される似たような処理を1つに纏めることが出来ました。制御構造による抽象化偉いぞ!!

さて、制御構造による抽象化で世界に平和が訪れた、めでたしめでたし。
…だったら良かったのですが、残念ながらまだそういうわけにはいきません。

例えばここで

 入力は1~50の範囲の整数が3回重複を認めて与えられるので、3数の和を求めて表示してください。
 次に、再度1~50の範囲の整数が3回重複を認めて与えられるので、3数の平均を求めて表示してください。

に問題が変わったとしましょう。書くとこうです

var min = 1;
var max = 50;
var length = 3;
var input,output;

input = [];
for(var i=0;i<length;++i){
input[i] = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください"+(i+1)+"回目"));
if((input[i]<min)||(input[i]>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(input[i]!==Math.floor(input[i])) throw new Error("整数よこせっていっただろ!!!");
}

output = 0;
for(var i=0;i<length;++i){
output += input[i];
}

window.alert(output); //和の出力

input = [];
for(var i=0;i<length;++i){
input[i] = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください"+(i+1)+"回目"));
if((input[i]<min)||(input[i]>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(input[i]!==Math.floor(input[i])) throw new Error("整数よこせっていっただろ!!!");
}

output = 0;
for(var i=0;i<length;++i){
output += input[i];
}
output /= length;

window.alert(output); //平均の出力
またなんかコピペしたようなコードが増えてしまいましたね…。
これは別にすぐ死ぬというわけではありませんが、流石に1000行ぐらいになってくると死にます

そこで人類は抽象化するのです。抽象化の第三歩は「関数による抽象化(※2)」です。

var min = 1;
var max = 50;
var length = 3;
var input,output;

input = GetInput(length); //入力を得る関数に抽象化
output = Sum(input); //和の関数に抽象化
window.alert(output);

input = GetInput(length); //入力を得る関数に抽象化
output = Ave(input); //平均の関数に抽象化
window.alert(output);

function GetInput(length){
var input = [];
for(var i=0;i<length;++i){
input[i] = Number(window.prompt(min+"~"+max+"の範囲の整数を入れてください"+(i+1)+"回目"));
if((input[i]<min)||(input[i]>max)) throw new Error("わけわかんない範囲の値あたえないで?");
if(input[i]!==Math.floor(input[i])) throw new Error("整数よこせっていっただろ!!!");
}
return input;
}

function Sum(arr){
var sum = 0;
for(var i=0;i<arr.length;++i){
sum += arr[i];
}
return sum;
}

function Ave(arr){
var sum = Sum(arr);
return sum/arr.length;
}
コピペされていた部分の処理について、一塊の手続きを関数化しました。
これにより、先ほどコピペされていたコードがなくなりました。関数化による抽象化偉いぞ!!

さて、関数化による抽象化で世界に平和が訪れた、めでたしめでたし。
…だったら良かったのですが、残念ながらまだそういうわけにはいきません。

が!そういうわけにいかなくなってくるのは、かなり大規模になってからです。
なので、中規模では十分抽象化出来ています。
これ以降の話はオブジェクト指向に触れる機会があればその時でも。



〜結局抽象化ってなんだったのさ〜
ここまで長々と例ばかりを出し続けていましたが、私が考える抽象化とはズバリこれです。

同じことを2度も3度も書かないために行う共通化

ほんとシンプルに、ざっくりと。この程度だと思っています。

何故人類は共通化を行うかというと、仕様変更があった際に、修正箇所を極限にまで減らしたいのが主な理由です。
というか他の理由が思いつかない。何かあったら教えて下さい。

そして共通化を実現する手段として、変数だとか、for文だとか、関数だとか(書いてないけど継承やポリモーフィズムだとか、テンプレート(ジェネリクス)やらメタプログラミングだとか)が存在するわけです。ただそれだけなのです。

また、ここでポイントなのは、人類は抽象化がしたいのではなく、共通化がしたいのです。
従って、抽象化したものを使って具象化するケースも大いに有りです。
というよりプログラミング言語自体が、超ウルトラ万能of万能な言語仕様(※3)を搭載している。
コイツ自体かなり抽象度が高い場合がほとんどなので、そこから自分のやりたいことに合わせて組み上げていくのは、むしろ具象化のそれだ。

従って、我々がずっと"抽象化"と呼んでいたものは、
実はプログラミング言語から見たら"抽象度の高めの具象化"に過ぎないのである。

15_08_06.png
※画像はイメージです。実際の抽象度を表しているものではありません。


つまり具象化こそが、プログラマのなすべきタスクなのである。
なので、やり過ぎた抽象化はプログラマが自ら具象化して使いやすい形にして最終的に使っていけばいい。

alert(Mul(3,2)); //これは使いにくい。

//税率を指定したTaxという関数に具象化
var Tax = function(price){
return Mul(price,1.08);
};
alert(Tax(100)); //これならまあまだ実用的

//やり過ぎた抽象化ですね、ゴミ関数です。
function Mul(a,b){
return a*b;
}
ここでポイントなのは
「抽象度の高いものを流用して、抽象度の低いものを作るのは容易い」
ということである。

だから抽象化良いよね、最高だよね、出来るならやっとこうね、という話になる。

もちろん、いい話には裏があって、実際は

・共通化処理を考えるのは慣れていても結構時間がかかる(行数も増えることが多い)
・抽象化の中には一部、実行速度を犠牲にするものが存在する
・相当先見性がないと、適度な抽象化、適度な抽象度を見極められない


辺りがネックになってくるので、そこはプロジェクトごとにケースバイケースで。経験値は大切。

まとめ

1.抽象化とは抽象度の高めの具象化
2.抽象化を実現する手段はいくつか存在する
3.抽象化したものは、開発者がちゃんと尻拭って、最終的には製品に合うように具象化する
4.抽象化にも問題点は当然ある

おわり。


(※1)こういうのをハードコーディングと呼びます
(※2)これも制御構造による抽象化なんですが頻繁に使われる抽象化パターンなのであえて呼び分けています
(※3)チューリング完全ってことが言いたかったんだ…許してくれ
【関連する記事】
posted by がお at 13:04| Comment(0) | 日記 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。