Kotlin Trick for passing arguments to Factory Methods

Mohammad Amarneh
5 min readAug 26, 2019

Factory pattern is one of the most used creational design patterns, it says that just define an interface or abstract class for creating an object but let the subclasses decide which class to instantiate.

Example

Example design

Lets take this example, we have the Shape interface, and its sub-classes: Circle and Rectangle, for each sub-class, it has its own fields, that they are required to construct them.

interface Shape {
fun draw()
}

class Circle(private val radius: Int): Shape {
override fun draw() {
...
}
}

class Rectangle(private val width: Int, private val height: Int) : Shape {
override fun draw() {
...
}
}

ShapesFactory, is the factory that will create a proper instance of Shape, based on the passed arguments (class, radius, width and height).

object ShapesFactory {
/**
*
@param T type of shape sub-class.
*
@param radius required to construct [Circle] instance.
*
@param width required to construct [Rectangle] instance.
*
@param height required to construct [Rectangle] instance.
*/
fun <T: Shape> getShape(shape: Class<T>, radius: Int, width: Int, height: Int): Shape {
if (shape.isAssignableFrom(Circle::class.java)) {
return Circle(radius)
} else if (shape.isAssignableFrom(Rectangle::class.java)) {
return Rectangle(width, height)
} else {
throw IllegalStateException("Cannot create shape instance for ${shape.simpleName}")
}
}
}

You can see that we have to pass always four parameters to getShape method, even if we don’t need some of them, also we may get an exception at run time, if we pass wrong shape class, or if we miss to handle some possible types.

Use the factory

val circle = ShapesFactory.getShape(
shape = Circle::class.java,
radius = 10,
width = 0, //useless argument
height = 0 //useless argument
)
val square = ShapesFactory.getShape(
shape = Rectangle::class.java,
width = 10,
height = 8,
radius = 0 //useless argument
)

The Problem with this implementation

  • We are passing too much arguments, but they are all required by the factory to handle all possible cases.
  • And if we pass an un-handled class or miss to handle some possible types, it will throw an exception at run time.
  • The code is hard to read and understand, because there are many dependencies we have to follow to get it’s purpose, and to know where they are used.

Solution

We need a way to pass the proper arguments that needed to construct just one instance, so if we would get a circle instance, then we expect to pass just a radius, and if we want to construct a rectangle, then we expect to pass just the width and the height of it, also, we need a way to restrict the acceptable arguments at compile time.

That solution can be found using Sealed kotlin classes.

Sealed classes are used for representing restricted class hierarchies, when a value can have one of the types from a limited set, but cannot have any other type. They are, in a sense, an extension of enum classes: the set of values for an enum type is also restricted, but each enum constant exists only as a single instance, whereas a subclass of a sealed class can have multiple instances which can contain state.

ShapesFactoryParam design
sealed class ShapeFactoryParam {
data class CircleParam(val radius: Int) : ShapeFactoryParam()
data class RectangleParam(val width: Int, val height: Int): ShapeFactoryParam()
}

We have created the sealed class ShapeFactoryParam, that can be CircleParam or RectangleParam, each param has its own parameters.

object ShapesFactory {
fun getShape(param: ShapeFactoryParam): Shape {
return when (param) {
is ShapeFactoryParam.CircleParam -> Circle(param.radius)
is ShapeFactoryParam.RectangleParam -> Rectangle(param.width, param.height)
}
}
}

The Factory method hides the complexity, and encapsulates a set of parameters into one sealed parameter, that can be one of restricted set options, and for every instance type, it has its own parameters, isolated from other instances dependencies.

So, lets use this factory:

val circle = ShapesFactory.getShape(ShapeFactoryParam.CircleParam(
radius = 10
))
val rectangle = ShapesFactory.getShape(ShapeFactoryParam.RectangleParam(
width = 10,
height = 8
))
  • The code is more realistic, readable, reliable, maintainable and extendable, if we want to construct a circle instance, we just need to pass the radius, and if want to construct the rectangle, we need to pass just the width and the height.
  • We have restricted the acceptable arguments set, so we can’t pass something other that CircleParam or RectangleParam.

Common case in Android

In android, we have a common pattern that we usually do, when we want to start an activity, or want to get an intent to start the activity, we usually define a static factory method to create the intent for that activity, it is a good practice and a recommended way to pass arguments into the intent in one maintainable place.

Example

Suppose that we have two modes for the activity, AddMode, and EditMode, for EditMode, we have to pass extra argument for itemId to get the item from DataSource.

/**
*
@param itemId if null then the mode is AddMode, else, the mode is EditMode.
*/
fun newIntent(context: Context, itemId: String): Intent {
return Intent(context, MainActivity::class.java)
.putExtra("itemId", itemId)
}

the activity may have to define a property to hold the itemId, that will be used when the mode is EditMode, and it will be useless when we start the activity in AddMode.

Solution

sealed class Mode {
object AddMode : Mode()
data class EditMode(val itemId: String) : Mode()
}

fun newIntent(context: Context, mode: Mode): Intent {
val intent = Intent(context, MainActivity::class.java)
return when (mode) {
is Mode.AddMode -> intent
is Mode.EditMode -> intent.putExtra("itemId", mode.itemId)
}
}

The activity should hold the mode instance, which it can be AddMode without parameters, or EditMode with itemId parameter.

--

--