DEV Community

Bryce
Bryce

Posted on • Edited on • Originally published at brycedooley.com

Refactoring: My 6 favorite patterns

Refactoring code has become one of my favorite things to do as a developer. It can have a major impact on code cleanliness, readability, and maintainability.

In this post I’ll outline 6 refactoring patterns that I've found to be very useful and provide examples of each. Many are inspired by Martin Fowler's “Refactoring” book, which I highly recommend if you're looking to better understand common refactoring patterns.

(Side note: having good test coverage is also a CRUCIAL part of refactoring, but is outside the scope of this post.)

While the examples are in JavaScript, each pattern should be applicable to any programming language.

6. Introduce Object Parameter

When functions have multiple parameters, you start running into a few issues:

  1. For the function to work correctly, the order of parameters needs to be maintained.
  2. The names of the arguments (the actual values) passed to a function might not necessarily be the same as the parameter names, which makes searching for certain types of data/logic hard to do.
  3. Adding/removing parameters is a chore; each use of the function needs to be examined.

To make function parameters more manageable, this pattern involves converting a list of parameters into a single object. This forces consistent parameter naming across all functions, and makes the parameter order insignificant.

// Before

function sayHello(toName, punctuation, fromName) {
  return `Hello, ${toName}${punctuation} From, ${fromName}.`
} 

sayHello(customerName, end, myName);

// After

function sayHello({ toName, punctuation, fromName }) {
  return `Hello, ${toName}${punctuation} From, ${fromName}.`
} 

sayHello({ toName, punctuation, fromName });

5. Replace Anonymous Function with Expression

In JavaScript it’s a common practice to pass an anonymous function into an array method, such as .map, .reduce, or .filter. One issue I frequently see with these anonymous functions is they become complicated and difficult to parse; and since there is no name for the function it can be difficult to quickly understand the intent of the code.

Instead, I’ve found it helpful to extract these anonymous functions into a function expression, which makes it much easier to understand the intent (this also resembles "point-free style" a.k.a. "tacit programming".).

// Before

const activeUsers = users.filter((user) => {
  if(user.lastPayment >= moment().startOf('week').toDate()) {
    return true;
  }

  return false;
});

// After

const activeUsers = users.filter(hasUserPaidThisWeek);

function hasUserPaidThisWeek(user) {
  if(user.lastPayment > moment().startOf('week').toDate() ) {
    return true;
  }

  return false;
}

4. Replace Primitive with Object

Using a primitive value such as a string, number, or boolean is a common practice in many programming languages. But problems can arise when requirements and/or rules around these primitive values become more complex.

Instead of using an uncontrolled primitive value, a helpful practice is to wrap these primitives in an object, which will give you more control over how the value is consumed and modified.

// Before

let isLoading = true;
// some code...
loading = false;

const phone = '1 617 484-4049';

const price = 11;

// After

class LoadingStatus {
  constructor(initialStatus) {
    if(!this.statusSet.has(initialStatus)) {
      throw new Error('Invalid status');
    } 

    this._status = initialStatus;
  }

  statusSet = new Set(['loading', 'success', 'error', 'idle'])

  get status() {
    return this._status;
  }

  set status(status) {
    if(!this.statusSet.has(status)) {
      throw new Error('Invalid status');
    } 

    this._status = status;
  }
}

class Phone {
  constructor(phone) {
    this._phone = this.parsePhone(phone);
  }

  parsePhone(phone) {
    const trimmedPhone = phone.trim();

    if(phone.length !== 10) {
      throw new Error('Invalid phone format');
    }

    const areaCode = trimmedPhone.slice(0,3);
    const prefix = trimmedPhone.slice(3,7);
    const lineNumber = trimmedPhone.slice(7, 10);

    return { areaCode, prefix, lineNumber };
  }

  get areaCode() {
    return this._phone.areaCode;
  }

  get formatted() {
    const { areaCode, prefix, lineNumber } = this._phone;

    return `${areaCode} ${prefix}-${lineNumber}` 
  }

  ...
}

class Price {
  constructor(price) {
    if(typeof price !== 'string') {
      throw new Error('Invalid price');
    }

    if(!(price).match(/^[0-9]*$/)) {
      throw new Error('Invalid price');
    }

    this._price = price;
  }

  get price() {
    this._price;
  }
}

3. Decompose Conditional

if/else statements can be a powerful tool when adding logic to your program. But they can also become unwieldy and confusing very quickly. One way to counteract this is by making the conditional logic easier to understand by extracting it into expressions that describe your intent.

// Before

if(user.hasEmail() && user.subscriptions.includes('email')) {
  sendEmail(user);
}

// After

const isSubscribed = user.hasEmail() && user.subscriptions.includes('email');

if(isSubscribed) {
  sendEmail(user);
}

2. Encapsulate Record (Bridge Pattern)

Most of the time building software involves consuming an existing API and/or providing your own. If your component is coupled with another API and that API changes, you may need to change your component as well; and this can sometimes be very time consuming.

Instead of coupling various APIs, I find it helpful to give each component an API that makes the most sense given its functionality, and adding a layer in between your component and any other API it is interacting with.

The Encapsulate Record refactoring pattern provides a great way to do this. This idea is also aligned with the Bridge pattern, which you can learn more about in "Design Patterns: Elements of Reusable Object-Oriented Software”.

// Before

const user = {
  name: 'A Name', 
  favorites: { 
    color: 'blue',
    food: 'pizza'
  }
}

const UserComponent = (user) => (
  <div>Name: {user.name} - Food: {user.favorites.food}</div>
);

UserComponent(user);

// After

const user = {
  name: 'A Name', 
  favorites: { 
    color: 'blue',
    food: 'pizza'
  }
}

class User {
  constructor(user) {
    this._user = user;
  }

  get name() {
    return this._user.name;
  }

  get food() {
    return this._user.favorites.food;
  }
}

const UserComponent = ({ name, food }) => (
  <div>Name: {name} - Food: {food}</div>
);

UserComponent(new User(user));

1. Replace Conditional with Polymorphism

This is probably my favorite refactoring pattern. Several times it has helped me make confusing conditional logic much more readable and maintainable. And once logic is encapsulated in an object, you then have the flexibility to utilize other OOP design patterns to help achieve your goals.

The idea here is that instead of using a bunch of nested if statements in your code, you create objects that represent different "types", and give each type method(s) that are in charge of performing certain actions. Then, the application can simply call the same method on each type, and it’s up to the type to perform the action in the correct way.

// Before

if(user.favorites.food === 'pizza') {
  sendPizzaEmail(user);
}

if(user.favorites.food === 'ice cream') {
  sendIceCreamEmail(user);
}

// After

class PizzaUser {
  constructor(user) {
    this._user = user;
  }

  sendEmail() {
    sendPizzaEmail(this._user);
  }
}

class IceCreamUser {
  constructor(user) {
    this._user = user;
  }

  sendEmail() {
    sendIceCreamEmail(this._user);
  }
}

// this would create the appropriate user using the above classes
const user = getUser(userData); 

user.sendEmail()

That's it! Happy refactoring!

Latest comments (35)

Collapse
 
richardcochrane profile image
Richard Cochrane

Great article and well explained Bryce!

Collapse
 
rcsm profile image
Rafael Cascalho • Edited

Just read the article and pretty sure I'm about to use some of these 15 mins from now :D

Collapse
 
marianban profile image
Marian Ban • Edited

Hi thanks for the article.
Regarding 1. Replace Conditional with Polymorphism I would rather think carefully before introducing PizzaUser and IceCreamUser for this concrete example. I know that this is probably not a real world example but I would rather point out few things before people start to go wild with inheritance 😃

So lets consider a new requirement: As an user I would like to receive emails for my favorite music and sms notifications for my favorite tv shows. So we have user.favorites.music, user.favorites.food, user.favorites.tvshow.

With our current implementation we are running into an obvious issue because we need something what is capable of sending pizza emails, favorite music emails and sms notifications. If we stick to our inheritance model then we will end up with many different classes like: (PizzaNirvanaGOTUser, IceCreamDoorsNarcosUser...). But we can do better with the command pattern 🚀. Example:

class User {
  constructor(user) {
    this._user = user;
  }
  notify(notificationSender) {
    notificationSender.execute(this);
  }
}

class CompositeNotificationSender {
  execute(user) {
    this._notificationSenders.forEach(x => x.execute(user));
  }
}

// concrete notification senders
class FavoriteFoodMailSender { ... }
class FavoriteMusicMailSender { ... }
class FavoriteTvShowSmsSender { 
  execute(user) {
    sendTvShowSms(user);
  }
}

// actual usage
const user = getUser();
const notificationSender = new CompositeNotificationSender(
  new FavoriteFoodMailSender(),
  new FavoriteMusicMailSender(),
  new FavoriteTvShowSmsSender()
); 
user.notify(notificationSender);

Happy coding!

Collapse
 
richardcochrane profile image
Richard Cochrane

That's a good point Marian and the crux behind the composition vs inheritance thinking but I think there's a place for both. My main decider (and it's beside the point to the one the author is making) is asking if that method really belongs on the class. So classes inheriting from Animal (Dolphin, Cat, Elephant) would all "make sound" (their own sound) so that would be a great case for inheritance but if the functionality is not related, eg. "send mail" (sorry, it's contrived but let's say in a talking animals universe, like Zootropolis, where animals can send letters either by running, flying or the postal service), the sending of emails is not intrinsic to being an animal and the same functionality could be rightfully applied to companies, people and other types of class that don't inherit from Animal, then composition is a better way to go. In my opinion, of course :-)

Collapse
 
gkatsanos profile image
George Katsanos • Edited

reading both the original recommendation as well as the alternative, I really have a strong feeling to stick with the conditions. Also, why not move simply move/abstract the condition inside the sendmail function? The information we need is already inside the user object. I find creating the extra classes for each type of favorite food overengineered TBH. And adding one new favorite food type, means we need a whole new class for it?

sendEmail(user) { return `<div>Hey ${user.firstName}, we saw you like ${user.favorites.food}</div>` };
Collapse
 
llcamara profile image
LLCamara

For #3, my usual suggestion for refactoring is to create a function instead of a const variable - in this case user.isSubscribed().

The nice thing I see in having a const variable is when there is a complex calculation used multiple times. Then sure, do the calculation once, store in a const and fire away.

Collapse
 
sebbdk profile image
Sebastian Vargr • Edited

This might be my functional programming bias talking.

But... in example, 4, 2 and 1 you managed to turn a few lines of code into many lines of code.
What is better in these examples?

6 tend be fragile in none-typed languages in my experience.

3, 5 are great tips, they increase verbosity tremendously. :)

Collapse
 
gracesnow profile image
Grace Snow

Thanks for this, it's really helpful to read as a beginner.

I find as a good rule of thumb, if I'm having to write comments to explain what the code is doing, I probably need to refractor it!

Collapse
 
brycedooley profile image
Bryce

Love it!

Collapse
 
hoffmann profile image
Peter Hoffmann

While still refactoring I'd suggest to replace in the second example (example 5)

(user) => {
  if(user.lastPayment >= moment().startOf('week').toDate()) {
    return true;
  }
  return false;
}

with

function (user) {
    return user.lastPayment >= moment().startOf('week').toDate()
}

and don't forget, that arrow functions don't create their own this-scope.

Collapse
 
ineam profile image
Amine M

very nice patterns, but I think on the number 6 you wanted to put it this way:

const myObject = { toName, punctuation, fromName };

// Before

function sayHello(myObject) {
  return `Hello, ${myObject.toName}${myObject.punctuation} From, ${myObject.fromName}.`
} 

sayHello(customerName, end, myName);

// After

function sayHello({ toName, punctuation, fromName }) {
  return `Hello, ${toName}${punctuation} From, ${fromName}.`
} 

sayHello(myObject);
Collapse
 
johannesjo profile image
Johannes Millan

Thank you for the article. Really interesting subject.

However, I would like to suggest to be very very (!!) cautious with the advises 4, 2 and 1, because of KISS and Yagni. Always think twice or better thrice before introducing a new abstraction level to your code.

Always ask yourself: Do we really need this now and what are possible costs of the abstraction?

Collapse
 
lexlohr profile image
Alex Lohr

For #4, TypeScript has enums, which are a lightweight and legible alternative to let a class do the heavy lifting.