オブジェクト指向の概念とプログラミングを教えるためのいくつかの実際的な例(Pythonで)
私は高校生に、「Pythonで退屈なものを自動化する」からいくつかのアイデアを取り入れて、いくつかの簡単なpythonスクリプトを書くように教えました。たとえば、フォルダー内のファイル名を特定の命名パターンに変更します。
私の次の目標は、オブジェクト指向の概念を彼らに教え、可能であれば、彼ら自身を試すためのいくつかの「実際の」例を与えることです。しかし、私が見つけたオブジェクト指向プログラミング(Pythonだけでなく)に関するほとんどの資料(グーグルのトップ検索結果のほとんど)には、私がそう言うかもしれないが、貧弱な例がたくさんあります。車/車や動物/犬を使ってオブジェクト指向の概念やOOPを教えるのは本当に好きではありません。
A.それは退屈です(あなたは高校生を知っています)
B。それは実際の用途がありません。
C.あるコメントが言ったように、「彼らは致命的な欠陥がある」。(ただし、動物の例を本当に使用したい場合は、この「オブジェクト指向デザイン」を確認してください)
私はpathlibを使用する予定です(ところで、彼らはすでにWindowsとUNIXの違いについての基本的な考えを持っています)
GUI開発はOOPを教えるためのもう一つの良い例かもしれませんが、私はまだ彼らにGUIを教えたくありません。
「実際の」例でOOを紹介する提案はありますか?
ところで、私はオブジェクト指向プログラミングの強力な支持者ではありません。しかし、pathlib.Path
高校生はそれを理解する可能性は低いですが、クラスは特にos.pathと比較して有用な抽象化です。
----更新----
Pythonロギングモジュールも良い例だと思いますが、経験の浅いプログラマーには複雑すぎるようです。
ロギングモジュールを使用する場合、主に3つのオブジェクト、Logger、Handler、Formatterを使用します。ロガーは、のようにファサードですhttps://docs.python.org/3/howto/logging.html#loggers 前記、
ロガーオブジェクトには3つの役割があります。まず、アプリケーションが実行時にメッセージをログに記録できるように、いくつかのメソッドをアプリケーションコードに公開します。次に、ロガーオブジェクトは、重大度(デフォルトのフィルタリング機能)またはフィルターオブジェクトに基づいて、どのログメッセージに基づいて動作するかを決定します。第3に、ロガーオブジェクトは、関連するログメッセージをすべての関心のあるログハンドラーに渡します。
ハンドラーは継承を使用する良い例です https://docs.python.org/3/howto/logging.html#useful-handlers ハンドラーは、LogRecordの複雑さを隠すフォーマッターを使用してログメッセージをフォーマットします。
これらのクラスに加えて、便利に使用できるモジュールレベルの関数がいくつかあります。
全体として、これはコンポジションとアグリゲーションを使用する良い例です。
しかし、私はpythonロギングモジュールの専門家ではありません。pythonロギングモジュールに精通している誰かが私の質問に答えを追加してくれることを願っています。
---更新2 ----
Alan Kayが、「優れたソフトウェアエンジニアリングの実践にとって重要であると考える、オブジェクト指向パラダイムの5つの機能は何ですか?」という質問に答えていることがわかりました。私の意見では、これらの言葉で、バフィーが与えた答えに共鳴します。しかし、これらのアイデアを高校生や経験の浅いプログラマーにどのように伝えるかは別の課題です。
内側を外側から保護できる「パーツ」構造、またはその逆。
相互作用を伝え、依存関係を処理できる「コミュニケーション」構造
再帰的にパーツに収まるパーツと通信の組み合わせである「システム」構造であり、すべてがこのように作成されます
伝達される「メッセージ」は、システムの観点からもあります
作られているシステムは同じ種類のシステムで作られています...
回答
私はこれを数回更新することを期待しており、最終的には長い答えが得られることを望んでいます。
しかし、あなたがすでに知っているかもしれないが、他の読者はそうではないかもしれないいくつかのことを説明することから始めましょう。
まず、オブジェクト指向プログラミングは基本的に継承に関するものではなく、あまりにも多くの本や著者がそれを理解していません。さらに、彼らはソフトウェアを理解しにくくし、維持するのを難しくする恐ろしい方法で継承を使用します。
たとえば、生物界のリンネスタイルの階層は、ほぼ完全に「インターフェイス」の1つであり、「抽象クラス」ではなく、ましてや具体的なクラスではないことに注意してください。たとえば、インスタンス化された「哺乳類」はありません。アイデアとしてではなく、物事が実際に存在するのは、階層の左側にあるだけです。もちろん、いくつかの遺伝的連続性があります。
第二に、オブジェクト指向デザインの作成と指導をガイドできるいくつかの原則がありますが、それらには規律が必要です。そして、その規律の一部は、ルールを「破る」状況を制御することです。
オブジェクト指向プログラマーになりたい人への私の最初のアドバイスは、継承の観点から考えるのではなく、むしろ構成の観点から考えることです。複雑なもの(オブジェクト)は、含まれているオブジェクトよりも少し単純で、いくつかの重要なサービスを提供する他のもの(オブジェクト)で構成されています。インスタンス変数のすべて(またはほとんど)が言語プリミティブであるクラスを作成する場合、実際にはそれを取得できません。そして、それらのインスタンス変数(オブジェクトまたはプリミティブ)に多くのゲッターとセッターがある場合は、オブジェクト指向プログラミングをまったく行っていません。
実際、上記の間違いを犯すには、プログラマーがプログラムのすべてのポイントですべての詳細を追跡する必要があります。OOは、決定をキャプチャするように設計されているため、再度「チェック」する必要はありません。いわば設定して忘れてください。
したがって、自動車を製造したい場合は、それを車両のサブクラスとは考えないでください(これでは何も得られません。個人用ドローンと戦艦はどちらも車両です)。エンジン、トランスミッション、コントロール、宿泊施設など、さまざまな部品で構成されているのではなく、考えてみてください。これらの部品は、それ自体が部品で構成されています。エンジンにはイグナイターやピストン、エキゾーストなどがあります。それらの多くは部品で構成されています。プリミティブを使用して構築するのは、最も低く、最も単純なレベルのみです。
覚えておく価値があり、ほとんどの場合従う価値のある2つの原則は、リスコフの置換原則とデメテルの法則です。
最初の提案は、サブクラスを使用してクラスを拡張する場合、サブクラスのパブリックインターフェイスも拡張しないことを示しています。その場合、すべてのサブクラスオブジェクトは置換可能であり、動作は異なりますが、インターフェイスは異なります。一方、Demeterは、読者に関係を明確にする、より明示的なコードを作成するように強制します。もちろん、それはまたあなたにもっと多くの名前を導入することを強制します、そしてそれらが名前を明らかにすることを意図しているならあなたのコードはより明確です。
もちろん、リスコフの原則はSOLIDの要素の1つであり、思考に組み込む必要もあります。
私自身のプログラミングでは、Liskovに非常に忠実であり、クラスを作成する前に、ほとんどのもののインターフェイスも定義しています。私は戦いの最中にデメテルに忠実ではなく、メッセージをカスケードします。しかし、私が実際に何を意味するのかを理解するために、それらのabcdの種類のカスケードを解明する必要がある場合もあります。
私がオブジェクト指向コードを書く際の目標の1つは、最小限の構造で非常に短いメソッドのみを記述しようとすることです。言い換えれば、循環的複雑度を最小限に抑えようとしています。メソッドの4番目のステートメントの後、または複雑さのレベルが3に達すると、手のひらがかゆくなり始めます。私はいつもそれで逃げることはできませんが、それは目標です。解決策は、複雑さを容赦なくリファクタリングすることです。もちろん、メソッドだけでなく「パーツ」も除外し、複雑さを管理するための新しいクラスを作成します。これらのクラスの多くがシングルトンである場合でも、コードは通常改善され、最初から目標を念頭に置いているため、リファクタリングの手順はそれほど必要ありません。
デザインパターンは、ほとんどのオブジェクト指向言語で効果的なプログラマーになるために必要なツールです。特に役立つのは、Strategy、Decorator、Observer、およびIteratorです。これらのほとんどは、実際にはさまざまなJavaライブラリを構築するために使用されます。
さて、ここで尋ねられた実際の質問にたどり着くために。ただし、学生が割り当てられた時間内にプロジェクトを完了できなくても、多くの学習が行われる可能性があることに注意してください。開発へのアジャイルアプローチ(たとえば、「顧客」としてのエクストリームプログラミング)では、すべての仕様が実装されていなくても、いくつかの機能が残ります。
ダンジョンゲーム
テキストベースのダンジョンゲームを作成します。主なオブジェクトは、キャラクター(人)、場所、物です。場所は、ある種の地図、迷路、またはグリッドで構成されています。キャラクターが場所に入ると物事が起こります。キャラクターは物を見つけて運びます。物事の種類に応じて、物事には行動があります。「呪文」とは、その行動が文脈に依存する可能性のある「もの」です。「トランスポーター」オブジェクトは、部屋(戦略オブジェクト)によって動作が異なる場合があります。
古典的なボードゲームのシュート(または蛇と梯子)とはしごは、これを簡略化したものであることに注意してください。テキストベースのバージョンは、多くの複雑さを回避します。
電卓
電卓には、キーやディスプレイなどのパーツがあります。内部メモリ、おそらくスタックはあまり目立ちません。操作でさえオブジェクトにすることができます。キーの動作は、計算の状態(ストラテジーパターン)に応じて変化します。実際には、単一のIFステートメントなしで単純な計算機を構築することは可能です。
アセンブリ言語を備えた抽象コンピュータ
スタックベースのコンピュータプロセッサシミュレーションは非常に簡単です。アキュムレータなどがあるかもしれませんが、すべての操作が実行される単一のスタックは単純で完全です。操作はオブジェクト(パーツ)にすることができます。この例の利点の1つは、必要なメソッドのほとんどが非常に短くなる可能性があることです。プログラムは、Javaスキャナーオブジェクトを使用して読み取ることができます。言語がサブルーチンをサポートする場合は、少なくともプログラムカウンターが必要であり、場合によってはフレームポインターが必要です。
付箋(私はこれを試していません)
ユーザーがメモと相互参照を保持し、それらを整理できるようにするアプリケーション。一部のクラスは、Notes、Keywords、Connections、Listsである可能性があります。
ジェパディ
テレビからの危険なゲームのためのシミュレーター。カテゴリ、回答、質問、チーム、スコア。
(すぐに戻って、多分)。
Pythonクラスは(「ありふれた」タスクのために)書くのがとても簡単で、開発中のコードを単純化してリファクタリングしたいという願望から自然に成長できることがわかりました。これは非常に実用的なボトムアップアプローチです。特定の目的のためにいくつかの単純なコードをハックすることを期待していた場合、それは少し大きくなり、タプルや配列、さらにはグローバルである「構造」を処理するいくつかの関数があることがわかります。突然、光を見てクラスを作成すると、コードサイズが2以上に分割され、非常に単純になります。
これは、「フラットな」具体的な既存のコードを取得し、作成できる抽象化を探し、データを関数パラメーターからクラスに移動し、を使用することで、OOPについて議論する簡単な方法self
です。
継承のようなものも同様にほぼ自然に発見できます。必要なことを完全に実行しない既存のクラスを使用していて、それを変更する必要があります。コピーして変更する代わりに、メソッドをサブクラス化して変更または追加します。
具体的な例として、コンピューターのハードウェア自体を見ることができます。低レベルでは、レジスタは多くの場合、いくつかの異なる関数に分割されます。機能ビットを1に設定する場合は、左に20ビットシフトし、レジスタの現在の値を読み取り、ビット0〜5を「1を書き込んでクリア」としてマスクアウトする必要があります。シリアルポート16550uartをエミュレートしてみてください。それは魂に良いです。そしてもちろん、実際のマイクロプロセッサで実行されているMicroPythonを使用している場合は、コードを試すこともできます。
データモデリングとOOP(どちらの場合も一種の正規化を中心に展開します)の両方の私の頼りになる例は、ビデオレンタル店です。これは非常に古い例かもしれません。他のことのために図書館やレンタル店に自由に変更してください。しかし、ビデオ店の例は、OOPとデータの正規化の複雑さを強調していると同時に、非常に単純であることがわかりました。把握するコンテキスト。
主な目標は、最大3つのテーブル/クラス図を作成することです:Customer
、Video
およびRental
(これは顧客とビデオの間のクロステーブルです)。
この回答の残りの部分は、特定のOOPの基本を手元の例に関連付ける方法に関するヒントにすぎません。
なぜオブジェクト?
さて、レンタルの3つのもの、つまり顧客名、住所、ビデオ名、および返品予定日を追跡する場合、ビデオデータをどのように保存しますか。
アリスがアンツを借り、ボブがビームービーを借り、チャーリーが車を借りたことを伝えることができる非常に簡単なプログラムを書くように生徒に依頼します。再利用可能なPrintRentalInfo
メソッドに依存させますが、適切と思われるメソッドパラメーターを定義できるようにします。
OOPをまだ見たことがない学生は、4つの異なる配列を使用し、4つの配列すべての同じインデックスで1つのビデオが見つかるという事実に依存します。顧客名の「バッグ」、住所の「バッグ」、ビデオ名の「バッグ」、および返品日の「バッグ」を用意するのは、実際には簡単ではないことを説明します。データフィールドごとに「バッグ」を作成するのではなく、レンタルごとに「バッグ」を作成した方が理にかなっているという考えを提案します。
Rental
4つのプロパティを使用してクラスを作成します。彼らが行ったのと同じアプリケーションを構築しますが、OOPを使用します。これにより、オブジェクトの初期化、異なるオブジェクトが同じ構造であるが個別に一意のコンテンツを持つ方法、およびオブジェクトを渡す方法(プリミティブ型の複数のメソッドパラメーターとは対照的)が示されます。
このレンタル「バッグ」をメソッド間で移動し、関連するすべての情報をまとめることがいかに簡単であるかを実際に強調します。
なぜ複数のクラスなのですか?
4番目の顧客が表示されます。彼女の名前もアリスです。ここで問題が発生します。これは、どのアリスがどのビデオをレンタルしたかがわからず、間違ったアリスに罰金を科したくないためです。
また、元のアリスから、住所が変更されたことを知らせるために電話がありました。アリスと一緒に移動しなかった同じ住所に住んでいる他の顧客がいる可能性があるため、すべてのレンタルを調べて、「old_address」を「new_address」に盲目的に変更することはできないことを理解することの難しさを指摘します。また、4番目のアリスもすでにいくつかのレンタルを行っているので、名前にも頼ることはできません。
名前と住所の組み合わせに基づいてそれができると生徒たちが抗議し続けると、2人のアリスが同じ住所に住んでいて、そのうちの1人だけが移動した場合にどうなるでしょうか。
すべての顧客とその住所の個別のリストを用意しておくと、名前だけでなく、1人の人物の詳細を簡単に変更できるように、多くの意味があることを生徒に提案します。
目標:Customer
クラスを作成し、名前/アドレスプロパティではなくプロパティがRental
含まれるように変更しCustomer
ます。
焦点:名前と住所が同じであっても、2つの異なる顧客オブジェクトをどのように持つことができるかを非常に強調します。
追加:Video
クラスの作成に同じアプローチを取ることができるので、あなたが持っている特定のビデオを追跡することができます。これは以前とほとんど同じなので、たぶんこれを生徒に練習問題として残してください。
この時点から、紹介したい内容に基づいてビジネスロジックを拡張できます。
- 継承-たぶん店は家賃
Video
とGame
、ですが、それでもあなたRental
はそれらのどちらかにリンクできるようにしたいです(基本RentableObject
クラスを使用して) - インターフェース-継承と同じ例を使用できます。
- データの正規化-利用可能な映画と個々の物理カセット(同じ映画を複数持つことができます)の両方を追跡するにはどうすればよいですか?顧客が被った罰金と、すでに支払った罰金をどのように追跡できるでしょうか。
- データ変換-上司は、行われたすべての賃貸料、返却された賃貸料、発生した罰金、およびまだ支払われていない以前の罰金に関する週次レポートを印刷することを望んでいます。
- 値対リファレンス-追加
Price
の両方にVideo
とRental
。にrental.Price
基づいて設定した方法を紹介しますvideo.Price
が、video.Price
後で変更しても変更rental.Price
されません。次に、参照オブジェクトを使用して同じ演習を繰り返します(たとえば、顧客の名前を変更します)。
この例のコンテキストは非常に理解しやすく、拡張の機会がたくさんあることがわかりました。これは、拡大し続ける長期プロジェクトに成長する可能性があります。これは、変化する要件とクリーンコーディングの利点、またはメンテナンスとレガシー開発を処理する方法について学生に教えたい場合に貴重なレッスンになる可能性があります。