OOPの習得:継承、インターフェース、抽象クラスの実用ガイド

公開: 2022-03-10
簡単な要約↬プログラミングの基礎を教えるためのおそらく最悪の方法は、それをいつどのように使用するかについては言及せずに、何かが何であるかを説明することです。 この記事では、Ryan M. Kayが、継承、インターフェイス、または抽象クラスをいつ使用するかを二度と不思議に思わないように、OOPの3つのコア概念について最もあいまいな用語で説明します。 提供されているコード例はJavaであり、Androidへの参照がいくつかありますが、従う必要があるのはJavaの基本的な知識だけです。

私の知る限り、ソフトウェア開発の分野で、理論的情報と実践的情報の適切な組み合わせを提供する教育コンテンツに出くわすことはめったにありません。 理由を推測すると、理論に焦点を当てる人は教える傾向があり、実践的な情報に焦点を当てる人は特定の言語やツールを使用して特定の問題を解決するために報酬を得る傾向があるためだと思います。

もちろん、これは大まかな一般化ですが、議論のために簡単に受け入れると、教師の役割を担う多くの人々(すべての人々ではない)は、貧弱であるか、まったく能力がない傾向があります。特定の概念に関連する実践的な知識を説明すること。

この記事では、ほとんどのオブジェクト指向プログラミング(OOP)言語に見られる、継承インターフェイス(別名プロトコル)、および抽象クラスの3つのコアメカニズムについて説明するために最善を尽くします。 それぞれのメカニズム何であるかについて技術的で複雑な口頭で説明するのではなく、それらが何をするの、そしていつそれらを使用するのかに焦点を当てるように最善を尽くします。

ただし、個別に説明する前に、理論的には健全でありながら実際には役に立たない説明を行うことの意味について簡単に説明したいと思います。 この情報を使用して、さまざまな教育リソースをふるいにかけ、物事が意味をなさないときに自分を責めないようにすることができれば幸いです。

ジャンプした後もっと! 以下を読み続けてください↓

さまざまな知識の程度

名前を知る

何かの名前を知ることは、間違いなく最も浅い形の知識です。 実際、名前は、同じものを参照するために多くの人々によって一般的に使用されている、および/またはそのことを説明するのに役立つという範囲でのみ、一般的に有用です。 残念ながら、この分野で時間を過ごした人なら誰でも知っているように、多くの人が同じものに異なる名前(インターフェースプロトコルなど)、異なるものに同じ名前(モジュールコンポーネントなど)、または難解な名前を使用しています。ばかげている点(例:どちらかのモナド)。 最終的に、名前はメンタルモデルへの単なるポインタ(または参照)であり、さまざまな程度の有用性を持つことができます。

この分野の研究をさらに困難にするために、ほとんどの個人にとって、コードを書くことは非常にユニークな経験である(または少なくともそうであった)と推測するのは危険です。 さらに複雑なのは、そのコードが最終的に機械語にコンパイルされ、時間の経過とともに変化する一連の電気インパルスとして物理的に表現される方法を理解することです。 プログラムで採用されているプロセス、概念、メカニズムの名前を思い出すことができたとしても、そのようなもののために作成するメンタルモデルが別の個人のモデルと一致するという保証はありません。 それらが客観的に正確であるかどうかは言うまでもありません。

これらの理由から、私は専門用語について自然に良い記憶を持っていないという事実と並んで、名前が何かを知る上で最も重要でない側面であると考えています。 名前が役に立たないというわけではありませんが、私は過去にプロジェクトで多くのデザインパターンを学び、採用してきましたが、一般的に使用される名前の数か月、あるいは数年後のことを知るためだけでした。

口頭での定義とアナロジーを知る

口頭での定義は、新しい概念を説明するための自然な出発点です。 ただし、名前と同様に、有用性と関連性の程度はさまざまです。 その多くは、学習者の最終目標が何であるかに依存します。 私が口頭で定義する際に見られる最も一般的な問題は、一般的に専門用語の形での知識であると想定されています。

たとえば、スレッドが特定のプロセスと同じアドレス空間を占めることを除けば、スレッドプロセスに非常によく似ていることを説明したとします。 すでにプロセスアドレス空間に精通している人には、スレッドプロセスの理解に関連付けることができる(つまり、同じ特性の多くを持っている)と基本的に述べましたが、明確な特性に基づいて区別することができます。

その知識を持っていない人にとって、私はせいぜい意味をなさず、最悪の場合、学習者は私が知っておくべきだと思っていることを知らないために何らかの形で不十分だと感じさせました。 公平に言えば、これは、学習者が本当にそのような知識を持っている必要がある場合(大学院生や経験豊富な開発者を教えるなど)は許容されますが、入門レベルの資料ではそうすることは非常に失敗だと思います。

多くの場合、学習者がこれまでに見たものとは異なり、概念の適切な口頭での定義を提供することは非常に困難です。 この場合、教師にとって、平均的な人に馴染みがあり、概念の同じ性質の多くを伝える限り、関連性のあるアナロジーを選択することが非常に重要です。

たとえば、ソフトウェア開発者にとって、ソフトウェアエンティティ(プログラムのさまざまな部分)が密結合または結合の場合の意味を理解することは非常に重要です。 庭の小屋を建てるとき、後輩の大工は、ネジの代わりに釘を使ってそれを組み立てる方が速く簡単だと思うかもしれません。 これは、間違いが発生するまで、または庭の小屋の設計を変更して小屋の一部を再構築する必要があるまで当てはまります。

この時点で、庭の小屋の部分をしっかりと結合するために釘を使用するという決定は、建設プロセス全体をより困難にし、おそらく遅くなり、ハンマーで釘を抜くと、構造を損傷するリスクがあります。 逆に、ネジは組み立てに少し時間がかかる場合がありますが、取り外しが簡単で、小屋の近くの部分を損傷するリスクはほとんどありません。 これは、私が緩く結合したことを意味します。 当然、本当に釘が必要な場合もありますが、その決定は批判的思考と経験に基づいて行われるべきです。

後で詳しく説明するように、プログラムの各部分を接続するためのさまざまなメカニズムがあり、さまざまな程度の結合を提供します。 ネジのように。 私の例えは、この非常に重要な用語が何を意味するのかを理解するのに役立つかもしれませんが、庭の小屋を建てるという文脈の外でそれをどのように適用するかについての考えをあなたに与えませんでした。 これは私を最も重要な種類の知識に導き、あらゆる探究の分野で曖昧で難しい概念を深く理解するための鍵となります。 ただし、この記事ではコードの記述に固執します。

コードで知る

私の意見では、厳密にソフトウェア開発に関して、概念を知るための最も重要な形式は、動作中のアプリケーションコードでそれを使用できることから来ています。 この形式の知識は、多くのコードを記述し、さまざまな問題を解決するだけで実現できます。 専門用語の名前と言葉による定義を含める必要はありません。

私自身の経験では、リモートデータベースおよびローカルデータベースと単一のインターフェイスを介して通信する問題を解決したことを思い出します(まだ行っていない場合は、すぐにそれが何を意味するかがわかります)。 クライアント(インターフェイスと通信するクラス)ではなく、リモートおよびローカル(またはテストデータベース)を明示的に呼び出す必要があります。 実際、クライアントはインターフェイスの背後にあるものを認識していなかったため、本番アプリで実行されているかテスト環境で実行されているかに関係なく、インターフェイスを変更する必要はありませんでした。 この問題を解決してから約1年後、「ファサードパターン」という用語に出くわしました。「リポジトリパターン」という用語は、どちらも前述のソリューションで使用されている名前です。

この前文はすべて、継承インターフェイス抽象クラスなどのトピックを説明する際に最も頻繁に発生する欠陥のいくつかを明らかにすることを目的としています。 3つのうち、継承は、使用と理解の両方で最も簡単なものである可能性があります。 プログラミングの学生としても教師としても私の経験では、他の2つは、前述の間違いを避けるために特別な注意を払わない限り、ほとんどの場合、学習者にとって問題になります。 この時点から、私はこれらのトピックをできるだけ単純にするために最善を尽くしますが、それ以上単純にすることはありません。

例に関する注記

私自身、Androidモバイルアプリケーションの開発に精通しているので、そのプラットフォームから取得した例を使用して、Javaの言語機能を紹介すると同時にGUIアプリケーションの構築について説明します。 ただし、Java EE、Swing、またはJavaFXを大まかに理解している人には、例が理解できないほど詳細には説明しません。 これらのトピックについて議論する私の最終的な目標は、ほぼすべての種類のアプリケーションで問題を解決するという文脈でそれらが何を意味するのかを理解できるようにすることです。

また、読者の皆様、私が特定の単語とその定義について不必要に哲学的で衒学的であるように見える場合があることを警告したいと思います。 その理由は、具体的なもの(本物)と抽象的なもの(本物よりも詳細ではない)の違いを理解するために必要な深い哲学的基盤が本当にあるからです。 この理解は、コンピューティングの分野以外の多くのことに当てはまりますが、ソフトウェア開発者が抽象化の性質を把握することは特に重要です。 いずれにせよ、私の言葉があなたに失敗した場合、コードの例はうまくいけば失敗します。

継承と実装

グラフィカルユーザーインターフェイス(GUI)を使用してアプリケーションを構築する場合、継承は、アプリケーションを迅速に構築できるようにするための最も重要なメカニズムであることは間違いありません。

後で説明する継承を使用することにはあまり理解されていない利点がありますが、主な利点はクラス間で実装を共有することです。 この「実装」という言葉は、少なくともこの記事の目的では、明確な意味を持っています。 英語で単語の一般的な定義を与えるために、私は何かを実装することはそれを現実にすることであると言うかもしれません。

ソフトウェア開発に固有の技術的な定義を与えるために、ソフトウェアを実装することは、そのソフトウェアの要件を満たす具体的なコード行を書くことであると言えます。 たとえば、 sumメソッドを作成しているとします。privatedoublesum private double sum(double first, double second){

 private double sum(double first, double second){ //TODO: implement }

上記のスニペットは、戻り型( double )と、引数( first, second )およびそのメソッドを呼び出すために使用できる名前( sum )を指定するメソッド宣言を記述しているところまで作成しましたが、次のようになっています。実装されていません。 これを実装するには、次のようにメソッド本体を完成させる必要があります。

 private double sum(double first, double second){ return first + second; }

当然、最初の例はコンパイルされませんが、インターフェイスは、この種の実装されていない関数をエラーなしで記述できる方法であることがすぐにわかります。

Javaでの継承

おそらく、この記事を読んでいる場合は、 extendsキーワードを少なくとも1回使用したことがあります。 このキーワードの仕組みは単純で、ほとんどの場合、さまざまな種類の動物や幾何学的形状を扱う例を使用して説明されています。 DogCatAnimalを拡張します。 初歩的な型理論を説明する必要はないと思いますので、 extendsキーワードを使用して、Javaでの継承の主な利点について説明しましょう。

Javaでコンソールベースの「HelloWorld」アプリケーションを構築するのは非常に簡単です。 Javaコンパイラ( javac )とランタイム環境( jre )を持っていると仮定すると、次のようなmain関数を含むクラスを作成できます。

 public class JavaApp{ public static void main(String []args){ System.out.println("Hello World"); } }

新しいアプリのスケルトン/ボイラープレートコードを生成するためのIDEの助けを借りて、ほとんどすべての主要なプラットフォーム(Android、Enterprise / Web、Desktop)でJavaでGUIアプリケーションを構築することも、キーワードをextendsします。

tvDisplayというTextView (テキストラベルなど)を含むactivity_main.xmlというXMLレイアウト(通常、Androidでレイアウトファイルを介して宣言的にユーザーインターフェイスを構築します)があるとします。

 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </FrameLayout>

また、 tvDisplayに「HelloWorld!」と表示させたいとします。 そのためには、 extendsキーワードを使用してActivityクラスから継承するクラスを作成する必要があります。

 import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ((TextView)findViewById(R.id.tvDisplay)).setText("Hello World"); }

Activityクラスの実装を継承することの効果は、そのソースコードをざっと見てみると最もよく理解できます。 システムとの対話に必要な8000以上の行のごく一部を実装する必要があり、テキストを含む単純なウィンドウを生成する必要がある場合、Androidが主要なモバイルプラットフォームになるとは非常に疑わしいです。 継承により、 Androidフレームワーク、または使用しているプラ​​ットフォームを最初から再構築する必要がなくなります。

継承は抽象化に使用できます

クラス間で実装を共有するために使用できる限り、継承は比較的簡単に理解できます。 ただし、継承を使用できるもう1つの重要な方法があります。これは、間もなく説明するインターフェイス抽象クラスに概念的に関連しています。

よろしければ、次の少しの間、最も一般的な意味で使用される抽象化は、物事のより詳細でない表現であると仮定します。 長い哲学的定義でそれを修飾する代わりに、私は抽象化が日常生活でどのように機能するかを指摘し、その後すぐにソフトウェア開発の観点からそれらを明確に議論しようとします。

あなたがオーストラリアに旅行していて、あなたが訪問している地域が特に高密度の内陸タイパンヘビの生息地であることを知っていると仮定します(彼らは明らかに非常に有毒です)。 画像やその他の情報を見て、ウィキペディアを参照して詳細を確認することにしました。 そうすることで、今まで見たことのない特定の種類のヘビに鋭く気づきます。

抽象化、アイデア、モデル、またはあなたがそれらと呼びたい他のものは、物事のあまり詳細な表現ではありません。 本物のヘビがあなたを噛む可能性があるため、本物よりも詳細でないことが重要です。 ウィキペディアのページの画像は通常そうではありません。 コンピューターと人間の脳の両方が情報を保存、通信、および処理する能力が限られているため、抽象化も重要です。 この情報を実用的な方法で使用するのに十分な詳細を持ち、メモリ内のスペースを取りすぎないようにすることで、コンピュータと人間の脳が同様に問題を解決できるようになります。

これを継承に結び付けるために、ここで説明している3つの主要なトピックはすべて、抽象化または抽象化のメカニズムとして使用できます。 「HelloWorld」アプリのレイアウトファイルで、 ImageViewButton 、およびImageButtonを追加することにしたとします。

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:layout_width="match_parent" android:layout_height="match_parent"> <Button android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageButton android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> <ImageView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>

また、アクティビティがクリックを処理するためにView.OnClickListenerを実装していると仮定します。

 public class MainActivity extends Activity implements View.OnClickListener { private Button b; private ImageButton ib; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //... b = findViewById(R.id.imvDisplay).setOnClickListener(this); ib = findViewById(R.id.btnDisplay).setOnClickListener(this); iv = findViewById(R.id.imbDisplay).setOnClickListener(this); } @Override public void onClick(View view) { final int id = view.getId(); //handle click based on id... } }

ここでの重要な原則は、 ButtonImageButton 、およびImageViewViewクラスから継承することです。 その結果、この関数onClickは、詳細度の低い親クラスとして参照することにより、異なる(階層的に関連している)UI要素からクリックイベントを受け取ることができます。 これは、 Androidプラットフォームであらゆる種類のウィジェットを処理するための個別のメソッドを作成するよりもはるかに便利です(カスタムウィジェットは言うまでもありません)。

インターフェースと抽象化

前のコード例は、私がそれを選んだ理由を理解していても、少し刺激的ではないことに気付いたかもしれません。 クラスの階層全体で実装を共有できることは非常に便利であり、継承の主要なユーティリティであると私は主張します。 共通の親クラスを持つクラスのセットをタイプが等しいものとして(つまり、親クラスとして)扱うことができるようにすることに関しては、継承の機能の使用は制限されています。

限定的に、私は、親クラスを介して参照される、または親クラスとして知られるために、子クラスが同じクラス階層内にあるという要件について話します。 言い換えれば、継承抽象化のための非常に制限的なメカニズムです。 実際、抽象化がさまざまな詳細レベル(または情報)間を移動するスペクトルであると仮定すると、継承はJavaでの抽象化の最も抽象的なメカニズムではないと言えます。

インターフェイスの説明に進む前に、 Java 8の時点で、デフォルトメソッド静的メソッドと呼ばれる2つの機能がインターフェイスに追加されていることをお伝えしておきます。 最終的には話し合いますが、とりあえず存在しないふりをしたいと思います。 これは、インターフェースを使用する主な目的を簡単に説明できるようにするためです。インターフェースは、当初、そして間違いなく、Javaでの抽象化の最も抽象的なメカニズムでした

詳細が少ないほど自由度が高い

継承のセクションでは、実装という言葉の定義を示しました。これは、これから説明する別の用語と対比することを目的としています。 明確にするために、私は単語自体、またはあなたがそれらの使用法に同意するかどうかは気にしません。 彼らが概念的に何を指しているのかをあなたが理解しているだけです。

継承は主に一連のクラス間で実装を共有するためのツールですが、インターフェイスは主に一連のクラス間で動作を共有するためのメカニズムであると言えます。 この意味で使用される振る舞いは、抽象メソッドを表す非技術的な言葉です。 抽象メソッドは、実際にはメソッド本体を含まないメソッドです。

 public interface OnClickListener { void onClick(View v); }

私にとって、そして私が最初にインターフェースを見た後、私が指導した多くの個人にとっての自然な反応は、戻り型メソッド名、およびパラメーター・リストのみを共有することの有用性は何であるか疑問に思いました。 表面的には、自分自身、またはインターフェイスimplementsするクラスを作成している他の人のために、追加の作業を作成するための優れた方法のように見えます。 答えは、インターフェイスは、クラスのセットを同じように動作させたい(つまり、同じパブリック抽象メソッドを持っている)が、異なる方法でその動作実装することを期待している状況に最適であるということです。

単純ですが関連性のある例を挙げると、Androidプラットフォームには、主にユーザーインターフェイスの一部を作成および管理する2つのクラス、 ActivityFragmentがあります。 したがって、これらのクラスには、ウィジェットがクリックされたとき(またはユーザーが操作したとき)にポップアップするイベントをリッスンする必要があることがよくあります。 議論のために、継承がそのような問題をほとんど解決しない理由を理解するために少し時間を取ってみましょう:

 public class OnClickManager { public void onClick(View view){ //Wait a minute... Activities and Fragments almost never //handle click events exactly the same way... } }

アクティビティフラグメントOnClickManagerから継承させると、イベントを別の方法で処理できなくなるだけでなく、必要に応じてそれを実行することさえできなくなります。 ActivityFragmentはどちらもすでに親クラスを拡張しており、Javaでは複数の親クラスを使用できません。 したがって、私たちの問題は、クラスのセットが同じように動作するようにしたいということですが、クラスがその動作実装する方法に柔軟性が必要です。 これにより、 View.OnClickListenerの前の例に戻ります。

 public interface OnClickListener { void onClick(View v); }

これは実際のソースコード( Viewクラスにネストされている)であり、これらの数行により、さまざまなウィジェット(ビュー)とUIコントローラー(アクティビティ、フラグメントなど)間で一貫した動作を保証できます。

抽象化は緩い結合を促進します

うまくいけば、Javaにインターフェースが存在する理由についての一般的な質問に答えました。 他の多くの言語の中で。 ある観点からは、これらはクラス間でコードを共有するための手段にすぎませんが、さまざまな実装を可能にするために、意図的に詳細度を下げています。 ただし、継承をコードの共有と抽象化の両方のメカニズムとして使用できるのと同じように(クラス階層に制限はありますが)、インターフェイス抽象化のためのより柔軟なメカニズムを提供します。

この記事の前のセクションで、ある種の構造を構築するためにネジを使用することの違いのアナロジーによって、緩い/密結合のトピックを紹介しました。 要約すると、基本的な考え方は、既存の構造の変更(修正ミスや設計変更などの結果である可能性がある)が発生する可能性がある状況でネジを使用することです。 は、構造物の一部を固定する必要がある場合に使用でき、近い将来に分解することを特に心配する必要はありません。

ネジは、クラス間の具体的および抽象的な参照依存関係という用語も適用されます)に類似していることを意味します。 混乱がないように、次のサンプルは私が何を意味するかを示しています。

 class Client { private Validator validator; private INetworkAdapter networkAdapter; void sendNetworkRequest(String input){ if (validator.validateInput(input)) { try { networkAdapter.sendRequest(input); } catch (IOException e){ //handle exception } } } } class Validator { //...validation logic boolean validateInput(String input){ boolean isValid = true; //...change isValid to false based on validation logic return isValid; } } interface INetworkAdapter { //... void sendRequest(String input) throws IOException; }

ここに、2種類の参照を持つClientというクラスがあります。 Clientがその参照の作成とは何の関係もないと仮定すると(実際にはそうすべきではありません)、特定のネットワークアダプターの実装の詳細から切り離されていることに注意してください。

この緩い結合にはいくつかの重要な意味があります。 手始めに、 INetworkAdapter実装を完全に分離してClientを構築できます。 2人の開発者のチームで作業していると想像してみてください。 1つはフロントエンドを構築するためのもので、もう1つはバックエンドを構築するためのものです。 両方の開発者がそれぞれのクラスを結合するインターフェースを認識している限り、実質的に互いに独立して作業を続けることができます。

次に、両方の開発者が、お互いの進捗状況に関係なく、それぞれの実装が適切に機能していることを確認できると言ったらどうでしょうか。 これはインターフェースで非常に簡単です。 適切なインターフェースimplementsするテストダブルを構築するだけです:

 class FakeNetworkAdapter implements INetworkAdapter { public boolean throwError = false; @Override public void sendRequest(String input) throws IOException { if (throwError) throw new IOException("Test Exception"); } }

原則として、抽象参照を使用すると、モジュール性、テスト容易性、およびファサードパターンオブザーバーパターンなどの非常に強力なデザインパターンへの扉が開かれます。 また、開発者は、実装の詳細にとらわれることなく、動作Program To An Interface )に基づいてシステムのさまざまな部分を設計することの幸せなバランスを見つけることができます。

抽象化に関する最後のポイント

抽象化具体的なものと同じようには存在しません。 これは、抽象クラスインターフェースがインスタンス化されない可能性があるという事実によって、Javaプログラミング言語に反映されています。

たとえば、これは間違いなくコンパイルされません。

 public class Main extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { //ERROR x2: Foo f = new Foo(); Bar b = new Bar() } private abstract class Foo{} private interface Bar{} }

実際、実装されていないインターフェイスまたは抽象クラスが実行時に機能することを期待するという考えは、UPSユニフォームがパッケージの配信に浮かんでいることを期待するのと同じくらい理にかなっています。 抽象化が有用であるためには、具体的な何かが抽象化の背後にある必要があります。 呼び出し側のクラスが、抽象参照の背後に実際に何があるかを知る必要がない場合でも。

抽象クラス:すべてをまとめる

あなたがこれまでにそれを成し遂げたならば、私はこれ以上哲学的な接線や専門用語を翻訳する必要がないことをあなたに伝えてうれしいです。 簡単に言えば、抽象クラスは、一連のクラス間で実装動作を共有するためのメカニズムです。 さて、私は抽象クラスをそれほど頻繁に使用していないことをすぐに認めます。 それでも、このセクションの終わりまでに、いつ彼らが求められているかを正確に知ることができることを願っています。

ワークアウトログのケーススタディ

JavaでAndroidアプリを構築してから約1年、私は最初のAndroidアプリを最初から再構築していました。 最初のバージョンは、ほとんどガイダンスのない独学の開発者に期待されるような恐ろしい大量のコードでした。 新しい機能を追加したいと思う頃には、だけで構築した密結合の構造は維持することが不可能であり、完全に再構築する必要があることが明らかになりました。

このアプリは、ワークアウトを簡単に記録できるように設計されたワークアウトログであり、過去のワークアウトのデータをテキストまたは画像ファイルとして出力する機能を備えています。 あまり詳しく説明せずに、(この説明とは関係のない他のフィールドの中で) Exerciseオブジェクトのコレクションで構成されるWorkoutオブジェクトが存在するようにアプリのデータモデルを構造化しました。

ある種の視覚媒体にワークアウトデータを出力する機能を実装しているときに、問題に対処する必要があることに気付きました。さまざまな種類のエクササイズにはさまざまな種類のテキスト出力が必要になるということです。

大まかなアイデアを与えるために、私は次のように運動の種類に応じて出力を変更したいと思いました。

  • バーベル:10 REPS @ 100 LBS
  • ダンベル:10 REPS @ 50 LBS x2
  • 体重:10 REPS @体重
  • 体重+:10 REPS @体重+45ポンド
  • タイミング:60 SEC @ 100 LBS

先に進む前に、他のタイプがあり(ワークアウトが複雑になる可能性があります)、表示するコードが記事にうまく収まるようにトリミングおよび変更されていることに注意してください。

以前の私の定義に沿って、抽象クラスを作成する目的は、抽象クラスすべての子クラスで共有されるすべて(変数定数などの状態も含む)を実装することです。 次に、上記の子クラス間で変更されるものについては、抽象メソッドを作成します。

 abstract class Exercise { private final String type; protected final String name; protected final int[] repetitionsOrTime; protected final double[] weight; protected static final String POUNDS = "LBS"; protected static final String SECONDS = "SEC "; protected static final String REPETITIONS = "REPS "; public Exercise(String type, String name, int[] repetitionsOrTime, double[] weight) { this.type = type; this.name = name; this.repetitionsOrTime = repetitionsOrTime; this.weight = weight; } public String getFormattedOutput(){ StringBuilder sb = new StringBuilder(); sb.append(name); sb.append("\n"); getSetData(sb); sb.append("\n"); return sb.toString(); } /** * Append data appropriately based on Exercise type * @param sb - StringBuilder to Append data to */ protected abstract void getSetData(StringBuilder sb); //...Getters }

当たり前のことを言っているかもしれませんが、抽象クラス実装すべきかどうかについて質問がある場合、重要なのは、すべての子クラスで繰り返されている実装の任意の部分を調べることです。

すべての演習に共通するものが確立されたので、文字列出力の種類ごとに特殊化された子クラスの作成を開始できます。

バーベルエクササイズ:

 class BarbellExercise extends Exercise { public BarbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append("\n"); } } }

ダンベル運動:

 class DumbbellExercise extends Exercise { private static final String TIMES_TWO = "x2"; public DumbbellExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(weight[i]); sb.append(POUNDS); sb.append(TIMES_TWO); sb.append("\n"); } } }

体重運動:

 class BodyweightExercise extends Exercise { private static final String BODYWEIGHT = "Bodyweight"; public BodyweightExercise(String type, String name, int[] repetitionsOrTime, double[] weight) { super(type, name, repetitionsOrTime, weight); } @Override protected void getSetData(StringBuilder sb) { for (int i = 0; i < repetitionsOrTime.length; i++) { sb.append(repetitionsOrTime[i]); sb.append(" "); sb.append(REPETITIONS); sb.append(" @ "); sb.append(BODYWEIGHT); sb.append("\n"); } } }

賢明な読者の中には、より効率的な方法で抽象化できたものを見つける人もいると思いますが、この例(元のソースから簡略化されています)の目的は、一般的なアプローチを示すことです。 もちろん、実行できるものがなければ、プログラミングの記事は完成しません。 テストしたい場合(すでにIDEを持っている場合を除く)、このコードを実行するために使用できるオンラインJavaコンパイラがいくつかあります。

 public class Main { public static void main(String[] args) { //Note: I actually used another nested class called a "Set" instead of an Array //to represent each Set of an Exercise. int[] reps = {10, 10, 8}; double[] weight = {70.0, 70.0, 70.0}; Exercise e1 = new BarbellExercise( "Barbell", "Barbell Bench Press", reps, weight ); Exercise e2 = new DumbbellExercise( "Dumbbell", "Dumbbell Bench Press", reps, weight ); Exercise e3 = new BodyweightExercise( "Bodyweight", "Push Up", reps, weight ); System.out.println( e1.getFormattedOutput() + e2.getFormattedOutput() + e3.getFormattedOutput() ); } }

Executing this toy application yields the following output: Barbell Bench Press

 10 REPS @ 70.0LBS 10 REPS @ 70.0LBS 8 REPS @ 70.0LBS Dumbbell Bench Press 10 REPS @ 70.0LBSx2 10 REPS @ 70.0LBSx2 8 REPS @ 70.0LBSx2 Push Up 10 REPS @ Bodyweight 10 REPS @ Bodyweight 8 REPS @ Bodyweight

さらなる考慮事項

Earlier, I mentioned that there are two features of Java interfaces (as of Java 8) which are decidedly geared towards sharing implementation , as opposed to behavior . These features are known as Default Methods and Static Methods .

I have decided not to go into detail on these features for the reason that they are most typically used in mature and/or large code bases where a given interface has many inheritors. Despite the fact that this is meant to be an introductory article, and I still encourage you to take a look at these features eventually, even though I am confident that you will not need to worry about them just yet.

I would also like to mention that there are other ways to share implementation across a set of classes (or even static methods ) in a Java application that does not require inheritance or abstraction at all. For example, suppose you have some implementation which you expect to use in a variety of different classes, but does not necessarily make sense to share via inheritance . A common pattern in Java is to write what is known as a Utility class, which is a simple class containing the requisite implementation in a static method :

 public class TimeConverterUtil { /** * Accepts an hour (0-23) and minute (0-59), then attempts to format them into an appropriate * format such as 12, 30 -> 12:30 pm */ public static String convertTime (int hour, int minute){ String unformattedTime = Integer.toString(hour) + ":" + Integer.toString(minute); DateFormat f1 = new SimpleDateFormat("HH:mm"); Date d = null; try { d = f1.parse(unformattedTime); } catch (ParseException e) { e.printStackTrace(); } DateFormat f2 = new SimpleDateFormat("h:mm a"); return f2.format(d).toLowerCase(); } }

Using this static method in an external class (or another static method ) looks like this:

 public class Main { public static void main(String[] args){ //... String time = TimeConverterUtil.convertTime(12, 30); //... } }

カンニングペーパー

We have covered a lot of ground in this article, so I would like to spend a moment summarizing the three main mechanisms based on what problems they solve. Since you should possess a sufficient understanding of the terms and ideas I have either introduced or redefined for the purposes of this article, I will keep the summaries brief.

I Want A Set Of Child Classes To Share Implementation

Classic inheritance , which requires a child class to inherit from a parent class , is a very simple mechanism for sharing implementation across a set of classes. An easy way to decide if some implementation should be pulled into a parent class , is to see whether it is repeated in a number of different classes line for line. The acronym DRY ( Don't Repeat Yourself ) is a good mnemonic device to watch out for this situation.

While coupling child classes together with a common parent class can present some limitations, a side benefit is that they can all be referenced as the parent class , which provides a limited degree of abstraction .

I Want A Set Of Classes To Share Behavior

Sometimes, you want a set of classes to be capable of possessing certain abstract methods (referred to as behavior ), but you do not expect the implementation of that behavior to be repeated across inheritors.

By definition, Java interfaces may not contain any implementation (except for Default and Static Methods ), but any class which implements an interface , must supply an implementation for all abstract methods, otherwise, the code will not compile. This provides a healthy measure of flexibility and restriction on what is actually shared and does not require the inheritors to be of the same class hierarchy .

I Want A Set Of Child Classes To Share Behavior And Implementation

Although I do not find myself using abstract classes all over the place, they are perfect for situations when you require a mechanism for sharing both behavior and implementation across a set of classes. Anything which will be repeated across inheritors may be implemented directly in the abstract class , and anything which requires flexibility may be specified as an abstract method .