[Yak Shaving] Data class or interface as input param?

This truly is very unimportant, but I sometimes ponder things like this and then start questioning my own wisdom/experience.

Back story

I remember back when I was doing Java that there was a “best practice” mantra: “Code to interfaces”. Basically, your code’s inputs and dependencies should always be the least restrictive (most minimal) interfaces or base classes, rather than a concrete class. This is old advice and the advantages are surely obvious and well-known to most people on this sub, but I’ll go ahead and mention two of the advantages:

  1. Flexibility for the caller. Your code is only asking that it receive an object that is capable of doing exactly what you need- no more, no less. Since Java (and Kotlin) has a nominal type system, rather than a structural type system, you’re freeing the caller to use their own custom classes/objects to fulfill your requirements, rather than forcing them to use a specific class.

  2. Allows for simple/easy tests when paired with “inversion of control” a.k.a. “dependency injection” techniques. Your tests can supply weird and non-standard implementations of the interfaces.

So, that’s all fine and great, and I think most people agree that this is generally a good idea. Well, at least for languages with similar semantics and performance-characteristics of Java. Other languages might see more of a relative hit from adding extra indirection, or might have different features that change the cost/benefit calculus (structural typing, type classes, etc).

Today’s musings

But, there’s some subtlety. What if our input needs are mostly “plain old data”? For example, let’s say we’re writing a function to register new users into some system. We could write our function in three or so different ways:

// #1 fun register(username: String, email: String, password: String): RegisteredUser = TODO() // #2 data class RegistrationInfo(val username: String, val email: String, val password: String) fun register(info: RegistrationInfo): RegisteredUser = TODO() // #3 interface RegistrationInfo { val username: String, val email: String, val password: String, } // maybe provide a default impl or whatever, too. fun register(info: RegistrationInfo): RegisteredUser = TODO() 

It would honestly be kind of silly to write the #2 and #3 versions. But, what if we want our API to include bulk registration? Then your input needs a way to accept more than one grouping of registration info. So you might have these:

// #1 data class RegistrationInfo(val username: String, val email: String, val password: String) fun register(infos: Iterable<RegistrationInfo>): List<RegisteredUser> = TODO() // #2 interface RegistrationInfo { val username: String, val email: String, val password: String, } // maybe provide a default impl or whatever, too. fun register(infos: Iterable<RegistrationInfo>): List<RegisteredUser> = TODO() 

In such scenarios I have always chosen to just define a data class and move on with my life. It’s more KISS and has more intuitive semantics (value equality, properties are backed fields rather than “getters” so they can’t change on you, etc).

The only advantage I can see to the interface approach is that the caller might already need a class that’s a super-set of the data needed (or might just need a few computed properties to adhere, etc). Or, perhaps you don’t want to mix your DTOs with your business logic, so your business logic requires an interface and your DTO implements it.

What are your thoughts? Do you lean more KISS or more flexible, and what informs your style choice?

submitted by /u/ragnese
[link] [comments]