#OOD for Ruby Notes
##Chapter 1: Object-Oriented Design
- The world is procedural. One step at a time (like a script).
- OO is about the objects that message back and forth between each other
- You have to "Design" code because that code WILL change. If it never changed, then you can code it whatever way you want and it wouldn't matter.
- By sending a message, the sender has to know something about the receiver. This of course means there's a dependency. And the key to OOD is MANAGING dependencies.
- There is always a battle between what needs to get done NOW vs creating something that is designed better that will take more time. This balance will come with experience (I HOPE)
- Agile is the method that allows design to be an iterative process. This allows for better OO design as well.
- The cost of design is 2 folds: 1) the cost to build it right now 2) the cost to maintain it in the future
- Making the decision to design is based on 2 things: 1) my skill and 2) timeframe
- Remember that in Ruby, everything is an object. So String is an object. And when I use String in my class, I'm doing OO programming and adding dependency (but then again, String won't change)
##Chapter 2: Designing Classes with a Single Responsibility
- Easy to change means
- Changes have no unexpected side effects
- Small changes in requirements == small changes in code
- Existing code is easy to reuse
- Easiest way to make change is to add code that itself is easy to change
- If a class has too much responsibility, when you want to reuse some of it, you may have to duplicate code. On the other hand, if you only want to use its methods, you could use the class but it would have much more than what you need
- What are the tests that I can run to determine "Single Responsibility" in an object?
- Ask the object its method as a question. (e.g. Mr. Gear, what is your ratio?, Mr. Gear, what is your tire size?)
- Try to describe the class in one sentence.
- When everything in a class has 1 central purpose, this is called highly coheisive
- When thinking about designing, ask yourself "What is the future cost of doing nothing today?" Basically, if new requirements come, make better design changes then. Don't try to anticipate what the future will be.
- Create code that embraces change
- When dealing with complex data structures, create classes or structs to provide the reader better understanding (aka readable and intention revealing). Having the a[0] == option1 and a[1] as option2 is not good.
Methods as well as classes should have single responsibility
This code currently has 2 responsibilities:
- It goes through each wheel
- It calculates the diameter
def dimeters
wheels.collect do |wheel|
wheel.rim + (wheel.tire * 2)
end
end
Let's now break this method into 2:
# First, iterate over the array
def diameters
wheels.collect { |wheel| diameter(wheel) }
end
# Second, calculate the diameter of ONE wheel
def diameter(wheel)
wheel.rim + (wheel.tire * 2))
end
The main benefits of having single responsibility methods are:
- Expose previously hidden qualities. It makes the set of things the class does more obvious
- Avoid the need for comments. Now that everything is broken up, the method names will describe what the comments would have.
- Encourage reuse. By breaking methods to smaller pieces, it could allow for more reuse in other projects or codes.
- It makes it easy to move to other classes since what the method does is short and with only 1 responsibility
- It starts showing how the class could be broken up if it starts having too much responsibility. Helps the process of better design.
##Chapter 3: Managing Dependencies All of the behavior is dispersed among the objects. Therefore, for any desired behavior, an object either:
- knows it personally (methods within the class)
- inherits it (using inheritance)
- or knows another object who knows it
This chapter is about the last one (objects that know another object). This knowing automatically creates a dependency and we have to manage it.
Example code with multiple dependencies:
class Gear
attr_reader :chainring, :cog, :rim, :tire
def initialize(chainring, cog, rim, tire)
@chainring = chainring
@cog = cog
@rim = rim
@tire = tire
end
def gear_inches
ratio * Wheel.new(rim, tire).diameter
end
def ratio
chainring / cog.to_f
end
# ...
end
class Wheel
attr_reader :rim, :tire
def initialize(rim, tire)
@rim = rim
@tire = tire
end
def diameter
rim + (tire * 2)
end
# ...
end
Gear.new(52, 11, 26, 1.5).gear_inches
In this code we have 4 major dependencies:
- Name of another class. Gear expects a class named Wheel to exist
- Name of a message that it intends to send to someone other than self. Gear expects a Wheel instance to respond to diameter
- Arguments that a message requires. Gear knows that Wheel.new requires a rim and a tire.
- The order of those arguments. Gear knows the first argument to Wheel.new should be rim, the second, tire.
You can't eliminate everything, but these 2 objects should have so many dependencies. The key is to minimize these links (aka couple) so that when change happens, the most minimal things need to happen.
An additional common dependencies are many linked messages (i.e. bike.wheel.gear.diameter). This sort of "train" is bad code because it means that the bike knows all the objects/classes and down to the last method. This is a very "untrusting" object.
Another common one is when tests are tightly coupled to code. This is the same issue as well code is too close to other code.
Ways/Methods to writing loosely coupled code:
- Inject Dependencies (aka Dependency Injection) (i.e. Gear.new(52, 11, Wheel.new(26, 1.5)).gear_inches)
- Isolate Dependencies (Put them in wrappers like their own methods on put all these Class dependencies in the initialize method)
- Isolate Vulnerable External Messages. The previous 2 ways were dealing with external Class names, this is in regards to external messages "sent to someone other than self." When you create a wrapper method for these external messages, you isolate the code that could change. Also, this allows the other methods that class this wrapper to hold true the "sent message to self."
- Remove Argument-Order Dependencies. Use a hash for the initialize arguments.
- Isolate Multiparameter Initialization. Sometimes, an external class may not allow you to use a hash to initialize. What you can do is create a wrapper module that takes in a hash and calls the external class for you.
#####Direction of your dependency In the previous example, Gear depended on Wheel. Is this the right direction or should Wheel depend on Gear? To determine this, you want to depend on things that change less often than you do.
##Chapter 4: Creating Flexible Interfaces
Classes are most important for the messages they sent between each other.
Even using good design techniques such as Single Responsibility and Dependency Injection, you can still have a poorly too coupled designed set of objects. This is because you have a problem with what the class reveals and not with what the class does.
There are 2 types of interfaces:
- One in which the methods are contained within a class
- One in which the methods are used across classes and is independent of any single class.
The key in designing and tackling a large problem is to not just think about the domain objects, but the messages they pass to one another. A great tool to use to help design the messages being passed back and forth is sequence diagrams. These diagrams exposes the messages being sent, their arguments, and shows if things make sense or not before writing any code or tests.
In the example, step 1 was having 2 objects. A Customer and Trip object. The message that the customer would send to the trip involved which date, how difficult the trip was, and which bikes were available. But after looking at the sequence diagram, a question came up as to whether the Trip object should be responsible for the bikes available. So, from looking at this, you can see that there should be another class called Bike that handled the available bike message that the customer would like to know.
However, the example above has another problem. The customer is dealing with more stuff. They have to know to first call a trip and then for all available trips, ask the bike class for some bikes. This logic shows that the customer knows "how" this shop should be doing its job instead of having the bike service do its work for them.
Look more into this idea of not even knowing what object you want to talk to is. Just knowing the method you're calling and sending self. In the case of a Trip and Mechanic class, there is a way that trip doesn't know of Mechanic, but knows it responds to prepare_trip and sends self. Mechanic doesn't know about Trip, but it knows when it receives a prepare_trip message, it takes the argument, the sender, and can that sender can respond to a bicycle call. The way I look at this, is that there is a contract between the talking objects that they must have these public interfaces, but their objects are unknown to eachother until runtime.
##Chapter 9: Design Cost-Effective Tests
Writing changeable code relies on 3 skills:
- Understand OO Design
- Able to refactor code
- Able to write high-value tests
Tests give you confidence to refactor and create better designed code. With tests, you know that the external behaviors still act the same as you changes things internally.
Only test the incoming and outgoing messages of a class/object. The incoming messages are the public interfaces. The outgoing messages are the public interfaces of the other objects this object is talking to.
Incoming messages are tests of state. They usually assert that once a message is sent to the object, a certain state is then sent back.
Outgoing messages are public interfaces of another object. Therefore, it is the other object's responsibility to test it's state. However, since we are coupled with this object, we should still test this. There are 2 types of these outgoing message tests:
- Some with no side effects and matter only to the sender. They are called queries. These are queries and already have tests since they are the public interfaces of the receiver.
- The other outgoing messages are commands. These are things that change the file, db records are written, or an action is taking by an observer. This involves testing that a messages is sent (or method is called). How many times? and with what arguments? These test behavior and not state.
Make sure to only test the public interfaces and the "sending" of the outgoing messages. Don't test the results of the outgoing messages (because that receiver will test that since its the public interface for it).
To test duck typing methods, use a module test and include it in each of the classes that stem from it.
The next issue is when you have objects linked with other objects. In these cases, interfaces may change and cause your tests to break. Solution, create test doubles/stubs.
To deal with duck type interfaces, use "Role Tests." These are modules that contain the sharable interface and should be include into each of the duck types.
Notes from Sandi's presentation link:
- There are 2 different types of messages/methods
- Query - ask for info but don't change anything
- Command - returns nothing but does change something
Rules
- Test incoming query messages by making assertions about what they send back
- Test the interface, not the implementation
- Test incoming command messages by making assertions about direct public side effects
- Do not test private methods. Do not assert about their results nor expect them to send. But sometimes break this rule if it can save you money in development
- Do not test outgoing query messages. This is already tested by the outgoing object's tests
- Expect to send outgoing command messages
Additional Notes:
- If the message has no visible side-effects the sender should not test it.