Avoiding repetition and hiding details when using generics

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.

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

2 Likes

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