Question

After picking up some Swift skills with Java as my strongest language, one feature of Swift that I really like is the ability to add extensions to a class. In Java, a pattern I see very often is Utils or Helper classes, in which you add your methods to simplify something you're trying to accomplish. This might be a silly question, but is there any good reason not to subclass the original class in Java and just import your own with the same name?

A swift example of a Date extension would be something like this

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

Then an implementation would look like this:

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

In Java you could have a DateHelper class, but this now seems archaic to me. Why not create a class with the same name, and extend the class you want to add a method to?

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

Then the implementation would look like this:

import com.me.extensions.Date

...

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

Then, just import your own Date class which now acts like a class with Swift extensions.

Has anyone had any success with doing this, or see any reasons to stay away from a pattern like this?

Was it helpful?

Solution

No, your subclassing does not have the same effect as Swift's extensions. Your com.me.extensions.Date and java.util.Date are different classes. An existing java.util.Date instance will not have the methods that you defined in your subclass. In contrast, an extension Date does not create a new class, but adds methods to an existing class. These methods will be available on all instances. Extensions are a bit like monkey-patching in dynamic languages (Python, Ruby, Perl, JavaScript).

This difference is particularly important when the Date instance is created in some other package that does not know about your extension. It will create a java.util.Date that does not provide your someUniqueValue() method.

The techniques used by Swift, Go, Rust, Scala, Haskell, … to allow functionality to be added to an existing type can also be used in Java, but the language won't assist us – we have to do everything manually. In particular, most Java APIs are not designed in a way that support the necessary extensibility.

In Java, we can have the same effect of adding a new final method to a type by declaring a static method that takes the instance as parameter. This is how C#'s extension methods work. In your case:

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

Instead of date.someUniqueValue() this will be called as someUniqueValue(date), but that is just syntax. Java allows you to import static if you want to use such a function in multiple files.

The big restriction here is that static methods don't work together with dynamic dispatch (aka. virtual methods). You may define new methods that you use, but you can't override existing methods so that everyone uses your version.

In case you want to extend a class with methods in order to implement an interface/protocol, we can use the object adapter pattern.

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;
  }
}

Whenever we have a date but want to use it as an UniquelyValued object, we must wrap it in the adapter: new AdaptDateToUniquelyValued(date).someUniqueValue(). You can interpret wrapping as upcasting to the interface type. In languages with first-class support for extensions, the wrapping and unwrapping happens automatically.

This works fine if the code you are interacting with depends on interfaces, not concrete classes (the Dependency Inversion Principle in SOLID). This is not necessarily the case.

An wrapper may also be a good choice if you could use static methods instead, but want to use method chaining to provide a fluent API.

The big drawback of adapters is that writing them takes a lot of effort, unless you are willing to use reflection or annotations. If you are adapting a class to an interface and the class already provides all necessary methods, you still have to explicitly forward each method in interface to the wrapped object.

Interface types also have a general problem that they perform “type erasure”. If you use an adapter to turn some object into an interface-instance, you lose the information that it was originally some kind of object. Downcasting an UniquelyValued instance to the adapter is unsafe. When designing an API, such problems can be minimized by using generics extensively. E.g. in an interface defining a fluent API

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

we cannot subclass that interface to add extra methods to the fluent API – the first call to add() will unnecessarily constrain us. With generics, we can just use the interface as a type constraint instead:

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()

Such an interface destroys much less type information, and makes it easier to temporarily wrap some object with an adapter.

Other techniques for designing systems in a manner that allows them to be extended later with new behaviour (in the sense of the Open–Closed Principle) including the adapter pattern have been discussed at great length in the book “Design Patterns. Elements of Reusable Object-Oriented Software” by Gamma et al.

Licensed under: CC-BY-SA with attribution
scroll top