Initialization of objects (OOP) in Kotlin and other JVM-based languages

We will explore a new approach to object initialization in Kotlin and other JVM-based languages. This approach is based on managing the internal state of objects using APIs defined by interfaces with default method implementations. As a reminder, the current paradigm for using default methods in interfaces is mainly to add new functionality to types without modifying the implementing classes. As you will see, in most cases, our pattern allows us to define method implementations in interfaces rather than in classes without violating OOP principles, so that private or overriding methods and fields can be, as before, defined in classes implementing those interfaces, which in turn can implement other interfaces as well. We will consider the programming style and APIs of our library that are supposed to be used to follow this approach and how our approach can be combined with the standard one in different scenarios and programming patterns. For example, it will be shown how to implement the Cake software pattern and use Spring Dependency Injection as a very simple application of our technique. 

Technically, our concept is about how we instantiate classes, implement methods and fields (i.e., where we keep the state of the object if needed), and what we get by using standard inheritance of classes. We will show how our approach based on a very simple system of rules simplifies cloning objects, creating objects, including ones extending several classes/interfaces, managing internal states of objects, adding new properties to types, or creating objects that are backed by others. Now, let's get down to the details. Below, we will start explaining it in small, simple steps, trying not to dive into the specific syntax sugar of Kotlin and clarifying it where it is necessary.

The code of a simple object, by a new pattern, is to be created as an interface with all methods with default implementation, i.e.

interface SimpleObj {
    fun create(): X = /* code */
    //... other methods
}

For instantiating, we can optionally use a class with a fixed simple name like O, which should implement this interface, i.e.

interface SimpleObj {
    fun create(): X = /* code */

    private class O: SimpleObject
}

For instantiating the object, we define functions in the companion object of this interface, which are effectively static in Kotlin. Using annotation, we can also turn it into static in the Java sense, but now we don't care about it:

interface SimpleObj {
    fun update(x: Boolean): Unit {
       /* ..*/
    }

    fun create(): X = /* code */

    private class O: SimpleObj // class O implements SimpleObj

    companion object {
        operator fun invoke(arg1: Boolean, vararg arg2: Int): SimpleObject = O().apply {
                                         // "apply" is a method for providing the object SimpleObj.O as "this" object inside "apply" block
             update(arg1)
             /* other code for setting state of object*/
        }
    }
}

As a result, using the syntax sugar of Kotlin, i.e., the operator keyword, we can instantiate our object by the following line:

SimpleObj(true)

This line calls the invoke method, which calls an empty constructor of the SimpleObj.O class and executes the initialization of the object in the body of the apply function. Now the reader can see that the complexity of instantiating objects is removed from the constructor and moved into an effective static area, in fact, using the classical Builder software pattern. Doing so allows us to simplify object instantiation, avoiding complexity if we need post-initialization. We can also create objects implementing multiple interfaces using JDK dynamic proxy or anonymous object creation syntax, i.e., by line object: SimpleObj1, SimpleObj2 {}.

At first glance, everything looks good, but we need to define the internal state of the SimpleObj object somehow. As you know, you can not define a field in an interface with its instantiation, and it is usually done in the class definition. Indeed, Kotlin's interface can have uninitialized fields, but we need to define corresponding fields in class. Evidently, we cannot use it since we will lose all advantages from our construction, i.e., we can forget about object instantiation simplicity and, in fact, multiple inheritance as well (to remember, a class can inherit multiple interfaces, but only one class). To say the truth, we can define an initialized field in a trait, Scala's interface, but it will add complexity to managing the state of such objects. Instead of it, to handle the problem, we will define a new interface, KtLazy, which contains a lot of useful functions helping to manage the internal state of an object, in fact, using a functional style. See example,

typealias Sp<T> = Function0<T> // = (() -> T)

fun illegal() = throw IllegalArgumentException()

interface SimpleAddress: KtLazy {
    fun firstName(f: Sp<String>? = null): String = calc(f) { illegal() }
    fun familyName(f: Sp<String>? = null): String = calc(f) { illegal() }
    fun middleName(f: Sp<String?>? = null): String? = calc(f) { null } 

    private class O: SimpleAddress // class O implements SimpleAddress

    companion object {
        operator fun invoke(firstName: Boolean, familyName: String, middleName: String? = null): SimpleAddress = O().apply {
             firstName{ firstName }
             familyName{ familyName }
             middleName?.let { middleName{ it } }
        }
    }
}

// For the last argument of the calc function, we use the "trailing lambda" syntax of Kotlin, which can be used if this argument is of a function type.    
// We define a default value for parameter f as null;
// this default value indicates a get operation. 
// For example, if the firstName field is not set, an execution of the "firstName()" expression throws an IllegalArgumentException.
// if f is not null, we execute updating of the corresponding field (for example, middleName).

The interface SimpleAddress inherits the predefined function calc from KtLazy, which is used for getting or setting properties. The above code is not optimized for educational purposes and looks verbose, but we will show how it can be improved in the coding guidelines section. Spoiler, we will also show that our approach is compatible with using Kotlin's property syntax (i.e., the var/val syntax) as well.

For accessing the value of a field, we use the notation ObjectName.fieldName() and for updating, ObjectName.fieldName{newValue}. For example,

fun updateMiddleNameIfNotSet(address: SimpleAddress, middleName: String = "") {
    with (address) { // setting address as "this" object to omit prefix "address." in calling the middleName method.
        val prev = middleName() // getting value of middleName

        prev ?: middleName { middleName } // if prev == null, we set the field middleName
    }
}

To summarize what the KtLazy interface is doing, it maps an instance of the SimpleAddress class to the key-value map. where key is the name of the field and value is the value of the field. So, the SimpleAddress object is backed by a Map<String, Any?> object. For this, we are using the fixed mapping Map<Any, Map<String, Any?>> (or some other approaches we will talk about further). Technically, to avoid a memory leak, we used a special version of the mapping Map<Any, Map<String, Any?>>, So, if a key is not referenced externally, it is removed from this map.

Now, it is clear how object cloning and making an object backed by another are implemented (for objects implementing KtLazy) in a simple case. Indeed, for object cloning, if we use only default methods, it is enough to recreate the object using an interface proxy and clone the key-value map from the original object. To have an object backed by another object, we need to set the same key-value map for it as for the original object.

As we promised, we'll now show you how to safely add a new field (in a new sense) to the object created by the above pattern. Indeed, it is as simple as creating an extension function for a class. See example,

fun SimpleAddress.phoneNumber(f: Sp<String?>? = null): String? = calc(f) { null }

This extension creates a new field, phoneNumber, for the SimpleAddress interface, and the safety of its use is supported by Kotlin.

To summarize what we got until now, we have considered, at first approximation, basic rules of our approach for creating flexible objects of general purpose using minimal instantiating code, with all method implementations bound to interfaces without any field!! but with an internal state backed by a Map<String, Any?> object defined by extending the KtLazy interface and using corresponding functions. However, using only default functions is not mandatory for our technology. For example, it also allows cloning instances of classes (extending the KtLazy interface), which override default and not default methods of interfaces so that the cloned object continues to use the overriding code. To avoid mistakes related to cloning, see the section "Best practices." Generally, there is no limitation on using private fields in classes as well, but they are supported only in some corner cases.

Next, we are going to consider object initialization and other questions related to using our technology.

Best practices

First, we would like to keep the information about how the object was created. The other thing is that there can be circular dependencies in the code. It can create a problem if we always use the fixed mapping Map<Any, Map<String, Any?>> to associate objects with their fields. Indeed, in this map we have to use weak references for keys and strong references for values, so using circular links can result in having keys in the values of the map, and it can prevent them from being garbage collected because this fixed mapping holds a strong reference for keys in this case. To avoid this problem and mistakes related to using our API for cloning in some corner cases, it is recommended to use the `postInit` function for object initialization:

1-st construction:

interface SimpleObj: KtLazy {
    fun value(f: Sp<Int>? = null): Int = calc(f) { 0 } // your methods can be here

    private class O: KtLazy.E(), SimpleObj // you can also override methods from interfaces here

    companion object {
        operator fun invoke(number: Int? = null) = O().postInit<SimpleObj> { //  provides the object SimpleObj as "this" inside this block
            number?.let { value { number } } // Your code should be here for the initialization of an object.
        }
        // Above we are using trailing lambda syntax since the last argument of the postInit function is of a function type.
    }
}

2-nd construction:

interface SimpleObj: KtLazy {
    fun value(f: Sp<Int>? = null): Int = calc(f) { 0 } // your methods can be here
    
    private class O: SimpleObj // you can also override methods from interfaces here

    companion object {
        operator fun invoke(number: Int? = null) = 0().postInit<SimpleObj> {
            number?.let { value { it } }  // Your code should be here for the initialization of an object.
        }
    }
}

// or

interface SimpleObj: KtLazy {
    fun value(f: Sp<Int>? = null): Int = calc(f) { 0 } // your methods can be here

    companion object {
        operator fun invoke(number: Int? = null) = object : SimpleObj {}.postInit<SimpleObj> { // you can also override methods in an anonymous object
            number?.let { value { it } } // Your code should be here for the initialization of an object.
        }
    }
}
/*
 The only difference in the last variant is that we don't define the class SimpleObj.O, 
 and instead of it, we use an anonymous object created by line "object : SimpleObj {}". 
 There are no other differences, and from the point of view of the new technique, 
 these variants are the same.
*/

As you can see, the first construction is a special case of the second, a more general one. Indeed, the main difference between the two constructions is that in the first, the class O have to extend class KtLazy.E, the ext field of which is used for keeping the backing map for the fields of the object SimpleObj (instead of the aforementioned fixed mapping between KtLazy objects and their backing maps). This way, we avoid issues related to the memory leak in case using circular dependencies. As for the second construction, we solve this problem by keeping a backing map in an object created using JDK proxy technology. So, both approaches are safe for managing the internal state of objects, as class fields are safe to use and interchangeable in most situations. However, the first approach you need to use where JDK proxy technology is not fully supported, for example, if you need to compile the code into a binary executable using GraalVM technology.

In both constructions, the postInit function memoizes its last argument (in the above example, it is defined by the expression { number?.let { value { it } } } ) into the init field of the KtLazy interface. It is used in scenarios where we need to recreate the initial object with the same parameters.

Regarding the other best practices, the reader can notice that the calc function should calculate the name of the calling function to get the name of the field. For better performance, we can explicitly define a field name in the calc function or use small unique hints (i.e., the hint argument) used in the calc function for optimizing the field name calculation. There are many approaches for automating this, so we won't elaborate on that here. This optimization can also be required for other functions (get, pp, pup) of KtLazy, which are also used for defining fields and properties. The discussion about these functions is out of scope of this article, so for details, see the project code in the github repository (the link is at the end of the article).

The other interesting question is how to use the encapsulation principle in interfaces. As it was mentioned before, our technology supports using private methods in classes. However, we can still use the encapsulation principle even in interfaces. For example, we use encapsulation for some methods in KtLazy, using the technique of extension functions defined on specific objects. This is a technique different from the classical implementation of encapsulation, but it promotes full understanding and readability of what is going on in the code. For example, we can differentiate functions, private in a new sense, by their extension objects.

Software patterns

Now we will look at how to integrate with Spring IoC technology and then talk about the Cake software pattern. For integration with Spring, we can define a configuration service bean using the annotations @Component and @Bean. See example:

interface HelloWorld: KtLazy {

	fun printer(f: Sp<Printer>? = null): Printer = calc(f) { throw IllegalArgumentException() }

	fun message(f: Sp<HelloWorldMessage>? = null): HelloWorldMessage = calc(f) { throw IllegalArgumentException() }

	fun printGreeting() { printer().print(message().text()) }
	
	private class O: HelloWorld

	companion object: Def()

    @Component
	open class Def {
		@Bean("helloWorld")
		open operator fun invoke(msg: HelloWorldMessage, printer: Printer) = O().postInit<HelloWorld> {
					message { msg }
					printer { printer }
				}
	}

}

// or

interface HelloWorld: KtLazy {

	fun printer(f: Sp<Printer>? = null): Printer = calc(f) { throw IllegalArgumentException() }

	fun message(f: Sp<HelloWorldMessage>? = null): HelloWorldMessage = calc(f) { throw IllegalArgumentException() }

	fun printGreeting() { printer().print(message().text()) }

    @Component
	open class Def {
		@Bean
		open fun helloWorld(msg: HelloWorldMessage, printer: Printer) = object: HelloWorld {}.postInit<HelloWorld> {
					message { msg }
					printer { printer }
				}
	}

} 
// This version can be used if we are not going to use the invoke method in
// the companion object defined in the first version of the code

By the above code, a Spring IoC container should create a bean with the name helloWorld by calling the function helloWorld/invoke with arguments msg, printer, which were previously created by Spring as well, i.e., the helloWorld bean should be created with dependencies injected by a Spring IoC container.

The next paragraph is devoted to the Cake software pattern, a pattern that originated in Scala and is used to inject service dependencies without the help of any specific framework like Guice or Spring. The beginning of the next paragraph can be omitted for most readers, at least until we present our variant of the Cake pattern for Kotlin, as we don't explain in depth the implementation details of the Cake pattern in Scala.

As a reminder, the main part of the Cake pattern is a registry component, i.e., an interface (with its implementations) with fields keeping dependencies, service objects, and which implements all other abstract components; every component (a) contains a field with its own service object, (b) defines their dependencies, other service objects, as provided using the specific syntax of Scala (a self-type annotation), and (c) contains their own service implementations in its internal classes, so they (these service implementations) can access their service dependencies in the outer classes, i.e., in their components. A registry object implements all components, providing implementations for all service interfaces; for that, it can use components' service implementations defined as their internal classes, or, in tests, it can use alternative implementations.  Using our technique, we can simplify the Cake pattern in Kotlin as the following:

  1. A registry interface implements all other abstract components.
  2. Every component defines abstract methods to return service object dependencies for its own service object. In fact, it inherits these methods from service holder interfaces; every one of them contains only one method returning its own service object.
  3. Every service interface implements its (own) component by delegation to an injected (own) component.
  4. A registry interface instantiates its fields, corresponding methods, injecting itself, as a registry object, into service objects.

Regarding condition 3, we can refuse component inheritance, but we are going to stick to it because it is convenient. Indeed, in this case, every service doesn't have to access service dependencies through its own component object. Below is an example of a Cake pattern implementation in Kotlin with three components, each of which depends on two others:

interface AComponent: BHolder, CHolder
interface AHolder: KtLazy {
	fun aService(): AService
}

interface AService: AComponent {
	fun AService.reg(f: Sp<AComponent> ? = null): AComponent = calc(f) { throw IllegalArgumentException()}
	override fun bService(): BService = reg().bService()
	override fun cService(): CService = reg().cService()

	//your service methods

	private class O: AService

	companion object {
		operator fun invoke(r: AComponent) = O().postInit<AService>{
			reg {r}
		}
	}
}


interface BComponent: AHolder, CHolder
interface BHolder: KtLazy {
	fun bService(): BService
}

interface BService: BComponent {
	fun BService.reg(f: Sp<BComponent> ? = null): BComponent = calc(f) { throw IllegalArgumentException()}
	override fun aService(): AService = reg().aService()
	override fun cService(): CService = reg().cService()

	//your service methods

	private class O: BService

	companion object {
		operator fun invoke(r: BComponent) = O().postInit<BService>{
			reg {r}
		}
	}
}

// CService dependencies are defined here
interface CComponent: BHolder, AHolder
interface CHolder: KtLazy {
	fun cService(): CService
}

interface CService: CComponent {
	fun CService.reg(f: Sp<CComponent> ? = null): CComponent = calc(f) { throw IllegalArgumentException()}
	override fun aService(): AService = reg().aService()
	override fun bService(): BService = reg().bService()

	//your service methods

	private class O: CService

	companion object {
		operator fun invoke(r: CComponent) = O().postInit<CService>{
			reg {r}
		}
	}
}


interface Registry: AComponent, BComponent, CComponent {
	override fun aService(): AService = get { AService(this) }
	override fun bService(): BService = get { BService(this) }
	override fun cService(): CService = get { CService(this) }
	// The get function is used the same way as the calc function, but it keeps the result of its first invocation. 
	// The calc recalculates it until the result is explicitly set. 

	private class O: Registry

	companion object {
		operator fun invoke() = O().postInit<Registry>()
	}
}

Note that all services defined above are lazy-initialized. To add dependency for YService from XService, it is enough to add a line override fun xService(): XService = get { XService(this) } to YService's definition and change a definition of YComponent into interface YComponent:..., XHolder to implement XHolder interface. But we can go further and use standard property syntax to access service dependencies:

interface Registry: AComponent, BComponent, CComponent {
    override val aService get() = get { AService(this) }
    override val bService get() = get { BService(this) }
    override val cService get() = get { CService(this) }

    private class O: Registry

    companion object {
        operator fun invoke() = O().postInit<Registry>()
    }
}

interface AComponent: KtLazy, BHolder, CHolder
interface AHolder {
    val aService: AService
}

interface AService : AComponent {
    fun AService.reg(f: Sp<AComponent>? = null): AComponent = calc(f) { throw IllegalArgumentException() }
    override val bService get() = reg().bService
    override val cService get() = reg().cService

    //your service methods

    private class O: AService

    companion object {
        operator fun invoke(r: AComponent) = O().postInit<AService> {
            reg { r }
        }
    }
}


interface BComponent: KtLazy, AHolder, CHolder
interface BHolder {
    val bService: BService
}

interface BService: BComponent {
    fun BService.reg(f: Sp<BComponent>? = null): BComponent = calc(f) { throw IllegalArgumentException() }
    override val aService get() = reg().aService
    override val cService get() = reg().cService

    //your service methods

    private class O: BService

    companion object {
        operator fun invoke(r: BComponent) = O().postInit<BService> {
            reg { r }
        }
    }
}

// CService dependencies are defined here
interface CComponent: KtLazy, BHolder, AHolder
interface CHolder  {
    val cService: CService
}

interface CService : CComponent {
    fun CService.reg(f: Sp<CComponent>? = null): CComponent = calc(f) { throw IllegalArgumentException() }
    override val aService get() = reg().aService
    override val bService get()= reg().bService

    //your service methods

    private class O: CService

    companion object {
        operator fun invoke(r: CComponent) = O().postInit<CService> {
            reg { r }
        }
    }
}

In the example above, we used the standard Kotlin getter syntax to define a val field and refactored a component to implement the KtLazy interface directly, not through a holder interface. Finally, you can notice that the component part of the classical Cake pattern (in our case, it is a XComponent) holds a reference to its own service (XService), but in our variant of the Cake pattern we don't have to do it, so a component, in fact, holds only dependencies of its own service, so XComponent can be renamed into XDependencies.

Now assume that we have two service beans (AService and BService) injected by the Spring IoC container or the Cake software pattern, and we have a third service (CService), which heavily depends on them, so it is reasonable that the third service should implement them, but, in this case, we have to inject all their service dependencies into the third, and, generally, it can be a difficult, error-prone task. See an example of how our technique can solve this problem:

interface CService: AService, BService {
 
   //your methods

   class O(): CService

   companion object {
	 operator fun invoke(
		a: AService,
		b: BService
	 ) = O().postInit<CService>{
		  a.init()?.invoke(this)
		  b.init()?.invoke(this)
		}
   }
}

As you see, we easily inject all dependencies applying initialization code which was previously kept in the init fields for AService and BService. It is simple, but there is a caveat of the approach. Before using this technique, you should check that all dependencies are eagerly injected into AService and BService, otherwise you risk creating CService not fully initialized. The last can happen in case of having circular dependencies, so we have to initialize some dependencies lazy. However, even in this case you can use the above technique by wrapping lazy dependencies into eager ones i.e. using wrappers which have an internal state initialized lazy and can be easily injected into CService.

Coding guidelines

Our goal is not to replace the standard Kotlin syntax sugar, like data class or the val/var syntax, but to show how our technique can take advantage of these constructions and how to write non-repetitive code even if we have to realize something like data class for some reasons. As a reminder, a data class is used to define POJO objects. Below is the definition of a data class with a typical scenario of its instantiation:

//Code snippet 1

// pojo definition
data class PojoExample(
   val field1: Type1 = defaultValue1,
   var field2: Type2 = defaultValue2, 
   //... other fields
   val fieldN: TypeN = defaultValueN,
)

// instantiation scenario looks like
fun functionX(...) {
    ...
    val field1 = calculate1()
    var field2: Type2  = run { /* some calculation */}
    // ...
    // finally, we intantiate PojoExample:
    PojoExample(field1, field2, ..., fieldN = calculateN())
    //...
}

The direct equivalent, using our technique, would be:

//Code snippet 2

// verbose equivalent of pojo definition
interface PojoExample: KtLazy(
   fun field1(f: Fn<Type1>? = null): Type1 = get(f) {defaultValue1}
   fun field2(f: Sp<Type2>? = null) = calc(f) {defaultValue2} // some type can be omitted
   //... other fields
   fun fieldN(f: Fn<TypeN>? = null): Type2 = get(f) {defaultValueN}
   
   class O: PojoExample
   
   companion object {
       operator fun invoke(
          field1: Type1 = defaultValue1,
          field2: Type2 = defaultValue2, 
          // ... other fields
          fieldN: TypeN = defaultValueN,
          ) = O().initPost<PojoExample> {
              field1{field1}
              field2{field2}
              ...
              fieldN{fieldN}
          }
   }
   
)

// The instantiation scenario doesn't change, so the last part would be:
...
    PojoExample(f1Value, f2Value, ..., fieldN =calculateN())
...

In the first scenario, we repeat field names no more than twice, but using a direct approach, we can repeat field names up to four times! So, you can stop reading here...  If you are still here, we will demonstrate approaches to handling it. Below is the code for the optimized approach:

//Code snippet 3

// The optimized equivalent of the pojo definition
interface PojoExample: KtLazy(
   fun field1(f: Fn<Type1>? = null): Type1 = get(f) {defaultValue1}
   fun field2(f: Sp<Type2>? = null) = calc(f) {defaultValue2} 
   ..
   fun fieldN(f: Sp<TypeN>? = null): Type2 = get(f) {defaultValueN}
   
   class O: PojoExample
   
   companion object {
       operator fun invoke(f: PojoExample.() -> Unit) = O().initPost<PojoExample> {f()}
   }
   
)

// The instantiation scenario looks like
fun functionX(...) {
    //...
    // intantiation of PojoExample
    PojoExample {
       field1 { calculate1() }
       field2 { 
            run { /* some calculation */}
       }
       // ...
       fieldN { calculateN() }
    }
    //...
}

As you can see above, the code is much more readable now and not verbose, even compared with the standard approach. It works especially well if all fields have default values (by the way, this is the approach used in the protobuf protocol of version 3). If not, we can add additional validation at the end of the last argument of the initPost function using trailing lambda syntax, i.e.,

initPost<PojoExample> {
      f() 
      validate()
   }

We now examine the scenario where we need to create our "pojo interface" out of a data class.

//Code snippet 4

data class DataPojo(
   val field1: Type1 = defaultValue1,
   var field2: Type2 = defaultValue2, 
   //... other fields
   val fieldN: TypeN = defaultValueN,
)

interface InterfacePojo: KtLazy(
   fun InterfacePojo.data(f: Sp<DataPojo>? = null) = calc(f) { illegal() }
   fun field1() = get {data().field}
   fun field2(f: Sp<Type2>? = null) = calc(f) {data().field2} 
   ..
   fun fieldN() = get {data().fieldN}
   
   class O: InterfacePojo
   
   companion object {
       operator fun invoke(dataPojo: DataPojo) = O().initPost<InterfacePojo> {
                                            data {dataPojo.copy()}
                                         }
   }
   
)

As you see, DataPojo and InterfacePojo can be safely refactored independently. The alternative approach is, first, to convert DataPojo into Map<String, Any?> and then to initialize the InterfacePojo object by that map. The invoke function InterfacePojo could be:

//Code snippet 5
   
   companion object {
       operator fun invoke(dataPojo: DataPojo) = O()
                                  .initPost<InterfacePojo> {
                                            map().putAll(toMap(dataPojo))
                                  } 
                                  // the map function defined in KtLazy to extract properties as a map object 
                                  // the toMap function is a user-defined function using standard lib for converting Pojo into Map
   }

The above approach is not so safe but can be used if needed. If we use only KtLazy objects, we can easily update fields of one object by the values of another or set one object to be backed by another one:

firstKtLazyObject.of(secondKtLazyObject) // firstKtLazyObject gets all fields from secondKtLazyObject.
firstKtLazyObject.by(secondKtLazyObject) // firstKtLazyObject is backed by secondKtLazyObject, so changes in firstKtLazyObject are propagated to secondKtLazyObject.

Evidently, the above function can also be used for initializing the KtLazy objects in the initPost function, but we won't dive into details here.

Now, as we promised, we will show how we can use the val/var syntax with our approach. See the next two code snippets:

// Code snippet 6 (equivalent to Snippet 2), i.e., if we have to stick to the straightforward approach

interface PojoExample: KtLazyExt{
   val field1: Type1 get() = getter()
   var field2: Type2 get() = getter(); set() = setter()
   ..
   val fieldN: TypeN get() = getter()
   
   class O: PojoExample
   
   companion object {
       operator fun invoke(
          field1: Type1 = defaultValue1,
          field2: Type2 = defaultValue2, 
          // ... other fields
          fieldN: TypeN = defaultValueN,
          ) = O().initPost<PojoExample> {
              set(::field1, field1)
              this.field2 = field2
              ...
              set(::fieldN, fieldN)
          }
   }
   
}

// Code snippet 7 (equivalent to Snippet 3),
// an optimized equivalent of a pojo definition
interface PojoExample: KtLazyExt(
   val field1: Type1 get() = getter {defaultValue1}
   var field2: Type2 get() = getter(); set() = setter()
   ..
   val fieldN: TypeN get() = getter()
   
   class O: PojoExample
   
   companion object {
       operator fun invoke(f: PojoExample.() -> Unit) = O().initPost<PojoExample> {f()}
   }
   
)

// The instantiation scenario looks like
fun functionX(...) {
    //...
    // intantiation of PojoExample
    PojoExample {
       set(::field1, calculate1()) // The "val" field can be updated by the "set" function, and it should be done only in initializing blocks.
       field2 = run { /* some calculation */}
       // ...
       set(::fieldN, calculateN()) // The type of expression "calculateN()" is also validated here.
    }
    //...
}

In the examples above, we took advantage of the syntax sugar of Kotlin for fields, the get and set syntax, which are used to define custom accessors for properties. The above KtLazyExt interface implements KtLazy to add the convenience methods set, getter, and setter, which are customized to use with custom accessors. A snippet of the working code can be found here.

Before, we demonstrated how dependencies can be injected by a Spring IoC container into KtLazy objects. Using custom accessors, a definition of a Spring bean can now look like this:

// Code snippet 8 (equivalent to Snippet 2) for injecting dependencies by a Spring IoC container,
// if we have to stick to the straightforward approach

interface ServiceX: KtLazyExt{
   val service1: Type1 get() = getter()
   val service2: Type2 get() = getter()
   ..
   val serviceN: TypeN get() = getter()
   
   // your methods, which you can have a lot!
  
   @Component
   class Def {
       @Bean
       fun serviceX(
          t1: Type1, // Args are injected by Spring, so we don't need to use the full names of services for argument names.
          t2: Type2, 
          // ... other fields
          tN: TypeN,
          ) = object: ServiceX {}.initPost<ServiceX> {
              set(::service1, t1) // a relatively small boilerplate code, which is validated by types
              set(::service2, t2) // Indeed, if we define an interface and its implementation, a spring component, by classical way, 
                                  // we have to write code for two types and duplicate all fields and method declarations!
                                  // However, we can go further; see the next code snippet in the article.
              ...
              set(::service3, t3)
          }
   }
   
)

As you can see in the above example, we used some repetitive boilerplate code, and generally, this approach is convenient, but it can also be combined with the following technique:

// Code snippet 9 (equivalent to Snippet 2) for injecting dependencies by a Spring IoC container,
// i.e., using the straightforward approach but with an optimized technique

interface ServiceX: KtLazyExtService{
   val service1 get() = init<Type1>()
   val service2 get() = init<Type2>()
   //..
   val serviceN get() = init<TypeN>()
   
   // your methods, which you can have here a lot!
  
   @Component
   class Def {
       @Bean
       fun serviceX(
             beanFactory: BeanFactory
          ) = object: ServiceX {}.initPost<ServiceX> {
              Private.factory { beanFactory } 
              // "Private" is a singleton object used to hide the 'factory' method in the ServiceX interface.
          }
   }
   
}

In the above, we use the KtLazyExtService to add the convenience method init and the factory field, which are used for setting spring beans into a ServiceX object by their types. A snippet of the working code can be found here.

As demonstrated, our approach is compatible with the property syntax of Kotlin and even takes advantage of it, making code more expressive and short; all helper functions and interfaces used in the above examples can be customized or redefined if needed, giving freedom for flexibility but hiding implementation detail complexity.

Miscellaneous considerations

As you can see, the essence of our approach is about managing internal state using a functional approach, sticking to the default functions defined in interfaces along with some other simple rules, which can be used with inheritance, composition, and other technique as we do in most high-level programming languages. Talking about the paradigm of the new approach, it is interesting to distinguish between the technical implementation of the new approach, which has already been discussed, and how we came to this technique. The initial goals were around further developing the Opt2Code language, a language concept for creating DSL languages, and that was time-consuming, so I felt I didn't have the capacity for it. Considering alternative approaches, I realized that there is a natural embedding of the Opt2Code language into the Kotlin language. Indeed, omitting details, each DSL dialect created by the Opt2Code language rules ideally corresponds to the default methods of an interface, and a script written by that dialect corresponds to the method call tree of that interface. So, the initial purpose of this approach is to embed the Opt2Code language into the Kotlin language and take advantage of the Kotlin eco-system (which includes the Java eco-system!). You can find more information about the Opt2Code language here.

Let's examine the possible uses of this pattern when writing bash scripts. Comparing bash commands and top-level bash scripts, every bash command with a set of arguments can be implemented as a low-level interface with default functions, and a top-level bash script executed with different sets of arguments can be implemented as a top-level interface depending on the low-level ones. These interfaces can be used in the DevOps domain for infrastructure management. Additionally, distracting from the above analogy, the technology appears to be useful for managing infrastructure as code (IaC).

Finally, considering the future, we can introduce a new concept: code as an infrastructure (CaI)! The latter implies that we write code that generates code for infrastructure as a code (IaC) management. Indeed, it seems that we can use the new technology to develop new advanced libraries for generating code for Terraform, Helm, or even for high-level languages like Java and Kotlin. As a result, it could potentially make it easier for developers to use specific programming languages, so we don't have to learn specific languages but libraries.

The code for using our technique as well as examples of its basic application are available here.

© 2024 metadatalang.com aka alexn0. All Rights Reserved.