2

I'm implementing a model:

  • It has structs ClientSummary and ClientDetails
  • ClientDetails struct has all properties of ClientSummary struct + some extra properties
  • Both structs have main initializer init(jsonDictionary: [String: Any])
  • inits of ClientSummary and ClientDetails share big part of the code
  • There is an extension which will work with shared functionality of those structs.

The most straightforward solution which came to my mind is just classic inheritance, but it doesn't work for value types.

I'm trying to solve that with protocols, but I can't implement those "shared inits". I was trying to move shared part of the init to the protocol extension but can't really make it. There are various errors.

Here is the test code.

protocol Client {
    var name: String { get }
    var age: Int { get }
    var dateOfBirth: Date { get }

    init?(jsonDictionary: [String: Any])
}


struct ClientSummary: Client {
    let name: String
    let age: Int
    let dateOfBirth: Date

    init?(jsonDictionary: [String: Any]) {
        guard let name = jsonDictionary["name"] as? String else {
            return nil
        }
        self.name = name
        age = 1
        dateOfBirth = Date()
    }
}

struct ClientDetails: Client {
    let name: String
    let age: Int
    let dateOfBirth: Date
    let visitHistory: [Date: String]?

    init?(jsonDictionary: [String: Any]) {
        guard let name = jsonDictionary["name"] as? String else {
            return nil
        }
        self.name = name
        age = 1
        dateOfBirth = Date()
        visitHistory = [Date(): "Test"]
    }
}

extension Client {
    // A lot of helper methods here
    var stringDOB: String {
        return formatter.string(from: dateOfBirth)
    }
}
2
  • Why don't you just use classes? What property of structs do you need so badly to sacrifice inheritance for it? Commented Aug 24, 2017 at 18:36
  • @DávidPásztor big part of the model does exist already in the production environment. Structs vs classes was out choice as it was more sense for us to use copies with vs references for independent state. Commented Aug 24, 2017 at 21:07

3 Answers 3

4

Inheritance is the wrong tool here. It doesn't make sense to say "details IS-A summary." Details are not a kind of summary. Step away from the structural question of whether they share a lot of methods, and focus on the essential question of whether one is a kind of the other. (Sometimes renaming things can make that true, but as long as they're "summary" and "detail" it doesn't make sense to inherit.)

What can make sense is to say that details HAS-A summary. Composition, not inheritance. So you wind up with something like:

struct ClientDetails {
    let summary: ClientSummary
    let visitHistory: [Date: String]?

    init?(jsonDictionary: [String: Any]) {
        guard let summary = ClientSummary(jsonDictionary: jsonDictionary) else {
            return nil
        }
        self.summary = summary
        visitHistory = [Date(): "Test"]
    }

    // You can add these if you need them, or to conform to Client if that's still useful.
    var name: String { return summary.name }
    var age: Int { return summary.age }
    var dateOfBirth: Date { return summary.dateOfBirth }
}
Sign up to request clarification or add additional context in comments.

5 Comments

Thanks Rob. Sorry, I forgot to mention - I refactor model for big existing project. ClientDetails is already implemented and is used in loads of places. I don't think I'll have a permission to touch it (at least its interface). I'm implementing smaller ClientSummary struct and thought that I probably can generalise common properties/init into some protocol.
Rather than that - your point is 100% valid. But unfortunately I'm not designing this model from scratch and I have to act inside my project constraints. Can you see any other solution than composition here? As I mentioned earlier - I can't change interface of the ClientDetails struct..
My answer wouldn't change the interface of ClientDetails. It has the same interface it did before (if you add the "you can add these if you need them" methods). If you can't modify the implementation, then inheritance wasn't going to work, either (in fact, if you can't touch ClientDetails.init, I'm not sure what problem you're trying to solve).
I don't know if it's me being dumb or what, but took me a while to realize ISA isn't some class name or some abbreviation, it's just you trying to make us read "is a" out loud. Same for HASA, it's "has a". Maybe putting a space in between would diffuse the vagueness.
Sorry about that. Jargon is dangerous. They are often written "is-a" and "has-a" and I should have included the hyphen (will fix).
1

I often wish that Swift had a built-in way to separate out parts of init methods. However, it can be done, admittedly somewhat awkwardly, with tuples, as below:

struct S {
    let foo: String
    let bar: Int
    let baz: Bool

    init() {
        (self.foo, self.bar, self.baz) = S.sharedSetup()
    }

    static func sharedSetup() -> (String, Int, Bool) {
        ...
    }
}

In your case, the sharedSetup() method can be moved to the protocol extension, or wherever it's convenient to have it.

3 Comments

Thanks. My structs have way more than 3 properties. I'd say more than 10 properties in ClientSummary class. I think this sharedSetup() method with such large tuple will look like a temporary hack.
Yeah, I hear that. What I actually did in a similar case was to make a private struct containing all the properties initialized by sharedSetup(), and have it return that.
Thanks, Charles Srstka, this method help me with a question I had: stackoverflow.com/questions/64942461/…
0

For structs you can use composition instead of relying on inheritance. Let's suppose you already have ClientSummary struct defined with the Client protocol:

protocol Client {
    var name: String { get }
    var age: Int { get }
    var dateOfBirth: Date { get }

    init?(jsonDictionary: [String: Any])
}


struct ClientSummary: Client {
    let name: String
    let age: Int
    let dateOfBirth: Date

    init?(jsonDictionary: [String: Any]) {
        guard let name = jsonDictionary["name"] as? String else {
            return nil
        }
        self.name = name
        age = 1
        dateOfBirth = Date()
    }
}

Now to create ClientDetails sharing ClientSummary logic you can just create a ClientSummary property in ClientDetails. This way have the same initializer as ClientSummary with your additional type specific logic and with use of dynamicMemberLookup you can access ClientSummary properties on ClientDetails type:

@dynamicMemberLookup
struct ClientDetails {
    var summary: ClientSummary
    let visitHistory: [Date: String]?

    init?(jsonDictionary: [String: Any]) {
        guard let summary = ClientSummary(jsonDictionary: jsonDictionary) else {
            return nil
        }
        self.summary = summary
        visitHistory = [Date(): "Test"]
    }
    
    subscript<T>(dynamicMember path: KeyPath<ClientSummary, T>) -> T {
        return summary[keyPath: path]
    }
    
    subscript<T>(dynamicMember path: WritableKeyPath<ClientSummary, T>) -> T {
        get {
            return summary[keyPath: path]
        }
        set {
            summary[keyPath: path] = newValue
        }
    }
    
    subscript<T>(dynamicMember path: ReferenceWritableKeyPath<ClientSummary, T>) -> T {
        get {
            return summary[keyPath: path]
        }
        set {
            summary[keyPath: path] = newValue
        }
    }
}

There is an extension which will work with shared functionality of those structs.

Now sharing code between ClientSummary and ClientDetails is tricky. By using dynamicMemberLookup you will be able to access all the properties in ClientSummary from ClientDetails but methods from ClientSummary can't be invoked this way. There is proposal to fulfill protocol requirements with dynamicMemberLookup which should allow you to share methods between ClientSummary and ClientDetails for now you have to invoke ClientSummary methods on ClientDetails using the summary property.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.