Ergonomic Generic Inheritance?

I'm attempting to formalize the model / logic / view controller pattern used in my apps by creating base classes to be used by all implementations. These are classes due to the shared storage and internal API that comes along with many of these types, but protocol abstractions would be considered if they made anything easier. These formalizations include two types: one to represent the observable state being offered by the model / logic controller, and another to represent the action. We start with a simple base class (simplified).

class ModelController<State, Action> { }

This is then refined by the LogicController type, as logic controllers may offer additional functionality. However, our generics start to get less ergonomic, as we have to define additional generic types, we can't just inherit those defined by ModelController.

class LogicController<State, Action>: ModelController<State, Action> {}

Obviously this seems rather redundant, but I understand the limitation here: classes can only inherit from concrete classes, so we must provide a fully defined superclass. I'm not sure whether this is an inherent limitation of inheritance or a Swift limitation, but it makes these relations ships a bit awkward.

The ergonomic issue gets worse when we want to define our concrete implementations of these controllers:

final class TestModelController: ModelController<TestModelController.State, TestModelController.Action> {
    struct State {}
    enum Action {}
}

Yikes. The subtypes not being visible when defining the inheritance clause is a bit annoying, as is the fact that types with the same names as the generics aren't picked up automatically, so they must be fully qualified.

However, the real issue comes when trying to implement a base view controller that uses a single type of logic controller.

class LogicalViewController<State, Action, Brain: LogicController<State, Action>>: UIViewController {}

Not only do we have to declare the Brain type we actually care about but the same placeholders we've defined twice before. Now, this wouldn't be so bad if the definitions we had to actually use wasn't so redundant:

final class TestViewController: LogicalViewController<TestLogicController.State, TestLogicController.Action, TestLogicController> {}

As far as I can tell, there's no way to define this inheritance in a way to allows the compiler to see that the State and Action types I defined for the view controller should automatically come from the TestLogicController type.

Now, this isn't too bad overall since we only define the inheritance once for each controller, and it's a pretty powerful set of abstractions so I'm willing to pay that complexity, but surely there's a better way here? I've tried replacing some of the inheritance with protocols but ran into the same issues: having to create overly generic concrete types to satisfy the associatedtype requirements.

Anyone have better suggestions for a design with the same capabilities?

That's curious. I think something like this looks fine:

protocol ModelController { 
    associatedtype State
    associatedtype Action
}

protocol LogicController: ModelController { }

final class TestLogicController: LogicController {
    struct State {}
    enum Action {}
}

class LogicalViewController<Brain: LogicController> {}
final class TestViewController: LogicalViewController<TestLogicController> {}

Is there some constraint, like LogicController must be class, that is missing?

Mainly common API and stored properties which aren't part of this example. Here's the full example:

class ModelController<State: StateType, Action: ActionType> {
    @Published private var state: State
    
    private var tokens: Set<AnyCancellable> = []
    
    init(state: State) {
        self.state = state
    }
    
    func observe() -> AnyPublisher<State, Never> {
        $state.removeDuplicates().eraseToAnyPublisher()
    }
    
    func addObservations(_ observations: @autoclosure () -> [AnyCancellable]) {
        observations().forEach { tokens.insert($0) }
    }
    
    func perform(_ action: Action) {
        fatalError("Subclasses must implement.")
    }
}

I could swap the @Published property for some subject directly so that it can be required by the protocol but I'd really like to include the concrete storage of these properties so that implementers don't have to provide their own.

Ok that's tricky. One thing I can think of, is to have a protocol LogicControllerProtocol, and class LogicController.

protocol LogicControllerProtocol: AnyObject {
  associatedtype State
  associatedtype Action
}

class LogicController<State, Action>: LogicControllerProtocol {
}

It'd help with specifying this in generics. Though not much when subclassing.

Yeah, that's what I hit before: once I started making inheritable concrete types, the generics were required and we're back to the same issue.

On that note, I think your approach would be as concise as it gets. Given that generic classes have this rule that all generic placeholders must be specified. Any more concise than this would require some implicit/unspecified form:

class Concrete: Generic<_> { ... }

which we don't have yet. There isn't much option when subclassing unless we have Generic<_>.

That aside, I'm not sure if you'd benefit much from generics given that all class methods are already dynamically dispatched. If you don't need that, maybe you can instead treat them all as the base LogicController:

class LogicalViewController<State, Action> {
  typealias Brain = LogicController<State, Action>
}

I'm somewhat surprised using Self doesn't work here:

class ModelController<State, Action> { }
final class TestModelController: ModelController<Self.State, Self.Action> { // State' is not a member type of 'Self'
    struct State {}
    enum Action {}
}

Yes, that produces a specific error saying that the covariant Self can't be used there. The fix-it will switch to the full type though, which is nice.

Terms of Service

Privacy Policy

Cookie Policy