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. |