Monday, May 1, 2023

Kotlin Factory method Comparative Analysis with Java

 Factory

 We'll start with the Factory Method formalized in Design Patterns by Gang of Four. This is one of the first patterns I teach my students. They're usually very anxious about the whole concept of design patterns, since it has an aura of mystery and complexity. So, what I do is ask them the following question. Assume you have some class declaration, 

for example: 

class Cat {

    val name = "Cat"

}

Could you write a function that returns a new instance of the class? Most of them would succeed:

 fun catFactory() : Cat {

    return Cat()

} Check that everything works: 

val c = catFactory() 

println(c.name) // Indeed prints "Cat" Well, that's really simple, right? Now, based on the argument we provide it, can this method create one of two objects? Let's say we now have a Dog: 

class Dog {

    val name = "Dog"

}

Choosing between two types of objects to instantiate would require only passing an argument: 

fun animalFactory(animalType: String) : Cat {

    return Cat()

Of course, we can't always return a Cat now. So we create a common interface to be returned: 

interface Animal {

   val name : String

What's left is to use the when expression to return an instance of the correct class: 

return when(animalType.toLowerCase()) {

    "cat" -> Cat()

    "dog" -> Dog()

    else -> throw RuntimeException("Unknown animal $animalType")

}

That's what Factory Method is all about: Get some value. Return one of the objects that implement the common interface. This pattern is very useful when creating objects from a configuration. Imagine we have a text file with the following contents that came from a veterinary clinic: dog, dog, cat, dog, cat, cat Now we would like to create an empty profile for each animal. Assuming we've already read the file contents and split them into a list, we can do the following:

 val animalTypes = listOf("dog", "dog", "cat", "dog", "cat", "cat")

for (t in animalTypes) {

  val c = animalFactory(t) 

  println(c.name)

}

  listOf is a function that comes from the Kotlin standard library that creates an immutable list of provided objects. If your Factory Method doesn't need to have a state, we can leave it as a function. But what if we want to assign a unique sequential identifier for each animal? Take a look at the following code block: 

interface Animal {

   val id : Int

   // Same as before

}

 class Cat(override val id: Int) : Animal { 

    // Same as before

}

class Dog(override val id: Int) : Animal {

    // Same as before

Note that we can override values inside the constructor. Our factory becomes a proper class now:

 class AnimalFactory { 

    var counter = 0

     fun createAnimal(animalType: String) : Animal {

        return when(animalType.trim().toLowerCase()) {

            "cat" -> Cat(++counter)

            "dog" -> Dog(++counter)

            else -> throw RuntimeException("Unknown animal $animalType")

        }

    } 

}

So we'll have to initialize it: val factory = AnimalFactory()

for (t in animalTypes) {

    val c = factory.createAnimal(t) 

    println("${c.id} - ${c.name}")

 Output for the preceding code is as follows: 1 - Dog 

2 - Dog 

3 - Cat 

4 - Dog 

5 - Cat 

6 - Cat This was a pretty straightforward example. We provided a common interface for our objects (Animal, in this case), then based on some arguments, we decided which concrete class to instantiate.

What if we decided to support different breeds? Take a look at the following code:

 val animalTypes = listOf("dog" to "bulldog", 

                         "dog" to "beagle", 

                         "cat" to "persian", 

                         "dog" to "poodle", 

                         "cat" to "russian blue", 

                         "cat" to "siamese") 

Much like the downTo function , it looks like an operator, but it's a function that creates a pair of objects: (cat, siamese, in our case). We'll come back to it when we discuss the infix function in depth. We can delegate the actual object instantiation to other factories: 

class AnimalFactory {

    var counter = 0

    private val dogFactory = DogFactory()

    private val catFactory = CatFactory()

fun createAnimal(animalType: String, animalBreed: String) : Animal {

        return when(animalType.trim().toLowerCase()) {

            "cat" -> catFactory.createDog(animalBreed, ++counter)

            "dog" -> dogFactory.createDog(animalBreed, ++counter)

            else -> throw RuntimeException("Unknown animal $animalType")

        }

    }

The factory repeats the same pattern again: class DogFactory {

    fun createDog(breed: String, id: Int) = when(breed.trim().toLowerCase()) {

        "beagle" -> Beagle(id)

        "bulldog" -> Bulldog(id)

        else -> throw RuntimeException("Unknown dog breed $breed")

}

You can make sure that you understand this example by implementing Beagle, Bulldog, CatFactory, and all the different breeds of cats by yourself. The last point to note is how we're now calling our

 AnimalFactory with a pair of arguments: for ((type, breed) in animalTypes) {

    val c = factory.createAnimal(type, breed)

    println(c.name)

}

 This is called a destructuring declaration, and is useful especially when dealing with such pairs of data.

Code Example :

import java.util.*

class FactoryMethod {

var counter = 0
private val dogFactory = DogFactory()
private val catFactory = CatFactory()

fun createAnimal(animalType: String, animalBreed: String): Animal {
return when (animalType.trim().lowercase(Locale.getDefault())) {
"cat" -> catFactory.createCat(animalBreed, ++counter)
"dog" -> dogFactory.createDog(animalBreed, ++counter)
else -> throw RuntimeException("Unknown animal $animalType")
}
}
}

class CatFactory {
fun createCat(animalBreed: String, i: Int): Animal {
return Cat(i)
}
}

class DogFactory {
fun createDog(breed: String, id: Int) = when (breed.trim().toLowerCase()) {
"beagle" -> Beagle(id)
"bulldog" -> Bulldog(id)
else -> throw RuntimeException("Unknown dog breed $breed")
}
}

class Beagle(id: Int) : Dog(id)
class Bulldog(id: Int) : Dog(id)

interface Animal {
val id: Int
// Same as before
}

class Cat(override val id: Int) : Animal {
// Same as before
}

open class Dog(override val id: Int) : Animal {
// Same as before
}

main.kt for testing :

fun main(args: Array<String>) {
// Singleton test
for (i in 1..10){
SingletonAnalysisWithJava.increment()
}
// Factory Method test
val animalTypes = listOf("dog" to "bulldog",
"dog" to "beagle",
"cat" to "persian",
"cat" to "russian blue",
"cat" to "siamese")

val factory = FactoryMethod()
for ((type, breed) in animalTypes) {
val c = factory.createAnimal(type, breed)
println(c)
}
}

Output :

Bulldog@1936f0f5

Beagle@4909b8da

Cat@2d3fcdbd

Cat@617c74e5

Cat@6537cf78

Conclusion :

The Factory Method is a design pattern that provides an interface for creating objects but lets subclasses decide which classes to instantiate. In other words, it encapsulates object creation by letting the subclasses decide how to create the objects. The pattern is used when there is a need for a method that can return one of several possible classes that share a common super class or interface.

The main benefits of using the Factory Method pattern are that it enhances modularity, flexibility, and maintainability of the code by isolating the client code from knowing about the details of the object creation process. Instead of having the client code instantiate an object directly, it calls a factory method to create the object, allowing the implementation details to be hidden from the client code.

In the example provided, the AnimalFactory is responsible for creating animal objects based on the arguments provided. By using a factory class, we can add new types of animals or modify the existing ones without having to change the client code that uses the factory method. The factory method creates a common interface for all the animal objects, which makes it easier to handle them in a uniform way.

The Factory Method pattern is widely used in software development and is particularly useful when dealing with complex object creation logic, such as when creating objects from a configuration or when creating objects that require additional setup or initialization beyond what the constructor can provide.


No comments: