🎃

オブジェクト指向を動物の例で説明するのは有害ではないか

2024/09/16に公開8

オブジェクト指向の説明で、生き物を使った例を時々見かける気がします。こういうやつです:

interface Animal {
    ...
}
class Dog implements Animal {
    ...
}
class Cat implements Animal {
    ...
}

こういう 「生き物のように感情移入しやすい対象」を例として使うとプログラミング技法の本質を見誤るのではないか? という話をします。

カプセル化

多くのクラスベースオブジェクト指向言語は、アクセス修飾子によってメンバーの公開範囲を制御できます。カプセル化というやつです。

class User {
    public String name; // 公開
    private int hidden_info; // 非公開
}

さて、publicなメンバーが誰でも見られるのはいいとして、privateなメンバーの公開先はどうあるべきでしょうか?候補は次の2つとなるでしょう。

  1. 公開範囲は「自分だけ」つまりprivateメンバーを触れるのは this/self 経由のみとする。
  2. 同じクラスであれば他のインスタンスのprivateメンバーでも触れるものとする。

クラスベースのオブジェクト指向を学んだ者であれば誰もが考えたことのあるこの疑問に、あなたはどう答えますか?あるいは、あなたが新しくプログラミング言語を作るならばprivateメンバーのアクセス範囲はどう設計しますか?

オブジェクトに感情移入する人であれば、「自分だけ」と答えるかもしれません。ホモサピエンス同士であっても私物を他人に無遠慮に触られると嫌ですからね。しかし、現実のプログラミング言語を見ると、privateなメンバーは同じクラスであればインスタンスに関わらず参照できるものが多いと思います。

カプセル化は何のためにあるか

そもそもカプセル化は何のためにあるのでしょうか?理由はいくつか挙げられるでしょう。

一つは「型の不変条件を守るため」です。型の不変条件が守られているかプログラマーがチェックする時、メンバーがpublicであればプログラム全体からそのクラスを触る箇所を探してチェックしなければいけません。一方、メンバーがprivateであればチェックする範囲はそのクラスの実装だけで済みます。

別の理由として「変更時の影響範囲を抑える」が考えられます。複数のパッケージを利用する開発の場合、一つのパッケージの一つのクラスの実装の詳細を変えたために他のパッケージが壊れた、ということになると厄介です。「バージョン違いで変わる可能性のある実装の詳細」に触れるのは自分のパッケージのみに制限しておけば、下流のパッケージを壊す心配がなくなります。

いずれにせよ、 カプセル化をする目的はプログラマーがコードを管理しやすくするため です。 決して「オブジェクトに自我があって、その私有物を尊重するから」ではない のです。

そして、この原則に立ち返れば「privateの対象がクラスという単位であること」に深い必然性はなく、「同じモジュールのみから参照可能」「同じパッケージのみから参照可能」という修飾子があっても良いと思えるでしょう(実際、言語によってはそういう修飾子があります。C#のinternalなど)。

オブジェクトに感情移入させるような教材は有害ではないか

「オブジェクトに自我はない」。当たり前ですね。プログラミング言語のオブジェクトなんてメモリ領域とメソッド(振る舞い)の組み合わせでしかないのですから。自我があるのはコードを管理するプログラマーです(AIがコードを管理するようになったらどうするのかという話はここではしません)。

しかし、この「当たり前」を初学者は「当たり前」と思えるのでしょうか?オブジェクト指向の教材では動物クラスや感情移入しやすいクラスを使ったミスリーディングな説明をしていないでしょうか?

というわけで、この記事のタイトルの問題提起なわけです。

(セキュリティーの話題だと複数の主体(攻撃側 vs 防御側)が絡んできますが、それはプログラミング言語の機能とは独立する問題なのでここでは扱いません。)

なお、Rubyでは

この記事の主張に反し、Rubyではprivateを触れるのは「自分のインスタンスだけ」のようです。私よりMatzの方が「オブジェクト指向」というものをよく理解しているに違いないので、きっと私の「オブジェクト指向」の理解が足りないか、あるいはRubyでは自我と羞恥心のある本物の「オブジェクト」を作れるのでしょう。そういうわけで、私の記事の主張は「オブジェクト指向半可通が書いたもの」として話半分で聞いてください。

Discussion

かえるかえる

ご存知でしたら申し訳ありませんが、リンネらによる生物の分類は英語でBiological Classificationと呼びます。これは綱とか門とか目とかそういったものが含まれます。これは英語圏で小学6年生ごろに習います。classは原義が「類」「分ける」の意味で、学校のクラスやクラシックが高級なのもそれ由来です。
そのためclass Animalは、もともと哺乳類(≒ 哺乳綱 / Mammalia)を意識したもので、自然な流れでできたものだと思います。

また数学の集合論にも「類(class)」がありますが、もともとの考え方はこちら由来だと考えています。
対象が動物であっても機械的にその性質を捉えるような理系的感覚で記述しているのかもしれません。

ただし、以上は推測ですので特にソースはありません。

Object(客観 / 対象 / 主観から見たモノ)と考えると主体はなく自我はありませんが、Agentと捉えて自我風味のものを搭載させることはできるので、OOPはその点がおもしろいと思います。もともとOOPはObject同士がメッセージパッシングし合うもので、全体の管理者がすべてを把握していなくても、classで定義した通りにすれば無限に広がりを持つというのが根本思想なのかなと感じています。Switchのブレスオブザワイルドのような感じでしょうか。DogのJohnyとSally、CatのMistyとSnowのような形で「生命を生成できる!」という部分の説明は魅力的な部分だと感じています。
ライフゲームなども、単純なルールの記述で生命らしきものが誕生する!というのが魅力的な部分ですし。

ObjectがObjectを自己参照するときにself となっているのは、まさにObjectに自我があるような思想で書かれたもののように見えます。「自己参照」という言葉自体も自我を示しているような感じがします。自ら己に参じて照らしているので、光り輝くナルシシズムを感じます。
キリスト教圏では「神が人間をプログラムした」という考えが受け入れやすいのもありそうです。「そもそも人間や動物には自我ってなくて、われわれは神の定めた運命の下僕だよね」みたいな思考です。
また、自分はclassに感情を設定して感情移入してもいいと思います。その証拠に、美しいコードは美しく、うっとりするではないですか。objectが勝手に発狂して他のobjectに勝手にデータを渡しに行くとか、羞恥心を覚えて重要な情報を渡さないobjectの存在は非常におもしろいと思います。感情移入は現場では全然即効性のある形で顕現せずアカデミックに寄り過ぎますが、もっとプログラミングは自由であっていいはずだと思います。

とはいえ、当時のプログラミング教育と現在のプログラミング教育は在り方から異なる上、日本語圏と英語圏では全然考え方は違うので、アップデートは施されなければならないと思います。
自分も以前はAnimalでの説明は有害だと考えていました。急に何の説明もなくDogやCatと言われても「それって何のつながりがあるの…」と思いますし、その後のコーディングと直結しないので関連性がわからずイライラすると思います。「classってなんだよ」とすら思います。
プログラミングによる生命創造について熱く語れない人がAnimalを例に使うのは単なる前例踏襲主義なので、class Carとかclass Engineとかにした方がいいというふうに思っています。もしくは最初にゲーム作りから始めるとDogやCatはすごく自然なもののように見えます。

それにしてもinterface Animalは凄まじく嫌悪感を感じますね。間違ってないとしてもセンスが悪く、自分は好きではないです。

だめぽだめぽ

オブジェクト指向に陶酔しているところ申し訳ありませんが、この記事にはタイトルと最初の段落以外にも内容があり、特にカプセル化について論じています。長文のコメントを残す暇があったらせめてカプセル化の「カ」の字くらいは言及していただきたいものです。

オブジェクト指向があなたのように文章を読まない人を生み出すのであれば、オブジェクト指向という考え方は人の目を曇らせる麻薬のようなものであり、有害であると判断しなければなりません。

Hidden comment
かえるかえる

言及はしていませんでしたが、カプセル化についても読んでいますよ。
カプセル化については正しいと思います。カプセル化について特に追加で言及するところはないです。
「動物で例えるのは有害ではないか?」という問題提起に見えましたので、「わからなくもないけど有害とまでは言わないのでは?」と思ったという、1つの意見として捉えていただければ幸いです。

またOOPを触っている人は私だけではないので、私の意見だけを見て「この人はOOPに陶酔しており、OOPの考え方は人の目を曇らせる麻薬のようなもの」と考えるのは早計ではないでしょうか。だとしたらこの世は有害プロダクトで構成された有害社会のようなものと位置付けられてしまうような気もします。あまりにも悲観的です。またこれは話半分で捉えていただきたいです。

kat00kat00

例え話だから、そこまで有害とはおもわない。
もともとSmalltalkとかC++の時代において、動物や乗り物で例えることのほうが、理解しやすい例えだったのだと思う。
私の場合、ある書籍に記載されていた例えで理解を深めた。
それは、Mr.Xという人物で例えたオブジェクト。

Mr.Xは自分の出生や名前、機密情報を持っているが、彼の持っている情報は他人からは知ることができない。特定の仕事の話(信頼する相手であれば機密情報を話してくれるというメソッドで)しか聞き出せない。プライベートな情報は聞き出せない。
というものだった。

そこに感情移入など存在しない。

もし感情移入するのであれば、それはソフトウェアの作りから逸脱して、余計な思考に話はそれてしまっていると思う。

オブジェクト指向でない例えで感情移入が余計だという例えをする。

ワニ革の財布がカッコいいと言いながら、『このワニはどこで狩られたのだろう。さぞ無念だっただろう。』と、余計な感情に浸ってしまい、買うことを躊躇する...みたいな話に聞こえてしまう。

今やクローンペット(遺伝子だけコピーだが別の個体のペット)や(古い話だが)Aiboなどのメカに感情移入することがあり得る時代だけど、例え話に過剰反応しなくていいレベルってあると思う。

倫理的に際どいたとえ出ない限りは、問題ないと私は考えますよ。

まりもまりも

カプセル化と、ポリモーフィズムと、動物の例(抽象化)は別に発達したものをオブジェクト志向としてまとめて扱ってるだけなんで、相互関係はそんなにないよ。

Hidden comment
さざんかぬふさざんかぬふ

面白い論点ですね。

  • カプセル化をする目的はプログラマーがコードを管理しやすくするためである
  • オブジェクトには自我はない
  • オブジェクトに自我があると誤解させる説明があるとしたら、それはよくない
  • 動物の例でオブジェクト指向を説明すると、オブジェクトに自我があると誤解させる可能性がある

という事は概ねその通りだと思いますが、いくつかわからないこともありました。

まず、privateの定義について。私は

クラスベースのオブジェクト指向を学んだ者であれば誰もが考えたことのあるこの疑問

を考えた事がなかったので、たぶんクラスベースのオブジェクト指向を学んでいないのですが、例示された1と2の考え方は、例えばscalaではprivate[this]とprivateによって区別されている概念ですね。
まずprivate[this]という書き方が存在している時点で何らか有効な場面がある(とscalaの設計者は考えた)という事がわかりますが、実際にこれは「型の不変条件を守るため」に貢献する場合があります。
以下、中身の量をリットルとキログラムの単位で保持するグラス(感情移入しにくいクラスですね)を例として、

class Glass:
  private var volumeL: Int = 0
  private var weightKg: Int = 0
  // 暗黙の不変条件:volumeL == weightKg

  def dropContents(): Unit =
    volumeL = 0
    weightKg = 0

  def fill(amount: Int): Unit =
    volumeL += amount
    weightKg += amount  // dirty

  def transferTo(other: Glass): Unit =
    other.volumeL += volumeL
    dropContents()
    // 不変条件が壊れる(バグっている)

  override def toString(): String = s"Glass: $volumeL/$weightKg"

end Glass

上記はtransferToでバグってしまった例ですが、private[this]の場合はother.volumeLを直接変更できないので、不変条件が壊れにくい書き方に誘導することができます。

class Glass:
  private[this] var volumeL: Int = 0
  private[this] var weightKg: Int = 0
  // 暗黙の不変条件:volumeL == weightKg

  def dropContents(): Unit =
    volumeL = 0
    weightKg = 0

  def fill(amount: Int): Unit =
    volumeL += amount
    weightKg += amount  // dirty

  def transferTo(other: Glass): Unit =
    other.fill(volumeL)
    dropContents()
    // 直接private[this]変数を修正できず、より不変条件の担保された操作を使用させやすい

  override def toString(): String = s"Glass: $volumeL/$weightKg"

end Glass

(この例だけでいえばimmutableにすべきなのでは?という気もしますが、あくまでも説明の便宜としてご理解ください)

そうすると、選択肢1も2も一定の価値を有するので、privateを1とするか2とするかというのは自我云々という事とは関係なく、単純に技術的な意味で検討余地のあることだとわかります。

また、動物を例にした場合に、たしかに論理的にはオブジェクトに自我があると誤解させる可能性はあるのですが、これが本当に問題になるぐらいに多いものなのかは、私には判断がつきませんでした。これは、私の周りにそういう人がいなかっただけで、巷にはそういう人が溢れているのかもしれず、実際のところが全然わかりませんでした。

だめぽだめぽ

ご意見ありがとうございます。

正直に言うと Glass の例は私にはあまり響かないですね。privateに直接触る場合は不変条件を保つ責任があるので、transferTo を実装する人の責任であり、コードレビューする人は Glass のメソッド全てを確認する義務があります。とはいえ、privateを触れる範囲をなるべく狭くしたいというモチベーションはわかります。transferTo がprivateに触る必要がないのなら拡張メソッドのような感じでクラスの外で定義できればいいのかもしれませんね。

とはいえ、Scalaの private[this] は初めて知りました。ググって出てきたscala - private[this] vs private - Stack Overflowによるとvarianceとの関係もあるようですね。Scala 3ではなくなってしまうようですが……(Dropped: private[this] and protected[this])。

さざんかぬふさざんかぬふ

ありがとうございます。なるほど、Scala 3ではなくなるのですね。
リンクを読むと、今後は単にprivate valだとクラスのfieldとして存在する事が保証されなくなり、もしreflectionでアクセスする可能性がある場合にはprivate[ClassName]などと書かないといけないようですね。
ソースコードレベルで非公開なメンバーとして定義されているものについて、実際にインスタンスレベルでフィールドとして存在するべきか否か(reflectionなどを通してアクセスできるべきか否か)というのも設計観点になるのですね。勉強になりました。