Use Data to Split a God Class

Problem and Solution Overview

This recipe just states how to execute a solution. Read our Legacy Newsletter Edition: Fix God Classes by Using Data (30 Oct) to understand the specific problem we are solving and the solution approach.

This recipe requires 5 steps.

Step 2 arguably makes readability worse, so do step 1 incrementally and push each to main, then branch when you start step 2 and get at least a few private methods through step 4 as quickly as possible so you can merge the branch.

Steps 3-5 follow the same strategy as in Start Fixing a God Class. The details differ, but the strategy should be familiar.

  1. Privatize all field usages.
  2. Make field usage obvious to find shared fields.
  3. Create helper class from parameters.
  4. Move behavior onto helper class.
  5. Extend helper class lifetime.

Part 1: Privatize all field usages

We want to eliminate all field access from public methods, without creating private getters or setters. Instead, we are going to create private methods that do something with one or more fields.

  1. Find usages of a field, and go to a usage in a public method.
  2. Identify a useful chunk of that method that includes the field access.
    • Pick the smallest enclosing paragraph or scope block that makes sense.
    • Use the whole method body if there isn’t a smaller chunk.
  3. Extract Method that chunk to make a new private method. Give it a good name.
  4. Commit and push to main.
  5. Repeat until there are no more field usages in any public method.

Part 2: Make field usage obvious and select shared fields

  1. Convert each private method to a static method.
    • Add fields before all other parameters in the parameter list.
    • Sort field parameters alphabetically.
  2. Cluster methods by one field at a time until you get a set of methods that share two or more fields.
    • If you are doing this extraction as part of a story, then start by picking the method that you need to change for this story. Look only for clusters that share fields with this one method.

These shared fields are our target.

What if I can’t find a cluster of methods that all share multiple fields?

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 appears in several methods and appears alone at least once. 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 helper class to bundle the data that move together

  1. Pick one private method that takes all of the target shared fields.
    • If you are doing this as part of a story, then start with the method that you need to change for that story.
  2. Introduce Parameter Object on the shared fields.
    • Give it the best name you can come up with in 10 seconds, but don’t worry about names much right now.
    • All the fields will be public so that the God Class’s methods can use them.
  3. Extract Method the instantiation of the type you created. Call this applesauce. It will take the God Class as its only parameter.
  4. Convert applesauce to an instance method. 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 {
  public void ExposedMethod(/* stuff */ {
    // ...
    commonData = getSomeData();
    someHelper(commonData, /* stuff */);
    firstField = commonData.firstField;
    secondField = commonData.secondField;
    // ...
  }

  private static void someHelper(commonData, /* stuff */) {
    foo = commonData.firstField;
    // ...
    commonData.secondField = bar;
  }

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

  private firstField;
  private secondField;
  private someUnrelatedField;
}

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

  public godClass;
  public firstField;
  public secondField;
}

Part 4: Add behavior to the helper class

The helper 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. Start with the helper method you performed Introduce Parameter Object on in Part 3.
    • It will be private and take an instance of your new helper class as one parameter.
  2. Convert that helper method to an instance method on the helper class.
    • The method may reference fields or methods back on the God Class. If so, make those fields and methods temporarily public.
  3. Commit.
  4. Find any parts of the helper method that reference parts of the God Class that you won’t be moving to this helper class—either methods or fields.
  5. Extract Method + Move Method to get each of those parts to a method on the God Class.
    • Extract reasonably-sized paragraphs where possible. You can pass data from the whole value’s fields as parameters if that makes good chunks possible.
  6. Repeat until you can make all the God Class’s fields & helper methods private again.
  7. Commit & merge to main.

Now move over some other helper methods.

  1. Pick one helper method that shares the same fields.
  2. Introduce Parameter Object on the same set of fields. Name it the same as your Whole Value object (so there is a name conflict).
  3. Find usages of the new object’s constructor and replace each with a call to the getter created in Part 3.
  4. Delete the class definition created in the step 2, so that this helper method now uses the same Whole Value that we extracted in Part 3.
  5. Convert the helper method to an instance method on the Whole Value.
  6. Commit.
  7. Extract Method + Move Method chunks back to the God Class like you did for the first helper method. Ensure you are able to get all the God Class fields and helper methods private again.
  8. Commit & merge to main.

Move over helper methods as time permits. Each story that requires modifying the God Class will allow you to move a couple more helper methods until you have gotten them all.

class GodClass {
  public void ExposedMethod(/* stuff */ {
    // ...
    commonData = getSomeData();
    commonData.someHelper(commonData, /* stuff */);
    firstField = commonData.firstField;
    secondField = commonData.secondField;
    // ...
  }

  private MyCommonData 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;
  }

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

  public godClass;
  public firstField;
  public secondField;
}

Part 5: Extend Helper Class Lifetime

Do this only after every helper method that uses the target shared fields has been moved to the helper class.

  1. Add a field to the God Class that is an instance of the helper class.
  2. Construct the new helper class 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 helper class.
  4. Delete the writes back to the God Class’s copy of the shared fields after calling each helper method.
  5. Delete the God Class’s copy of shared fields.
  6. The helper class’s 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_.
  7. Rename the helper class to a name that matches its behaviors.

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

class GodClass {
  public void ExposedMethod(/* stuff */ {
    // ...
    getSomeData().someHelper(commonData, /* stuff */);
    // ...
  }

  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;
  }

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

  public godClass;
  public _onlyPublicToSupportRefactoringThisOffTheGodClass_firstField;
  public _onlyPublicToSupportRefactoringThisOffTheGodClass_secondField;
}