Runtime Era

Friday, April 24, 2015

The final SOLID principle is known as the Dependency Inversion principle. Arguably the most important of the five principles, the Dependency Inversion principle can be thought of as a culmination of the principles preceding it. Systems that abide by the other SOLID principles tend to follow the Dependency Inversion principle as a result. The principle states:

"High-level modules should not depend on low-level modules."

A better way to think about it is:

"Abstractions should not depend upon details. Details should depend upon abstractions."

In a static-typed language like Java, "abstractions" can be implemented and enforced explicitly via interfaces. However, in a dynamic language like Ruby, we depend on duck-typing to describe an object's interface. Even without explicit interfaces in Ruby, the Dependency Inversion principle still holds value! We should still aim to depend on abstractions rather than details.

Now, our Transformer takes strings and transforms them into one of two different types: a Ruby hash or its binary representation. At this point, we should notice some code-smell! The transformed_string method is very dependent on JSON.parse and String.unpack. These are implementation details that our Transformer shouldn't care about.

Let's apply the Dependency Inversion principle by making Transformer depend on an abstraction rather than coupling to concrete details!

The Transformation Abstraction

The basic functionality of our Transformer class is to transform strings into several different types of objects or values. It does this by utilizing different transformations. This seems like an abstraction we can extract and encapsulate! We'll make Transformer depend on a new abstraction called Transformation:

Rather than having Transformer depend on low-level implementation details (JSON.parse and String.unpack), it now depends on a single method: transform. This single method is what makes up the interface of our Transformation abstraction! Now, we can create as many Transformations as we want without modifying Transformer:

Conclusion

As you can see, the Open/Closed principle is highly correlated with the Dependency Inversion principle! We actually end up following the Open/Closed principle by abiding by the Dependency Inversion principle. In fact, some form of dependency abstraction is often required to abide by all the other SOLID principles. If there's one principle to remember out of all the SOLID principles, it's the Dependency Inversion principle: depend on abstractions, not low-level details!

Friday, March 27, 2015

The Interface Segregation Principle is probably the most straight-forward of all the SOLID principles. It states:

"Clients should not be forced to depend on methods that they do not use."

In dynamic languages, this isn't really much of an issue because there is no way to define and force the implementation of interfaces on classes (like in Java). Instead, a set of methods determines whether or not an object implements an interface. If an object responds to "a particular set of methods", it has implemented that "particular interface".

In Ruby, modules can be used to define and share sets of methods across multiple classes. Using this construct, we can define different "interfaces". So, when we say "keep our interfaces segregated", we're really saying "keep our modules segregated". This leads to highly cohesive modules.

There are two main benefits to cohesive modules in Ruby: less coupling and more readable code.

Implementing Phones

By keeping our modules small and focused, we are simply applying the Single Responsibility Principle, but for modules. For example, let's create a module called Phone:

class CellPhone
include BasicPhone
include MobilePhone
end
class RotaryPhone
include BasicPhone
end

A Readable, Loosely-Coupled Solution

The behaviors of each class are more clearly defined by the explicitness of the modules it includes. Also, CellPhone and RotaryPhone are only coupled by the methods in BasicPhone, which makes sense since they both require the basic behaviors or call and hangup. Both of our issues above are solved!

Conclusion

Although the Interface Segregation Principle is less important in dynamic languages like Ruby, it still leads to cohesive, readable classes. By keeping modules focused, we end up with looser coupling and cleaner "interface" definitions. They aren't major wins, but wins nonetheless!

Thursday, March 5, 2015

Barbara Liskov introduced her substitution principle back in 1987 during her keynote titled Data Abstraction and Heirarchy. Today, it is one of the five SOLID principles in object-oriented programming. The original definition is as follows:

"Let q(x) be a property provable about objects x of type T. Then q(y) is provable for objects y of type S, where S is a subtype of T."

Simply put:

"Instances of any type should be replaceable by instances of its subtypes without creating incorrect behaviors."

How can we ensure that our classes abide by the Liskov Substitution Principle? For starters, we must ensure that any subtype implements the interface of its base type. In the world of dynamic languages, this is better stated as a subtype must respond to the same set of methods as its base type.

We must also ensure that methods in any subtype preserve the original promises of methods in its base type. What "promises" are we talking about? For that, we turn to another design principle known as Design by Contract.

Instances of Bird are very simple. They eat only certain types of food, lay eggs, and can go from sitting on the ground to flying in the air. For now, ignore the fact that our Bird cannot go back on the ground. Here's a small program that uses our Bird:

Remember, any subtypes from Bird should be able to work in our program above. Now, let's create some subtypes of Bird and see how we can apply the Liskov Substitution Principle.

The subtype must implement the base type's interface.

In most programming languages, we can achieve this through basic inheritance. Since we already have a base class defined, we'll take this approach. However, there are many ways to achieve this across many languages. In Ruby, we can use modules to share methods (see duck-typing). In Java, we can implement interfaces.

We've broken our program yet again! Since we've actually made the postconditions in our method less restrictive than in the Bird class, we've violated the Liskov Substitution Principle.

Instead, let's say that MutantPigeons actually return a more specific type of Egg. We'll call it MutantPigeonEgg, and it behaves just like Egg with a hatch! method. Then, we've strengthened the postconditions and are well within our rule:

Looks like another break in our program! By doing nothing in our new fly method, we've broken the guarantee that the state of our @flying variable would be "true". Again, we've violated the Liskov Substitution Principle.

Now, this introduces an interesting problem. Penguins cannot just be made to fly, right?!

Real-Life Relationships != Inheritance-Model Relationships

Objects in the real world may show an obvious inheritance relationship. However, in object-oriented design, we only care about inheritance relationships regarding object behavior. Think of the classes in our system as representations of real-world objects. Those representations are fully defined by their external behavior (or interface).

Sure, penguins are birds in the real world, but Penguins are not Birds in our system because they do not behave like Birds. They don't have a properly functioning fly method.

Liskov Substitution and the Open/Closed Principle

Consider the examples above. Suppose we actually violated the Liskov Substitution Principle by creating our Pigeon class with a more restrictive eat method? Our existing program would have to be modified to handle our new class:

As we know from the Open/Closed Principle, we shouldn't have to change existing code to add new requirements or features. By violating the Liskov Substitution Principle, we are forced to violate the Open/Closed Principle!

Conclusion

As with all programming principles, it's important to find a balance when applying the Liskov Substitution Principle in real-world scenarios. There is some debate over the benefits or detriments of the principle. Always keep it simple first, then refactor as needed.