OgreSwamp
OgreSwamp

Reputation: 4692

How to use protocols for stucts to emulate classes inheritance

I'm implementing a model:

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)
    }
}

Upvotes: 2

Views: 134

Answers (3)

Soumya Mahunt
Soumya Mahunt

Reputation: 2802

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.

Upvotes: 0

Rob Napier
Rob Napier

Reputation: 299643

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 }
}

Upvotes: 4

Charles Srstka
Charles Srstka

Reputation: 17060

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.

Upvotes: 1

Related Questions