Avoiding repetition and hiding details when using generics

generics

(Juri Pakaste) #1

I have a slightly complicated set of view models, view controllers and associated classes that use generics for dealing with grouped content. It looks something like this:

final class RecordingsViewController<Content, Strategy> where Strategy: RecordingsViewModelStrategy, Strategy.Content == Content {
    typealias ViewModel = RecordingsViewModel<Content, Strategy>

    private let viewModel: ViewModel

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
    }
}

final class RecordingsViewModel<Content, Strategy> where Strategy: RecordingsViewModelStrategy, Strategy.Content == Content {
    private let strategy: Strategy

    init(strategy: Strategy) {
        self.strategy = strategy
    }
}

protocol RecordingsViewModelStrategy {
    associatedtype Content

    func readContent(at index: Int) -> Content?
}

final class GroupsStrategy: RecordingsViewModelStrategy {
    typealias Content = Models.Group

    func readContent(at index: Int) -> Content? { return nil }
}

final class ItemsStrategy: RecordingsViewModelStrategy {
    typealias Content = Models.Item

    func readContent(at index: Int) -> Content? { return nil }
}

enum Models {
    struct Group {}
    struct Item {}
}

My issue is with the very detailed type of the view controller. If I was using dynamic dispatch and subclasses this would be much more concise, but I don't want to do that. Is there any other way to avoid having Content and Strategy and the where clause in RecordingsViewController's declaration? It works fine, it's just a lot to read every time I go over this code, and repeating the where clause there feels like repetition for repetition's sake.

An earlier version wasn't using strategy objects but instead customised RecordingsViewModel with a bunch of closures. That way I didn't need to include the Strategy type in all declarations, but changing anything in that code caused very misleading error messages.


(Anthony Latsis) #2

Type safety implies that you'll have to write the constraints if you want to abstract over a generic type that has constraints. But here's what you can do.

Content is a parameter that is derived from Strategy, so you can instead use a typealias. That will allow you to get rid of the additional constraint and the where clause.

final class RecordingsViewModel<Strategy: RecordingsViewModelStrategy> {
    typealias Content = Strategy.Content
    private let strategy: Strategy

    init(strategy: Strategy) { self.strategy = strategy }
}

The view controller's generic signature would then be simplified to

final class RecordingsViewController<Strategy: RecordingsViewModelStrategy> {
    typealias ViewModel = RecordingsViewModel<Strategy>

    private let viewModel: ViewModel

    init(viewModel: ViewModel) { self.viewModel = viewModel }
}

Also, as an alternative approach, you can use constrained protocol extensions to customize functionality and avoid creating a separate class for each Model:

protocol RecordingsViewModelStrategy {
  associatedtype Content

  func readContent(at index: Int) -> Content?
}

extension RecordingsViewModelStrategy {
  func readContent(at index: Int) -> Content? { return nil }
}
extension RecordingsViewModelStrategy where Content == Models.Group {
  func readContent(at index: Int) -> Content? { ... }
}
extension RecordingsViewModelStrategy where Content == Models.Item {
  func readContent(at index: Int) -> Content? { ... }
}

struct Strategy<Content>: RecordingsViewModelStrategy {}


(Juri Pakaste) #3

Thank you, both great suggestions. Deriving Content from Strategy does clean up the signatures quite a bit.