web-dev-qa-db-ja.com

Javaの継承を使用したSwiftのような拡張機能

いくつかのSwiftスキルJavaを私の最強言語として使用した後、Swiftが私が本当に好きなのは、クラスに拡張機能を追加する機能。Javaでよく目にするパターンはUtilsまたはHelperクラスであり、ここでメソッドを追加して、達成しようとしていることを単純化します。これはばかげた質問かもしれませんが、元のクラスをJavaでサブクラス化せず、同じ名前で独自のクラスをインポートするだけの理由はありますか?

Swift日付拡張の例は次のようになります

extension Date {
    func someUniqueValue() -> Int {
        return self.something * self.somethingElse
    }
}

その場合、実装は次のようになります。

let date = Date()
let myThing = date.someUniqueValue()

Javaでは、DateHelperクラスを使用することもできますが、これは私には古風です。同じ名前のクラスを作成し、メソッドを追加するクラスを拡張してみませんか?

class Date extends Java.util.Date {
    int someUniqueValue() {
        return this.something * this.somethingElse;
    }
}

その場合、実装は次のようになります。

import com.me.extensions.Date

...

Date date = new Date()
int myThing = date.someUniqueValue()

次に、Swift拡張子を持つクラスのように機能する独自のDateクラスをインポートします。

誰かがこれを行うことで成功しましたか、またはこのようなパターンから離れる理由があると思いますか?

5
Styler

いいえ、サブクラス化にはSwiftの拡張機能と同じ効果はありません。 _com.me.extensions.Date_と_Java.util.Date_は異なるクラスです。既存の_Java.util.Date_インスタンスには、サブクラスで定義したメソッドがありません。対照的に、_extension Date_は新しいクラスを作成しませんが、既存のクラスにメソッドを追加します。これらのメソッドは、すべてのインスタンスで使用できます。拡張機能は、動的言語(Python、Ruby、Perl、JavaScript)でのモンキーパッチングに少し似ています。

この違いは、拡張機能を認識しない他のパッケージでDateインスタンスが作成される場合に特に重要です。 someUniqueValue()メソッドを提供しない_Java.util.Date_を作成します。

既存の型に機能を追加するためにSwift、Go、Rust、Scala、Haskellなどで使用されている手法はJavaでも使用できますが、この言語は役に立ちません。すべてを手動で行う必要があります。特に、ほとんどのJava APIは、必要な拡張性をサポートするように設計されていません。

Javaでは、インスタンスをパラメーターとして受け取る静的メソッドを宣言することにより、新しいfinalメソッドを型に追加するのと同じ効果があります。これは、C#の拡張メソッドが機能する方法です。あなたの場合:

_static int someUniqueValue(Date self) {
  return self.something * self.somethingElse;
}
_

date.someUniqueValue()の代わりに、これはsomeUniqueValue(date)として呼び出されますが、これは単なる構文です。 Javaを使用すると、そのような関数を複数のファイルで使用する場合に_import static_を使用できます。

ここでの大きな制限は、静的メソッドは動的ディスパッチ(別名仮想メソッド)と一緒に機能しないことです。使用する新しいメソッドを定義することはできますが、既存のメソッドをオーバーライドして誰もが自分のバージョンを使用できるようにすることはできません。

インターフェイス/プロトコルを実装するためにクラスをメソッドで拡張する場合は、オブジェクトアダプターパターンを使用できます。

_interface UniquelyValued { int someUniqueValue(); }

// extension Date: UniquelyValued { ... }
class AdaptDateToUniquelyValued implements UniquelyValued {
  private final Date self;

  AdaptDateToUniquelyValued(Date self) {
    this.self = self;
  }

  public Date getDate() { return self; }

  @Override
  public int someUniqueValue() {
    return self.something * self.somethingElse;
  }
}
_

日付があるが、それをUniquelyValuedオブジェクトとして使用したい場合は常に、それをアダプターでラップする必要があります:new AdaptDateToUniquelyValued(date).someUniqueValue()ラッピングは、インターフェイスタイプへのアップキャストとして解釈できます。拡張機能がファーストクラスでサポートされている言語では、ラッピングとアンラッピングが自動的に行われます。

これは、対話するコードが具象クラスではなくインターフェースに依存している場合に正常に機能します(SOLIDの依存関係逆転原理)。これは必ずしもそうではありません。

代わりに静的メソッドを使用できても、メソッドチェーンを使用してFluent APIを提供したい場合は、ラッパーも適しています。

アダプターの大きな欠点は、リフレクションまたはアノテーションを使用するつもりがない限り、アダプターの作成に多くの労力がかかることです。クラスをインターフェイスに適合させ、そのクラスが必要なすべてのメソッドをすでに提供している場合でも、インターフェイスの各メソッドをラップされたオブジェクトに明示的に転送する必要があります。

インターフェイスタイプには、「タイプ消去」を実行するという一般的な問題もあります。アダプタを使用してオブジェクトをインターフェイスインスタンスに変換すると、元々はある種のオブジェクトであったという情報が失われます。 UniquelyValuedインスタンスをアダプターにダウンキャストすることは安全ではありません。 APIを設計する場合、ジェネリックを広範囲に使用することにより、このような問題を最小限に抑えることができます。例えば。 Fluent APIを定義するインターフェース

_interface FluentAddition {
  FluentAddition plus(int x);
  int result();
}
// class FluentMath implements FluentAddition<FluentMath> { ... }
// new FluentMath().plus(1).plus(2).plus(3).result()
_

そのインターフェイスをサブクラス化してFluent APIに追加のメソッドを追加することはできません。add()への最初の呼び出しは、不必要に制約されます。ジェネリックスでは、代わりにインターフェイスを型制約として使用できます。

_interface FluentAddition<Self extends FluentAddition<Self>> {
  Self plus(int x);
  int result();
}
// class FluentMath implements FluentAddition<FluentMath>,
//   FluentMultiplication<FluentMath> { ... }
// new FluentMath().plus(1).plus(2).times(3).result()
_

このようなインターフェースは、タイプ情報をはるかに少なく破壊し、一部のオブジェクトをアダプターで一時的にラップすることを容易にします。

アダプターパターンを含む新しい動作(オープン-クローズドプリンシプルの意味で)を使用して後で拡張できるようにシステムを設計するためのその他の手法については、「デザインパターン」で詳しく説明しています。再利用可能なオブジェクト指向ソフトウェアの要素」(ガンマ他).

9
amon