Showing posts with label interface. Show all posts
Showing posts with label interface. Show all posts

Friday, April 24, 2015

SOLID Review: Dependency Inversion Principle

Note: This is part of a series of articles reviewing the five SOLID Principles of object-oriented programming.

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.

Let's look at an example! We'll revisit a simple example from my blog post on the Open/Closed Principle.

A Simple String Transformer

Suppose we have a class called Transformer that takes a string and transforms it into a some other object or value. For starters, we'll have it transform JSON strings into Ruby hashes:
require 'json'

class Transformer
  def initialize(string)
    @string = string
  end

  def transformed_string
    JSON.parse(@string)
  end
end

Transformer.new('{"foo": "bar"}').transformed_string
# { "foo" => "bar" }

Now, we'll extend the functionality of our Transformer by allowing it to transform strings into binary:
require 'json'

class Transformer
  def initialize(string)
    @string = string
  end

  def transformed_string(type)
    if type == :json
      JSON.parse(@string)
    elsif type == :binary
      @string.unpack('B*').first
    end
  end
end

Transformer.new('Hello').transformed_string(:binary)
# "0100100001100101011011000110110001101111"
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:
class Transformer
  def initialize(string)
    @string = string
  end

  def transformed_string(transformation)
    transformation.transform(string)
  end
end

class BinaryTransformation
  def self.transform(string)
    string.unpack('B*').first
  end
end

Transformer.new('Hello').transformed_string(BinaryTransformation)
# "0100100001100101011011000110110001101111"

require 'json'

class JSONTransformation
  def self.transform(string)
    JSON.parse(string)
  end
end

Transformer.new('{"foo": "bar"}').transformed_string(JSONTransformation)
# { "foo" => "bar" }
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:
require 'digest'

class MD5Transformation
  def self.transform(string)
    Digest::MD5.hexdigest string
  end
end

Transformer.new('Hello').transformed_string(MD5Transformation)
# "8b1a9953c4611296a827abf8c47804d7"

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!

Happy coding!

Friday, March 27, 2015

SOLID Review: Interface Segregation Principle


Note: This is part of a series of articles reviewing the five SOLID Principles of object-oriented programming.

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:
module Phone
  def call(number)
    "Calling #{number}..."
  end

  def hangup
    "Hanging up!"
  end

  def text(number, message)
    "Texting '#{message}' to #{number}."
  end
end
Here, we have a set of common behaviors for phones. We can make use of this by including them in our class. Let's create a CellPhone:
class CellPhone
  include Phone
end
Now, our CellPhone class implements the methods in Phone! Any instance of CellPhone can call, hangup, and text other numbers.

Let's create a new class called RotaryPhone:
class RotaryPhone
  include Phone

  # Eek... code smell.
  def text(number, message)
    raise 'Cannot text on this type of phone!'
  end
end
Since we are overriding one of the methods in our module, it's a sign our module isn't cohesive enough. Our RotaryPhone is being littered with methods it does't need!

Another issue worth noting is the tight coupling between our two classes caused by sharing the same, non-cohesive module. Suppose we don't override the text method in RotaryPhone:
class RotaryPhone
  include Phone
end
Any errors caused by text in our Phone module would end up in both classes, even though RotaryPhone doesn't care about text! This tight coupling between CellPhone and RotaryPhone is unnecessary.

Segregate the Modules

A good solution for our problem is to segregate the basic phone behaviors from the mobile phone behaviors:
module BasicPhone
  def call(number)
    "Calling #{number}..."
  end

  def hangup
    "Hanging up!"
  end
end

module MobilePhone
  def text(number, message)
    "Texting '#{message}' to #{number}."
  end
end
Now, each of our classes implement only the modules they require:
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!

Happy coding!