Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Liskov substitution principle and Streams

Does the fact that there are Stream derived classes that cannot be written or sought break the Liskov substitution principle?

For example, the NetworkStream cannot be sought, it will throw a NotSupportedException if the method Seek is called.

Or because the existence of CanSeek flag it is ok?

Considering the well known example of Square inheriting from Rectangle... would the addition of the flags DoesHeightAffectsWidth and DoesWidthAffectsHeight to Rectangle fix the issue?

Doesn't this open the door for fixing things by adding flags?

like image 516
vtortola Avatar asked Nov 29 '13 17:11

vtortola


People also ask

What is the principle behind the Liskov Substitution Principle?

Simply put, the Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of its subclasses without breaking the application. In other words, what we want is to have the objects of our subclasses behaving the same way as the objects of our superclass.

What violates Liskov Substitution Principle?

A very common violation of this principle is the partial implementation of interfaces or base class functionality, leaving unimplemented methods or properties to throw an exception (e.g. NotImplementedException).

What is the most accurate example of the Liskov Substitution Principle?

The classic example of the inheritance technique causing problems is the circle-elipse problem (a.k.a the rectangle-square problem) which is a is a violation of the Liskov substitution principle. A good example here is that of a bird and a penguin; I will call this dove-penguin problem.

What is the difference between Liskov and interface segregation?

The Liskov Substitution Principle is about subtyping and inheritance. The Interface Segregation Principle (ISP) is about business logic to clients communication. In all modular applications there must be some kind of interface that the client can rely on.


2 Answers

CanSeek technically keeps the stream classes from violating LSP. Only if it returns true, does seeking promise to work.

I personally consider it a severe bending of ISP and possibly SRP, and my inner designer would have preferred something like a SeekableStream subclass/interface that seekable streams could inherit from. But i'm sure that brings problems of its own (for instance, in streams that are only sometimes seekable)...and frankly, real-world usability trumps principle.

That is something to keep in mind. Once in a while, principle and reality collide. SOLID principles help minimize unneeded complexity in most cases, and generally keep OO systems maintainable and prevent them from collapsing under their own weight. If purity results in a system that's more complex, though -- for instance, because now an only-sometimes-seekable stream doesn't fit well into the hierarchy -- then perhaps an occasional bit of ugliness is justifiable.

But it should never be the first choice just because the letter of the law allows it. SOLID principles aren't just rules; they're principles. They're the ideas behind the words -- the spirit of the law. If you stick to the letter while lawyering your way past the spirit, you're missing the whole point of the principle.

As for the Square/Rectangle problem...technically, having properties/functions that determine whether changing the height will change the width as well, could be considered in keeping with the letter of LSP. Again, though, it feels like lawyering, and is pushing the bounds of other SOLID principles. It's also definitely not an optimal solution from reality's perspective, as it increases complexity and introduces the possibility for accidental side effects; now everything that wants to say rect.Height = 50; can unintentionally change the width as well.

like image 134
cHao Avatar answered Sep 23 '22 05:09

cHao


The Can... methods mean that Stream doesn't break LSP. Stream provides the capability to read, write and seek, but no guarantee that any implementing class will honour it. The Can... methods make this an explicit feature of the Stream contract - derived classes must implement them to allow client code to check whether a derived class implements certain behaviour before a call is made. So, any code that attempts to write to a Stream should check CanWrite before calling Write, for example, and this can be done with any correctly-implemented derivative of Stream. Ergo, they're interchangeable, as LSP requires.

I think it's certainly true that adding methods to flag whether or not derived classes implement a particular feature could be abused - if a team were undisciplined, they may end up with a very broad, bloated interface that breaks ISP. I think that Stream and IList<T> are well-designed in this respect - they don't break LSP, and they define a narrow enough contract of closely-related behaviours to stay within ISP. Clearly, their design has been considered.

I think that in the case of Square inheriting from Rectangle, you could certainly add DoesHeightAffectsWidth and DoesWidthAffectsHeight to fix the issue, but the team must decide whether that's acceptable, or whether the addition of those methods breaks ISP. Is the addition of AreAllInternalAnglesEqual to support trapezoids too far? To a certain extent, it's up to the engineers writing the code.

like image 35
Chris Mantle Avatar answered Sep 23 '22 05:09

Chris Mantle