Intermediate Ruby - Object Oriented Design

Question Click to View Answer

Suppose the Dog class is responsible for parsing a CSV file to create dog objects and implementing the behavior of dog objects. Should the Dog class be refactored?

Yes, the Dog class should be refactored because it has two responsibilities (parsing the CSV and implementing behavior). Classes should only have a single responsibility. When classes have a single responsibility, the code is easier to test, reuse, and maintain.

Why is good design important? Is good design ever unimportant?

Good design creates software that is easy to maintain and change. Design does not matter if a program is completely specified in advanced and never changes, but this is almost never the case. Good design is almost always important as software is changed to meet new requirements.

What is the main goal of software design?

Make software that is easy to change. Creating flexible software is achieved by managing dependencies.

Identify the dependency in the following code:

["nice", "person"].join(" ")

The code depends on arrays responding to the :join message (in other words, the Array class must define a join method). Depending on the Ruby core library is not too bad because it does not change frequently. If the Array.join() method was deleted for some reason, the code would break and this is why dependencies are dangerous. When dependencies cascade throughout an application (i.e. Class A depends on Class B, which depends on Class C...), a single change can break the entire application.

Identify the dependencies in the following code:

class A
  def self.add(x, y)
    x + y
  end
end

class B
  def lala
    "2 plus 2 equals #{A.add(2, 2)}"
  end
end

p B.new.lala

Class B depends on class A to respond to the :add message with exactly two arguments. Additionally, class B depends on class A to be called A and not renamed something else. Class B will break if class A is renamed, the A.add method is deleted, or if the A.add method is changed to not take 2 arguments.

Identify the dependencies in the following code.

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
end

class Beatles
  def drummer
    Person.new("Ringo Starr", 74)
  end
end

The Beatles#drummer method has the following dependencies: 1. The name of the Person class (i.e. if the Person class is renamed to be Human, the Beatles#drummer method will break) 2. The Person#initialize method has two required arguments 3. The Person class must be initialized with name first, then age. Person.new(74, "Ringo Starr") does not raise an exception, but will not behave as intended.

A dependency can also be thought of as stuff a class needs to know about another class. In this example, the Beatles class needs to know the Person class name, that the Person class is initialized with two arguments, and the arguments must be name first and then age.

Refactor the following class to remove the argument-order dependency.

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end
end

class Yankees
  def captain
    Person.new("Jeter", 39)
  end
end

The Yankees#captain method knows that the Person class must be initialized with the name argument first and the age argument second (referred to as an argument-order dependency). If the Person class is initialized with a hash, the order of the :name and :age keys does not matter and the argument-order dependency is eliminated.

class Person
  def initialize(args)
    @name = args.fetch(:name)
    @age = args.fetch(:age)
  end
end

class Yankees
  def captain
    Person.new({ name: "Jeter", age: 39 })
  end
end

Use dependency injection to refactor the following code.

class Person
  def initialize(args)
    @name = args.fetch(:name)
    @age = args.fetch(:age)
  end
end

class Hockey
  attr_reader :great_one
  def initialize
    @great_one = Person.new({ age: 52, name: "Gretzky" })
  end
end

p Hockey.new.great_one

With dependency injection, the Hockey class is initialized with a person object, so it doesn't need to know as much stuff about the person class. Notice how the Hockey class does not need to know the name of the Person class or how many arguments the Person class is initialized with in the following refactoring. Eliminating these dependencies is how to write code that is loosely coupled and flexible.

class Person
  def initialize(args)
    @name = args.fetch(:name)
    @age = args.fetch(:age)
  end
end

class Hockey
  attr_reader :great_one
  def initialize(great_one)
    @great_one = great_one
  end
end

person = Person.new({ age: 52, name: "Gretzky" })
p Hockey.new(person).great_one

In the following example, the DataStructureConversion class depends on the Person class. Refactor the code to reverse the dependency direction and have the Person class depend on the DataStructureConversion class.

class Person
  def initialize(name)
    @name = name
  end
end

class DataStructureConversion
  def initialize
    @person = Person.new("bob")
  end

  def to_hash
    @person.instance_variables.inject({}) do |memo, var|
      memo[var] = @person.instance_variable_get(var)
      memo
    end
  end
end

p DataStructureConversion.new.to_hash
class Person
  def initialize(name)
    @name = name
  end

  def to_hash
    DataStructureConversion.to_hash(self)
  end
end

class DataStructureConversion
  def self.to_hash(object)
    object.instance_variables.inject({}) do |memo, var|
      memo[var] = object.instance_variable_get(var)
      memo
    end
  end
end

p Person.new("bob").to_hash

The DataStructureConversion class can now be easily reused by other classes. In general, it is best to have less stable classes depend on more stable classes.

Class A is a rapidly changing class and Class B is relatively stable. Should Class A depend on Class B or should Class B depend on Class A?

Class A should depend on Class B.

If Class B depends on Class A, Class B will frequently break because Class A is rapidly changing. If Class A depends on Class B, Class A may break if Class B changes, but the breakages will be less frequent since Class B is relatively stable.

Explain the following statement with an example: "a dependency is when a message sender has to know stuff about the message receiver."

In the following example, an instance of the B class is the message sender and the A class is the message receiver. The message sender needs to know the name of the message receiver's class (i.e. A), that the A class responds to the :add message, and that the A.add method takes two arguments. Since the B class knows "stuff" about the A class and depends on this knowledge to function, there is a dependency.

class A
  def self.add(x, y)
    x + y
  end
end

class B
  def lala
    "2 plus 2 equals #{A.add(2, 2)}"
  end
end

p B.new.lala

Why should software developers spend so much time managing dependencies and creating flexible code that is easy to change. Wouldn't it be easier to just predict future changes and write the code accordingly?

In the real world, programmers do not have a crystal ball and cannot predict future requirements accurately. Programming based on a guess of what future requirements will be is a futile endeavor. The best software engineers can do is design flexible software that can accommodate a wide range of future requirements. Limiting dependencies is how to creates loosely couple code that is easy to maintain and enhance with new features.

Explain why poorly designed applications with a lot of dependencies are difficult to work with.

Changes made to a poorly designed program will cascade throughout the application and break everything. When the design gets bad enough, even simple changes will cause lots of existing features to break. Programmers hate working on applications that are badly designed and businesses don't like it either because of the slow development cycle. It is easiest to just design software well in the first place.