Review the video about the testing, and try to get more from this lecture:
"The Clean Code Talks -- Inheritance, Polymorphism, & Testing"
https://www.youtube.com/watch?v=4F72VULWFvcaction items:
1. put sample code in C#, and then, check in git;
2. write down key words, and easy for review and follow as rules.
Julia's sample code in C#:
C# code using conditional implementation, one class Node
The source code folder is here.
better solution: Node, OpNode, ValueNode:
optimal solution:
Perfect example to learn S.O.L.I.D. OO principles, open for extension, close for change. The above optimal solution does not have any if statement, and any new arithmetic operation just needs a new class, no need to touch existing code. Julia passes the learn Open/Close principle. Move on to next one, Liskov substitution principle!
Notes:
Premise - Most ifs can be replaced by polymorphism
Why?
Easy to read, test without ifs.
polymorphic systems are easier to maintain.
Julia: write down the sentence in the talk: supporting facts: one execution path, so easy to understand, test, and extend.
Use polymorphism
If an object should behave differently based on its state.
If you have to check the same conditions in multiple places.
- binding is not on compile time,
Use conditionals
Mainly to do comparisons of primitive objects: >,<,==, !=
There other uses, but today we focus on avoiding if
Do not return null in the method
To be if free
Never return a null, instead return a Null object, e.g. an empty list
Don't return error codes, instead throw an Exception (Run Time please!)
Rampant (wild, unchecked) subclassing
Polymorphism uses subclassing
Be careful about runaway subclassing (another talk focuses on that)
avoid pitfall: inheritance hierarchy - too complex
State based behavior
Replace conditionals with polymorphism
You have a conditional that chooses different behavior depending on the type of an object.
Move each leg of the conditional to an overriding method in a subclass.
Make the original method abstract.
Example:
double getSpeed(){
switch (_type){
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * _numberOfCoconuts;
case NORWEGIAN_BLUE:
return (_isNailed)? 0 : getBaseSpeed(_voltage);
}
throw new RuntimeException ("Should be unreachable");
}
Suggestion to solve the problem: use subclasses
Isn't there already a type system?
An Exercise:
Model this!
1 + 2 * 3
favorite interview question by the speaker:
Represent this as a tree
+
/ \
1 *
/ \
2 3
Node object to store the information
evaluate()
computes the result of an expression
Most of people come out solution like the following:
Using conditionals
class Node{
char operator;
double value;
Node left;
Node right;
double evaluate(){
switch(operator){
case '#': return value;
case '+': return left.evaluate() + right.evaluate();
case '*" return left.evaluate() * right.evaluate();
case ... // edit this for each new operator
}
}
}
Big problem on this:
graphic representation,
Node
op:char
value: double
left: Node
right:Node
--------------
evaluate():double
Julia could not figure out the analysis here <- first time to memorize this analysis, and also try to learn reasoning
Analyzing attributes
# + *
function yes yes
value yes
left yes yes
right yes yes
Two different behaviors are fighting here, (see the above table),
if you are the operation node, then you need your left and right child; whereas value node, you just need value.
Either you need value or you need left and right, but you never need both.
One class have multiple tasks entangled, polymorphism is through if statement, not through polymorphism.
video time: 11:42/38:24
Let us break it up:
Node
--------------
evaluate(): double
| |
ValueNode OpNode
value: double op: char
--------------- left: Node
evaluate: double right: Node
----------------
evaluate(): double
As showing above, break Node into ValueNode and OpNode, so ValueNode does not have left and right child because of no meaning over there.
Operations and values
abstract class Node{
abstract double evaluate();
}
class ValueNode extends Node{
double value;
double evaluate(){
return value;
}
}
class OpNode extends Node{
char operator;
Node left;
Node right;
double evaluate(){
switch(operator) {
case '+': return left.evaluate() + right.evaluate();
case '-': return left.evaluate() + right.evaluate();
case ... // edit this for each new operator
}
}
}
How to extend this? Every time you add a new operator, need to hold on source code, and add code in switch statement, how to make it better?
Tree looks like:
OpNode
+
/ \
ValueNode OpNode
1 *
/ \
ValueNode ValueNode
2 3
OpNode divides into AdditionNode and MultiplicationNode
OpNode
------------------
left: Node
right: Node
------------------
evaluate(): double
AdditionNode MultiplicationNode
---------------------------- --------------------------
evaluate(): double evaluate(): double
abstract class Node{
abstract double evaluate();
}
class ValueNode extends Node{
double value;
double evaluate(){
return value;
}
}
class abstract OpNode extends Node{
Node left;
Node right;
abstract evaluate();
}
video time: 14:39/38:24
Operation classes
class AdditionNode extends OpNode{
double evaluate(){
return left.evaluate() + right.evaluate();
}
}
class MultiplicationNode extends OpNode{
double evaluate(){
return left.evaluate() + right.evaluate();
}
}
Now, the new tree diagram:
AdditionalNode
+
/ \
ValueNode MultiplicationNode
1 *
/ \
ValueNode ValueNode
2 3
Julia's C# implementation, the SOLID principle applied solution. Here is the code.
Further exploration
Define toString() prints the infix expression placing parenthesis only when necessary.
Add new math operators: exponentiation, factorial, logarithm, trigonometry
Summary
A polymorphic solution is often better because:
1. new behavior can be added without having the original source code, and
2. each operation/concern is separated in a separate file which makes it easy to test/understand.
Prefer polymorphism over conditionals:
switch almost always means you should use polymorphism
if is more subtle ... sometimes an if is just an if
Repeated Condition
22:36/38:24
Two piles
piles of Objects pile of Construction
. business logic . factories
. the fun stuff . builders
. Provider<T>
. given the collaborators needed .created and provides collaborators (Denpendency Injection)
Construction
class Consumer{
Consumer(Update u) {...}
}
class Factory{
Consumer build(){
Update u = FLAG_i18n_ENABLED? new I18NUpdate() : new NonI18NUpdate();
return new Consumer(u);
}
}
Benefits
Conditional is localized in one place
No more duplication
Separation of responsibilities, and global state
Common code is in one location
Testing independently easily, and in parallel
Looking at the subclasses makes it clear what the differences are
When to use polymorphism
Behavior changes based on state
Parallel conditionals are in multiple places in code
Be pragramatic
You will still have some conditionals
Question and answers:
Argument: Easy to read switch statement vs a lot of subclasses
File over thousand line of length
Easy to create a new class
Single responsibility
Behavior is controlled by a lot of flags vs. a lot of classes collaboration
Julia's comment:
1. Julia watched the video over 3 times, she likes the teaching and sample code.
2. Julia likes to refactor the C# code, learn OO design. Strongly recommend this video to friends.
3. C# code using conditional implementation, one class Node. Here is the link.
No comments:
Post a Comment