A O
A O

Reputation: 5698

Multiple classes with the same generic properties and setup, how can I reduce duplicate code?

There are a lot of questions out there that are similar, but I'm having a hard time finding something that explains exactly what I'm looking for

I have multiple Services, which handle a generic Data type. They currently all subclass an semi-"abstract" class, DataService

This is because all of these Services have the same stored properties:

class DataService<T: Data> {
  let id: String
  let eventDataProviders: [EventDataProvider]
  private let storedDataProviders: [StoredDataProvider]

and their init are the same as well:

init(id: String, eventDataProviders: [EventDataProvider], storedDataProviders: [StoredDataProvider]) {
    self.id = id
    self.storedDataProviders = storedDataProviders
    self.eventDataProviders = eventDataProviders

    self.setupStoredDataProviders()
    self.setupEventDataProviders()
}

the setup methods are also the same


The difference between these classes is how they handle data, currently defined in an "abstract" function

func eventReceivedHandler() -> ((T) -> Void) {
    fatalError("DataService does not have eventReceivedHandler defined")
}

Most resources recommend Protocols and Protocol Extensions. Which I would prefer as well, but I think the two things that make that difficult are:

  1. Trying to reduce duplicate code by keeping the stored property declarations in one place, and by sharing an init

  2. The generic type on the class, it doesn't seem straight-forward to maintain that through a protocol

But the problem with this current solution is

  1. The "abstract" class is exposed, when we don't want anybody to actually instantiate an instance
  2. The eventReceivedHandler isn't compiler-enforced

Is there a correct solution here? I thought this architecture may be common enough, but I've really been struggling finding anything around online, my search queries contain too many overused terms

Upvotes: 0

Views: 288

Answers (2)

AnderCover
AnderCover

Reputation: 2671

You might want to use a protocol with an associated type :

protocol Service {
    associatedtype T
    var eventDataProviders: [EventDataProvider<T>] { get }
    var storedDataProviders: [StoredDataProvider<T>] { get }
}

Coupled with generic providers classes :

class EventDataProvider<T> {

}

class StoredDataProvider<T> {

}

And a concrete class :

class DataService<T>: Service {
    let id: String
    let eventDataProviders: [EventDataProvider<T>]
    let storedDataProviders: [StoredDataProvider<T>]

    init(id: String, eventDataProviders: [EventDataProvider<T>], storedDataProviders: [StoredDataProvider<T>]) {
        self.id = id
        self.storedDataProviders = storedDataProviders
        self.eventDataProviders = eventDataProviders

        self.setupStoredDataProviders()
        self.setupEventDataProviders()
    }

    func setupStoredDataProviders() {

    }

    func setupEventDataProviders() {

    }
}

Would allow you to have DataService instances handling different types of data eg. :

let data = DataService<Data>(id: "1", eventDataProviders: [EventDataProvider<Data>()], storedDataProviders: [StoredDataProvider<Data>()])
let data2 = DataService<Int>(id: "2", eventDataProviders: [EventDataProvider<Int>()], storedDataProviders: [StoredDataProvider<Int>()])

And you would also gain more type safety as :

let data3 = DataService<Data>(id: "3", eventDataProviders: [EventDataProvider<Data>()], storedDataProviders: [StoredDataProvider<Int>()])

would trigger :

Cannot convert value of type '[EventDataProvider]' to expected argument type '[EventDataProvider<_>]'

Unfortunately, you loose access control as storedDataProviders is no longer private.

Upvotes: 1

PGDev
PGDev

Reputation: 24341

You can create a protocol that has eventReceivedHandler() method, i.e.

protocol EventHandler {
    func eventReceivedHandler() -> ((Data)->())
}

Since you're defining T: Data, I don't think there is even a need for generic type T since always Data is expected in that place.

Now, you can create your class DataService as already did,

class DataService {
    let id: String
    let eventDataProviders: [EventDataProvider]
    private let storedDataProviders: [StoredDataProvider]

    init(id: String, eventDataProviders: [EventDataProvider], storedDataProviders: [StoredDataProvider]) {
        //....
    }

    func setupStoredDataProviders()  {
        //.....
    }

    func setupEventDataProviders()  {
        //.....
    }
}

Now, the Service classes will be created like,

class Service1: DataService, EventHandler {
    func eventReceivedHandler() -> ((Data) -> ()) {
        //....
    }
}

class Service2: DataService, EventHandler {
    func eventReceivedHandler() -> ((Data) -> ()) {
        //....
    }
}

In case you still have any doubts regarding that, you can ask.

Upvotes: 1

Related Questions