Skip to content

Instantly share code, notes, and snippets.

@jonnyjava
Created May 11, 2017 15:12
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonnyjava/b04c909e449ab6a93981fd07f015a767 to your computer and use it in GitHub Desktop.
Save jonnyjava/b04c909e449ab6a93981fd07f015a767 to your computer and use it in GitHub Desktop.
Refactoring - Ruby edition

Refactoring

Preface

What is refactoring?

Refactoring is the process of changing a software system in such a way that it not alter the external behaviour of the code yet improves its internal structure It's a process of clean up to minimize the chances to introduces bugs. Refactoring means improve the code design

Chapter 1 - A first example

  • Start writing a good test coverage
  • First refactor, then add new features
  • Refactor changes the code in small steps.
  • Any fool can write code that a computer can understand. Good programmers write code that humans can understand
  • Good programmer know that is hard or impossible write clean code at first try

Chapter 2 - Principles in refactoring

  • Refactoring is not a one shot activity, but a continuos process
  • Refactoring is not another step in code generation, refactoring is code generation.

Refactoring means make a change in the internal structure of the code to make it easier to understand and cheaper to modify without changing its observable behaviour

This means:

  • Refactoring improves the design of the software
  • Refactoring makes the software easier to understand (code is a conversation with the computer, the clearer the conversation, the easier is to understand it)
  • Refactoring helps to find bugs
  • Refactoring helps to program faster

A good rule to know when to refactor is the rule of three which means: refactor when you are doing something for the third time. But there are some others good opportunities

  • when adding a function
  • when fixing a bug
  • when reviewing code

Refactor adds two greats values: immediate and for the future. In fact code hard to read is hard to modify, duplicated logic is hard to modify, complex conditional logic is hard to modify, code to modify running code is hard to modify. Refactoring solves all these kind of problems.

Refactoring brings to add some new layers of indirections

  • Indirection enables to share logic
  • Indirection helps to separate intention and implementation
  • Indirection lets to isolate changes
  • Indirection allows to encode conditional logic

Refactoring could be bring to some breaking changes, for example in classes interfaces; destructive changes should be managed by a some deprecation mechanism.

Refactoring a database could be destructive too, it could be useful to introduce a layer of indirection with database migrations to modify the schema before migrating datas.

Design changes in progress can make refactoring harder.

Sometimes is easier rewrite instead of refactor, for example when the code is broken, or does not work.

Refactoring could be avoided when a deadline is close but only if there is a management mechanism to pay the technical debt as soon as possible. Indeed a chronical lack of time is a big signal that refactoring is needed.

Change code only because you philosophically disagree is wrong. Creating working software is always the biggest priority, even above it's beauty.

Refactoring complements design, is better design a bit and then refactor than do big designs and big refactors. A flexible design is generally complex, refactoring can lead to flexible solutions without sacrificing design, instead, it lets emerge the architecture.

Performance should lead the refactors, not speculations

Chapter 3 - Bad smells in code

There are some patterns or structures which suggest the possibility of a refactor, they are indications of possible troubles. The name for them is bad smell

  • Duplicated code. Because every change needs to be repeated
  • Long methods. Because the longer they are the harder is to understand their behaviour.
  • Comments. A comment is always a fail. It means the code is so complex to understand that requires an additional explanation. A commented code block can be replaced by code whose name is based on the comment
  • Large class. As long methods, the larger they are the harder is to understand their behaviour. Common prefixes or suffixes in a subsets of variables or methods suggest the opportunity for a component.
  • Long parameter list. Global data is evil and painful.
  • Divergent change. Any change should be easy to apply in a single point. If not, probably is better to split the code in separate pieces.
  • Shotgun surgery. When a change in a point brings more changes and each one brings more and more everywhere is a signal that an aggregation is needed.
  • Feature envy. When a method reference more an external entity than the one where is, probably it needs to be moved. Is better to put together things that change together
  • Data clumps. Sometimes there are datas hanging together. It's a sign of a new (value?) object
  • Primitive obsession. Creating special kind of data with a specific behaviour means that an object is needed
  • Case statements. Is better to use polymorphims
  • Parallel inheritances or hierarchies. Is recognizable by the set of prefixes or suffixes repeated in more than one class in differents hierarchies.
  • Lazy class. A useless one
  • Speculative generality. Useless delegation or code used only by tests shuld be simply deleted.
  • Temporary field. When a variable has a values only under specific conditions.
  • Message chains
  • Middle man is a class which only (or almost only) delegates to another.
  • Inappropriate intimacy is tipical on inheritance, it happens when a class knows too much of another one.
  • Alternative classes with different interfaces.
  • Incomplete library.
  • Data class.
  • Refused bequest. Inheritance where only private parts are overwritten
  • Metaprogreamming madness.
  • Disjointed API.
  • Repetitive boilerplate.

Chapter 4 - Building tests

To refactor, the essential precondition is having solid tests. One of the most useful times to write tests is before programming because they help to go as faster as possible. No test, no refactor. Not only unit tests, but functionals too could do the job. The key is to test the areas where there is more possibility to do something wrong. Exceptions should be tested too. Is better to spend a reasonable amount of time to try to catch the most part of bugs than spending a big amount of time to catch them all.

Chapter 5-12 - Refactoring catalog

Chapter 6 - Composing methods

  • Extract method extract a piece of logic in a separate, more meaningful, method.
  • Inline method put the method inside its caller.
  • Inline temp replace a temp variable with its expression.
  • Replace temp with query replace the temp variable with a method containing the expression.
  • Replace temp with chain change methods to support chaining thus removing the need of a temp variable.
  • Introduce explaining variable when an expression is complex is better to put its result into a meaningful variable.
  • Split temporary variable make a temp variable for each separate assignment.
  • Remove assignments to parameters with a temp variable
  • Replace method with method object when a long method uses internal variables and extract method cannot be applied, is better to turn the long method in an object
  • Substitute algorithm when there is an easier way to do it.
  • Replace loop with collection closures
  • Extract surrounding method when the surrounding or preparatory code is duplicated but the algorithm is different, extract the duplicate code into a method that accepts a block and yields back to the caller to execute the unique code.
  • Introduce class annotation declaring the behaviour in the class definition
  • Introduce named parameter when there is a long list of params, convert it into an hash where the keys are the params names
  • Remove named parameter when it adds more complexity instead of improving the fluency
  • Remove unused default param when default is never used
  • Dynamic method definition as enum does.
  • Replace dynamic receptor with dynamic method definition
  • Move eval from runtime to parse time

Chapter 7 - Moving features between object

  • Move method to another class
  • Move field to another class
  • Extract class splitting one into two
  • Inline class when feature of class are used only by another one
  • Hide delegate
  • Remove middle man when there is too much unnecessary delegation

Chapter 8 - Organizing data

  • Self encapsulate field creating getters and setters
  • Replace data value with value object
  • Change value to reference when the object does not stand for a real, concrete and mutable thing in real world
  • Change reference to value when the object should be immutable
  • Replace array with object when the array does not contain a collection of similar datas
  • Replace hash with object when the hash is used for different purposes
  • Change unidirectional association to bidirectional adding backpointers
  • Change bidirectional association to unidirectional dropping the unneeded end of the association
  • Replace magic numbers with simbolic constants
  • Encapsulate collection
  • Replace subclass with fields and eliminate the subclass
  • Lazy initialized attribute on access instead of at construction time
  • Eagerly initialized attribute on construction

Replacing type codes techniques

Instead of using if-else chains or case statements

  • Using polymorphism when there is a specialization of the same behaviour
  • Using module extension using mixing when the behaviour can be aggregated into an unique module
  • Using state pattern when the condition depends on a particular state
  • Using strategy pattern when simplifying an algorithm

Chapter 9 - Simplifying conditional expressions

  • Decompose into boolean methods
  • Recompose when boolean methods are too trivial
  • Consolidate when more than one expression return the same value
  • Consolidate duplicated fragments moving in an unique block all the logic repeated in conditional branches
  • Remove control flag with guard clause or break
  • Replace conditional with polymorphism
  • Introduce assertion when the execution depends on the value of a condition

Chapter 10 - Making method calls simpler

The convention is separate query and modifiers methods. Good interfaces show only what they have to do and no more

  • Rename method
  • Add parameter
  • Remove parameter
  • Separate query from modifier to separate the code which changes the state of an object from the one which returns the value
  • Parameterize method when some methods do similar things depending on different values in the method body
  • Replace parameter with explicit method
  • Preserve whole object passing it instead of a long list of params
  • Introduce parameter object when a group of values always go together
  • Remove setting method
  • Hide method making it private
  • Replace constructor with factory method when there is some conditional logic determining which object to create
  • Replace error code with exception raise an exception instead
  • Replace exception with test calling the tests first
  • Introduce gateway to simplify API access
  • Introduce expression builder to provide a fluent interface on the top of the API

Chapter 11 - Dealing with generalization

  • Pull up method from subclasses into an unique one in the superclass
  • Push down method
  • Extract module
  • Inline module when a module is not worth
  • Extract subclass when some features are used only by a particular case
  • Introduce inheritance when two classes have similar features
  • Collapse hierarchy when inheritance is not worth
  • Form template method when two ore more classes have similar methods secuences but their behaviour is different the solution is bring the secuence into the superclass/module and leave the steps implementation in the subclasses
  • Replace inheritance with delegation when the subclass only uses some and not all the features of the superclass
  • Replace delegation with hierarchy putting all the delegation in a module
  • Replace abstract superclass with module when the superclass is never instantiated

Chapter 12 - Big refactorings

They are different from the day-to-day refactors because they require a larger time investment. Generally they need multiple techinques to be applied at the same time over a big portion of the code. Some commons scenarios are:

  • Tease apart inheritance when an inheritance hierarchy is doing more than one thing at once, the solution is split all the hierarchy into two and use delation to invoke one from the other
  • Convert procedural design to objects working with objects does not mean working object oriented
  • Separate domain from presentation getting rid of all logic contained in views and controllers
  • Extract hierarchy when a class is doing too much; a good signal of this is a complex conditional logic in its code

Chapter 13 - Putting it all together

Some rules of thumb to enjoy refactoring:

  • Do not refactor to pursue truth and beauty Refactors should solve bad smells. So before refactoring is good to set a goal to achieve
  • Stop when unsure is better to ask or wait more information than waste time
  • Backtrack Do refactors one by one. Is easier to rollback in case of troubles
  • Duet Refactor with a mate.
  • Only wear an hat at time the refactoring one or the new funcionality one

First make it work, after make it right and then make it faster

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment