JavaRush /Java Blog /Random-JA /クラスとインターフェイスの設計 (記事の翻訳)
fatesha
レベル 22

クラスとインターフェイスの設計 (記事の翻訳)

Random-JA グループに公開済み
クラスとインターフェイスの設計 (記事の翻訳) - 1

コンテンツ

  1. 導入
  2. インターフェース
  3. インターフェースマーカー
  4. 関数インターフェイス、静的メソッド、およびデフォルトのメソッド
  5. 抽象クラス
  6. 不変 (永続) クラス
  7. 匿名クラス
  8. 可視性
  9. 継承
  10. 多重継承
  11. 継承と構成
  12. カプセル化
  13. 最終的なクラスとメソッド
  14. 次は何ですか
  15. ソースコードをダウンロードする

1. はじめに

どのようなプログラミング言語を使用するとしても (Java も例外ではありません)、適切な設計原則に従うことが、クリーンで理解しやすく検証可能なコードを作成するための鍵となります。また、存続期間が長く、問題解決を簡単にサポートできるように作成します。チュートリアルのこの部分では、Java 言語が提供する基本的な構成要素について説明し、より適切な設計上の決定を行えるように、いくつかの設計原則を紹介します。より具体的には、インターフェイスと、デフォルト メソッド (Java 8 の新機能) を使用したインターフェイス、抽象クラスと最終クラス、不変クラス、継承、合成について説明し、簡単に触れた可視性 (またはアクセシビリティ) ルールを再検討します。パート 1 のレッスン「オブジェクトの作成と破棄の方法」

2. インターフェース

オブジェクト指向プログラミングでは、インターフェイスの概念がコントラクト開発の基礎を形成します。一言で言えば、インターフェイスは一連のメソッド (コントラクト) を定義し、その特定のインターフェイスのサポートを必要とする各クラスはそれらのメソッドの実装を提供する必要があります。これは非常にシンプルですが強力なアイデアです。多くのプログラミング言語には何らかの形式のインターフェイスがありますが、特に Java はこれに対する言語サポートを提供します。Java での簡単なインターフェイス定義を見てみましょう。
package com.javacodegeeks.advanced.design;

public interface SimpleInterface {
void performAction();
}
上のスニペットでは、 と呼ばれるインターフェイスはSimpleInterface、 と呼ばれるメソッドを 1 つだけ宣言しますperformAction。インターフェイスとクラスの主な違いは、インターフェイスはコンタクトがどうあるべきかを概説しますが(メソッドを宣言します)、その実装は提供しないことです。ただし、Java のインターフェイスはより複雑になる場合があり、ネストされたインターフェイス、クラス、カウント、注釈、定数が含まれる場合があります。例えば:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";

    enum InnerEnum {
        E1, E2;
    }

    class InnerClass {
    }

    interface InnerInterface {
        void performInnerAction();
    }

    void performAction();
}
このより複雑な例では、インターフェースが入れ子構造とメソッド宣言に無条件に課すいくつかの制限があり、これらは Java コンパイラーによって強制されます。まず、明示的に宣言されていなくても、インターフェイス内のすべてのメソッド宣言はパブリックです (パブリックにしかなれません)。したがって、次のメソッド宣言は同等です。
public void performAction();
void performAction();
インターフェース内のすべてのメソッドは暗黙的にabstractとして宣言されており、これらのメソッド宣言も同等であることに注意してください。
public abstract void performAction();
public void performAction();
void performAction();
宣言された定数フィールドについては、 public であるだけでなく、暗黙的にstaticとなり、 finalとマークされます。したがって、次の宣言も同等です。
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";
最後に、ネストされたクラス、インターフェイス、またはカウントは、 public であることに加えて、暗黙的にstatic として宣言されます。たとえば、これらの宣言は次と同等です。
class InnerClass {
}

static class InnerClass {
}
選択するスタイルは個人的な好みですが、インターフェイスのこれらの単純なプロパティを知っておくと、不必要な入力を省くことができます。

3. インターフェースマーカー

マーカー インターフェイスは、メソッドや他のネストされた構造を持たない特別な種類のインターフェイスです。Java ライブラリでは次のように定義されています。
public interface Cloneable {
}
インターフェイス マーカーはそれ自体はコントラクトではありませんが、特定の特性をクラスに「付加」または「関連付け」るためのある程度役立つテクニックです。たとえば、 Cloneableに関しては、クラスは cloneable としてマークされていますが、これを実装できる方法、または実装する必要がある方法はインターフェイスの一部ではありません。非常に有名で広く使用されているインターフェイス マーカーのもう 1 つの例は次のとおりですSerializable
public interface Serializable {
}
このインターフェイスは、クラスがシリアル化と逆シリアル化に適していることをマークしますが、これをどのように実装できるか、または実装する必要があるかは指定しません。インターフェイス マーカーは、コントラクトであるというインターフェイスの主な目的を満たしていませんが、オブジェクト指向プログラミングにおいてその役割を果たしています。 

4. 関数インターフェイス、デフォルトメソッドおよび静的メソッド

Java 8 のリリース以降、インターフェイスには、静的メソッド、デフォルト メソッド、ラムダ (関数型インターフェイス) からの自動変換など、非常に興味深い新機能がいくつか追加されました。インターフェースのセクションでは、Java のインターフェースはメソッドを宣言できるだけで、その実装は提供できないという事実を強調しました。デフォルトのメソッドでは状況が異なります。インターフェイスはメソッドにデフォルトのキーワードをマークし、その実装を提供できます。例えば:
package com.javacodegeeks.advanced.design;

public interface InterfaceWithDefaultMethods {
    void performAction();

    default void performDefaulAction() {
        // Implementation here
    }
}
インスタンス レベルでは、デフォルト メソッドは各インターフェイス実装によってオーバーライドできますが、インターフェイスには静的メソッドも含めることができます。たとえば、次のようになります。
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}
インターフェイスでの実装の提供は、コントラクト プログラミングの目的全体を無効にしていると言えるでしょう。しかし、これらの機能が Java 言語に導入されたのには多くの理由があり、それらがどれほど便利であったり混乱したりしても、これらの機能はユーザーが使用するために存在しています。関数型インターフェイスの場合は話が別で、言語への非常に便利な追加機能であることが証明されています。基本的に、関数型インターフェイスは、抽象メソッドが 1 つだけ宣言されたインターフェイスです。Runnable標準ライブラリ インターフェイスは、この概念の非常に良い例です。
@FunctionalInterface
public interface Runnable {
    void run();
}
Java コンパイラは関数型インターフェイスを異なる方法で処理し、ラムダ関数を意味のある関数型インターフェイス実装に変換できます。次の関数の説明を考えてみましょう。 
public void runMe( final Runnable r ) {
    r.run();
}
Java 7 以前でこの関数を呼び出すには、インターフェイスの実装をRunnable(たとえば、匿名クラスを使用して) 提供する必要がありますが、Java 8 では、ラムダ構文を使用して run() メソッドの実装を提供するだけで十分です。
runMe( () -> System.out.println( "Run!" ) );
さらに、@FunctionalInterfaceアノテーション(アノテーションについてはチュートリアルのパート 5 で詳しく説明します) は、インターフェイスに抽象メソッドが 1 つだけ含まれているかどうかをコンパイラーがチェックできることを示唆しているため、将来インターフェイスに加えられる変更はこの前提に違反しません。 。

5. 抽象クラス

Java 言語でサポートされているもう 1 つの興味深い概念は、抽象クラスの概念です。抽象クラスは Java 7 のインターフェイスに似ており、Java 8 のデフォルトのメソッド インターフェイスに非常に似ています。通常のクラスとは異なり、抽象クラスはインスタンス化できませんが、サブクラス化できます (詳細については「継承」セクションを参照してください)。さらに重要なのは、抽象クラスには抽象メソッド、つまりインターフェイスのような、実装のない特殊な種類のメソッドを含めることができます。例えば:
package com.javacodegeeks.advanced.design;

public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }

    public abstract void performAnotherAction();
}
この例では、クラスは抽象SimpleAbstractClassとして宣言されており、宣言された抽象メソッドが 1 つ含まれています。抽象クラスは非常に便利で、実装詳細のほとんどまたは一部を多くのサブクラス間で共有できます。それにもかかわらず、それらはまだドアを半開きにし、抽象メソッドを使用して各サブクラスに固有の動作をカスタマイズできるようにします。パブリック宣言のみを含めることができるインターフェイスとは異なり、抽象クラスはアクセシビリティ ルールを最大限に活用して抽象メソッドの可視性を制御できることは 言及する価値があります。

6. 即時クラス

今日のソフトウェア開発では、不変性がますます重要になっています。マルチコア システムの台頭により、データ共有と並列処理に関連する多くの問題が生じています。しかし、問題が 1 つ確実に生じています。変更可能な状態がほとんどない (またはまったくない) と、拡張性 (スケーラビリティ) が向上し、システムについての推論が容易になります。残念ながら、Java 言語はクラスの不変性に対するまともなサポートを提供していません。ただし、技術を組み合わせて使用​​すると、不変のクラスを設計することが可能になります。まず、クラスのすべてのフィールドが Final ( Finalとしてマークされている) である必要があります。これは良いスタートではありますが、保証はありません。 
package com.javacodegeeks.advanced.design;

import java.util.Collection;

public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection<String> collectionOfString;
}
次に、適切な初期化を確保します。フィールドがコレクションまたは配列への参照である場合、それらのフィールドをコンストラクター引数から直接割り当てず、代わりにコピーを作成します。これにより、コレクションまたは配列の状態が外部で変更されなくなります。
public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection<String> collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}
そして最後に、適切なアクセス (ゲッター) を確保します。コレクションの場合、不変性はラッパーとして提供される必要があります Collections.unmodifiableXxx。配列の場合、真の不変性を提供する唯一の方法は、配列への参照を返すのではなく、コピーを提供することです。これは配列のサイズに大きく依存し、ガベージ コレクターに多大な負荷をかける可能性があるため、実際的な観点からは受け入れられない可能性があります。
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}
この小さな例でも、Java では不変性がまだ第一級市民ではないことがよくわかります。不変クラスに別のクラスのオブジェクトを参照するフィールドがある場合、状況が複雑になる可能性があります。これらのクラスも不変である必要がありますが、これを保証する方法はありません。FindBugs や PMD など、コードをチェックして Java プログラミングの一般的な欠陥を指摘するのに大いに役立つ、優れた Java ソース コード アナライザーがいくつかあります。これらのツールは、Java 開発者の強い味方です。

7. 匿名クラス

Java 8 以前の時代では、クラスがオンザフライで定義され、即座にインスタンス化されることを保証するには、匿名クラスが唯一の方法でした。匿名クラスの目的は、ボイラープレートを削減し、クラスをレコードとして表す短くて簡単な方法を提供することでした。Java で新しいスレッドを生成する典型的な昔ながらの方法を見てみましょう。
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}
この例では、インターフェイスの実装がRunnable匿名クラスとしてすぐに提供されます。匿名クラスにはいくつかの制限がありますが、匿名クラスを使用する主な欠点は、Java 言語として義務付けられている非常に冗長な構成構文であることです。何もしない匿名クラスであっても、記述するたびに少なくとも 5 行のコードが必要になります。
new Runnable() {
   @Override
   public void run() {
   }
}
幸いなことに、Java 8、ラムダ、および関数型インターフェイスを使用すると、これらの固定概念はすべてすぐになくなり、最終的には Java コードの記述が本当に簡潔になるでしょう。
package com.javacodegeeks.advanced.design;

public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

8. 可視性

Java の可視性とアクセシビリティのルールについては、チュートリアルのパート 1 ですでに少し説明しました。このパートでは、サブクラス化のコンテキストでこのトピックをもう一度取り上げます。 クラスとインターフェイスの設計 (記事の翻訳) - 2さまざまなレベルでの可視性により、クラスが他のクラスやインターフェイスを認識したり (たとえば、それらが異なるパッケージ内にある場合、または相互にネストされている場合)、またはサブクラスが親のメソッド、コンストラクター、およびフィールドを参照したりアクセスしたりすることが許可または禁止されます。次のセクション「継承」では、これが実際に動作する様子を見ていきます。

9. 継承

継承はオブジェクト指向プログラミングの重要な概念の 1 つであり、関係のクラスを構築するための基礎として機能します。継承により、可視性とアクセシビリティのルールを組み合わせることで、クラスを拡張および維持できる階層に設計できるようになります。概念的なレベルでは、Java の継承は、親クラスとともにサブクラス化とextendsキーワードを使用して実装されます。サブクラスは、親クラスのすべての public 要素と protected 要素を継承します。さらに、両方 (サブクラスとクラス) が同じパッケージ内にある場合、サブクラスは親クラスの package-private 要素を継承します。そうは言っても、何を設計しようとしているとしても、クラスがパブリックに公開する、またはそのサブクラスに公開するメソッドの最小限のセットに固執することが非常に重要です。たとえば、クラスParentとそのサブクラスを見てChild、可視性レベルの違いとその効果を示してみましょう。
package com.javacodegeeks.advanced.design;

public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";

    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;

    // No one can see it
    private class PrivateClass {
    }

    // Only visible to subclasses
    protected interface ProtectedInterface {
    }

    // Everyone can call it
    public void publicAction() {
    }

    // Only subclass can call it
    protected void protectedAction() {
    }

    // No one can call it
    private void privateAction() {
    }

    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
package com.javacodegeeks.advanced.design;

// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }

    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }

    public void childAction() {
        this.protectedField = "value";
    }
}
継承はそれ自体が非常に大きなトピックであり、Java に特有の詳細が数多く含まれています。ただし、従うのが簡単で、クラス階層を簡潔に保つのに非常に役立つルールがいくつかあります。Java では、final として宣言されていない限り、各サブクラスは親の継承メソッドをオーバーライドできます。ただし、メソッドをオーバーライドとしてマークするための特別な構文やキーワードがないため、混乱が生じる可能性があります。@Overrideアノテーションが導入されたのはこのためです。継承されたメソッドをオーバーライドすることが目的の場合は、@Overrideアノテーションを使用してそれを簡潔に示してください。Java 開発者が設計において常に直面するもう 1 つのジレンマは、(具象クラスまたは抽象クラスによる) クラス階層の構築とインターフェイスの実装です。可能な限り、クラスまたは抽象クラスよりもインターフェイスを優先することを強くお勧めします。インターフェイスは軽量で、テストと保守が容易で、実装変更による副作用も最小限に抑えられます。Java 標準ライブラリでのプロキシ クラスの作成などの高度なプログラミング手法の多くは、インターフェイスに大きく依存しています。

10. 多重継承

C++ やその他の言語とは異なり、Java は多重継承をサポートしません。Java では、各クラスは直接の親を 1 つだけ持つことができます (そのクラスがObject階層の最上位にあります)。ただし、クラスは複数のインターフェイスを実装できるため、Java で多重継承を実現 (またはシミュレート) する唯一の方法はインターフェイスのスタッキングです。
package com.javacodegeeks.advanced.design;

public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }

    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
複数のインターフェースの実装は実際には非常に強力ですが、多くの場合、その実装を何度も繰り返し使用する必要があるため、Java の多重継承サポートの欠如を克服する方法として深いクラス階層が必要になります。 
public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
など... Java 8 の最近のリリースでは、デフォルトのメソッド インジェクションの問題にある程度対処しています。デフォルトのメソッドのため、インターフェイスは実際にはコントラクトだけでなく実装も提供します。したがって、これらのインターフェイスを実装するクラスも、これらの実装されたメソッドを自動的に継承します。例えば:
package com.javacodegeeks.advanced.design;

public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }

    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}

// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}
多重継承は非常に強力ですが、同時に危険なツールであることに留意してください。有名な Diamond of Death 問題は、多重継承の実装における大きな欠陥としてよく引用され、開発者はクラス階層を非常に慎重に設計する必要があります。残念ながら、デフォルトのメソッドを備えた Java 8 インターフェイスもこれらの欠陥の影響を受けます。
interface A {
    default void performAction() {
    }
}

interface B extends A {
    @Override
    default void performAction() {
    }
}

interface C extends A {
    @Override
    default void performAction() {
    }
}
たとえば、次のコード スニペットはコンパイルに失敗します。
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
現時点では、言語としての Java は常にオブジェクト指向プログラミングのコーナーケースを回避しようとしてきたと言っても過言ではありませんが、言語が進化するにつれて、それらのケースのいくつかが突然現れ始めています。 

11. 継承と構成

幸いなことに、クラスを設計する唯一の方法は継承ではありません。多くの開発者が継承よりもはるかに優れていると信じているもう 1 つの代替手段は合成です。考え方は非常にシンプルです。クラスの階層を作成する代わりに、クラスを他のクラスで構成する必要があります。この例を見てみましょう:
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}
このクラスは、Vehicleエンジンとホイール (さらに、わかりやすくするために脇に置いた他の多くのパーツ) で構成されます。Vehicleただし、クラスはエンジンとも  言えるため、継承を使用して設計することができます。
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}
どの設計ソリューションが正しいでしょうか? 一般的なコア ガイドラインは、IS-A (is) および HAS-A (contains) 原則として知られています。IS-A は継承関係です。サブクラスは親クラスのクラス仕様と親クラスのバリエーションも満たします。サブクラス) はその親を拡張します。あるエンティティが別のエンティティを拡張するかどうかを知りたい場合は、一致テスト - IS を実行してください。 -A (is).") したがって、HAS-A は構成関係です。つまり、クラスはそのオブジェクトを所有 (または含んでいます) します。 ほとんどの場合、HAS-A 原則は、次のようなさまざまな理由から IS-A よりもうまく機能します。 
  • 設計はより柔軟になります。
  • 変更がクラス階層を通じて伝播しないため、モデルはより安定しています。
  • クラスとその構成は、親とそのサブクラスが密に結合する構成と比較して、疎に結合されています。
  • クラス内のすべての依存関係が 1 か所に含まれるため、クラス内の論理的な思考の流れはより単純になります。 
いずれにせよ、継承には役割があり、既存の設計上の問題の多くをさまざまな方法で解決できるため、無視すべきではありません。オブジェクト指向モデルを設計するときは、これら 2 つの選択肢を念頭に置いてください。

12. カプセル化。

オブジェクト指向プログラミングにおけるカプセル化の概念は、すべての実装の詳細 (動作モード、内部メソッドなど) を外部から隠すことです。カプセル化の利点は、保守性と変更の容易さです。クラスの内部実装は隠蔽されており、クラス データの操作はクラスのパブリック メソッドを通じてのみ行われます (多くの人が使用するライブラリやフレームワークを開発している場合には、大きな問題になります)。Java でのカプセル化は、可視性とアクセシビリティのルールによって実現されます。Java では、(フィールドが Final としてマークされている場合を除き) ゲッターとセッターを介してのみフィールドを直接公開しないことがベスト プラクティスと考えられています。例えば:
package com.javacodegeeks.advanced.design;

public class Encapsulation {
    private final String email;
    private String address;

    public Encapsulation( final String email ) {
        this.email = email;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getEmail() {
        return email;
    }
}
この例は、Java 言語でJavaBeans と呼ばれるものを思い出させます。標準 Java クラスは一連の規則に従って記述され、そのうちの 1 つは、ゲッター メソッドとセッター メソッドを使用した場合にのみフィールドにアクセスできます。継承のセクションですでに強調したように、カプセル化の原則を使用して、クラス内の最小限の宣伝契約を常に遵守してください。パブリックにしてはいけないものはすべてプライベートにする必要があります (解決している問題に応じて、保護/パッケージをプライベートにすることもできます)。これにより、破壊的な変更を行わずに (または少なくとも最小限に抑えて) 自由に設計できるようになるため、長期的には効果が得られます。 

13. 最終的なクラスとメソッド

Java には、クラスが別のクラスのサブクラスになることを防ぐ方法があります。つまり、他のクラスを Final として宣言する必要があります。 
package com.javacodegeeks.advanced.design;

public final class FinalClass {
}
メソッド宣言内の同じ最後の キーワードにより、サブクラスがメソッドをオーバーライドできなくなります。 
package com.javacodegeeks.advanced.design;

public class FinalMethod {
    public final void performAction() {
    }
}
クラスまたはメソッドを Final にするかどうかを決定するための一般的なルールはありません。最終的なクラスとメソッドは拡張性を制限するため、クラスを継承すべきか否か、または将来メソッドをオーバーライドすべきかオーバーライドすべきかを事前に考えることは非常に困難です。このような設計上の決定はライブラリの適用可能性を大幅に制限する可能性があるため、これはライブラリ開発者にとって特に重要です。Java 標準ライブラリには最終クラスの例がいくつかありますが、最も有名なのは String クラスです。初期段階では、開発者が文字列を実装するための独自の「より良い」ソリューションを考え出そうとする試みを防ぐために、この決定が行われました。 

14. 次は何だろう

レッスンのこの部分では、Java のオブジェクト指向プログラミングの概念について説明しました。また、コントラクト プログラミングについても簡単に検討し、いくつかの機能概念に触れ、言語が時間の経過とともにどのように進化したかを確認しました。レッスンの次の部分では、ジェネリックスと、それがプログラミングにおけるタイプ セーフへのアプローチ方法をどのように変えるかについて説明します。 

15. ソースコードのダウンロード

ここからソースをダウンロードできます - Advanced-Java-part-3 ソース:クラスを設計する方法
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION