Search code examples
swiftswift-structs

Swift Struct idiom to replace OOP Type Inheritance Pattern?


Let's say I have a program that works with Rectangles (it's a simplified example of my real problem), which I model as

struct Rectangle {
    let left:Int
    let right:Int
    let top:Int
    let bottom:Int

    func rationalized() -> Rectangle {
        return Rectangle(left: min(self.left, self, right), right: max(self.left, self.right), top: min(self.top, self.bottom)
    }
}

A rationalized rectangle, is basically one that has positive width and height. For many operations (e.g. scaled, translated, union, intersect, etc), it's nice to guarantee that the rectangle is rationalized. In fact, one could intern the rationalization logic in an init(...) so that you can't ever create one that is irrational. But then there are cases where you'd like to support irrational constructs, at least temporarily, maybe for editing during drag and drop, e.g.

      func replacing(left newValue:Int) -> Rectangle {
          return Rectangle(left: newValue, right: self.right, top: self.top, bottom: self.bottom)
      }

Wanting to do this would make it so that putting the rationalization logic in the init() would backfire.

But the alternate, is to litter my code nearly everywhere (except at the drag-drop sites) with rationalized() calls. I'm trying to determine if I could do this with structs and types somehow.

If I were using classes, I could have a Rectangle superclass, and a RationalizedRectangle subclass, where the RationalizedRectangle override the init to do the work. Then I could usually work with RationalizedRectangles (even specify that as type when appropriate), but allow editing them by using a Rectangle, and converting to a RationalizedRectangle at the end.

But Swift structs don't support inheritance. So I'm at a loss at how to idiomatically accomplish this. I could add a isRationalizing:Boolean to the struct, and branch on that, but that just seems cheesy.

Is there a struct based idiom that would work here?


Solution

  • You should be able to accomplish this with protocols. You would move the common logic into a Protocol and then create two classes that would conform to this protocol with different initializers. Then where you would refer to a particular object type, you could refer to the protocol instead.

    protocol RectangleProtocol {
        var left:Int {get}
        var right:Int {get}
        var top:Int {get}
        var bottom:Int {get}
    }
    
    struct Rectangle: RectangleProtocol {
        let left: Int
        let right: Int
        let top: Int
        let bottom: Int
    
        init(leftValue:Int, rightValue:Int, topValue:Int, bottomValue:Int) {
            self.left = leftValue
            self.right = rightValue
            self.top = topValue
            self.bottom = bottomValue
        }
    }
    
    struct RationalRectangle: RectangleProtocol {
        let left: Int
        let right: Int
        let top: Int
        let bottom: Int
    
        init(leftValue:Int, rightValue:Int, topValue:Int, bottomValue:Int) {
            self.left = min(leftValue, rightValue)
            self.right = max(leftValue, rightValue)
            self.top = min(topValue, bottomValue)
            self.bottom = max(topValue, bottomValue)
        }
    }
    
    let rectangle: RectangleProtocol = Rectangle(leftValue: 4, rightValue 4, topValue: 8, bottomValue: 8)
    let rationalRectangle: RectangleProtocol = RationalRectangle(leftValue: 4, rightValue:8, topValue: 7, bottomValue: 4)
    
    // Now both of these represent a struct that conforms to the RectangleProtocol.