Extract Method is one of the most commonly used automated refactorings. Many of us have been using it for well over two decades in tools like Eclipse and IntelliJ (and its family of IDEs). It’s a workhorse.
And, while I appreciate it for what it can do, I’ve been tired of what it can’t do for a long time now. I wrote about this problem in a 2014 blog called Improving Extract Method.
In this blog, I’m going to show you how I used ChatGPT 03-mini and Junie (join the EAP for this excellent tool) to perform a more intelligent Extract Method. The code we’ll look at is from the Parrot Kata, a code example from Martin Fowler’s Refactoring book, which was turned into a Kata (in many languages) by Emily Bache.
The code I want to refactor is the test code. As you can see below, for each test in ParrotTest, a Parrot instance is instantiated with specific values. As you can also see, there are three types of parrots (European, African and Norwegian):
@Test
public void getSpeedOfEuropeanParrot() {
Parrot parrot = new Parrot(ParrotTypeEnum.EUROPEAN, 0, 0, false);
assertEquals(12.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_One_Coconut() {
Parrot parrot = new Parrot(ParrotTypeEnum.AFRICAN, 1, 0, false);
assertEquals(3.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_Two_Coconuts() {
Parrot parrot = new Parrot(ParrotTypeEnum.AFRICAN, 2, 0, false);
assertEquals(0.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_No_Coconuts() {
Parrot parrot = new Parrot(ParrotTypeEnum.AFRICAN, 0, 0, false);
assertEquals(12.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedNorwegianBlueParrot_nailed() {
Parrot parrot = new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 1.5, true);
assertEquals(0.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedNorwegianBlueParrot_not_nailed() {
Parrot parrot = new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, 0, 1.5, false);
assertEquals(18.0, parrot.getSpeed(), 0.0);
}
The Parrot Kata is about introducing polymorphism into the code, and I have an approach to doing that which is driven outside-in, from the test code to the production code.
I begin by extracting Parrot instantiation into its own creation method via the Extract Method automated refactoring. The trouble is, IntelliJ cannot do this the way I want. Here’s what it gives me when applying Extract Method on the first parrot instantiation of an African parrot:
@Test
public void getSpeedOfAfricanParrot_With_One_Coconut() {
Parrot parrot = createParrot();
assertEquals(3.0, parrot.getSpeed(), 0.0);
}
private Parrot createParrot() {
return new Parrot(ParrotTypeEnum.AFRICAN, 1, 0, false);
}
@Test
public void getSpeedOfAfricanParrot_With_Two_Coconuts() {
Parrot parrot = new Parrot(ParrotTypeEnum.AFRICAN, 2, 0, false);
assertEquals(0.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_No_Coconuts() {
Parrot parrot = new Parrot(ParrotTypeEnum.AFRICAN, 0, 0, false);
assertEquals(12.0, parrot.getSpeed(), 0.0);
}
Notice how it did not change all African parrot instantiations to use the new creation method? This happened because the other instantiations of African parrots happen to pass in different constructor values. What it generated wasn’t even close to what I wanted and therefore I did not even bother to rename it to createAfricanParrot(). The Extract Method refactoring has been incapable of figuring this out for way too long.
It’s time for a change.
Extract Method with ChatGPT
Let’s see how AI can help. Using ChatGPT 03-mini, I give it the ParrotTest source code and ask it to do the following:
“In the ParrotTest, please extract a single creation method (a method that creates a Parrot instance) from all of the code that instantiates a Parrot class.”
It does the work in seconds, giving me the following:
@Test
public void getSpeedOfEuropeanParrot() {
Parrot parrot = createParrot(ParrotTypeEnum.EUROPEAN, 0, 0, false);
assertEquals(12.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_One_Coconut() {
Parrot parrot = createParrot(ParrotTypeEnum.AFRICAN, 1, 0, false);
assertEquals(3.0, parrot.getSpeed(), 0.0);
}
private Parrot createParrot(ParrotTypeEnum parrotTypeEnum, int numberOfCoconuts,
int voltage, boolean isNailed) {
return new Parrot(parrotTypeEnum, numberOfCoconuts, voltage, isNailed);
}
Now I have one place in this test code where I instantiate parrots. It’s good, and I’m glad AI can produce this for me, but I actually want it to do more. What I want is one creation method for each parrot type.
It can do that too. I begin with the original test code and ask ChatGPT the following:
“Instead of making one createParrot method, can you make three: one for each ParrotTypeEnum that is instantiated?”
It immediately does the work and gives me the following:
private Parrot createEuropeanParrot(int numberOfCoconuts, double voltage, boolean isNailed) {
return new Parrot(ParrotTypeEnum.EUROPEAN, numberOfCoconuts, voltage, isNailed);
}
private Parrot createAfricanParrot(int numberOfCoconuts, double voltage, boolean isNailed) {
return new Parrot(ParrotTypeEnum.AFRICAN, numberOfCoconuts, voltage, isNailed);
}
private Parrot createNorwegianBlueParrot(int numberOfCoconuts, double voltage, boolean isNailed) {
return new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, numberOfCoconuts, voltage, isNailed);
}
@Test
public void getSpeedOfEuropeanParrot() {
Parrot parrot = createEuropeanParrot(0, 0, false);
assertEquals(12.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_One_Coconut() {
Parrot parrot = createAfricanParrot(1, 0, false);
assertEquals(3.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedNorwegianBlueParrot_nailed() {
Parrot parrot = createNorwegianBlueParrot(0, 1.5, true);
assertEquals(0.0, parrot.getSpeed(), 0.0);
}
This is exactly what I want at this stage in the Parrot Kata. Had this been client code or my own organization’s code, I may not have wanted to expose it to a public AI tool. Ideally, we could get similar AI help for Extract Method from directly within our IDE. That’s where Junie comes in.
Extract Methods with Junie
Junie is a new AI plugin by JetBrains, currently in an Early Access Program. I’ve been playing with it and am really enjoying how it works. For this blog, I’ll show you how I interact with it to get help with Extract Methods.
Inside of IntelliJ, I ask Junie to do the following:
“In ParrotTest, create one Creation Method for each Parrot Type, passing in number of coconuts, voltage and isNailed, even if they aren’t needed.”
After a few seconds, It gives me what I want:
private Parrot createEuropeanParrot(int numberOfCoconuts, double voltage, boolean isNailed) {
return new Parrot(ParrotTypeEnum.EUROPEAN, numberOfCoconuts, voltage, isNailed);
}
private Parrot createAfricanParrot(int numberOfCoconuts, double voltage, boolean isNailed) {
return new Parrot(ParrotTypeEnum.AFRICAN, numberOfCoconuts, voltage, isNailed);
}
private Parrot createNorwegianBlueParrot(int numberOfCoconuts, double voltage, boolean isNailed) {
return new Parrot(ParrotTypeEnum.NORWEGIAN_BLUE, numberOfCoconuts, voltage, isNailed);
}
@Test
public void getSpeedOfEuropeanParrot() {
Parrot parrot = createEuropeanParrot(0, 0, false);
assertEquals(12.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_One_Coconut() {
Parrot parrot = createAfricanParrot(1, 0, false);
assertEquals(3.0, parrot.getSpeed(), 0.0);
}
@Test
public void getSpeedOfAfricanParrot_With_Two_Coconuts() {
Parrot parrot = createAfricanParrot(2, 0, false);
assertEquals(0.0, parrot.getSpeed(), 0.0);
}
You may wonder why I had to tell Junie what parameters to pass in to each Creation Method. Junie is clever. If I didn’t specify that, Junie would omit any parameters that it could simply hardcode into the constructor calls, if they happen to be the same values for all instantiations (e.g. a “false” value for isNailed).
BTW, if and when Junie does something you don’t want, you can simply tell it to undo the work and it will gracefully retreat to the prior state of your code.
So far, I’m loving how Junie performs. I’d encourage you to play with it by getting into the Early Access Program.
Conclusion
Refactoring has traditionally been an atomic operation: you implement or execute one refactoring at a time. To complete the design change you want, this may involve performing a sequence of refactorings. I’ve been doing that for well over 2 decades now and frankly, I’m tired of it. I’m lazy and I simply want my refactoring tools to do more. Thanks to AI, that’s now possible.
Today, I’m using a combination of automated + agentic refactoring when improving the design of code. While I can ask AI to perform very large steps, that tends to be more dangerous. So I’m asking it to perform slightly more powerful refactorings. And keep in mind that I could not and would not do this if I didn’t have thorough, automated checks to ensure that my changes preserve behavior. With or without AI, refactoring must be safe.
The use of AI in refactoring is a rich new area, changing rapidly. If we use it wisely, it can help us improve the design of code more efficiently and effectively. I for one am overjoyed that I am no longer stuck with the limited functionality in the available set of automated refactoring tools. AI has the potential to help us make more intelligent design transformations. We just need to make sure that these transformations are safe.