When using RXJS, we sometimes end up with a pipe that
we want to split in half. If we have five method calls one after another, we can extract a method using
the extract method refactoring. If we have five pipe expressions, and want to extract the method that returns
an observable and half of the pipe expressions, the extract method refactoring does not apply. That’s because the series
of pipe statements are all parameters to the pipe
method, and we want to take only the first half.
The “Split Pipe” refactoring allows us to extract a method halfway through a pipe. This is useful any time you would want to extract a method where the extracted code spans half a pipe.
Starting state
private unsafeCheckForFlibbles(serial: string, deviceId: number): Observable<boolean>
return this.prefetchService.getEverythingWeNeedForComputingFlibbles(serial, deviceId).pipe(
map(x => this.flibbleAssemblerService.assemble(x)),
map(flibbles => this.adaptResponseToTheCommonInterface(flibbles)),
);
}
We want to extract the call to getEverythingWeNeedForComputingFlibbles
and the map to flibbleAssemblerService
.
Aside: safe refactoring
We want to complete this transformation by keeping all our tests passing after every transformation. After a series of safe transformations, the code is structured the way we want it to, but also we know the behavior is unchanged since our tests passed the whole time. (We ensured we have sufficient test coverage before we began.)
Step 1: extract variable
Instead of returning the observable directly, we put it into a variable using the extract variable refactoring.
private unsafeCheckForFlibbles(serial: string, deviceId: number): Observable<boolean> {
const extractedVariable = this.prefetchService.getEverythingWeNeedForComputingFlibbles(serial, deviceId).pipe(
map(x => this.flibbleAssemblerService.assemble(x)),
map(flibbles => this.adaptResponseToTheCommonInterface(flibbles)),
);
return extractedVariable;
}
Step 2: call pipe a second time
Before returning that variable, pipe it (but don’t put anything in the pipe). This is a noop.
private unsafeCheckForFlibbles(serial: string, deviceId: number): Observable<boolean> {
const extractedVariable = this.prefetchService.getEverythingWeNeedForComputingFlibbles(serial, deviceId).pipe(
map(x => this.flibbleAssemblerService.assemble(x)),
map(flibbles => this.adaptResponseToTheCommonInterface(flibbles)),
);
return extractedVariable.pipe();
}
Step 3: move methods from the first pipe to the second
Everything you want to extract stays in the first pipe; everything else goes in the second.
private unsafeCheckForFlibbles(serial: string, deviceId: number): Observable<boolean> {
const extractedVariable = this.prefetchService.getEverythingWeNeedForComputingFlibbles(serial, deviceId).pipe(
map(x => this.flibbleAssemblerService.assemble(x)),
);
return extractedVariable.pipe(
map(flibbles => this.adaptResponseToTheCommonInterface(flibbles)),
);
}
Step 4: extract method
We can now perform the extract method refactoring that we wanted to do at the start.
private unsafeCheckForFlibbles(serial: string, deviceId: number): Observable<boolean> {
const extractedVariable = this.extractedMethod(serial, deviceId);
return extractedVariable.pipe(
map(flibbles => this.adaptResponseToTheCommonInterface(flibbles)),
);
}
private extractedMethod(serial: string, deviceId: number) {
const extractedVariable = this.prefetchService.getEverythingWeNeedForComputingFlibbles(serial, deviceId).pipe(
map(x => this.flibbleAssemblerService.assemble(x)),
);
return extractedVariable;
}
Step 5: cleanup
Finally, we use inline variable refactoring to remove our extracted variables.
private unsafeCheckForFlibbles(serial: string, deviceId: number): Observable<boolean> {
return this.extractedMethod(serial, deviceId).pipe(
map(flibbles => this.adaptResponseToTheCommonInterface(flibbles)),
);
}
private extractedMethod(serial: string, deviceId: number) {
return this.prefetchService.getEverythingWeNeedForComputingFlibbles(serial, deviceId).pipe(
map(x => this.flibbleAssemblerService.assemble(x)),
);
}
Remarks about tool support
By using an IDE that has automated refactoring support, the only steps that we have to do by hand is adding the empty
pipe()
and cut+pasting the pipe methods from the first to the second.