Kotlin - improved factory model

Welcome to WeChat official account: FSA full stack operation 👋

1, Foreword

Design pattern is a guide to solve specific problems in software engineering. We often say that there are 23 design patterns in Java. As a better Java, Kotlin with multi paradigm has many new language features. What improvements can be made when using the design patterns commonly used in Java?

Note: this series is a summary of my understanding of the design mode in Chapter 9 of Kotlin core programming, so there will be some changes in the summary content. In addition, some chapters are of little significance, so they are not summarized in this series. If you are interested, you can consult the original books by yourself.

  • Factory mode
    • Function: hide the creation logic of object instances without exposing them to the client.
    • Breakdown: simple factory, factory method and abstract factory.

2, Improved simple factory

  • Example: computer manufacturers produce computers (servers, home PC s)
  • Key points: associated objects, operator overloads (invoke)
/**
 * Computer interface
 *
 * @author GitLqr
 */
interface Computer {
    val cpu: String
}

/**
 * Computer
 *
 * @author GitLqr
 */
class PC(override val cpu: String = "Core") : Computer
class Server(override val cpu: String = "Xeon") : Computer

/**
 * Computer type enumeration
 *
 * @author GitLqr
 */
enum class ComputerType {
    PC, SERVER
}

Simple factories generally use the singleton mode (privatize the constructor and force it to be used through class methods). Let's look at the Java version first:

/**
 * Simple computer factory (Java version)
 *
 * @author GitLqr
 */
public static class ComputerFactory {
    private ComputerFactory() {
    }

    public static Computer produce(ComputerType type) {
        if (type == ComputerType.PC) {
            return new PC();
        } else if (type == ComputerType.SERVER) {
            return new Server();
        } else {
            throw new IllegalArgumentException("There are no other types of computers");
        }
    }
}

// use
Computer pc = ComputerFactory.produce(ComputerType.PC);
Computer server = ComputerFactory.produce(ComputerType.SERVER);

Kotlin has implemented singleton at the language level (replacing class with object keyword), so the above code is transformed into kotlin version:

/**
 * Simple computer factory (Kotlin version)
 *
 * @author GitLqr
 */
object ComputerFactory {
    fun produce(type: ComputerType): Computer {
        return when (type) {
            ComputerType.PC -> PC()
            ComputerType.SERVER -> Server()
        }
    }
}

// use
val pc = ComputerFactory.produce(ComputerType.PC)
val server = ComputerFactory.produce(ComputerType.SERVER)

The above is just a translation of Java code with Kotlin. Let's get to the point. From the use of this simple factory, we know that the core is to let ComputerFactory create computer instances according to different ComputerType parameters, and the name of the produce method is not important. Combined with Kotlin's operator overloading, the code can be further simplified:

/**
 * Simple factory improvement: overloading the invoke operator
 *
 * @author GitLqr
 */
object ComputerFactory {
    operator fun invoke(type: ComputerType): Computer {
        return when (type) {
            ComputerType.PC -> PC()
            ComputerType.SERVER -> Server()
        }
    }
}

// use
val pc = ComputerFactory(ComputerType.PC)
val server = ComputerFactory(ComputerType.SERVER)

You may think that the code like ComputerFactory(ComputerType.PC) is too strange. It is more like creating a factory instance. If it can be changed to the code like Computer(ComputerType.PC), it will be more logical. No problem. Just make a static factory method in the Computer interface to replace the constructor. Let's take a look at the Java version first:

/**
 * Static factory method (Java version)
 *
 * @author GitLqr
 */
interface Computer {
    class Factory{ // Equivalent to public static class Factory
        public static Computer produce(ComputerType type){
            if (type == ComputerType.PC) {
                return new PC();
            } else if (type == ComputerType.SERVER) {
                return new Server();
            } else {
                throw new IllegalArgumentException("There are no other types of computers");
            }
        }
    }
    ...
}

// use
Computer pc = Computer.Factory.produce(ComputerType.PC);
Computer server = Computer.Factory.produce(ComputerType.SERVER);

Translate the above Java code into Kotlin version:

/**
 * Static factory method (Kotlin version)
 *
 * @author GitLqr
 */
interface Computer {
    companion object Factory{
        fun produce(type: ComputerType): Computer {
            return when (type) {
                ComputerType.PC -> PC()
                ComputerType.SERVER -> Server()
            }
        }
    }
    ...
}

// use
val pc = Computer.Factory.produce(ComputerType.PC)
val server = Computer.Factory.produce(ComputerType.SERVER)

Note: the name of the associated object is Companion by default and can be customized. However, no matter the name is not specified, it can be omitted and not written. Therefore, the above code can also be computer Produce (computertype. PC), except for one case. If you extend the attribute or method to the associated object, you must write the user-defined name of the associated object.

We know that the name of the associated object in Kotlin can be omitted without writing. Combined with operator overloading, the above code can be further simplified as follows:

/**
 * Static factory method improvement: overload the [associated object] invoke operator
 *
 * @author GitLqr
 */
interface Computer {
    companion object {
        operator fun invoke(type: ComputerType): Computer {
            return when (type) {
                ComputerType.PC -> PC()
                ComputerType.SERVER -> Server()
            }
        }
    }
    ...
}

// use
val pc = Computer(ComputerType.PC)
val server = Computer(ComputerType.SERVER)

3, Improved Abstract Factory

  • Example: computer brand manufacturers produce computers
  • Key points: inline function (inline + reified)

Now we have added the concept of brand manufacturers, such as Dell and Asus. If we use the simple factory mode, we need to create n more factory classes of brand manufacturers, such as DellComputerFactory and AsusComputerFactory. There are many brand manufacturers. In order to expand later, we need to draw an image of the factory:

/**
 * Computers of various brands
 *
 * @author GitLqr
 */
interface Computer
class Dell : Computer
class Asus : Computer

/**
 * Manufacturers of various brands
 *
 * @author GitLqr
 */
abstract class AbstractFactory {
    abstract fun produce(): Computer
}

class DellFactory : AbstractFactory() {
    override fun produce() = Dell()
}

class AsusFactory : AbstractFactory() {
    override fun produce() = Asus()
}

Abstract factory mode generally provides a method to obtain specific factory instances according to parameters in the abstract factory class. Combined with Kotlin's overloaded operator feature, this method can be written as follows:

/**
 * Abstract factory: build factory instances based on method parameters
 *
 * @author GitLqr
 */
abstract class AbstractFactory {
    abstract fun produce(): Computer

    companion object {
        operator fun invoke(type: String): AbstractFactory {
            return when (type) {
                "dell" -> DellFactory()
                "asus" -> AsusFactory()
                else -> throw IllegalArgumentException()
            }
        }
    }
}

// use:
val factory = AbstractFactory("dell")
val computer = factory.produce()

Note: there is no limit to what this parameter is. It can be string, enumeration, specific factory instance, etc. in Kotlin core programming, the parameter used by the case is passed into the specific factory instance. I personally think this is inappropriate, because the core of the factory mode is to hide the creation logic of the object instance without exposing it to the client, So here I use string to distinguish.

Here's a question to consider. It's unreasonable to use string to distinguish factory types in actual projects, because it will cause the problem of hard coding at the call. It's more appropriate to use enumeration, but enumeration will add class files. So what scheme can avoid the above two disadvantages? Very simple. Directly pass the class type of a specific Computer into the method for judgment:

import java.lang.reflect.Type

/**
 * Abstract factory parameter passing optimization (Java reflection package)
 *
 * @author GitLqr
 */
abstract class AbstractFactory {
    abstract fun produce(): Computer

    companion object {
        operator fun invoke(type: Type): AbstractFactory {
            return when (type) {
                Dell::class -> DellFactory()
                Asus::class -> AsusFactory()
                else -> throw IllegalArgumentException()
            }
        }
    }
}

// use
val factory = AbstractFactory(Dell::class.java)
val computer = factory.produce()

Note: in Java, the corresponding class type is Java lang.reflect. Type, usually through class Class is obtained in this way. The writing method corresponding to Kotlin is class:: class java

You should notice that this type is a class in the java reflection package, and Kotlin is fully compatible with Java, so you can naturally use Java lang.reflect. Type to represent the class type. However, Kotlin itself has a set of reflection mechanism. You can use Kotlin reflect. Kclass is used to represent the class type. Therefore, the Kotlin reflection API version is as follows:

import kotlin.reflect.KClass

/**
 * Abstract factory parameter transfer optimization (Kotlin reflection package)
 *
 * @author GitLqr
 */
abstract class AbstractFactory {
    abstract fun produce(): Computer

    companion object {
        // operator fun invoke(type: KClass<*>): AbstractFactory
        operator fun invoke(type: KClass<out Computer>): AbstractFactory {
            return when (type) {
                Dell::class -> DellFactory()
                Asus::class -> AsusFactory()
                else -> throw IllegalArgumentException()
            }
        }
    }
}

// use
val factory = AbstractFactory(Dell::class)
val computer = factory.produce()

Note: the * in kclass < * > is a wildcard, indicating that any class type is received; The out keyword is used in kclass < out Computer >, which is a generic covariant in Kotlin, indicating that only the class types of Computer subclasses are received.

In fact, it is enough to obtain specific factory instances through AbstractFactory(Dell::class), but using generics can further simplify the code. The final writing method is: AbstractFactory < Dell > (). Is it more intuitive and elegant? So how do you use generics to transform the above method? You should know that Java generics are only available after 1.5. In order to be backward compatible, generic parameter types will be erased during compilation (pure historical burden = =), so it is very difficult to obtain a generic parameter type in Java. However, Kotlin's inline function (inline) combined with the reified keyword can materialize the generic parameter type, This makes it particularly easy to get generic parameter types:

/**
 * Abstract factory improvement: build factory instances according to generic parameter types (inline + reified materialize generic parameter types)
 *
 * @author GitLqr
 */
abstract class AbstractFactory {
    abstract fun produce(): Computer

    companion object {
        inline operator fun <reified T : Computer> invoke(): AbstractFactory {
            return when (T::class) { // If reified is not used, T::class will report an error
                Dell::class -> DellFactory()
                Asus::class -> AsusFactory()
                else -> throw IllegalArgumentException()
            }
        }
    }
}

// use
val factory = AbstractFactory<Dell>()
val computer = factory.produce()

4, Supplement

stay [II. Improved simple factory] The problem of specifying the name of associated objects is mentioned in the part. Here is a supplement. Assuming that the Computer is written by other colleagues or third-party personnel, it is not convenient for us to directly add new functions to its associated objects. At this time, we can use the extension feature of Kotlin to extend the associated objects, such as adding a function to the Computer, and judging the Computer type through the CPU model:

/**
 * Companion object extension method (default name)
 *
 * @author GitLqr
 */
fun Computer.Companion.fromCPU(cpu: String): ComputerType? = when (cpu) {
    "Core" -> ComputerType.PC
    "Xeon" -> ComputerType.SERVER
    else -> null
}

// use
val type = Computer.fromCPU(pc.cpu)

If the associated object in the Computer has been assigned the name Factory at this time, the corresponding associated object name must be used instead of the default Companion when extending the method to the associated object:

interface Computer {
    // Custom companion object name
    companion object Factory { ... }
}

/**
 * Associated object extension method (custom name)
 *
 * @author GitLqr
 */
fun Computer.Factory.fromCPU(cpu: String): ComputerType? = when (cpu) { ... }

// use
val type = Computer.fromCPU(pc.cpu)

Conclusion:

  • When extending the associated object method, if the associated object has a user-defined name, use the user-defined name for extension.
  • Whether the associated object has a user-defined name or not, it can be omitted when calling.

If the article is helpful, please click on my WeChat official account. FSA full stack operation , this will be my greatest inspiration The official account is not only Android technology, but also articles like iOS and Python. There may be some skills you want to know.

Keywords: Java Design Pattern kotlin

Added by B of W on Sun, 27 Feb 2022 17:41:15 +0200