3

I have several classes that implement an abstract class "Product". To follow the Open-Closed-Principle I would like the implementation classes in a way that they register themselves in a ProductRegistry singleton object. When adding new Product classes, it should be sufficient to write them or put them in the class path without having to touch the ProductRegistry class.

Below are my first steps but it seems that the objects are lazy-initialized. Can I change that? Or should I use a init-block in every subclass? (didn't like that approach as it's easy to forget).

 abstract class Product {
    val id : Int
 
    constructor(id : Int) {
        println("ctor with id=$id")
        this.id = id
        ProductRegistry.register(this)
    }
    abstract fun getColor(): String
 }

object BrownProduct : Product(2) {
    override fun getColor() = "brown"
}

object ProductRegistry {
    private val registry = HashSet<Product>()

    fun register(product: Product) {
        println("Registering: $product")
        registry.add(product)
    }

    fun fromId(id: Int): Product {
        return registry.filter { it.id == id }.first()
    }
}

Test:

class TestProdFac {
    @Test
    fun test1() {
        // println(RedProduct) <- if called before, then test works
        assertEquals(RedProduct::class, ProductRegistry.fromId(1)::class)
    }
}
Yogesh Umesh Vaity
  • 41,009
  • 21
  • 145
  • 105
lathspell
  • 3,040
  • 1
  • 30
  • 49

1 Answers1

2

Unfortunately, I don't think this is possible without some extra work. The main possibilities I can think of are:

  1. Use the standard ServiceLoader provided by the Java standard library. This is the official way to do things, but will require either creating a manifest file listing implementations (docs here) or specifying implementations in a Java 9 module-info, which I don't think Kotlin supports. To avoid this, you can use Google's auto service and kapt, allowing you to simply annotate your implementations to have service metadata generated at compile-time.

  2. You could manually scan the classpath for all implementations. This has various other downsides: More work to implement, requires assumptions to be made about the classpath, and slower (since you need to find and check every class). There are other questions that cover this method in detail, such as this one.

  3. Write your own Kotlin compiler plugin that generates service metadata, and use in tandem with the first option. This would require the most work to set up, but the least effort to use. Unfortunately, as far as I know there isn't a lot of good information on compiler plugins - your best bet would probably be to examine the source code of an official compiler plugin - like allopen.

Ultimately, I would recommend using ServiceLoader, kapt, and auto service, as described in the first point. It's relatively straightforward to set up - if you're using Gradle, you just need to add the kapt plugin and auto service dependency (Only needed at compile time), and then use the @AutoService annotation on your implementations. It also doesn't have the downsides of the second option.

apetranzilla
  • 5,331
  • 27
  • 34