Your solution of making an IStackable still breaks Liskov principle, because you cannot substitute it by an instance of Stack or UniqueStack. A simple test (which you also show) is calling Push(1) twice and calling Pop() twice, and the app will crash when using an instance of UniqueStack. Your example was good but the solution does not make sense, it does not solve the problem. Modelling the tests in a way which does not break Liskov is not also a solution.
I think proper design it's about ensuring that your interfaces are as generic as possible, but in this case the interface is already as simple as it can be. It's still not evident from the video (again, my perspective) how you come about not violating LSP if a dependency (or method argument) is defined using the base interface (i.e. IStack), and you're assigning an specialized implementation (i.e. ProperStack or UniqueStack). As the consumer, all I care about is the contract of IStack, and not the specialized implementation. It's a real head scratcher and something that's easy to violate, yet rarely causes issues.
Exactly! The generative tools are still incapable of addressing design issues. There are many reasons for that and it looks like this generation of AI tools will never crack that problem. We need something else.
I've always heard of the LSP as applying at the argument-parameter boundary (or the return value boundary). Covariance and Contravariance. If B inherits from A, then pretty much by definition it has all the methods and properties of A. But this seems to take it a bit further. Going back to wiki, these seem to be only the most basic requirements of the principle. Part of what bothers me is that there's no obvious clarification of the contract you are agreeing to with each interface. This is most likely just the lack of documentation headers for a simple demo project, given you also have tests that are designed to clarify exactly how each interface should behave. Or maybe it's the naming. What is a "proper" stack supposed to be? Nitpicky stuff that's only tangential to the topic. However, suppose we add a third type of stack. Say, a stack with a limited size (eg: cannot add more than 10 items). It's not going to pass either of the other set of tests once you go beyond the size limit. So now do you have to create a third interface in order to create a third set of tests? And if you're just creating one interface per implementation, what is even the point of the interfaces? And does it belong at the bottom of the hierarchy or at the top? Or is it a V shape? It seems like every addition to a class hierarchy has the potential to require a complete overhaul of the entire hierarchy structure in order to retain compliance with the LSP. For example, with the demo, if you had only started with the basic stack, and then later added the unique stack, you're having to create an entirely new base class (or interface) that the original now derives from. But that has the potential to completely screw with naming conventions and API compatibility.
Your example with the limited stack is what I meant when I asked if throwing an exception is violating LSP. If the base type does not indicate that the call might fall, then yes it does. The limited stack would violate the stack interface's contact in that case and you should not assign it to the stack interface.
@@zoran-horvat but since the limited stack does implement the base interface, what's keeping the consumer from assigning a specialized (i.e. violating implementation) to the interface, or pass it to a consumer that accepts said interface? Is the simple answer in this case that the specialized implementation shouldn't implement the interface at all?
I don't know why people complicate these things, invent "principles" and other buzzwords. There is only one "principle" in software development - everything must be specified and work as specified. Then, when it comes to OOP, the deal is simple: a subclass must fulfill the specification of its superclass, period.
And how would you write a test, then? Needless to say that you have forgotten that the subclass can relax preconditions, therefore the claim that the subclass must fulfill the specification of its superclass is elementary untrue. Pity you didn't get it along the way.
@@zoran-horvat Checking Liskov Substition Principal is undecidable. I.e. if the language in which to write down the properties ("behaviour") is powerful enough, the compiler cannot check it automatically.
One example is the obvious desirable property "pop() should eventually return an element and not loop forever" (totality / termination). If I do a really stupid implementation of the stack that loops on some input values, the compiler may fail to catch the error (halting problem).
So showing that something is violating LSP is "easy", proving that something is not violating LSP is "very hard". LSP in the extreme pretty much says "don't change a working implementation, you can never be sure that you break something" :)
@@karlmehltretter2677 What is the purpose of this deliberation? I know the theory pretty well, I have a CS degree, and I have also been developing software for more than 25 years. Yet, the halting problem never popped up in my work. Did it ever cause a consequence in your work?
I think LSP is an important step in quality code could reduce bugs. It does seem to require quite a bit of a discipline. Consider a developer writes code against an IStackable interface. However, they have written code that depends on properties not guaranteed by the IStackable interface (e.g, pushing twice causes count to be >= 2.) How could they know? The IDE and Compiler isn't helping them. They could read the documentation for the interface but that just declares what properties are true, it doesn't provide an infinite list of properties NOT guaranteed. From a common sense point of view the code seems perfectly valid. It takes quite a bit of discernment to notice the code is relying on what is not actually guaranteed.
You are right. But isn't that true for any design? We are left to documentation, clear naming, and unit testing - each helping reduce a chance for misunderstandings, but not removing it entirely.
It is just a utility class which you can write in a dozen ways. It is not important for the story. By the way, in a proper project, you would use proper unit tests.
hmmm, UniqueStack will not work for Push(1, 2), Push(1), Pop() == 1. And as it was said in test method Stackable should just return last pushed value. In this example it will return value 2 not 1. Or am I wrong?
And I see something weird. In code you wrote test for Stackable with rule "Pop decrements stack size". Then in result there's no this test for Stackable, only for Stack. EDIT: ok, I assume it was just typo when copying code. Test should be for Stack.
@@ekhm The ultimate conclusion I came to in the video after going through all those pains was that the unique stack is not a stack no matter how hard I twisted the logic.
@@zoran-horvat Yes, that's the answer. The unique stack isn't a stack to the extent that it satisfies the initial requirements of the interface, and as such, shouldn't implement that interface, but define it's own unique stack interface and implement that (i.e. without any inheritance of the basic stack interface, as "it's not a basic stack", basically).
I learned the hard way it all depends on the contract. For example, in java the 'Collection.add' operation's contract is... whatever the implementation does! So, you can't mutate it because it might be immutable and throw an exception, but you can't just hang onto it either, because... maybe it *is* mutable and someone else has a reference. But hey, we met the contract. boolean add(E e) Ensures that this collection contains the specified element (optional operation).
@@zoran-horvat That's the problem; it does not state even that. It says it *might* work, depending on the implementation, by declaring it an "optional operation" in the 'contract'. void foo(List list) You do not know if you can add to this list, or if some *other* reference can either! But they did avoid having to split im/mutable apis out. To throw salt into the wound, for years the only good factory method for creating a `List` created the immutable kind 😢
Hi Zoran, very great video! Correct me if I am wrong, so the lsp indicate that you shouldn't derived classes if all operations and conditions of base class are met in derived class too, so with interface we know only the operations to implement and not the base implementation?
LSP defines properties that both base and derived types must possess. It doesn't matter what those types are: classes, interfaces, an interface and a class, a covariant/contravariant generic class, a class with an implicit or explicit conversion operator to another class...
@@zoran-horvat Thank you for response, so to implement an interface and one of the implementation methods use the throw new Notimplementedexception Is considered breaking the lsp and I should divide that interface in more interfaces?
The second approach worked as expected but definition of different interfaces means different comcrete lass implementation. Its prone to code duplication.. as I understood LSP puts more restrictions during base-child class relation. I thinkg LSP definition does not match on real common cases all the times, rule required seperate class at all
LSP fits very well in object-oriented design. The problem is usually in programmers who prefer less formal design, writing code fast and then patching it when it doesn't work. In other words, it is the lack of knowledge, not shortcomings of the SOLID principles that we are facing in practice.
@@zoran-horvat If LSP fit so well into OOP, it wouldn't be such a controversial and incomprehensible topic. That's why most programmers can misperceive it for different purposes. Knowledge is resolved through experience, as long as its source is accurate.
@@qorxmazmaharram8300 LSP is not controversial. It is rather that our industry is full of hotheaded half-baked programmers who didn't learn the basics but that doesn't stop them from teaching others the lesson. There are plenty of comments on my earlier videos saying I have violated LSP here, violated there, and those comments repeat all day long. And all those comments share one constant: those who posted them had no clue what they said. LSP is one of the cleanest and best documented principles in programming.
What if we reverse the interfaces in a way that IStackable is implemented in Stack classe, but the IUniqueStackable (istead of using IProperStack interface) is implemented in UniqueStack class
That is an essential principle in programming, so I would suggest you to come back to it after a while and figure it out. That will change the way you see programming, trust me.