Refactoring to Patterns
Recently, I had occasion to dig out Joshua Kerievsky’s 2004 book Refactoring to Patterns (RtP) and it’s still a great read for software crafters who want to add to their software toolbox.
This week, I was faced with refactoring a large (200+ line) method that bootstrapped a service.
To do this, it instantiated various dependencies, setup metrics and health endpoints and started some goroutines to perform the functionality.
Reading a 200+ line method is not easy, and there was a lot of duplication, so…
Compose Method
First off, I need to get that 200+ line method into shape to make it a bit more readable, so I want to apply the “Compose Method” refactoring.
https://industriallogic.com/xp/refactoring/composeMethod.html
You can’t rapidly understand a method’s logic.
Transform the logic into a small number of intention-revealing steps at the same level of detail.
This originates in one of my favourite books, Kent Beck’s Smalltalk Best Practice Patterns (SBPP).
From SBPP:
Divide your program into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction. This will naturally result in programs with many small methods, each a few lines long.
Small methods ease maintenance. They let you isolate assumptions. Code that has been written with the right small methods requires the change of only a few methods to correct or enhance its operation. This is true whether you are fixing bugs, adding features, or tuning performance.
Note that this advice is for Smalltalk programmers, but there’s nothing specific to Smalltalk in there, small methods are easier to read, which is how it made its way into the more Java-oriented RtP.
func setupDatabaseAndHealthAndMetricsAndStartServerAndAPI() {
...200 lines of code
}
Except, despite the “intention revealing selector”, this would most likely end up as…
func setup()
...200 lines of code
}
But, applying the Compose Method pattern:
func setup()
db, checks := createDatabaseConnection()
metricsCheck := startMetricsService(db)
checks = append(checks, metricsCheck)
server, serverCheck := createServer(db)
checks = append(checks, serverCheck)
apiCheck := startAPI(server)
checks = append(checks, apiCheck)
startHealthChecks(checks)
}
Move Accumulation to Collecting Parameter
https://industriallogic.com/xp/refactoring/accumulationToCollection.html
You have a single bulky method that accumulates information to a local variable.
Accumulate results to a Collecting Parameter that gets passed to extracted methods.
This second refactoring is less obvious, and again originates in SBPP.
To quote directly from RtP:
A Collecting Parameter is an object that you pass to methods in order to collect information from those methods. This pattern is often coupled with Composed Method.
This allows further simplifification:
func setup() {
db, checks := createDatabaseConnection()
startMetricsService(db, checks)
server := createServer(db, checks)
startAPI(server, checks)
startHealthChecks(checks)
}
Each of the functions that receives the checks
parameter adds whatever checks that are necessary to the collecting parameter, and this is used to start the health checking.