1

This has really nothing to do with MVC but I came across the issue and also found a related question in that context. Unfortunately the related question fell short of describing where the LSP violation occurs in its original example and thus did not produce a satisfactory answer.

Suppose we have an MVC (or any other) framework/library providing a couple of base classes that work together as aggregates, here View and Model:

class Model:

    def __init__(self, data):
        self.data = data


class View:
    
    def set_model(self, model: Model):
        self._model = model

    def draw(self):
        self._draw_data(model.data)

(incomplete code obviously, but you get the idea)

Let's say I want the views in my application to always show a title. So I'll derive all my model classes from MyModel:

class MyModel(Model):

    def __init__(self, data):
        self.data 
        self.title = ''

So far so good, no LSPs violated yet. I'll also create a new view base class which shall require its model to be of type MyModel so that it can draw a title.

class MyView(View):

    def set_model(self, model: MyModel):
        super().set_model(model)

    def draw(self):
        super().draw()
        self._draw_title(model.title)

Now I have a covariant argument on set_model and thus violated LSP. What would be a better way to design this? Or does Liskov not apply here? If so, why?

I could obviously check the type of model in the draw method but in a more complex real-world example I may end up adding lots of type checking code which doesn't feel right either.

2
  • Is this based on a real framework? What code calls the set_model function of the framework's View class? That's the code that might fail due to the LSP violation.
    – bdsl
    Aug 2, 2020 at 9:26
  • This sounds like a case where the Bridge pattern could come in handy: en.wikipedia.org/wiki/Bridge_pattern
    – Helena
    Dec 30, 2020 at 10:08

3 Answers 3

1

LSP is tricky in cases like these, because you have multiple strands of the type-hierarchy to consider and what subtyping means in each case, what the semantics are in each case.

Consider using generics to avoid the problems that come with inheritance of un-parameterised types. In some languages like Java it is possible to have self-referential generics, a well-known example being stuff like

public class Date implements Comparable<Date> {...}

So you could achieve code-reuse by having a generic, maybe abstract super class for controllers, views, and models respectively and having up to three type parameters on each for types of the other objects. For example you could have

class AbstractController<M, V extends AbstractView<M,C>, C extends AbstractController<M, V, C>>{
   M getModel(){ return this.model;}

   void setModel(M newData){
        this.model = newData;
        getView().onDataChanged(newData);
   }

   V getView(){...}

   ...
}
abstract class AbstractView<M,C extends AbstractController<M,?,C>>{
   C getController(){...}
   void onDataChanged(M newData){...}
   abstract draw();
}

and

class MyModel{}
class MyView extends AbstractView<MyModel,MyController>{...}
class MyController extends AbstractController<MyModel,MyView,MyController>{...}

Now, if you want to reuse code by inheriting from MyView, you could instead generify MyView to

class MyView<M> extends AbstractView<M,MyController>{...}

and then inheriting like so

class MySpecialisedModel extends MyModel {...}     
class MySpecialisedView extends MyView<MySpecialisedModel>{
   @Override
   draw(){
      super.draw();
      alsoDrawTitle(getModel().getTitle());
   }
}

Of course, with MyController you probably run into the same kind of problems. This happens a lot, when classes are too interrelated which in turn happens a lot with the three parts of the MVC-Pattern.

That is one of the main drawbacks of the implementations of MVC I'm familiar with (and I'm not that familiar with that many, so take this with a grain of salt). The tight coupling makes it hard to wrap ones head around inheritance. It is not even clear what inheritance means in these situations (see first sentence) because the controller most likely handles exactly one kind of situation which is displayed with exactly one kind of view and both depend on exactly one type of model. The question "what happens when we switch the view class for something different" or even more strangely "what happend when we switch to different controller" rarely gets asked, but that is necessary for thinking through LSP.

1

LSP is about subtyping and behavioral contracts.

This means that it must be possible to use MyView objects in place of View objects and that a MyView object shall pass all the View tests with success.

Clearly, if MyView would not offer a set_model(self, model: Model) you’d not be LSP compliant, because basic View test would fail. For example one would imagine that setting the model would at least cause the View to subscribe as observer to the Model and a simple test to verify that it happens. I’m not sufficiently knowledgeable about python to know if the specialized set_model replaces or complements the more general set_model of its parent. In the second case you could still be ok.

The good news is that MyView can offer more than what View offers and the way you’ve implemented set_model(self, model: MyModel) is consistent with the LSP’s history constraint.

Now the whole question is about what contract View guarantees for set_model: is it guaranteed to always work for all models? Or is the success of the operation conditional to the suitability of the Model? More generally, the contract is about preconditions, postconditions and invariants. Depending on how you define the contract of the base class, you could very well design an LSP compliant specialized MVC, provided it offers at least the same interface than the base classes.

0

This is a text book case for sticking to "composition over inheritance". In the model layer, subclassing models to add more data can make sense. In the view layer, hence the popularity of ui components.

I'm a python noob, but I hope this is understandable:

class View(ViewBase)
    def set_model(model):
        self._model = model

class View(ViewBase)
    def draw(self):
        self._draw_data(model.data)

class MyView(ViewBase):
    def __init__(self)
        self.view = View()
        
    def set_model(self, model: MyModel):
        super().set_model(model)
        self.view.set_model(model)

    def draw(self):
        self.view.draw()
        self._draw_title(model.title)

The ViewBase is fully optional here of course, but typically it'd do more work (e.g. do some dirty marking and schedule redraws in set_model).

The main point is that MyView reuses View by means of composition, rather than inheritance. Even if the data required by MyView were not a subtype of the data required by View, the reuse could still occur, merely by deriving data compatible with View.set_model in MyView.set_model.

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.