Break out command object
In many applications, long functions are very hard to work with. In a legacy codebase, they are often contained in large classes that have many dependencies. The work that it takes to bring such functions and classes into a test harness is often significant and may even be overkill for the changes you need to make.
If the function that you need to work with is large or uses instance variables and functions, consider using Break Out Command Object. To make a long story short, this refactoring technique gets you to move a long function to a new class. Objects created using that new class are called command objects because they are mostly built around a single function, whose request and execution is the purpose of the object. It is an object that encapsulates a request, following the command pattern in Design Patterns.
After you’ve used Break Out Command Object, you can often write tests for the new class easier than you could for the old function. Local variables in the old function can become instance variables in the new class. Often that makes it easier to break dependencies and improve the code.
Here's an example in Typescript (large parts of the class have been removed to preserve pixels):
class ApplicantInfoComponent {
applicantCommonModel = new ApplicantCommonEditModel();
isSmartMoveActive = false;
datePickerComponents: QueryList<CustomDatePicker>;
...
constructor(private flashMessageService: NotifyFlashMessageService, ...) {
...
}
validateDates() { ... }
...
validateBirthDate(finalValidation) {
let isValid = true;
let tempDate = new Date(this.applicantCommonModel.DateOfBirth);
if (
this.isSmartMoveActive && new Date(
tempDate.getFullYear() + 18, tempDate.getMonth() - 1,
tempDate.getDay()
) >= new Date()
) {
isValid = false;
let birthDate = this.datePickerComponents['_results']
.find(item => item.Name == 'birthDate');
birthDate.invalidDate = true;
this.flashMessageService.warning('Age must not be less than 18 years.');
}
else if (
this.isSmartMoveActive && new Date(
tempDate.getFullYear() - 125, tempDate.getMonth() - 1,
tempDate.getDay()
) >= new Date()) {
isValid = false;
let birthDate = this.datePickerComponents['_results']
.find(item => item.Name == 'birthDate');
birthDate.invalidDate = true;
this.flashMessageService.warning('Age must not be more than 125 years.');
}
if (finalValidation)
return isValid && this.validateDates();
return isValid;
}
...
}
The ApplicantInfoComponent
class has a long function named validateBirthDate
. We can’t easily write tests for it, and it is going to be very difficult to create an instance of ApplicantInfoComponent
in a test harness. Let’s use Break Out Command Object to move validateBirthDate
to a new class.
The first step is to create a new class that will do the birth date validation work. We can call it BirthDateValidator
. After we’ve created it, we give it a constructor. The arguments of the constructor should be a reference to the original class, and the argument to the original function.
class BirthDateValidator {
constructor(component: ApplicantInfoComponent, finalValidation: boolean) {}
}
You might be looking at this and saying, “Wait a minute, it looks like we are going to end up in the same place. We are accepting a reference to a ApplicantInfoComponent
, and we already decided that we don't want to instantiate one of those in our test harness. What's the point of all this?” Wait, we are going to make things better.
After we’ve made the constructor, we can add another function to the class, a function that will do the work that was done in the validateBirthDate()
function. We can call it validate()
.
class BirthDateValidator {
constructor(component: ApplicantInfoComponent, finalValidation: boolean) {}
validate() {}
}
Now we add the body of the validateBirthDate()
function to BirthDateValidator
. We copy the body of the old validateBirthDate()
function into the new validate()
function.
class BirthDateValidator {
constructor(component: ApplicantInfoComponent, finalValidation: boolean) {}
validate() {
let isValid = true;
let tempDate = new Date(this.applicantCommonModel.DateOfBirth);
if (
this.isSmartMoveActive && new Date(
tempDate.getFullYear() + 18, tempDate.getMonth() - 1,
tempDate.getDay()
) >= new Date()
) {
isValid = false;
let birthDate = this.datePickerComponents['_results']
.find(item => item.Name == 'birthDate');
birthDate.invalidDate = true;
this.flashMessageService.warning('Age must not be less than 18 years.');
}
else if (
this.isSmartMoveActive && new Date(
tempDate.getFullYear() - 125, tempDate.getMonth() - 1,
tempDate.getDay()
) >= new Date()) {
isValid = false;
let birthDate = this.datePickerComponents['_results']
.find(item => item.Name == 'birthDate');
birthDate.invalidDate = true;
this.flashMessageService.warning('Age must not be more than 125 years.');
}
if (finalValidation)
return isValid && this.validateDates();
return isValid;
}
}
If the validate()
on BirthDateValidator
has any references to instance variables or functions from ApplicantInfoComponent
, our compile will fail. To make it succeed, we can make getters for the variables and make the functions that it depends on public. In this case, we depend on a few variables and one validateDates()
function, all of which are public except for the flashMessageService
variable. After we make it public on ApplicantInfoComponent
, we can access it from a reference to the BirthDateValidator
class and the code compiles.
Now we can make ApplicantInfoComponent
’s validateBirthDate
function delegate to the new BirthDateValidator
.
class ApplicantInfoComponent {
...
validateBirthDate(finalValidation) {
birthDateValidator = new BirthDateValidator(this, finalValidation);
birthDateValidator.validate();
}
...
}
Now back to the ApplicantInfoComponent
dependency. The whole reason why we decided to create a new class is that ApplicantInfoComponent
would be too hard to instantiate in a test harness. As of right now, we still have that problem because our new BirthDateValidator
class still depends on a reference to ApplicantInfoComponent
. What we can do is extract a new interface to break the dependency on ApplicantInfoComponent
completely. To do that we create a new interface, we'll call it DateValidator
. Then we change the reference that the BirthDateValidator
holds from ApplicantInfoComponent
to DateValidator
, compile, and let the compiler tell us what functions and properties have to be on the interface. Here is what the code looks like at the end:
interface DateValidator {
applicantCommonModel: ApplicantCommonEditModel;
isSmartMoveActive: boolean;
datePickerComponents: QueryList<CustomDatePicker>;
flashMessageService: NotifyFlashMessageService;
validateDates(): boolean;
}
class ApplicantInfoComponent {
...
validateBirthDate(finalValidation) {
birthDateValidator = new BirthDateValidator(this, finalValidation);
birthDateValidator.validate();
}
...
}
class BirthDateValidator {
constructor(component: DateValidator, finalValidation: boolean) {}
validate() {
let isValid = true;
let tempDate = new Date(component.applicantCommonModel.DateOfBirth);
if (
component.isSmartMoveActive && new Date(
tempDate.getFullYear() + 18, tempDate.getMonth() - 1,
tempDate.getDay()
) >= new Date()
) {
isValid = false;
let birthDate = component.datePickerComponents['_results']
.find(item => item.Name == 'birthDate');
birthDate.invalidDate = true;
component.flashMessageService.warning('Age must not be less than 18 years.');
}
else if (
component.isSmartMoveActive && new Date(
tempDate.getFullYear() - 125, tempDate.getMonth() - 1,
tempDate.getDay()
) >= new Date()) {
isValid = false;
let birthDate = component.datePickerComponents['_results']
.find(item => item.Name == 'birthDate');
birthDate.invalidDate = true;
component.flashMessageService.warning('Age must not be more than 125 years.');
}
if (finalValidation)
return isValid && component.validateDates();
return isValid;
}
}
Now that we've extracted the code that we need to modify to a new class, we're ready to write some tests, refactor and make whatever changes we came here to make. You can see me work with Break Out Command Object, and the following steps of writing tests and refactoring code, this video series I made on working with legacy code.
Suggested reading: