1

That is quite specific circumstances I've come across, and I somewhat struggle to find proper way how to approach this.

I'm given a class written in swift-language, which has a control property, like this (I generalized some names to avoid reference to actual product):

class FooViewController: UIViewController {
    ...
    var controlView: FooControlViewProtocol?
    ...     
}

FooControlViewProtocol is a quite simple protocol, which has both directive methods and event-based methods, this is kind of communication interface between a UIViewController instance and a so-called controlView which is essentially a UIView instance conforming to the protocol.

protocol FooControlViewProtocol {
    func doFirstMethod();
    func doSecondMethod();

    func eventDidStart(_ event: Event);
    func eventDidFinish(_ event: Event);
}

And now here is the complicated part: these parts are rather concrete, and they come from a framework another team is working on, and we should not change that at all cost, because some other parts of the app (and even other projects) also rely on this. However we need to customize it so it meets the needs of our project. Mostly we didn't even bother at all. That couple covered most of the requirements we had. However at some point we had to implement a custom scrolling behavior and inform this control view of current scrolling position, but for only one of dozens ViewControllers in the app. I ended up extending the protocol like this:

protocol ScrollableFooControlViewProtocol: FooControlViewProtocol {
    func scrollDidEnd(_ offset: CGFloat)
}

And then, in a FooViewController subclass the property is set like this:

class ScrollableFooViewController: FooViewController {

    var scrollableControlView: ScrollableFooControlViewProtocol?
    
    override var controlView: FooControlViewProtocol? {
        set {
            scrollableControlView = newValue as? ScrollableFooControlViewProtocol
        }
        get {
            scrollableControlView
        }
    }
}

The apparent problem is that if a new controlView instance does not conform to ScrollableFooControlViewProtocol, the application just ignore it silently and keeps working AND I find this mistake extremely likely to happen, because existing code use given abstractions heavily (FooViewController and FooControlViewProtocol). I cannot throw an exception from the property and it's somewhat too complicated to switch the property to a method (because of abstraction). One option I've put into consideration is force unwrapping newValue cast like this:

set {
    if newValue = newValue {
        scrollableControlView = newValue as! ScrollableFooControlViewProtocol
    } else {
        scrollableControlView = nil
    }
}

And the application can just crash in case of mistake, revealing the problem immediately. However this approach is far from perfect - the feature this protocol adds is not essential for application lifecycle and it can work seemingly flawless without it. So making it crash because of it is kind of overacting.

Integration test might be an answer here, where i can check these two components interaction, however i doubt this will pay off for only such a tiny case.

So the question is how to make this code safe? Probably I should have implemented it somehow differently in the first place.

8
  • 3
    Ignore the downvotes, the community here has some members which regularly downvote questions without explaning why they voted that way. IMHO your question is fine here.
    – Doc Brown
    Jun 23, 2020 at 12:59
  • 1
    I'm not sure I understand your question. If you understand the problem with your program, then why can't you simply write some code to solve it? Is your problem solved with logging? Jun 23, 2020 at 13:05
  • 1
    Also, despite Doc Brown's protestations, Stack Overflow is almost certainly a better choice for code troubleshooting questions. But they're going to ask the same question I did. Jun 23, 2020 at 13:07
  • 1
    @RobertHarvey: The downvoters could have asked a question for clarification like you. They could have suggested Stackoverflow as a better place, like you. Given they have enough rep, they could have voted for closing with "needs more clarity". But none of this happened.
    – Doc Brown
    Jun 23, 2020 at 14:04
  • 2
    ... and I think the question (despite it's Swift focus) looks pretty language agnostic to me. It is more of "I can write an Integration test for resolving the issue, but the overhead does not seem worth it, so is there a way to achieve the same confidence with less overhead?" Is this really a coding question? To me, it looks more like a conceptual question.
    – Doc Brown
    Jun 23, 2020 at 14:10

1 Answer 1

1

OK here is my understanding of your problem.

public class 3rdPartyController
{
    public 3rdPartyView View {get;set;}
}

You can't change either 3rd party class, but you need to add some functionality to subclasses of them, enforcing that the subclassed Controller use the subclassed view

The view is easy:

public class MyView : 3rdPartyView 
{
    public newFunction {get;set;}
}

But the Controller can't inherit from the 3rdPartyController, without breaking Liskov substitution.

public class MyController  // cant : 3rdPartyController :( also presumably cant : IController?
{
    public MyView View {get;set;}
}

Is it not acceptable to have a controller that doesn't inherit from the 3rdParty class?

Any run time checking works, but is error prone.

You could perhaps wrap the view? if you are able to apply newFunction to any 3rdPartyView:

public class MyView : 3rdPartyView 
{
    public MyView(3rdPartyView base)
    public newFunction {get;set;}
}

public class MyController : 3rdPartyController
{
    private MyView view;
    public 3rdPartyView View {
       set { this.view = new MyView(value)}
...
}

It seems like this wouldn't work on unscrollable Views though. So you would need to fall back to some other behaviour. Would that be acceptable?

Another alternative would be to use constructor injection as this avoids inheritance restrictions:

public class MyController : 3rdPartyController
{
    private MyView view;
    public MyController(MyView view)
    {
         this.view = view;
    }
    public 3rdPartyView View {...//expose MyView
}

But I'm guessing the property setter is used to set the view by the framework.

The obvious solution would be to change the 3rd Party component to be a generic Controller<ViewType>. But of course you can't.

Just thought of another solution, put the controller logic into the view. Now you can put this view in any controller

public class MyView : 3rdPartyView 
{
    private OnScrollEnd() { ..code i would normally put in the controller }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.