さようなら、オブジェクト指向プログラミング

私は何十年もの間、オブジェクト指向言語でプログラミングを行ってきました。最初に使用したオブジェクト指向言語は、C ++、次にSmalltalk、最後に.NETとJavaでした。

私は、継承、カプセル化、および多態性の利点を活用するために熱心でした。パラダイムの3つの柱。

私は再利用の約束を獲得し、この新しくエキサイティングな風景の中で私の前に来た人々が得た知恵を活用したいと思っていました。

私の実世界のオブジェクトをクラスにマッピングする考えに興奮を抑えることができず、全世界がきちんと収まることを期待していました。

私はこれ以上間違っていなかっただろう。

相続、最初の落下の柱

一見、継承はオブジェクト指向パラダイムの最大の利点のようです。新しく教化された人の例としてパレードされた形状階層の単純化された例はすべて、理にかなっているようです。

そして、再利用は今日の言葉です。いや…それを一年にして、おそらくこれからもずっと。

私はこの全体を飲み込み、新たな洞察で世界に飛び出しました。

バナナモンキージャングルの問題

心の中に宗教と解決すべき問題があるため、クラス階層の構築とコードの記述を開始しました。そして、すべては世界に正しかった。

既存のクラスから継承することにより、再利用の約束を獲得する準備ができていたその日を決して忘れません。これは私が待っていた瞬間でした。

新しいプロジェクトがやって来たので、そのクラスに戻って考えたので、最後のプロジェクトが大好きでした。

問題ない。救助に再利用します。私がやらなければならないのは、単に他のプロジェクトからそのクラスを取得して使用することです。

ええと…実際…そのクラスだけではありません。親クラスが必要になります。しかし…しかし、それだけです。

うーん…待って…私たちも親の親が必要になるように見えます...そして…すべての親が必要になります。わかりました…わかりました…これを処理します。問題ない。

そして素晴らしい。今ではコンパイルされません。なぜ??ああ、なるほど…このオブジェクトにはこの他のオブジェクトが含まれています。それで私もそれが必要になります。問題ない。

待って…私はそのオブジェクトが必要なだけではありません。オブジェクトの親とその親の親などが必要です。含まれるすべてのオブジェクトと、それらの親、親、親と一緒に含まれるもののすべての親…

あー

Erlangの作成者であるJoe Armstrongによる素晴らしい引用があります。

オブジェクト指向言語の問題点は、それらが持ち歩くこの暗黙の環境をすべて持っていることです。バナナが欲しかったのですが、バナナとジャングル全体を保持しているゴリラが手に入りました。

バナナモンキージャングルソリューション

深すぎる階層を作成しないことで、この問題を解決できます。しかし、継承が再利用の鍵である場合、そのメカニズムに制限を加えると、確実に再利用の利点が制限されます。右?

右。

それでは、Kool-aidの健全な支援を受けた貧しいオブジェクト指向プログラマーとは何でしょうか?

収容および委任。これについては後で詳しく説明します。

ダイヤモンドの問題

遅かれ早かれ、次の問題はitsいものになり、言語によっては解決できない頭になります。

ほとんどのOO言語はこれをサポートしていませんが、これは理にかなっているように見えます。オブジェクト指向言語でこれをサポートするのが難しいのは何ですか?

さて、次の擬似コードを想像してください:

クラスPoweredDevice {
}
クラススキャナーはPoweredDevice {
  関数start(){
  }
}
クラスPrinterはPoweredDevice {
  関数start(){
  }
}
Class Copierは、スキャナー、プリンター{
}

ScannerクラスとPrinterクラスの両方がstartという関数を実装していることに注意してください。

Copierクラスはどの開始関数を継承しますか?スキャナーの1つですか?プリンター1?両方にすることはできません。

ダイヤモンドソリューション

解決策は簡単です。しないでください。

はい、そうです。ほとんどのオブジェクト指向言語ではこれができません。

しかし、しかし…これをモデル化しなければならない場合はどうなりますか?再利用したい!

次に、封じ込めて委任する必要があります。

クラスPoweredDevice {
}
クラススキャナーはPoweredDevice {
  関数start(){
  }
}
クラスPrinterはPoweredDevice {
  関数start(){
  }
}
クラスコピー機{
  スキャナースキャナー
  プリンタープリンター
  関数start(){
    printer.start()
  }
}

ここで、Copierクラスには、プリンターとスキャナーのインスタンスが含まれていることに注意してください。開始関数をPrinterクラスの実装に委任します。スキャナーに簡単に委任できます。

この問題は、継承の柱のさらに別の亀裂です。

壊れやすい基本クラスの問題

そのため、階層を浅くし、循環させないようにしています。私にはダイヤモンドはありません。

そして、すべては世界に正しかった。それは…までです

ある日、私のコードは機能し、翌日は機能しなくなります。キッカーです。コードを変更しませんでした。

まあ、多分それはバグかもしれません…しかし、待ってください...何かが変化しました...

しかし、それは私のコードにはありませんでした。変更は、私が継承したクラスにあったことが判明しました。

Baseクラスを変更するとコードが破損する可能性がありますか?

こうやって…

次のBaseクラスを想像してください(Javaで記述されていますが、Javaを知らない場合は理解しやすいはずです):

import java.util.ArrayList;
 
パブリッククラスArray
{
  private ArrayList  a = new ArrayList ();
 
  public void add(オブジェクト要素)
  {
    a.add(element);
  }
 
  public void addAll(Object elements [])
  {
    for(int i = 0; i 

重要:コメント化されたコード行に注意してください。この行は後で変更されるため、問題が発生します。

このクラスのインターフェイスには、add()とaddAll()の2つの関数があります。 add()関数は単一の要素を追加し、addAll()はadd関数を呼び出して複数の要素を追加します。

そして、派生クラスは次のとおりです。

パブリッククラスArrayCountはArrayを拡張します
{
  private int count = 0;
 
  @オーバーライド
  public void add(オブジェクト要素)
  {
    super.add(element);
    ++ count;
  }
 
  @オーバーライド
  public void addAll(Object elements [])
  {
    super.addAll(elements);
    count + = elements.length;
  }
}

ArrayCountクラスは、一般的なArrayクラスの特殊化です。唯一の動作上の違いは、ArrayCountが要素数のカウントを保持することです。

これらのクラスの両方を詳しく見てみましょう。

Array add()は、ローカルのArrayListに要素を追加します。
Array addAll()は、各要素に対してローカルのArrayList addを呼び出します。

ArrayCount add()は親のadd()を呼び出してからカウントをインクリメントします。
ArrayCount addAll()は、親のaddAll()を呼び出してから、要素の数だけカウントを増やします。

そして、すべて正常に動作します。

さて、重大な変化です。 Baseクラスのコメント化されたコード行は次のように変更されます。

  public void addAll(Object elements [])
  {
    for(int i = 0; i 

Baseクラスの所有者に関する限り、アドバタイズされたとおりに機能します。そして、自動化されたテストはすべて合格しています。

しかし、所有者はDerivedクラスを忘れています。そして、派生クラスの所有者は失礼な目覚めを求めています。

現在、ArrayCount addAll()は親のaddAll()を呼び出します。これは、Derivedクラスによって上書きされたadd()を内部的に呼び出します。

これにより、Derivedクラスのadd()が呼び出されるたびにカウントが増加し、DerivedクラスのaddAll()で追加された要素の数だけAGAINが増加します。

2回カウントされます。

これが発生する可能性があり、実際に発生する場合、Derivedクラスの作成者は、Baseクラスの実装方法を知っている必要があります。また、派生クラスを予測不可能な方法で破壊する可能性があるため、Baseクラスのすべての変更について通知する必要があります。

うん!この大きな亀裂は、貴重な継承の柱の安定性を永久に脅かしています。

壊れやすい基本クラスソリューション

もう一度封じ込めて委任します。

ContainおよびDelegateを使用することにより、ホワイトボックスプログラミングからブラックボックスプログラミングに移行します。ホワイトボックスプログラミングでは、基本クラスの実装を検討する必要があります。

ブラックボックスプログラミングでは、その機能の1つをオーバーライドしてBaseクラスにコードを挿入することはできないため、実装を完全に無知にすることができます。私たちは、インターフェースにのみ関心を持つ必要があります。

この傾向は不安です...

継承は、Reuseにとって大きな勝利になるはずでした。

オブジェクト指向言語では、ContainおよびDelegateを簡単に実行できません。それらは、継承を容易にするように設計されました。

あなたが私のような人なら、この継承のことについて疑問に思い始めています。しかしより重要なことは、これにより、階層を介した分類の力に対する自信が揺らぐはずです。

階層の問題

新しい会社を始めるたびに、会社のドキュメントを置く場所を作成するときの問題に苦労します。従業員ハンドブック。

Documentsというフォルダーを作成し、その中にCompanyというフォルダーを作成しますか?

または、Companyというフォルダーを作成し、その中にDocumentsというフォルダーを作成しますか?

両方とも機能します。しかし、どちらが正しいですか?どちらがベストですか?

カテゴリ階層の考え方は、より一般的な基本クラス(親)があり、派生クラス(子)はそれらのクラスのより特殊なバージョンであるというものでした。そして、継承チェーンを下るにつれて、さらに専門化されます。 (上記のシェイプ階層を参照)

しかし、親と子が勝手に場所を切り替えることができる場合、このモデルには明らかに問題があります。

階層ソリューション

間違っているのは…

カテゴリ階層は機能しません。

それでは、階層は何に適していますか?

封じ込め。

現実の世界を見ると、あらゆる場所に包含(または排他的所有権)階層が表示されます。

見つからないのは、カテゴリー階層です。少し沈めましょう。オブジェクト指向パラダイムは、オブジェクトで満たされた現実世界に基づいていました。しかし、その後、壊れたモデル、つまりを使用します。現実世界の類推がないカテゴリー階層。

しかし、現実の世界は封じ込め階層で満たされています。封じ込め階層の好例は靴下です。彼らはあなたの家などに含まれているあなたの寝室に含まれているあなたのドレッサーの1つの引き出しに含まれている靴下引き出しにあります。

ハードドライブ上のディレクトリは、包含階層のもう1つの例です。ファイルが含まれています。

それでは、どのように分類しますか?

会社のドキュメントについて考えるなら、私がどこに置いたかはほとんど関係ありません。ドキュメントのフォルダーまたはStuffと呼ばれるフォルダーに配置できます。

私がそれを分類する方法は、タグを使用することです。ファイルに次のタグをタグ付けします。

資料
会社
ハンドブック

タグには順序や階層はありません。 (これにより、ダイヤモンドの問題も解決されます。)

タグは、ドキュメントに複数のタイプを関連付けることができるため、インターフェイスに似ています。

しかし、非常に多くの亀裂があるため、継承の柱が崩れたように見えます。

さようなら、相続。

カプセル化、落下する2番目の柱

一見、カプセル化はオブジェクト指向プログラミングの2番目に大きな利点のようです。

オブジェクト状態変数は、外部アクセスから保護されています。つまり、オブジェクトにカプセル化されています。

who-knows-whoがアクセスしているグローバル変数について心配する必要はもうありません。

カプセル化は変数にとって安全です。

このカプセル化の事は信じられない!!

長時間のカプセル化…

それは…までです

参照問題

効率のために、オブジェクトは値ではなく参照によって関数に渡されます。

つまり、関数はオブジェクトを渡すのではなく、オブジェクトへの参照またはポインターを渡します。

オブジェクトが参照によってオブジェクトコンストラクターに渡される場合、コンストラクターはそのオブジェクト参照をカプセル化によって保護されているプラ​​イベート変数に入れることができます。

しかし、渡されたオブジェクトは安全ではありません!

何故なの?他のコードには、オブジェクトへのポインタがあるためです。コンストラクターを呼び出したコード。オブジェクトへの参照が必要です。そうでないと、コンストラクタに渡すことができませんか?

リファレンスソリューション

コンストラクターは、渡されたオブジェクトを複製する必要があります。そして、浅いクローンではなく、深いクローン、つまり、渡されたオブジェクトに含まれるすべてのオブジェクト、およびそれらのオブジェクト内のすべてのオブジェクトなどです。

効率のために。

これがキッカーです。すべてのオブジェクトを複製できるわけではありません。オペレーティングシステムリソースが関連付けられているものもありますが、クローン作成は役に立たないか、最悪の場合は不可能になります。

そして、すべての単一のメインストリームOO言語にこの問題があります。

さようなら、カプセル化。

ポリモーフィズム、落下する3番目の柱

多型は、オブジェクト指向の三位一体の赤毛の継子でした。

グループのラリーファインのようなものです。

彼らはどこへ行っても彼はそこにいたが、彼はただの支持者だった。

ポリモーフィズムが優れているわけではなく、オブジェクト指向言語を必要としないだけです。

インターフェイスはこれを提供します。そして、オブジェクト指向のすべての荷物なし。

また、インターフェイスを使用すると、さまざまな動作を混在させることができます。制限はありません。

苦労することなく、OOポリモーフィズムに別れを告げ、インターフェイスベースのポリモーフィズムに挨拶します。

壊れた約束

まあ、OOは確かに初期の段階で多くのことを約束しました。そして、これらの約束は、教室に座って、ブログを読んで、オンラインコースを受講する素朴なプログラマーに対してまだなされています。

オブジェクト指向が私にどのように嘘をついたかを理解するのに何年もかかりました。私も目が広く、経験がなく、信頼していました。

そして、私は火傷しました。

さようなら、オブジェクト指向プログラミング。

それで何?

こんにちは、関数型プログラミング。過去数年間、あなたと一緒に仕事できてとても良かったです。

ご存知の通り、私はあなたの約束を額面どおりに受け止めていません。私はそれを信じるために見なければならないでしょう。

一度燃えたら、二度恥ずかしがり屋ですべて。

分かるでしょう。

これが気に入ったら、下のをクリックして、他の人がこれを中に表示するようにします。

ElmのFunctional Programmingを使用してWebアプリの開発を学び、支援するWeb開発者のコ​​ミュニティに参加したい場合は、FacebookグループのLearn Elm Programming https://www.facebook.com/groups/learnelm/をご覧ください。

私のTwitter:@cscalfani