Monday, June 2, 2014

The LSP in Dynamic Languages

A question was raised with me about the effect of dynamic languages on the Liskov Substitution Principle. If you are a little vague on the LSP it goes beyond the basic idea of substitution to include behavior. I have another article that explains why the LSP cannot allow a square to be a rectangle because of the expected behavior.

Java and C# are examples of strongly-typed languages that support both early and late binding. They will check at compile time to make sure that a class has the correct interface to support an operation (method overloading), and at run time a decision is made as to which method will actually be used based on the object type (method overriding). This is a class diagram that shows an AccountCollection can contain a group of Account objects and does not concern itself with the actual subclass that each object is:



That the AccountCollection only looks at Account objects is an example of design to the abstraction, where the Account is an abstract view of all the subclasses that inherit from it. There isn't any way to add an object that does not inherit from Account into the collection, the compiler will stop you if you try. By the way, the InvestmentAccount is only on the side to show that anything that inherits from Account will work here.

The question was raised as a perceived problem because Groovy never uses early binding to verify a class has the correct interface. Dynamic languages such as Groovy uses late binding at runtime to check if an object has the method that is requested, and if it doesn't then the program fails. JavaScript is a hugely popular dynamic language with the exact same issue. We have a name for this kind of late binding: duck typing. It's based on quote attributed to James Whitcomb Riley: "When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck" (Heim). And that is what a dynamic language does! So Groovy's and JavaScript's point of view is that everything is just an object:


The compiler will never stop you from adding any object into the AccountCollection, and in fact JavaScript is not even compiled. But Groovy and JavaScript both support inheritance! Well they do, but it is not used to check method invocations at compile time. In dynamic languages inheritance is still used to avoid duplication, to support the principle of "Don't Repeat Yourself" (Hunt).

So the perception in a strongly typed languages is that if the class of object inherits its interface or promises to implement a particular interface then all is well with the LSP.  Unfortunately this perception is flawed, but the problem actually goes much deeper.

An underlying goal of all software development projects is to achieve RAM: produce an application that is reliable, adaptable, and maintainable. RAM is reached through the application of accepted software design principles. These principles are accepted because they result in products that perform as expected. When the principles are ignored the design will be brittle and the results unstable.

So there are six commonly addressed principles and many derivatives. Robert Martin identified his first five fundamental principles.with the acronym SOLID, and Andy Hunt with Dave Thomas gave us Don't Repeat Yourself (Martin, Hunt). I will not explain the details here, but feel free to read my article about SOLID and DRY.

The fundamental issue with all of the principles, especially the LSP, is that programmers sometimes expect their language or their environment to enforce them. But these are design principles and they must be applied during the design of the application. Design is a very subjective task and there are not many opportunities for a tool to enforce a.principle. Yes, there are tools that that look for issues with cohesion, coupling, and so forth, but most of these work on the code created after the design, not on the design documents before the code is written. It is your responsibility during design to enforce these principles, not the responsibility of the language!

And that that answers the question. The principle tool that we have to catch a design flaw is a compiler. In Java and C# the compiler will protest if an object doesn't present the correct interface. But even in a strongly typed language that does not mean that the LSP is satisfied. Using an object that provides the expected behavior is always the responsibility of the designer. All that Groovy and JavaScript do is dispense with the compiler flag that something may be wrong. So what? If we meet our responsibilities during design then it really does not matter.

References

See the references page.

No comments:

Post a Comment