design pattern to express that "container" over derived class is derived from container of a base class

Viewed 110

I have a class Track which holds a set of Points and represent person location in time. In order to get this result I run an iterative optimization routine combining different data. In order to do it I extend Point with a class OptimizedPoint which holds data for optimization for this point and current value. I also introduce OptimizedTrack which is a collection of OptimizedPoints and additional data needed for optimization associated with the whole track.

I run an optimization on OptimizedTrack and at the last iteration, I return to the user clean data (Track) which only has the result and doesn't have additional data. However, I can not find a way to express with OOP that OptimizedTrack is somehow an extension of the Track and introduce common routines for them. F.e getting a length of the track which should be available for both of them as it only uses data which can be found both in OptimizedTrack and Track

Right now I have such architecture Point{x, y, z}, OptimizedPoint extends Point {additional_data}. Track {array<Point>}, OptimizedTrack {array<OptimizedPoint>, additional_data}. I don't understand how to express that OptimizedTrack is an extension of Track as I can not express array<OptimizedPoint> extens array<Point>. So I can not introduce a common routine length which can be calculated for array and therefore also from array.

I do not insist on my current architecture. It's most probably wrong, I only write it here to express the problem I am facing. How would you propose to refactor my architecture in this case?

3 Answers

I believe that the basic premise of what you are trying to do is faulty if you are following what is considered to be proper use of inheritance to express subtyping relationships.

Inheritance can be used for various purposes and I am not wishing to pontificate upon the subject, but the opinion of most authorities is that inheritance is best and most safely used when used for subtyping. In short, an instance of a subclass should be able to be substituted for an instance of its base class without "breaking" the program (see: Liskov Substitution Principle).

Let us assume that OptimizedPoint is a subtype of Point. Then all the methods defined in class Point when invoked on an instance of OptimizedPoint will continue to function as expected. That means that OptimizedPoint cannot require any more stringent preconditions on any of these method invocations nor can it weaken any of the promissed postconditions that the contract Point has made.

But it is a common fallacy that just becuase OptimizedPoint is a subtype of Point that a container of OptimizedPoint, i.e. OptimizedTrack, is a subtype of a container of Point, i.e Track. This is because you cannot substitute an instance of OptimizedTrack for an instance of Track (due to your not being able to add an instance of Point to an instance of OptimizedTrack).

So, if you are trying to follow "good object-oriented design principles", it is disastrous trying to somehow make OptimizedTrack a subclass of Track, because it can certainly never be a subtype. You can, of course, reuse Track to build OptimizedTrack using composition, i.e. OptimizedTrack would contain within an instance of Track to which methods such as length would be delegated.

I'm not sure why you want to return a Track to your client code after the optimisation process, considering that OptimizedTrack is a Track itself. Below is a quick example of what I think you're trying to achieve (written in Kotlin because is less verbose).

You can achieve a lot more flexibility and solve the type issue if you consider Track to be an iterable object of points of type Point. This way, when you extend OptTrack from Track, you will be able to:

  1. Substitute Track and OptTrack with no problem (even if your optimised track object has not computed a simplified Track object).
  2. Simplify through optimize and return a Track from OptTrack with no issues (the optimize function on Point is irrelevant, you can return an OptPoint inside your Track because it extends the object Point)
    open class Point(val x: Int, val y: Int, val z: Int) {
        override fun toString(): String = 
            "Point(${this.x}, ${this.y}, ${this.z})"
    }

    data class OptPoint(val point: Point, val additional: Int): 
        Point(point.x, point.y, point.z) {
        
        override fun toString(): String = 
            "OptPoint(${this.point}, ${this.additional})"
            
        fun optimize(): Point {
            return Point(this.x, this.y, this.z)
        }
    }

    open class Track(private val points: Iterable<Point>): Iterable<Point> {
        
        override operator fun iterator(): Iterator<Point> {
            return this.points.iterator()
        }
        
        override fun toString(): String = 
            "Track(${this.points})"
    }

    data class OptTrack(private val points: Iterable<OptPoint>): Track(listOf()) {
        
        override operator fun iterator(): Iterator<Point> {
            return this.points.iterator()
        }
        
        fun optimize(): Track {
            return Track(this.points.map{ it.optimize() })
        }
    }

    fun main(args: Array<String>) {
        val track: Track = OptTrack(listOf(
                 OptPoint(Point(1, 2, 3), 4))).optimize()
        println(track)        
        // Track([Point(1, 2, 3)])
        val other: Track = OptTrack(listOf(OptPoint(Point(1, 2, 3), 4)))
        println(other)
        // OptTrack(points=[OptPoint(Point(1, 2, 3), 4)])
    }

In OOP you should prefer object composition to object inheritance. In your problem, I think creating interfaces for point and track could help. In order to achieve the proper result, I think, you should create two interfaces, IPoint & ITrack. Both Track and OptimizedTrack implement the ITrack interface and for common operations, you could create another class that both classes delegate the requests to it. After that you could create an strategy class, taking in an ITrack and returns another optimized ITrack. In the ITrack you could create GetPath which returns a list of objects of type IPoint.

Related