読者です 読者をやめる 読者になる 読者になる

プログラミングの「抽象化」ってどういう意味で、なぜ必要なのか

<追記>いろいろ反応あってたしかになーって思いましたが、ここで説明されてるのは「汎化」とか「パラメタライズ」としたほうが正しいですね。抽象化というと、一塊の手続きをブラックボックスにして、実装を隠蔽する面のほうが正解に近いです。でもまあそこを差し引いて読んでいただければ、それなりに有用ではある記事だと思うので、このまま残しておきます</追記>

プログラミングに限らない話かもしれませんが、ふだんの生活で触れないような概念というのは、一度わかってしまえば便利なんだけど、どうしてもとらえどころがない、というようなことが多いと思います。プログラミングにもそういう概念はたくさんあって、わたしのような凡人は新しい概念にぶち当たるたびに苦労しています。今日はそんな中で「抽象化」という言葉について、「昔の自分にこうやって説明してあげたかったな〜」という説明をします。

プログラミングを学んでいく中で、「とりあえず処理が書ける」というところから一歩進んで「良い設計ってなんだろう」ということを考え始めたとき、いきなり「抽象化」という言葉が出てきて面食らったことはありませんか?わたしはあります。「抽象化ってこういうことだよ」という説明のないまま、「抽象化」の事例をたくさん見ることで「うーん抽象化ってなんとなくこんな雰囲気……」みたいな感じでお茶を濁す感じになる事例というのはけっこう多い気がしています。

そんなふわっとした「抽象化」という言葉ですが、わからないときは逆の言葉の意味を考えて見るといいでしょう。「抽象的」の反対は「具体的」ですね。つまり、「抽象化」というのは「具体的じゃない状態にする」ということだと思えばいいでしょう。まだちんぷんかんぷんですね。例で考えてみましょう

たとえば、「user_id:1のユーザーの所持金額のうち50Gを、user_id:2のユーザーに移行する処理を作ってくれ」という指示が出ているとします。これを愚直に実装すると、以下のような感じになるでしょうか(トランザクションとかそういう話は今回の話に直接関係ないので無視します)。

def transfer_gold
  # user_1 から50G引いて
  user_1 = User.find(1)
  user_1.gold -= 50
  user_1.save!

  # user_2に50G足す
  user_2 = User.find(2)
  user_2.gold += 50
  user_2.save!
end

このコードを、「具体的じゃなくして」行ってみましょう。

今はuser_id:1のユーザーとuser_id:2のユーザーという「具体的な」ユーザーを直接コードで書いていますね。ここの「具体性」をなくしてみましょう。

def transfer_gold(from_user_id, to_user_id)
  user_a = User.find(from_user_id)
  user_a.gold -= 50
  user_a.save!
  
  user_b = User.find(to_user_id)
  user_b.gold += 50
  user_b.save!
end

はい、ユーザーidを引数で与えるようにしました。これで、コードからは「具体的なユーザーid」を排除して、ちょっと「具体的じゃないコード」になりましたね。

もうちょっと具体性を排除していきましょう。今は50Gという具体的な金額がまだコード上に残っています。これも引数で渡してあげましょう。

def transfer_gold(amount, from_user_id, to_user_id)
  user_a = User.find(from_user_id)
  user_a.gold -= amount
  user_a.save!
  
  user_b = User.find(to_user_id)
  user_b.gold += amount
  user_b.save!
end

はい。これでコードからはさらに「具体性」がなくなりました。こんな感じで、「具体的な値」とかをコードから排除していってコードの具体性を減らす行為全般を「抽象化」と呼ぶと考えて良いでしょう。

ところで、具体的な値をコードから追い出したことによって、なにが起こったでしょうか?具体的な値がベタ書きされていたときと違って、コードが「いろんな値に対応するようになった」ということが言えるでしょう。引数に10Gとuser_id:3とuser_id:4を渡せば、user_id:3からuser_id:4に10G移行することもできるし、20Gとuser_id:5とuser_id:6を渡せばuser_id:5からuser_id:6に20G移行することもできます。

さて、いいことづくめのように見える抽象化ですが、今度は「そのコードを使う側」の視点から見てみましょう。もしプログラムに具体的な値がベタ書きされていた場合、使う側はなにも考えずにそのコードを実行すれば良いだけです。一方、抽象化されたコードの場合、使う側が「どのユーザからどのユーザにいくら渡すのか」ということを判断しなければなりません。

そこに注目すると、抽象化というのは、具体的な値としてなにを使うのかという「判断の責任」を「そのコードを使う側」に押し付けることでもあることが見えて来ると思います。

言い換えます。抽象化されていない具体的なコードでは、メソッドの側に「具体的な値はこれとこれ!」ということが書いてあるので、呼ぶだけで具体的に意味のある処理が実現されます。一方、抽象化されたメソッドでは、メソッドを呼ぶ側がそこに「具体的な値」を与えることではじめて具体的で意味のある処理が実現されます。

まとめると、抽象化によって、使うときにいろんな値を突っ込んで使うことができ、柔軟になるというメリットを得る一方で、使うときにその都度使う側が「どんな値が適切なのか」ということを判断する責任を負うというデメリットも負うことになるわけです。

note:ところで、さきほど「具体的な値」「とか」 をコードから排除する、と書きましたが、とか、というからには、値以外のものも抽象化することが可能です。抽象化のレベルが上がってくると、値だけではなく、たとえば計算内容そのものを抽象化したり(高階関数やストラテジーパターンなどがそれを実現します)、型を抽象化したり、というパターンも出てきます。しかし、どんな抽象化も、「具体的な<なにか>」をコードから排除して、そこを「入れ替え可能なもの」にするという点では共通しています。「抽象化」という言葉がでてきたときには、「ここではどんな具体的なものをコードから引っぺがしているのだろう」と考えてみると、すっきりと理解できることが多いでしょう。

さて、以上の議論を踏まえた上で、「世界一抽象化されたコード」というのを今から書いてみましょう。一瞬でかけます。

# nop

上にあげたものがそうです。つまり、「なにもしない」というコードこそが、世界一抽象化されたコードです。冗談を言っているわけではありません。抽象化とは「具体的な値や処理などをなくして、使う側が必要なそれを選べるようにする」ものでした。では「はい、まったくなにも書かれてません。ここに必要な処理を書いていってください」というのが、究極に抽象化されたコードになるのはなにも変なことではありません。こんな落語みたいなことを言ってなにが主張したいのかというと、それは「抽象化すればいいってもんじゃない」ってことが言いたいのです。

抽象化はあくまで「具体的な判断を遅らせることで柔軟性を得る」ためのものです。コードが具体的な処理を行うためには、必ずどこかでその「具体的な判断」をしなければなりません。冒頭の例ならば、必ずどこかで引数に具体的なuser_idや金額を与えなければならないわけです。

そうである以上、プログラムの設計では「どこを抽象化するのか」「どこまで抽象化するのか」という判断が必要になってきます。しかし、「抽象化」のメリットデメリットを理解した今なら、その判断の一助になるものさしを持っています。それは、「具体的な判断は誰がするべきなの?」という視点です。

たとえば最初にあげた抽象化の例ですが、アプリケーションの仕様上、本来はお金の移行というのはありえない処理で、事故処理のためにめっちゃ特別に今回限りのスペシャル対応としてたった一回だけ行われる処理なのであれば、抽象化されてないメソッドのほうが、「使う側の負担」は少なくなりますね。なぜなら、使う側が「えーっと今回の事故処理ではだれからだれにいくら移行しなきゃいけないんだっけ」ってことを考えなくていいからです。一方で、どんなユーザーからどんなユーザーにいくらでもお金の移行が行われ得るような仕様のアプリケーションならば、移行が行われる都度「だれからだれにいくら」という判断をしなければいけないので、抽象化して「メソッドを使う側」にその判断をしてもらうべきでしょう。

かように、「正しい抽象化」というのはアプリケーションに求められている性質によって異なってきます。プログラムの設計とは、「このアプリケーションにはこういう性質があるから、だからここは抽象化して「使う側」に判断の責任を持ってもらおう、逆にここは具体的に書いておこう」ということを考えることでもあると言えるでしょう。

抽象化するための方法はいろいろあります(メソッドにして引数で渡すようにするとか、デザインパターンとか、型クラスとか……)が、それはあくまで「実装方法」であり、設計そのものではありません。実装方法をたくさん知っていることは武器にはなりますが、実装方法を知っているからといってむやみやたらに抽象化をしまくったとしても、それは単に責任の所在がいろんなところにとっちらかってて使いにくいプログラムにしかなりません。ギターの早弾きができることは武器になりうるけど、音楽的に意味のない早弾きは曲芸でしかないみたいなもんです。抽象化の方法をたくさん知った上で、アプリケーションが求める抽象化を正しく見出せるようになりたいものですね。

ブックマークコメントでいろいろ補足や指摘されていて、もっともな話が多いので、そちらも参考にしてください。とくにこのコメントは「まじでそのとおりですね!」という感じです。

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

これは抽象化の役割のうちパラメタライズに注目した説明。抽象化には他にも様々な問題に共通する構造を取り出すという役割があり、それは時に思考の足場や指針を与える。nopの例えが変なのは後者を考えてないから。

2015/08/05 09:00

なお過去に書いた抽象化の記事として適切に抽象化されたコードとはなにかって話というのがあり、こちらではこの記事で触れることができなかった抽象化の側面について触れています。よろしければそちらもお読みください。

宣伝

8/20,21,22に開催される、プログラマフジロック(あるいはプログラマの夏コミ)とも言えるYAPC::Asia 2015 にて、Perlで学ぼう!文系プログラマのための、知識ゼロからのデータ構造と計算量という発表をします。今回の記事の内容とはまたちょっと趣が変わって、もうちょっと実装よりの話になりますが、なるべくわかりやすい発表を心がけますので、気になった方はぜひ聴きに来てください。

なお、すでにチケットは完売しておりますが、なんとイベントレポーターになるとタダでYAPCに参加できるという裏技が存在しております!ぜひご検討ください。