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.