Slide 1

Slide 1 text

clean code from theory to practice by Luís Zamith

Slide 2

Slide 2 text

Clean Code TAKING YOUR PROGRAMS TO THE LAUNDRY

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

1. Looks like you cared

Slide 5

Slide 5 text

1. Looks like you cared 2. Pretty much what you expected

Slide 6

Slide 6 text

1. Looks like you cared 2. Pretty much what you expected 3. Language looks as perfect fit for problem

Slide 7

Slide 7 text

1. Looks like you cared 2. Pretty much what you expected 3. Language looks as perfect fit for problem My definition

Slide 8

Slide 8 text

circular friction reduction device with central axis

Slide 9

Slide 9 text

circular friction reduction device with central axis =

Slide 10

Slide 10 text

Design Patterns circular friction reduction device with central axis =

Slide 11

Slide 11 text

class Report def initialize @title = 'Monthly Report' @text = [ 'Things are going', 'really, really well.' ] end ! def output_report ...HTML specific stuff... end end template method: reports

Slide 12

Slide 12 text

bad code class Report def initialize @title = 'Monthly Report' @text = [ 'Things are going', 'really, really well.' ] end ! def output_report ...HTML specific stuff... end end template method: reports

Slide 13

Slide 13 text

template method: reports class Report def initialize @title = 'Monthly Report' @text = ['Things are going', 'really, really well.'] end ! def output_report output_start output_head @text.each do |line| output_line(line) end output_end end ! def output_start; end ! def output_head output_line(@title) end ! def output_line(line) raise 'Called abstract method: output_line' end ! def output_end; end end

Slide 14

Slide 14 text

template method: reports better code class Report def initialize @title = 'Monthly Report' @text = ['Things are going', 'really, really well.'] end ! def output_report output_start output_head @text.each do |line| output_line(line) end output_end end ! def output_start; end ! def output_head output_line(@title) end ! def output_line(line) raise 'Called abstract method: output_line' end ! def output_end; end end

Slide 15

Slide 15 text

Design Principles THE THEORY BEHIND THE THEORY

Slide 16

Slide 16 text

Optimize for Readability and Maintainability

Slide 17

Slide 17 text

S O L I D Uncle Bob Martin

Slide 18

Slide 18 text

S O L I D ingle responsibility Uncle Bob Martin

Slide 19

Slide 19 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 20

Slide 20 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 21

Slide 21 text

class TokenizedModel < SimpleDelegator! def save! __getobj__.token ||= SecureRandom.urlsafe_base64! __getobj__.save! end! ! def to_param! __getobj__.token! end! end TokenizedModel.new(invitation) Decorator Pattern

Slide 22

Slide 22 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def deliver! Mailer.invitation_notification(self).deliver! end! end

Slide 23

Slide 23 text

Responsibility = Reason to Change

Slide 24

Slide 24 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 25

Slide 25 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 26

Slide 26 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 27

Slide 27 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 28

Slide 28 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 29

Slide 29 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 30

Slide 30 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 31

Slide 31 text

class Invitation < ActiveRecord::Base! EMAIL_REGEX = /\A([^@\s]+)@((:?[a-z0-9]+\.)+[a-z]{2,})\z/i! STATUSES = %w(pending accepted)! ! belongs_to :sender, class_name: 'User'! belongs_to :survey! ! before_create :set_token! ! validates :recipient_email, presence: true, format: EMAIL_REGEX! validates :status, inclusion: { in: STATUSES }! ! def to_param! token! end! ! def deliver! Mailer.invitation_notification(self).deliver! end! ! private! ! def set_token! self.token = SecureRandom.urlsafe_base64! end! end

Slide 32

Slide 32 text

S O L I D

Slide 33

Slide 33 text

S O L I D pen closed

Slide 34

Slide 34 text

# does not follow OCP! class Purchase! def charge_user!! Stripe.charge(user: user, amount: amount)! end! end

Slide 35

Slide 35 text

# does not follow OCP! class Purchase! def charge_user!! Stripe.charge(user: user, amount: amount)! end! end # follows OCP by depending on an abstraction! class Purchase! def charge_user!(payment_processor)! payment_processor.charge(user: user, amount: amount)! end! end

Slide 36

Slide 36 text

class Printer! def initialize(item)! @item = item! end! ! def print! thing_to_print = case @item! when Text! @item.to_s! when Image! @item.filename! when Document! @item.formatted! end! ! send_to_printer(thing_to_print)! end! end

Slide 37

Slide 37 text

class Printer! def initialize(item)! @item = item! end! ! def print! send_to_printer(@item.printable_representation)! end! end Dependency Injection

Slide 38

Slide 38 text

# does not follow OCP! class Unsubscriber! def unsubscribe!! SubscriptionCanceller.new(user).process! CancellationNotifier.new(user).notify! CampfireNotifier.announce_sad_news(user)! end! end

Slide 39

Slide 39 text

class UnsubscriptionCompositeObserver! def initialize(observers)! @observers = observers! end! ! def notify(user)! @observers.each do |observer|! observer.notify(user)! end! end! end! ! class Unsubscriber! def initialize(observer)! @observer = observer! end! ! def unsubscribe!(user)! observer.notify(user)! end! end Composite Pattern

Slide 40

Slide 40 text

class UnsubscriptionCompositeObserver! def initialize(observers)! @observers = observers! end! ! def notify(user)! @observers.each do |observer|! observer.notify(user)! end! end! end! ! class Unsubscriber! def initialize(observer)! @observer = observer! end! ! def unsubscribe!(user)! observer.notify(user)! end! end Observer Pattern

Slide 41

Slide 41 text

class UnsubscriptionCompositeObserver! def initialize(observers)! @observers = observers! end! ! def notify(user)! @observers.each do |observer|! observer.notify(user)! end! end! end! ! class Unsubscriber! def initialize(observer)! @observer = observer! end! ! def unsubscribe!(user)! observer.notify(user)! end! end

Slide 42

Slide 42 text

A word of caution

Slide 43

Slide 43 text

S O L I D

Slide 44

Slide 44 text

S O L I D iskov substitution

Slide 45

Slide 45 text

Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T. “

Slide 46

Slide 46 text

blah q(x) blah blah blah blah blah blah blah blah x blah T. Blah q(y) blah blah blah blah blah blah y blah blah S blah S blah blah blah blah T. “

Slide 47

Slide 47 text

class Signup! def initialize(attributes)! @attributes = attributes! end! ! def save! account = Account.create!(@attributes[:account])! account.users.create!(@attributes)! end! end! ! class SignupsController < ApplicationController! def create! build_signup.save! end! ! def build_signup! Signup.new(params[:signup])! end! end

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

A wild requirement appears…

Slide 50

Slide 50 text

class InvitationSignup < Signup! def save(invitation)! user = super! invitation.accept(user)! user! end! end A wild requirement appears…

Slide 51

Slide 51 text

class SignupsController < ApplicationController! def create! signup = build_signup! if params[:invitation_id]! invitation = Invitation.find(params[:invitation_id])! signup.save(invitation)! else! signup.save! end! end! ! def build_signup! if params[:invitation_id]! InvitationSignup.new(params[:signup])! else! Signup.new(params[:signup])! end! end! end

Slide 52

Slide 52 text

class SignupsController < ApplicationController! def create! signup = build_signup! if params[:invitation_id]! invitation = Invitation.find(params[:invitation_id])! signup.save(invitation)! else! signup.save! end! end! ! def build_signup! if params[:invitation_id]! InvitationSignup.new(params[:signup])! else! Signup.new(params[:signup])! end! end! end

Slide 53

Slide 53 text

def InvitationSignup < Signup! def initialize(attributes, invitation)! super(attributes)! @invitation = invitation! end! ! def save! user = super! @invitation.accept(user)! user! end! end The public API remains the same

Slide 54

Slide 54 text

class SignupsController < ApplicationController! def create! build_signup.save! end! ! def build_signup! if params[:invitation_id]! InvitationSignup.new(params[:signup])! else! Signup.new(params[:signup])! end! end! end

Slide 55

Slide 55 text

S O L I D

Slide 56

Slide 56 text

S O L I D nterface segragation

Slide 57

Slide 57 text

public class WebPage implements IWebPage {! public String currentUrl() {};! public void setUrl(String url) {};! public String[] consoleMessages() {};! public String[] alertMessages() {};! public String[] confirmMessages() {};! public String[] promptMessages() {};! }

Slide 58

Slide 58 text

public class WebPage implements IWebPage {! public String currentUrl() {};! public void setUrl(String url) {};! public String[] consoleMessages() {};! public String[] alertMessages() {};! public String[] confirmMessages() {};! public String[] promptMessages() {};! } Not all methods are related

Slide 59

Slide 59 text

public class VisitCommand {! public VisitCommand(WebPage page, String url) {! this.page = page;! this.url = url;! }! ! public void start() {! page.setUrl(url)! }! ! private WebPage page;! private String url;! } Command Pattern

Slide 60

Slide 60 text

public interface HasUrl {! public String currentUrl() {};! public void setUrl(String url) {};! }! ! public interface HasMessages {! public String[] consoleMessages() {};! public String[] alertMessages() {};! public String[] confirmMessages() {};! public String[] promptMessages() {};! } public class WebPage implements HasUrl, HasMessages {

Slide 61

Slide 61 text

public class VisitCommand {! public VisitCommand(HasUrl page, String url) {! this.page = page;! this.url = url;! }! ! public void start() {! page.setUrl(url)! }! ! private HasUrl page;! private String url;! }

Slide 62

Slide 62 text

public class VisitCommand {! public VisitCommand(HasUrl page, String url) {! this.page = page;! this.url = url;! }! ! public void start() {! page.setUrl(url)! }! ! private HasUrl page;! private String url;! }

Slide 63

Slide 63 text

http://tenderlovemaking.com/2014/06/02/yagni-methods-are-killing-me.html loop do params[:foo] params.delete :foo params[:foo] = [Object.new] end def convert_value_to_parameters(value) if value.is_a?(Array) && !converted_arrays.member?(value) converted = value.map { |_| convert_value_to_parameters(_) } converted_arrays << converted converted elsif value.is_a?(Parameters) || !value.is_a?(Hash) value else self.class.new(value) end end

Slide 64

Slide 64 text

merge! delete_if keep_if select! reject!

Slide 65

Slide 65 text

S O L I D

Slide 66

Slide 66 text

S O L I Dependency inversion

Slide 67

Slide 67 text

“High level modules should not depend on low level modules, they should both depend on abstractions

Slide 68

Slide 68 text

#Violates DIP! class Copier! def self.copy! reader = KeyboardReader.new! writer = Printer.new! ! keystrokes = reader.read_until_eof! writer.write(keystrokes)! end! end

Slide 69

Slide 69 text

#Obeys DIP. Is decoupled and reusable.! class Copier! def initialize(reader, writer)! @reader = reader! @writer = writer! end! ! def copy! keystrokes = @reader.read_until_eof! @writer.write(keystrokes)! end! end! ! Copier.new(KeyboardReader.new, Printer.new)! Copier.new(KeyboardReader.new, NetworkPrinter.new)

Slide 70

Slide 70 text

class NetworkPrinterAdapter! def initialize(network_printer)! @network_printer = network_printer! end! ! def write(keystrokes)! @network_printer.print(keystrokes)! end! end! ! network_printer = ! NetworkPrinterAdapter.new(NetworkPrinter.new)! Copier.new(KeyboardReader.new, network_printer) Adapter Pattern

Slide 71

Slide 71 text

class Product < ActiveRecord::Base! has_many :purchases! end! ! class Purchase < ActiveRecord::Base! belongs_to :Product! ! def has_receipt?! receipt.present?! end! ! def receipt_id! receipt.id! end! ! def receipt_address! receipt.details.address! end! ! private! ! def receipt! Stripe::Receipt.find(stripe_order_id)! end! end

Slide 72

Slide 72 text

class PurchaseWithReceipt < SimpleDelegator! def initialize(purchase, receipt_finder)! super(purchase)! @receipt_finder = receipt_finder! end! ! def has_receipt?! receipt.present?! end! ! def receipt_id! receipt.id! end! ! def receipt_address! receipt.details.address! end! ! private! ! def receipt! @receipt_finder.find(stripe_order_id)! end! end class Product < ActiveRecord::Base! has_many :purchases! end! ! class Purchase < ActiveRecord::Base! belongs_to :product! end

Slide 73

Slide 73 text

S O L I D ingle responsibility pen closed iskov substitution nterface segragation ependency inversion Uncle Bob Martin

Slide 74

Slide 74 text

RECAP DESIGN PRINCIPLES DESIGN PATTERNS + = CLEAN CODE

Slide 75

Slide 75 text

Code Smells Sir, your issues have some code.

Slide 76

Slide 76 text

long method Smell number 1

Slide 77

Slide 77 text

No content

Slide 78

Slide 78 text

Can’t tell what it does at a glance, it’s

Slide 79

Slide 79 text

Can’t tell what it does at a glance, it’s TOO LONG

Slide 80

Slide 80 text

Can’t tell what it does at a glance, it’s TOO LONG More than one level of nesting, probably

Slide 81

Slide 81 text

Can’t tell what it does at a glance, it’s TOO LONG More than one level of nesting, probably TOO LONG

Slide 82

Slide 82 text

Can’t tell what it does at a glance, it’s TOO LONG More than one level of nesting, probably TOO LONG More than one level of abstraction, may be

Slide 83

Slide 83 text

Can’t tell what it does at a glance, it’s TOO LONG More than one level of nesting, probably TOO LONG TOO LONG More than one level of abstraction, may be

Slide 84

Slide 84 text

Can’t tell what it does at a glance, it’s TOO LONG More than one level of nesting, probably TOO LONG TOO LONG More than one level of abstraction, may be More than 5 lines

Slide 85

Slide 85 text

Can’t tell what it does at a glance, it’s TOO LONG More than one level of nesting, probably TOO LONG TOO LONG More than one level of abstraction, may be More than 5 lines TOO LONG

Slide 86

Slide 86 text

Can’t tell what it does at a glance, it’s TOO LONG More than one level of nesting, probably TOO LONG TOO LONG More than one level of abstraction, may be More than 5 lines TOO LONG REFACTOR

Slide 87

Slide 87 text

Extract Method: Split method into many auxiliary methods Solution #1

Slide 88

Slide 88 text

Replace temp with query: Move local variables into methods Solution #2

Slide 89

Slide 89 text

large class Smell number 2

Slide 90

Slide 90 text

No content

Slide 91

Slide 91 text

Need to scroll to know what it does, it’s

Slide 92

Slide 92 text

Need to scroll to know what it does, it’s TOO BIG

Slide 93

Slide 93 text

Need to scroll to know what it does, it’s TOO BIG More private methods than public, it’s

Slide 94

Slide 94 text

Need to scroll to know what it does, it’s TOO BIG More private methods than public, it’s TOO BIG

Slide 95

Slide 95 text

Need to scroll to know what it does, it’s TOO BIG More private methods than public, it’s TOO BIG More than seven methods, probably

Slide 96

Slide 96 text

Need to scroll to know what it does, it’s TOO BIG More private methods than public, it’s TOO BIG TOO BIG More than seven methods, probably

Slide 97

Slide 97 text

Need to scroll to know what it does, it’s TOO BIG More private methods than public, it’s TOO BIG TOO BIG More than seven methods, probably More than 100 lines

Slide 98

Slide 98 text

Need to scroll to know what it does, it’s TOO BIG More private methods than public, it’s TOO BIG TOO BIG More than seven methods, probably More than 100 lines TOO BIG

Slide 99

Slide 99 text

Need to scroll to know what it does, it’s TOO BIG More private methods than public, it’s TOO BIG TOO BIG More than seven methods, probably More than 100 lines TOO BIG REFACTOR

Slide 100

Slide 100 text

Extract class: If the class has multiple responsibilities Solution #1

Slide 101

Slide 101 text

Extract decorator: If the class has many objects related to a single action Solution #2

Slide 102

Slide 102 text

class Report def initialize @title = 'Monthly Report' @text = [ 'Things are going', 'really, really well.' ] end ! def output_report ...HTML specific stuff... end end strategy pattern: reports

Slide 103

Slide 103 text

bad code class Report def initialize @title = 'Monthly Report' @text = [ 'Things are going', 'really, really well.' ] end ! def output_report ...HTML specific stuff... end end strategy pattern: reports

Slide 104

Slide 104 text

class Report def initialize(formatter) @title = 'Monthly Report' @text = [ 'Things are going', 'really, really well.' ] @formatter = formatter.new(@title, @text) end ! def formatter=(formatter) @formatter = formatter.new(@title, @text) end ! def output_report @formatter.output_report end end strategy pattern: reports

Slide 105

Slide 105 text

class Report def initialize(formatter) @title = 'Monthly Report' @text = [ 'Things are going', 'really, really well.' ] @formatter = formatter.new(@title, @text) end ! def formatter=(formatter) @formatter = formatter.new(@title, @text) end ! def output_report @formatter.output_report end end better code strategy pattern: reports

Slide 106

Slide 106 text

strategy pattern: reports report = Report.new(HTMLFormatter) report.output_report ! report.formatter = PlainTextFormatter report.output_report

Slide 107

Slide 107 text

beware of the god class

Slide 108

Slide 108 text

feature envy Smell number 3 All your features are mine

Slide 109

Slide 109 text

Smelling feature envy: 4SYMPTOMS

Slide 110

Slide 110 text

Smelling feature envy: Many references to the same object

Slide 111

Slide 111 text

Smelling feature envy: Local variables or params that are used more than instance variables and methods

Slide 112

Slide 112 text

Smelling feature envy: Methods that include a class name in their name

Slide 113

Slide 113 text

Smelling feature envy: Private methods on the same class with the same parameter

Slide 114

Slide 114 text

Smelling feature envy: class Completion! def score! answers.inject(0) do |result, answer|! question = answer.question! result + question.score(answer.text)! end! end! end

Slide 115

Slide 115 text

Extract method: If possible move part of a method to the appropriate class Solution #1

Slide 116

Slide 116 text

Move method: Move the entire method into the right class Solution #2

Slide 117

Slide 117 text

Solving feature envy: class Answer! def score! question.score(text)! end! end! ! class Completion! answers.inject(0) do |result, answer|! result + answer.score! end! end

Slide 118

Slide 118 text

DESIGN PRINCIPLES DESIGN PATTERNS + = CLEAN CODE

Slide 119

Slide 119 text

missing some style

Slide 120

Slide 120 text

Learn the principles patterns Know the use the smells

Slide 121

Slide 121 text

BLEEDING EDGE

Slide 122

Slide 122 text

Hexagonal Architecture Domain Driven Design (DDD) Presenters Service Objects Value Objects and Immutability Behavior Driven Development (BDD) Object Composition vs Inheritance

Slide 123

Slide 123 text

Practical Object-Oriented Design in Ruby

Slide 124

Slide 124 text

Practical Object-Oriented Design in Ruby Growing Object-Oriented Software

Slide 125

Slide 125 text

Practical Object-Oriented Design in Ruby Growing Object-Oriented Software Test-Driven Development

Slide 126

Slide 126 text

Practical Object-Oriented Design in Ruby Growing Object-Oriented Software Test-Driven Development Clean Code

Slide 127

Slide 127 text

thanks!