Using Kotlin’s Delegation to Add Superpowers to a Data Class

Brandon Trautmann
Capital One Tech
Published in
5 min readMay 27, 2020
forest of green pine trees surrounded by fog

If you’ve been writing code in Kotlin for any amount of time you’ve almost certainly run into delegation. One of the most common usages of the delegate pattern is by lazy, which enforces that a value not be calculated until it’s requested, and this value isn’t re-calculated upon subsequent requests.

Another common usage of the delegate pattern is when it’s used to implement an interface by some injected parameter, like CoroutineScope by injectedScope. This allows us to call public members on the injected parameter without having to continuously refer to it directly, similar to when we use with(something).

Enhancing Fixed-Size State Containers

Until recently, my experience with delegation was limited to the two examples above. However, I have since come across an elegant way to give fixed-size data containers the powers of a Collection by making use of delegation.

Fixed-size data containers are a requirement I’ve come across quite often in my career. Often these are a group or list of entities that are known at compile time but need massaging or the fulfillment of various fields by an API response or other runtime input. One example of such a container is a suite of features that the user is eligible to use. The potential features are known at compile time, but whether the user is eligible for the feature and all the characteristics of each feature is not known until the app is run.

In Kotlin, we generally represent this type of data using one of two things: either a Data Class or Set<T>.

Data Class

A data class is a good starting point when designing a container such as this one.

data class FeatureSuite(
val featureOne: Feature.TypeA.FeatureOne,
val featureTwo: Feature.TypeA.FeatureTwo,
val featureThree: Feature.TypeB.FeatureThree,
...
)

One major benefit of having a data class representation of these features is that we can guarantee every feature is accounted for when the suite is constructed. That's because each feature is a required parameter of the constructor of our class. In addition, we have non-nullable access to each feature as public properties of the suite, so if we need to check if a given feature is enabled, all we must do is suite.featureOne.isEnabled(). As an aside, using a data class over a normal class ensures that every parameter must be a val or var which helps enforce the container characteristics of this domain entity.

However, there is a downside to the data class. If we want to get the list of features that are of type TypeA, we have no way other than a "hard-coded" public method that creates and returns a list of all of the known TypeA features. If we are to add a new TypeA feature to the suite, there is a good chance we will forget to add it to this list, and it also means we are tracking the same data in two separate places which adds complexity. Unit tests to catch this would be brittle and best-effort, which is not something we aspire to when writing tests.

Set<Feature>

The second approach we can take is using a Set<Feature> as a container for our features. This might look something like:

typealias FeatureSuite = Set<Feature>

We could then create a FeatureSuite by doing:

val featureSuite = setOf(
Feature.TypeA.FeatureOne(...),
Feature.TypeA.FeatureTwo(...),
Feature.TypeB.FeatureThree(...),
...
)

By using a Set, we are now able to do things like filterIsInstance to get all features of type TypeA, without having to worry about missing one if we add a new feature.

However, we’ve lost the benefits of a data class! We no longer can guarantee that each feature is passed in when creating the suite, and we don't have null-safe access to each feature because there's no guarantee the feature exists within the Set! That lack of null-safety will propagate to all usages of the suite, and could cause a RuntimeException if a feature that wasn't added to the suite is requested.

So, how do we get the benefits of the data class and the Set, all in one entity? You guessed it, delegation!

Delegation, Your New Superpower!

We should define our FeatureSuite as follows:

data class FeatureSuite(
val featureOne: Feature.TypeA.FeatureOne,
val featureTwo: Feature.TypeA.FeatureTwo,
val featureThree: Feature.TypeB.FeatureThree,
...
) : Set<Feature> by setOf(
featureOne,
featureTwo,
featureThree,
...
)

Note that we now have the benefits of both the data class and the Set by using delegation to a Set consisting of all the feature parameters.

  • Guarantee that all features are passed into the suite via the constructor
  • Null-safe access to features via featureSuite.someFeature
  • Ability to filterIsInstance and receive all features of a given type without worrying about missing one

We can begin to add additional functionality to make call sites cleaner as well:

val typeAFeatures: List<Feature.TypeA> by lazy {
filterIsInsance(Feature.TypeA::class.java)
}
val enabledFeatures: List<Feature> by lazy {
filter { it.isEnabled() }
}

Testing

We should enforce a couple things via unit tests to mitigate the risk of any future developer error:

  • All features should be mapped to the underlying Set (need to make sure we don't leave one out!). This is the most important test.
  • The features passed to the suite are in fact the exact same (object equality) features present in the underlying Set

The first test is simple. Because Set by nature doesn't allow duplicates, we just need to make sure that the created Set has the same size as the number of parameters of the suite itself.

// Notes: `primaryConstructor` is made available through `kotlin-reflect` and `FakeFeatureSuite` is just a convenience method to create a (real) test object that can be re-used in other tests.@Test
fun `All features mapped to Set`() {
val featuresRequiredInConstructor =
FeatureSuite::class.primaryConstructor!!.parameters.size
val featuresAddedToSet = FakeFeatureSuite().size assertThat(featuresAddedToSet)
.isEqualTo(featuresRequiredInConstructor)
}

The second test is less bulletproof, but still worthwhile:

@Test
fun `Object equality of features`() {
val featureSuite = FakeFeatureSuite() val featureOneFromSet = featureSuite.single {
it is Feature.TypeA.FeatureOne
}
assertThat(featureSuite.featureOne)
.isSameAs(featureOneFromSet)
}

The risk with this test is that a new feature will be added to the suite, but not added to this test. One way we can attempt to guard against this is by checking that the number of assertions made is always equal to the number of features within the suite.

val assertions = listOf<AbstractAssert<*,*>>(
// Wrap our assertions in a List
assertThat(featureSuite.featureOne)
.isSameAs(featureOneFromSet)
)
val expectedAssertionCount =
FeatureSuite::class.primaryConstructor!!.parameters.size
assertThat(assertions.size)
.`as`( "Ensure all constructor params checked for object equality").isEqualTo(expectedAssertionCount)

Now, if a new feature is added but not added to our assertions, this test will fail. The only way we can mess it up now is by accidentally duplicating assertions in the list rather than adding a unique assertion for a new feature.

Conclusion

By combining the benefits of a normal data class with those of a Set, we are able to surface all the desired functionality for our suite of features in a safe way that avoids RuntimeExceptions. As always, if you have any suggestions on how I might improve on this implementation, definitely shoot them my way!

DISCLOSURE STATEMENT: © 2020 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.

Originally published at https://brandontrautmann.com on May 27, 2020.

--

--