Reducing duplication

Designing and Maintaining Software (DAMS)
 Louis Rose

Tactics Accentuate similarities to find differences Favour composition over inheritance Know when to reach for advanced tools
 (metaprogramming, code generation)

Accentuate similarities Aim: make similar code identical to find differences class StuffedCrust def bake(dough) base = stuff(roll(dough)) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class DeepPan def bake(dough) base = roll(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

Accentuate similarities Aim: make similar code identical to find differences class StuffedCrust def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class DeepPan def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

Accentuate similarities Aim: make similar code identical to find differences class StuffedCrust def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end

class DeepPan def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end

def prepare(dough) stuff(roll(dough)) end end

def prepare(dough) roll(dough) end end

Once and Only Once Now we can specify the baking logic in one place class StuffedCrust < Pizza def prepare(dough) stuff(roll(dough)) end end

class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class DeepPan < Pizza def prepare(dough) roll(dough) end end

Template Method Pattern Defer some parts of an algorithm to subclasses class StuffedCrust < Pizza def prepare(dough) stuff(roll(dough)) end end

class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class DeepPan < Pizza def prepare(dough) roll(dough) end end

Template Method Pattern Defer some parts of an algorithm to subclasses Pizza prepare(dough) top(base) cook(raw_pizza, duration)

StuffedCrust prepare(dough)

DeepPan prepare(dough)

Template Method Pattern Defer some parts of an algorithm to subclasses Pizza prepare(dough) top(base) cook(raw_pizza, duration)

StuffedCrust prepare(dough)

DeepPan prepare(dough)

Calzone prepare(dough) top(base)

Template Method Pattern Defer some parts of an algorithm to subclasses Pizza <> abstract prepare(dough) top(base) cook(raw_pizza, duration)

StuffedCrust prepare(dough)

DeepPan prepare(dough)

Calzone prepare(dough) top(base)

Template Method Caveats Some OO languages don’t support
 abstract classes

Template Method Caveats Some OO languages don’t support
 abstract classes including Ruby

Template Method Pattern The best we can is to raise in “abstract” methods class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end def prepare(dough) raise "No implementation” end end

Template Method Caveats Some OO languages don’t support
 abstract classes Can be difficult to communicate which
 methods are to be overridden Breaks down if there is more than
 one axis of change…

Feature Request Some customers like their pizzas to be “well done”

Feature Request Some customers like their pizzas to be “well done” class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class WellDone def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 15.minutes) end end

Feature Request Some customers like their pizzas to be “well done” class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, bake_time) end

class WellDone def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, bake_time) end

def bake_time 12.minutes end end

def bake_time 15.minutes end end

Feature Request Some customers like their pizzas to be “well done” class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, bake_time) end def bake_time 12.minutes end end

class WellDone < Pizza def bake_time 15.minutes end end

But what about prepare? Recall that Pizza doesn’t provide an implementation class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, bake_time) end def bake_time 12.minutes end def prepare(dough) raise "No implementation” end end

class WellDone < Pizza def bake_time 15.minutes end end

Inheritance to the rescue Recall that Pizza doesn’t provide an implementation class WellDoneDeepPan < DeepPan def bake_time 15.minutes end end class WellDoneStuffedCrust < StuffedCrust def bake_time 15.minutes end end

Once and Only Once If only we had multiple inheritance… class WellDoneDeepPan < DeepPan def bake_time 15.minutes end end class WellDoneStuffedCrust < StuffedCrust def bake_time 15.minutes end end

Once and Only Once Ruby modules to the rescue class WellDoneDeepPan < DeepPan include WellDone end class WellDoneStuffedCrust < StuffedCrust include WellDone end module WellDone def bake_time 15.minutes end end

Favour composition… … over inheritance when there is >1 axis of change

prepare

bake_time

StuffedCrust is-a Pizza? Arguably not. class Pizza def bake(dough) base = prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end def prepare(dough) raise "No implementation” end end

class StuffedCrust < Pizza def prepare(dough) stuff(roll(dough)) end end class DeepPan < Pizza def prepare(dough) roll(dough) end end

StuffedCrust is-a Pizza? Arguably not. class Pizza def initialize(recipe) @recipe = recipe end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end def prepare(dough) raise "No implementation” end end

class StuffedCrust < Pizza def prepare(dough) stuff(roll(dough)) end end class DeepPan < Pizza def prepare(dough) roll(dough) end end

StuffedCrust is-a Pizza? Arguably not. class Pizza def initialize(recipe) @recipe = recipe end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end def prepare(dough) raise "No implementation” end end

class StuffedCrust def prepare(dough) stuff(roll(dough)) end end class DeepPan def prepare(dough) roll(dough) end end

StuffedCrust is-a Pizza? Arguably not. class Pizza def initialize(recipe) @recipe = recipe end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class StuffedCrust def prepare(dough) stuff(roll(dough)) end end class DeepPan def prepare(dough) roll(dough) end end

StuffedCrust is-a Pizza? Arguably not. class Pizza def initialize(recipe) @recipe = recipe end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

Pizza.new(StuffedCrust.new)

class StuffedCrust def prepare(dough) stuff(roll(dough)) end end class DeepPan def prepare(dough) roll(dough) end end

Back to WellDone Recall that latest feature request… class Pizza def initialize(recipe) @recipe = recipe end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class WellDone def bake_time 15.minutes end end

Back to WellDone Recall that latest feature request… class Pizza def initialize(recipe) @recipe = recipe end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, 12.minutes) end end

class WellDone def bake_time 15.minutes end end class Medium def bake_time 12.minutes end end

Back to WellDone Recall that latest feature request… class Pizza def initialize(recipe, doneness = Medium.new) @recipe = recipe @doneness = doneness end

class WellDone def bake_time 15.minutes end end

def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, @doneness.bake_time) end end

class Medium def bake_time 12.minutes end end

Back to WellDone Recall that latest feature request… class Pizza def initialize(recipe, doneness = Medium.new) @recipe = recipe @doneness = doneness end end

Back to WellDone Recall that latest feature request… class Pizza def initialize(recipe, doneness = Medium.new) @recipe = recipe @doneness = doneness end end Pizza.new(StuffedCrust.new) Pizza.new(StuffedCrust.new, WellDone.new) Pizza.new(DeepPan.new) Pizza.new(DeepPan.new, WellDone.new)

Strategy Pattern Defer some parts of an algorithm to collaborators Pizza top(base) cook(raw_pizza, duration)

<>

<>

"Recipe" prepare(dough) "Doneness" bake_time

Open/Closed Principle New functionality added without altering existing code Pizza top(base) cook(raw_pizza, duration)

<>

<>

DeepPan prepare(dough) StuffedCrust prepare(dough) WellDone bake_time

Open/Closed Principle New functionality added without altering existing code Pizza top(base) cook(raw_pizza, duration)

<>

<>

DeepPan prepare(dough) StuffedCrust prepare(dough) Medium bake_time WellDone bake_time

Summary Refactor to isolate differences by
 making code more alike Use inheritance and Template Method
 to isolate duplication single axis of change Favour Strategies when
 there are several axes of change

Also important “Do nothing” can be a variation point
 when accentuating similarities. Null Object pattern is, essentially,
 a special form of Strategy. “Nothing is Something”
 Sandi Metz (RailsConf 2015)

Why not inject data? class Pizza def initialize(recipe, doneness = Medium.new) @recipe = recipe @doneness = doneness end

class WellDone def bake_time 15.minutes end end

def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, @doneness.bake_time) end end

class Medium def bake_time 12.minutes end end

Why not inject data? class Pizza def initialize(recipe, bake_time = 12) @recipe = recipe @bake_time = bake_time end

class WellDone def bake_time 15.minutes end end

def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, @bake_time) end end

class Medium def bake_time 12.minutes end end

Why not inject data? class Pizza def initialize(recipe, bake_time = 12) @recipe = recipe @bake_time = bake_time end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, @bake_time) end end

Why not inject data? class Pizza def initialize(recipe, bake_time = 12) @recipe = recipe @bake_time = bake_time end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, @bake_time) end end

Pizza.new(StuffedCrust.new, 15)

Why not inject data? class Pizza def initialize(recipe, bake_time = 12) @recipe = recipe @bake_time = bake_time end def bake(dough) base = @recipe.prepare(dough) raw_pizza = top(base) cook(raw_pizza, @bake_time) end end

Pizza.new(StuffedCrust.new, 15)

Pizza.new(StuffedCrust.new, 17) Pizza.new(StuffedCrust.new, 42)

Designing and Maintaining Software (DAMS) - GitHub

Reducing duplication. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Tactics. Accentuate similarities to find differences. Favour composition over inheritance. Know when to reach for advanced tools. (metaprogramming, code generation). Page 3. Accentuate similarities. Aim: make similar code identical ...

364KB Sizes 0 Downloads 213 Views

Recommend Documents

Designing and Maintaining Software (DAMS) - GitHub
ASTs are tree data structures that can be analysed for meaning (following JLJ in SYAC 2014/15) ... More Cohesive. Avoids Duplication. Clearer. More Extensible.

Designing and Maintaining Software (DAMS) - GitHub
Open-source. Influenced by Perl, Smalltalk, Eiffel, Ada and Lisp. Dynamic. Purely object-oriented. Some elements of functional programming. Duck-typed class Numeric def plus(x) self.+(x) end end y = 5.plus(6) https://www.ruby-lang.org/en/about · http

Designing and Maintaining Software (DAMS) - GitHub
Page 1. Getting Lean. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Lean software… Has no extra parts. Solves the problem at hand and no more. Is often easier to change (i.e., is more habitable). Page 3. The Advice I Want to Give.

Designing and Maintaining Software (DAMS) - GitHub
Why not duplicate? Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Habitable Software. Leaner. Less Complex. Loosely Coupled. More Cohesive. Avoids Duplication. Clearer. More Extensible ??? Page 3. Bad Practice. Page 4. Don't Repeat Yo

Designing and Maintaining Software (DAMS) - GitHub
“We have tried to demonstrate that it is almost always incorrect to begin the decomposition of a system into modules on the basis of a flowchart. We propose instead that one begins with a list of difficult design decisions or design decisions which

Designing and Maintaining Software (DAMS) - GitHub
Tools: Vagrant. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Bugs that appear in production and that can't be reproduced by a developer on their machine are really hard to fix. Problem: “It works on my machine”. Page 3. Why does

Designing and Maintaining Software (DAMS) - GitHub
Clear Documentation. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Bad documentation. Misleading or contradictory find_customer(id). CustomerGateway. Used to look up a customer by their customer number. Page 3. Bad documentation. Red

Designing and Maintaining Software (DAMS) - GitHub
%w.rack tilt date INT TERM..map{|l|trap(l){$r.stop}rescue require l};. $u=Date;$z=($u.new.year + 145).abs;puts "== Almost Sinatra/No Version has taken the stage on #$z for development with backup from Webrick". $n=Module.new{extend. Rack;a,D,S,q=Rack

Designing and Maintaining Software (DAMS) - GitHub
R&D: sketch habitable solutions on paper, using UML. 4. Evaluate solutions and implement the best, using TDD. Probably start again at 3. 5. Give to the product owner to validate. Probably start again at 1. 6. Put into production for customers to eval

Designing and Maintaining Software (DAMS) - GitHub
Habitable Software. Leaner. Less Complex. Loosely Coupled. More Cohesive. Avoids Duplication. Clearer. More Extensible ??? Page 3. Lean. “Perfection is finally achieved not when there is no longer anything to add, but when there is no longer anythi

Designing and Maintaining Software (DAMS) - GitHub
Fixes issue #42. Users were being redirected to the home page after login, which is less useful than redirecting to the page they had originally requested before being redirected to the login form. * Store requested path in a session variable. * Redi

Designing and Maintaining Software (DAMS) - GitHub
Designing and Maintaining Software (DAMS). Louis Rose ... Loosely coupled software is… Flexible: the ... http://www.objectmentor.com/resources/articles/dip.pdf ... Monthly. Sales. Reporting. Sales. Data. Provider. MySQL. Database. Access ...

Designing and Maintaining Software (DAMS) - GitHub
What is it? Several pieces of data are often used together. Why is it problematic? Behaviour that operates on the clump has no home. (and consequently is often duplicated). When does it arise? High cohesion of the clump has not been detected. D

Designing and Maintaining Software (DAMS) - GitHub
Observers. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Page 3. Delivery people need to know when pizzas are ready class Pizza def initialize(delivery_person). @delivery_person = delivery_person end def bake cook # blocking call. @d

Designing and Maintaining Software (DAMS) - GitHub
“We want the reading of code to be easy, even it makes the writing harder. (Of course, there's no way to write code without also reading it, so…)” - Bob Martin. Clean Code. Prentice Hall, 2009. Page 5. Page 6. User Experience. “A person of av

Designing and Maintaining Software (DAMS) - GitHub
Getting loose coupling. Designing and Maintaining Software (DAMS). Louis Rose ... should not depend on low-level modules. Both should depend on abstractions.” “Abstractions should not depend on details. Details should depend on abstractions.” -

Designing and Maintaining Software (DAMS) - GitHub
Automatically detect similar fragments of code. class StuffedCrust def title. "Stuffed Crust " +. @toppings.title +. " Pizza" end def cost. @toppings.cost + 6 end end class DeepPan def title. "Deep Pan " +. @ingredients.title +. " Pizza" end def cost

Designing and Maintaining Software (DAMS) - GitHub
Ruby Testing Frameworks. 3 popular options are: RSpec, Minitest and Test::Unit. We'll use RSpec, as it has the most comprehensive docs. Introductory videos are at: http://rspec.info ...

Designing and Maintaining Software (DAMS) - GitHub
Clear Names. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Naming is hard. “There are only two hard things in Computer. Science: cache invalidation and naming things.” - Phil Karlton http://martinfowler.com/bliki/TwoHardThings.ht

Designing and Maintaining Software (DAMS) - GitHub
Coupling Between Objects. Counts the number of other classes to which a class is coupled (other than via inheritance). CBO(c) = |d ∈ C - (1cl U Ancestors(C))| uses(c, d) V uses(d, c). - Chidamber and Kemerer. A metrics suite for object-oriented des

Designing and Maintaining Software (DAMS) - GitHub
Plug-ins. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Problem. Page 3. Current Architecture. Shareable. Likeable. Food. Pizza. Liking and sharing foods are primary business concerns, so shouldn't be implemented as delegators. Page

Designing and Maintaining Software (DAMS) - GitHub
When we are testing the way that a unit behaves when a condition is met, use a stub to setup the condition. Solution: use stubs for queries class Subscription ... def bill(amount) unless payments.exists(subscription_id: id) payments.charge(subscripti

Designing and Maintaining Software (DAMS) - GitHub
Getting Cohesion. Designing and Maintaining Software (DAMS). Louis Rose. Page 2. Single Responsibility. Principle. A class should have only one reason to change. - Martin and Martin. Chapter 8, Agile Principles, Patterns and Practices in C#, Prentice

Designing and Maintaining Software (DAMS) - GitHub
Size != Complexity. “Imagine a small (50 line) program comprising. 25 consecutive "IF THEN" constructs. Such a program could have as many as 33.5 million distinct control paths.” - Thomas J. McCabe. IEEE Transactions on Software Engineering, 2:4,