CS 346 (W23)
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Refactoring

We all want to write perfect code, but given , we often end-up with less-than-perfect solutions.

  • Rushed features: Sometimes we don’t have enough time, so we cut corners.

  • Lack of tests: We might think that the code is ready, but we haven’t tested it adequately.

  • Lack of communication: Perhaps we misunderstood requirements, or how a feature would integrate into the larger product.

  • Poor design: Possibly our design is rigid and makes adding or modifying features difficult.

These are all actions that may cost us time later. We may need to stop and redesign a rushed feature, or we may need to fix bugs later.

We refer to this as technical debt — the deferred cost of doing something poorly.

What is refactoring?

Martin Fowler (2000) also introduced the notion of refactoring: systematically transforming your working code base into a cleaner, improved version of itself that is easier to read, maintain and extend.

Refactoring is a controlled technique for improving the design of an existing code base. Its essence is applying a series of small behavior-preserving transformations, each of which “too small to be worth doing”. However the cumulative effect of each of these transformations is quite significant.

– Martin Fowler, 2018.

Refactoring suggests that code must be continually improved as we work with it. We need to be in a process of perpetual, small improvements.

The goal of refactoring is to reduce technical debt by making small continual improvements to our code. It doesn’t reduce the likelihood of technical debt, but it amortizes that debt over many small improvements.

Refactoring your code means doing things like:

    • Cleaning up class interfaces and relationships.
      • Fixing issues with class cohesion.
      • Reducing or removing unnecessary dependencies.
      • Simplifying code to reduce unnecessary complexity.
      • Making code more understandable and readable.
      • Adding more exhaustive tests.

In other words, refactoring involves code improvement not related to adding functionality.

TDD and Refactoring work together. You continually refactor as you expand your code, and you rely on the tests to guarantee that you aren’t making any breaking changes to your code.

image-20220209133014597

How to refactor code?

The Rule of Three

  • When you’re doing something for the first time, just get it done.
  • When you’re doing something similar for the second time, do the same thing again.
  • When you’re doing something for the third time, start refactoring.

When adding a feature

  • Refactor existing code before you add a new feature, since it’s much easier to make changes to clean code. Also, you will improve it not only for yourself but also for those who use it after you.

When fixing a bug

  • If you find or suspect a bug, refactoring to simplify the existing code can often reveal logic errors.

During a code review

  • The code review may be the last chance to tidy up the code before it becomes available to the public.

Refactorings

Martin Fowler. 2018. Refactoring: Improving the Design of Existing Code. 2nd Edition. Addison-Wesley. ISBN 978-0134757599.

The author has also made them available at: https://refactoring.com/catalog/

IntelliJ IDEA also makes this easy by providing automated ways of safely transforming your code. These refactorings often involve operations that would be tricky to do by-hand but easy for the tool do perform for you (e.g. renaming a method that is called from multiple locations).

To invoke refactorings, select an item in your source code (e.g. variable or function name) and press Ctrl-T to invoke the refactoring menu. You can also access from the application menu.

refactor this popup

They have a complete list of these in the IntelliJ IDEA documentation. Refactorings include:

Refactoring Purpose
Rename Change an identifier to something that is more meaningful or memorable.
Move Move classes or functions to different packages; move methods between classes.
Extract method Take a code fragment that can be grouped, move it into a separated method, and replace the old code with a call to the method
Extract field Extract an expression into a variable, and insert the expression dynamically.
Safe delete Check for usage of a symbol before you are allowed to delete it.
Change signature Change the method name, add, remove, reorder, and rename parameters and exceptions.
Type migration Change a member type (e.g. from integer to string), method return types, local variables, parameters etc. across the entire project.
Replace constructor with factory Modify class the become a singleton (returns a single instance).

Code Smells

A “code smell” is a sign that a chunk of code is badly designed or implemented. It’s a great indication that you may need to refactor the code.

  • Adjectives used to describe code:

    • “neat”, “clean”, “clear”, “beautiful”, “elegant” <— the reactions that we want
      • “messy”, “disorganized”, “ugly”, “awkward” <— the reactions we want to avoid
  • A negative emotional reaction is a flag that your brain doesn’t like something about the organization of the code - even you can’t immediately identify what that is.

  • Conversely, a positive reaction indicates that your brain can easily perceive and following the underlying structure.

The following categories and examples are taken from refactoring.guru.

Bloaters

Bloaters are code, methods and classes that have increased to such gargantuan proportions that they are hard to work with. These smells accumulate over time as the program evolves (and especially when nobody makes an effort to eradicate them).

  • Long method: A method contains too many lines of code. Generally, any method longer than ten lines should make you start asking questions.
  • Large class: A class contains many fields/methods/lines of code. This suggests that it may be doing too much. Consider breaking out a new class, or interface.
  • Primitive obsession: Use of related primitives instead of small objects for simple tasks (such as currency, ranges, special strings for phone numbers, etc.). Consider creating a data class, or small class instead.
  • Long parameters list: More than three or four parameters for a method. Consider passing an object that owns all of these. If many of them are optional, consider a builder pattern instead.

Object-Oriented Abusers

All these smells are incomplete or incorrect application of object-oriented programming principles.

  • Alternative Classes with Different Interfaces: Two classes perform identical functions but have different method names. Consolidate methods into a single class instead, with support for both interfaces.
  • Refused bequest: If a subclass uses only some of the methods and properties inherited from its parents, the hierarchy is off-kilter. The unneeded methods may simply go unused or be redefined and give off exceptions. This violates the Liskov-substitution principle! Add missing behaviour, or replace inheritance with delegation.
  • Switch Statement: You have a complex switch operator or sequence of if statements. This sometimes indicates that you are switching on type, something that should be handled by polymorphism instead. Consider whether a class structure and polymorphism makes more sense in this case.
  • Temporary Field: Temporary fields get their values (and thus are needed by objects) only under certain circumstances. Outside of these circumstances, they’re empty. This may be a place to introduce nullable types, to make it very clear what is actually happening (vs. constantly checking fields for the presence of data).

Dispensibles

A dispensable is something pointless and unneeded whose absence would make the code cleaner, more efficient and easier to understand.

  • Comments: A method is filled with explanatory comments. These are usually well-intentioned, but they’re not a substitute for well-structured code. Comments are a maintenance burden. Replace or remove excessive comments.
  • Duplicate Code: Two code fragments look almost identical. Typically, done accidentally by different programmers. Extract the methods into a single common method that is used instead. Alternately, if the methods solve the same problem in different ways, pick and keep the most efficient algorithm.
  • Dead Code: A variable, parameter, field, method or class is no longer used (usually because it’s obsolete). Delete unused code and unneeded files. You can always find it in Git history.
  • Lazy Class: Understanding and maintaining classes always costs time and money. So if a class doesn’t do enough to earn your attention, it should be deleted. This is tricky: sometimes a small data class is clearer than using primitives (e.g. a Point class, vs using x and y stored as doubles).

Couplers

All the smells in this group contribute to excessive coupling between classes or show what happens if coupling is replaced by excessive delegation.

  • Feature envy: A method accesses the data of another object more than its own data. This smell may occur after fields are moved to a data class. If this is the case, you may want to move the operations on data to this class as well.
  • Inappropriate intimacy: One class uses the internal fields and methods of another class. Either move those fields and methods to the second class, or extract a separate class that can handle that functionality.
  • Middle man: If a class performs only one action, delegating work to another class, why does it exist at all? It can be the result of the useful work of a class being gradually moved to other classes. The class remains as an empty shell that doesn’t do anything other than delegate. Remove it.

XKCD Style Guide

https://xkcd.com/1513