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?
Best Answer
No, your subclassing does not have the same effect as Swift's extensions. Your
com.me.extensions.Date
andjava.util.Date
are different classes. An existingjava.util.Date
instance will not have the methods that you defined in your subclass. In contrast, anextension 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 yoursomeUniqueValue()
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:
Instead of
date.someUniqueValue()
this will be called assomeUniqueValue(date)
, but that is just syntax. Java allows you toimport 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.
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 APIwe 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: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.