Search code examples
kotlin

Implementation problem of an interface in Kotlin


I have a question about dealing with interfaces. I get an error when I try to use an interface in this way.

When I try to implement the interface this way, I get the error that the setter cannot be marked as "private". How can you implement that a variable is in an interface, can be changed with methods and the variable itself cannot be changed from the outside when implemented in a class?

It would be possible to declare the variable with "val" in the interface and then adopt it as "var" in the class, but then no standard implementation of the methods in the interface is possible.

If this is not possible in principle, what is a correct solution approach that is OOP compliant?

Many thanks for your help!

interface Product{
    var count: Int

    fun add(i: Int) {
        count += 1
    }

    fun remove(i: Int) {
        count -= i
    }
}

class Salami(count: Int) : Produkt {
    override var count: Int = count
        private set // Error
}

Solution

  • Good question! In principle, the visibility modifier you're looking for is called protected.

    // NOTE: Non-working code; see below for explanation
    interface Product {
        var count: Int
            protected set
    
        fun add(i: Int) {
            count += i
        }
    
        fun remove(i: Int) {
            count -= i
        }
    }
    
    class Salami(count: Int) : Product {
        override var count: Int = count
            protected set // Error
    }
    

    If this worked, it would say "count can be read publicly but can only be assigned from Product and its subclasses", which sounds like what you want.

    Unfortunately, the JVM (and Kotlin by extension) doesn't allow the protected modifier inside of interfaces. The reasons for this have been discussed in other places, but regardless of the reasons, it won't work right now, which makes life difficult for the exact design pattern you're trying to write.

    So we're left resorting to older Java-style patterns. You could always make your interface an abstract class (with all of the limitations that come with that).

    abstract class Product(count: Int) {
        var count: Int = count
          private set
    
        fun add(i: Int) {
            count += i
        }
    
        fun remove(i: Int) {
            count -= i
        }
    }
    
    class Salami(count: Int) : Product(count) {}
    

    But now callers have to extend a class rather than implement an interface, which restricts them to single-inheritance.

    You could instead use the abstract implementation pattern, which was used by standard Java libraries like Swing before we had default implementations in interfaces. With this pattern, your interface Product is completely abstract and has no default implementations. But then you provide an abstract class AbstractProduct which implements all of the methods neatly. Callers are still free to implement the methods themselves if they need to extend another class, but most callers will just extend AbstractProduct. In your case, that would look like this.

    interface Product {
        val count: Int
        fun add(i: Int): Unit
        fun remove(i: Int): Unit
    }
    
    abstract class AbstractProduct(count: Int) : Product {
        // Note: final modifier is required here since we have a private
        // setter, which cannot be open.
        final override var count: Int = count
          private set
    
        override fun add(i: Int) {
          count += i
        }
    
        override fun remove(i: Int): Unit {
          count -= i
        }
    
    }
    

    Now most applications would look like this.

    class Salami(count: Int) : AbstractProduct(count) {}
    

    But if Salami needed to extend Meat or some other class, it could instead do the work itself like so.

    class Salami(count: Int) : Meat(), Product {
        final override var count: Int = count
          private set
    
        override fun add(i: Int) {
          count += i
        }
    
        override fun remove(i: Int): Unit {
          count -= i
        }
    }
    

    So users can still get the full generality if needed, but in the simple case, everything will just work. It's not perfect, but it gets close to what you want. Just remember that functions taking a product should always take Product, not AbstractProduct, as the latter is an implementation detail.

    Composition over Inheritance

    Finally, you could always favor composition over inheritance. This count setup you've got is sort of its own encapsulated subsystem, so we could make that explicit.

    
    class ProductCount(initialValue: Int) {
    
        var value: Int = initialValue
            private set
    
        fun add(i: Int): Unit {
          value += i
        }
    
        fun remove(i: Int): Unit {
          value -= i
        }
    
    }
    
    interface Product {
      val productCount: ProductCount
    }
    
    class Salami(count: Int) : Product {
      override val productCount = ProductCount(count)
    }
    

    Now only ProductCount can ever modify the count. Not even Product can do it, aside from the add and remove methods provided to it. We've decoupled a product's count from the product itself. And, if you want, you can even provide delegator methods on product which call the same methods on ProductCount, to make the API look even more like your original one.

    interface Product {
      val productCount: ProductCount
    
      fun add(i: Int): Unit = productCount.add(i)
      fun remove(i: Int): Unit = productCount.remove(i)
      val count: Int
          get = productCount.value
    }
    

    Now your public API is exactly what you wanted it to be. You've got a count integer variable which cannot be modified from the outside, and you've got add and remove methods that do modify it. The fact that all of this is encapsulated inside a ProductCount class is fairly well hidden from the users of your API.