Start Fixing a God Class

Converting their pawns to make your army

Problem and Solution Overview

This recipe just states how to execute a solution. Read our Legacy Newsletter Edition: Start Fixing God Classes (28 May) to understand the specific problem we are solving and the solution approach.

This recipe requires 9 steps. Future developers will unconsciously start helping you if you get through at least step 5.

  1. Pick a target procedure.
  2. Make data usage obvious.
  3. Create Data-Transfer Object.
  4. Convert Data-Transfer Object to Whole Value.
  5. Convert Whole Value to Whole Object and use it in god class.
  6. Add even more wholeness.
  7. Name your new concept.
  8. Carry on.

Part 1: Pick a target procedure

Select one pawn to convert

Look at the procedures that use the god class. These are the enemy pawns that are protecting the king. You are going to convert one to start attacking the king instead. Pick the one that meets the most of these criteria (in descending priority order):

  • It both reads data from and mutates the god class (probably by calling member methods).
  • It is fairly long.
  • It has some decent names or we know what it does. We can easily work around this if it’s missing, but it is nice.

Note: it is OK if the procedure uses other God Classes. That doesn’t make a pawn any better or worse.

Part 2: Make data usage obvious

Prepare the pawn

The method likely consists of one or more repetitions of this pattern:

// Section A: Read some values from the god class (and other classes) into local variables.
// Section B: Compute something.
// Section C: Call one or more mutators on the god class or other classes.

Pick any one instance of this pattern that looks interesting. Then:

  1. Extract Method for the whole pattern (A+B+C) to isolate it from the rest of the procedure. Get to an Honest name (or Completely Honest if possible).
  2. Extract Method Sections B+C as applesauce. Don’t bother getting a good name; you will inline it away later.
  3. Inline variable for each variable in Section A.

Now you should have:

procedureToChange(godClass, /* context arguments from elsewhere in the procedure */)
{
  applesauce(godClass.firstFieldUsed, godClass.secondFieldUsed, ..., /* context arguments */);
}

applesauce(/* params from god class & wider context */)
{
  // Section B
  // Section C
}

Look over the set of fields used from the God Class. Identify any that commonly move together. Fields move together if procedures that use any of them tend to use all of them.

To find these, you can:

  • Look for names with common parts.
  • Apply the recipe up to this point to several procedures and look for repetition in parameter lists.

These fields that move together are our target.

What if I can’t find fields that move together?

You don’t need a perfect set of fields — which is good because your codebase may not have any perfect sets! Here are two options that will let you make forward progress.

Use a single field. Choose one that has some matching behavior. You will end up creating a Whole Object with one field, but this will still start breaking up the God Class.

Use an imperfect set. Choose fields that often move together and sometimes appear as subsets. You will end up creating an object that has some methods that use all fields and some methods that only use some. This is halfway between a God Class and a single-responsibility class. But you can split it up more later.

Either approach will break out part of the God Class and make it easier to see the next opportunity.

Part 3: Create a Data-Transfer Object to bundle the data that move together

Convert the pawn to our side

  1. Introduce Parameter Object on the fields that move together. Give it the best name you can come up with in 10 seconds, but don’t worry about name much right now.
  2. Inline applesauce.
  3. Extract Method the instantiation of the type created in step 1. Call this bananasauce. It will take the God Class as its only parameter.
  4. Convert bananasauce to an instance method on the God Class. Name it as a getter.
  5. Add a field to the new class that is a reference to the God Class instance. Add it to the constructor and set it in the getter.

You should now have:

class GodClass {
  // ...
  getSomeData () {
    return new MyCommonData(this, this.firstField, this.secondField);
  }

  public firstField;
  public secondField;
  private someUnrelatedField;
}

class MyCommonData {
  constructor(godClass, firstField, secondField) {
    this.godClass = godClass;
    this.firstField = firstField;
    this.secondField = secondField;
  }

  public godClass;
  public firstField;
  public secondField;
}

procedureToChange(godClass, /* context arguments from elsewhere in the procedure */)
{
  myNewPawn = godClass.getSomeData();

  // Section B
  // ...
  something = myNewPawn.firstField;

  // Section C
  // ...
  godClass.getSomeData().mutatorMethod();
}

Part 4: Convert Data-Transfer Object to Whole Value

Make our new pawn more powerful

The class we created was good, but we really want it to become a Whole Value (and eventually a Whole Object). The next thing it needs is behavior.

  1. Pick one God Class method called in Section C that is in some way related to the fields in your Whole Value.
  2. Extract the entire body of that method as cherrysauce.
  3. If cherrysauce does not already take your new Whole Value as a parameter, add it now. Supply it by invoking the getter.
  4. Find all updates of any of the duplicated fields (the ones that are now in both the God Class and the Whole Value)
  5. Duplicate each update, so that it updates both the field in the God Class and the parallel field in the Whole Value.
  6. Convert cherrysauce to static, taking the God Class as a parameter (rather than taking each individual field). This may require making fields on the God Class public. That’s OK.
  7. Convert cherrysauce to be an instance method on the Whole Value class.
  8. If there are any parts of cherrysauce that only modify the God Class, use Extract Method + Move Method to move them back to the God Class. Get their names up to at least the Completely Honest level.
  9. Make the non-duplicated God Class fields private again. Now the only public fields should be the ones that are also in the Whole Value. If this step fails to compile, go back to step 8 and do more extractions.
  10. Rename cherrysauce to the same name as it had when it was on the God Class in step 1.
  11. Inline the method from Step 1. Now all callers of the God Class will go directly to the Whole Value instead.
  12. Repeat this entire Part if there are more appropriate God Class methods called from Section C.
class GodClass {
  // ...
  getSomeData() {
    return new MyCommonData(this, this.firstField, this.secondField);
  }

  mutateUnrelatedFieldInSomeWay() {
  }

  public firstField;
  public secondField;
  private someUnrelatedField;
}

class MyCommonData {
  constructor(godClass, firstField, secondField) {
    this.godClass = godClass;
    this.firstField = firstField;
    this.secondField = secondField;
  }

  mutatorMethod() {
    // ...
    this.firstField = something;
    godClass.firstField = this.firstField;
    // ...
    godClass.mutateUnrelatedFieldInSomeWay();
    // ...
  }

  public godClass;
  public firstField;
  public secondField;
}

procedureToChange(godClass, /* context arguments from elsewhere in the procedure */)
{
  myNewPawn = godClass.getSomeData();

  // Section B
  // ...
  something = myNewPawn.firstField;

  // Section C
  // ...
  godClass.getSomeData().mutatorMethod();
}

Part 5: Convert Whole Value to Whole Object and use it in the God Class

Put our new pawn in the right place

  1. Add a field to the God Class that is an instance of the Whole Value.
  2. Construct the new Whole Value field at the same time as the duplicated fields used to be assigned (typically at construction).
  3. Modify the getter that you created at the end of Part 3 to just return the field, rather than making a new copy of the Whole Value.
  4. Use Move Field on each duplicated field from the God Class to the Whole Value. This will create pairs of duplicate fields on the Whole Value, instead of one half of each pair on the Whole Value and one on the God Class.
  5. Rename the one element out of each duplicate pair to the other’s name, creating a conflict. Then delete one of the two, effectively merging the two fields.
  6. Delete all the duplicate assignments you created in Part 4, Step 5. Use Find write usages to find them all.
  7. The fields are likely still used by other code in the God Class, so you won’t be able to make them private. That is OK. If you want to ensure devs won’t backslide, rename them with an evil prefix like _onlyPublicToSupportRefactoringThisOffTheGodClass_.

At this point the new class is a regular Whole Object, not just a value. It can (and should) be used with referential semantics.

class GodClass {
  constructor() {
    purposeForTheCommonData = new WhatTheCommonDataRepresents(this, firstValue, secondValue);
  }
  // ...
  getSomeData() {
    return purposeForTheCommonData;
  }

  private purposeForTheCommonData;
  private someUnrelatedField;
}

class WhatTheCommonDataRepresents {
  constructor(godClass, firstField, secondField) {
    this.godClass = godClass;
    this._onlyPublicToSupportRefactoringThisOffTheGodClass_firstField = firstField;
    this._onlyPublicToSupportRefactoringThisOffTheGodClass_secondField = secondField;
  }

  mutatorMethod() {
    // ...
    this._onlyPublicToSupportRefactoringThisOffTheGodClass_firstField = something;
    // ...
    godClass.mutateUnrelatedFieldInSomeWay();
    // ...
  }

  public godClass;
  public _onlyPublicToSupportRefactoringThisOffTheGodClass_firstField;
  public _onlyPublicToSupportRefactoringThisOffTheGodClass_secondField;
}

procedureToChange(godClass, /* context arguments from elsewhere in the procedure */)
{
  myNewPawn = godClass.getSomeData();

  // Section B
  // ...
  something = myNewPawn._onlyPublicToSupportRefactoringThisOffTheGodClass_firstField;

  // Section C
  // ...
  myNewPawn.getSomeData().mutatorMethod();
}

Part 6: Even more wholeness

Support the pawn even more

  1. Go back to the procedure you prepared in Part 2. Find Section B.
  2. Identify any sections that belong on your Whole Object (typically computations based in part on its data).
  3. Extract Method + Move Method to put them on the Whole Object.

Part 7: Name your new concept

Give your Whole Object class a better name. Get at least completely honest, preferably domain abstraction. Use the methods you’ve added to guide you to a name.

Part 8: Carry on

Repeat as time allows. You could repeat with a different procedure that uses the same fields. This will make your new class stronger. You could also repeat with a new set of fields from your same procedure. This will create a second new class.

Stop when you run out of time or run out of fields in the God Class.

You should now have a couple well-named Whole Objects next to your God Class. Other developers will start extending them rather than the God Class, since you have moved the data.

Repeat this for a while and the old God Class will become a simple Facade, coordinating all the new Whole Objects. At that point you can inline it.