Refactoring & Documentation with AI
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Examples on this page use the current GitHub Copilot Chat extension for VS Code and the current Claude Code CLI,
both checked against their official documentation as of 2026-09-28. The running example is again DocsAssistant
API: the controller at src/main/java/com/example/docsassistant/DocsController.java, its tests under
src/test/java/com/example/docsassistant/, and its embeddings built by the Python script at
ingest/build_index.py. This page assumes
Prompting Coding Assistants and builds on it:
every recipe below is the specificity checklist applied to a specific kind of request.
Refactoring recipes with AI
Each recipe follows the same shape: describe the smell, ask for the specific transformation (not just "clean this up"), and read the diff before accepting it. Doing the transformation in one small, reviewable step — rather than asking for a whole-file rewrite — is what keeps the diff easy to check against the original behavior; see Reviewing AI-Generated Code for the checklist to run over the result.
Extract method
DocsController builds an excerpt twice: once for the page itself, once for each related page in the response.
@GetMapping("/api/pages/{id}/related")
public List<RelatedPage> getRelatedPages(@PathVariable Long id,
@RequestParam(defaultValue = "5") int limit) {
Page page = pageRepository.findById(id).orElseThrow();
String text = page.getContent();
String excerpt = text.length() > 200 ? text.substring(0, 200) + "..." : text;
page.setExcerpt(excerpt);
List<RelatedPage> related = embeddingIndex.nearest(id, limit);
for (RelatedPage r : related) {
String relatedText = r.getContent();
r.setExcerpt(relatedText.length() > 200 ? relatedText.substring(0, 200) + "..." : relatedText);
}
return related;
}
$ claude "in @src/main/java/com/example/docsassistant/DocsController.java, extract the repeated
200-character excerpt logic in getRelatedPages into a private excerpt(String text) method and
use it in both places. Keep behavior identical and run the tests after."
#DocsController.java Extract the duplicated 200-character excerpt truncation in getRelatedPages
into a private excerpt(String text) helper method, and call it from both places it currently
appears inline. Don't change the truncation length or the "..." suffix.
@GetMapping("/api/pages/{id}/related")
public List<RelatedPage> getRelatedPages(@PathVariable Long id,
@RequestParam(defaultValue = "5") int limit) {
Page page = pageRepository.findById(id).orElseThrow();
- String text = page.getContent();
- String excerpt = text.length() > 200 ? text.substring(0, 200) + "..." : text;
- page.setExcerpt(excerpt);
+ page.setExcerpt(excerpt(page.getContent()));
List<RelatedPage> related = embeddingIndex.nearest(id, limit);
- for (RelatedPage r : related) {
- String relatedText = r.getContent();
- r.setExcerpt(relatedText.length() > 200 ? relatedText.substring(0, 200) + "..." : relatedText);
- }
+ related.forEach(r -> r.setExcerpt(excerpt(r.getContent())));
return related;
}
+
+private String excerpt(String text) {
+ return text.length() > 200 ? text.substring(0, 200) + "..." : text;
+}
Both tools produce the same shape of change here because "extract method" is an unambiguous, mechanical transformation once the target lines are pointed out; see Claude Code — Common workflows ("Refactor code") and GitHub — Prompt engineering for GitHub Copilot Chat.
Decompose conditional
A nested if/else that only ever returns an empty list on any failing branch is a common early-DocsAssistant pattern that reads better as a guard clause plus one extracted predicate:
public List<RelatedPage> recommendRelatedPages(Long pageId, int limit) {
List<RelatedPage> results;
if (pageId != null) {
if (pageRepository.existsById(pageId)) {
if (limit > 0) {
results = embeddingIndex.nearest(pageId, limit);
} else {
results = List.of();
}
} else {
results = List.of();
}
} else {
results = List.of();
}
return results;
}
$ claude "decompose the nested if/else in recommendRelatedPages (same file) into a guard clause
plus a private isValidRequest(Long pageId, int limit) predicate. Every branch that currently
returns List.of() should be covered by the single guard. Run the tests after."
#DocsController.java Decompose the nested if/else in recommendRelatedPages into a single guard
clause that returns List.of(), plus a private isValidRequest(Long pageId, int limit) predicate.
Keep every branch that currently returns List.of() covered by that guard.
public List<RelatedPage> recommendRelatedPages(Long pageId, int limit) {
- List<RelatedPage> results;
- if (pageId != null) {
- if (pageRepository.existsById(pageId)) {
- if (limit > 0) {
- results = embeddingIndex.nearest(pageId, limit);
- } else {
- results = List.of();
- }
- } else {
- results = List.of();
- }
- } else {
- results = List.of();
- }
- return results;
+ if (!isValidRequest(pageId, limit)) {
+ return List.of();
+ }
+ return embeddingIndex.nearest(pageId, limit);
+}
+
+private boolean isValidRequest(Long pageId, int limit) {
+ return pageId != null && limit > 0 && pageRepository.existsById(pageId);
}
This is the kind of change worth doing in one small step and re-running tests immediately, rather than folding it into a larger rewrite; see Claude Code — Best practices ("Give Claude a way to verify its work") and GitHub — Prompt engineering for GitHub Copilot Chat.
Rename
A poorly named method survives from an early prototype: calc computes a cosine-similarity relevance score
between two embedding vectors, but nothing in the name says so.
#DocsController.java Rename the method calc(double[] queryVec, double[] pageVec) to
computeRelevanceScore, and update every call site in this file and its test class.
$ claude "rename calc(double[] queryVec, double[] pageVec) in
@src/main/java/com/example/docsassistant/DocsController.java to computeRelevanceScore, update every
call site under src/, including DocsControllerTest.java, and run the tests after."
-public double calc(double[] queryVec, double[] pageVec) {
+public double computeRelevanceScore(double[] queryVec, double[] pageVec) {
return cosineSimilarity(queryVec, pageVec);
}
A rename that only touches one file is safe to ask for directly; a rename of a method used across many files is exactly the kind of multi-file change where planning first pays off — see Plan-First Workflows and Claude Code — Best practices, whose own guidance is the opposite for a small, one-file rename: "If you could describe the diff in one sentence, skip the plan." For the Copilot side, see GitHub — Prompt engineering for GitHub Copilot Chat.
Remove dead code
An earlier, unindexed implementation of related-page lookup was left behind once embeddingIndex.nearest took
over, along with the two imports only it needed:
import java.util.Comparator;
import java.util.stream.Collectors;
// ...
@Deprecated
private List<RelatedPage> legacyLinearScan(Long pageId, int limit) {
return pageRepository.findAll().stream()
.filter(p -> !p.getId().equals(pageId))
.sorted(Comparator.comparingDouble(p -> -computeRelevanceScore(embed(pageId), embed(p.getId()))))
.limit(limit)
.collect(Collectors.toList());
}
$ claude "search @src/main/java/com/example/docsassistant/ for calls to legacyLinearScan. If nothing
else calls it, delete the method and any imports that become unused."
-import java.util.Comparator;
-import java.util.stream.Collectors;
-
// ...
-@Deprecated
-private List<RelatedPage> legacyLinearScan(Long pageId, int limit) {
- return pageRepository.findAll().stream()
- .filter(p -> !p.getId().equals(pageId))
- .sorted(Comparator.comparingDouble(p -> -computeRelevanceScore(embed(pageId), embed(p.getId()))))
- .limit(limit)
- .collect(Collectors.toList());
-}
Asking the assistant to search for call sites first, rather than assuming a method is unused, is the same habit as the specificity checklist’s "constraints" column in Prompting Coding Assistants: an assistant that only sees the file you attached has no way to know whether another module still calls that method.
The "majority problem" and making AI code your own
Code that no one on the team wrote entirely themselves tends to lose the property that makes it maintainable: someone who can explain why it looks the way it does, not just what it does. Call this the majority problem — once most of a file’s logic arrived through generated diffs that were skimmed and accepted rather than written and understood, the team’s shared mental model of that file erodes, and the next bug or feature in it costs more than the tests-passing diff suggested it would.
The fix is not to write less AI-assisted code; it is to close the understanding gap before the diff lands, the same way you would review a colleague’s pull request:
-
Read every line before accepting it, not just the parts the diff highlights — the same "trust-then-verify gap" failure pattern applies whether the code was generated or copy-pasted from search results. See Claude Code — Best practices.
-
Ask the assistant to explain its own diff rather than only re-reading it silently — a prompt like
explain why you used a guard clause here instead of the nested if/elsesurfaces reasoning that a diff alone does not, and any gap in that explanation is worth resolving before merging. -
Re-derive the tricky parts by hand once, even when the generated version is correct — for the parts of a change most likely to need a fix in six months, spending a few minutes tracing the logic yourself is what turns "code that runs" into "code you own."
-
Use a fresh review pass as a second opinion, not as a substitute for your own understanding: a subagent or a second session reviewing the diff against its stated intent catches gaps a tired first read misses, but it does not transfer understanding to you. See Claude Code — Best practices ("Add an adversarial review step") and Reviewing AI-Generated Code.
Docstrings, Javadoc, README and ADR generation
Docstrings and Javadoc
Asking for documentation on an existing method works the same way in both tools, but the exact command differs:
GitHub’s own GitHub Copilot Chat cheat sheet
lists a dedicated /doc slash command only for the Visual Studio and Xcode Copilot integrations, not for VS Code,
so in VS Code the equivalent is a direct chat request against the selected code rather than a slash command.
#DocsController.java Add a Javadoc comment to getRelatedPages explaining the id and limit
parameters, what it returns, and that it throws when no page matches the id. Match the Javadoc
style already used on listPages in this file.
$ claude "add Javadoc to getRelatedPages in @src/main/java/com/example/docsassistant/DocsController.java,
matching the style of the existing listPages Javadoc in the same file"
+/**
+ * Returns pages related to the page with the given id, ranked by embedding similarity.
+ *
+ * @param id the id of the page to find related pages for
+ * @param limit the maximum number of related pages to return
+ * @return the related pages, most similar first
+ * @throws java.util.NoSuchElementException if no page has the given id
+ */
@GetMapping("/api/pages/{id}/related")
public List<RelatedPage> getRelatedPages(@PathVariable Long id,
@RequestParam(defaultValue = "5") int limit) {
See Claude Code — Common workflows ("Handle documentation") and GitHub — Prompt engineering for GitHub Copilot Chat.
README generation
GitHub ships a reusable prompt-file template for this: saved as create-readme.prompt.md under
.github/prompts/, it is then invoked from the Copilot Chat view as /create-readme and analyzes the whole
workspace to draft a README. Claude Code has no equivalent built-in slash command, so the same result comes from
asking directly, scoped to what the README should and should not cover:
/create-readme
$ claude "write a README.md for this repository: what DocsAssistant API does, how to run
src/main/java/com/example/docsassistant with Maven, how to run ingest/build_index.py to rebuild
the embedding index, and how to run the tests. Don't include a license section or a full API
reference -- link to those separately if they exist."
ADR generation
An architecture decision record captures a decision, its context and its consequences so a later reader does not have to reconstruct the reasoning from the code alone. Asking an assistant for one works best when the prompt states which decision, what alternatives were considered, and which template to follow:
$ claude "write an ADR using the MADR template (Context and Problem Statement, Considered Options,
Decision Outcome, Consequences) documenting why DocsAssistant API's embedding index uses FAISS (in
ingest/build_index.py) rather than a pgvector-backed table, given that the index is rebuilt offline
and served read-only. List both FAISS and pgvector under Considered Options. Save it as
docs/adr/0007-embedding-index-choice.md."
# Use a FAISS index file for related-page lookup
## Context and Problem Statement
DocsAssistant API's related-pages feature needs nearest-neighbor lookup over page embeddings.
The index is rebuilt offline by ingest/build_index.py and served read-only by DocsController;
no write path exists at query time. Where should the embedding index live?
## Considered Options
* FAISS index file built by ingest/build_index.py and loaded at startup
* pgvector-backed table queried per request
## Decision Outcome
Chosen option: "FAISS index file built by ingest/build_index.py and loaded at startup", because
the index is rebuilt offline and only ever read at query time, so a database write path adds
nothing.
### Consequences
* Good, because query latency does not depend on the database under load.
* Bad, because the index must be rebuilt and redeployed after every content change, instead of
updating in place.
* Bad, because filtering related pages by metadata (e.g. by section) needs a post-filter step,
since FAISS itself is not metadata-aware.
The headings follow the minimal MADR template; see MADR — Markdown Architectural Decision Records for the full template and its optional sections (Decision Drivers, Confirmation, Pros and Cons of the Options). The generated draft is a starting point, not a finished record — the "Consequences" section in particular needs a human check that it reflects what the team actually decided to accept, not just what the assistant inferred from the code.
Legacy modernisation and translating between languages
Legacy modernisation
Modernising old code follows the same loop as any other refactor, just aimed at deprecated APIs and outdated language features instead of a single method: find the legacy usage, get a recommendation, apply it in a verifiable increment, and run the tests.
$ claude "find deprecated Spring MVC annotations in @src/main/java/com/example/docsassistant/,
suggest their current replacements, and apply them one file at a time, running the tests after
each file"
For porting a large codebase to a new language or framework wholesale, rather than modernising it in place, treat it as a fan-out of many small, independently verifiable migrations instead of one large one; see Anthropic — How Anthropic teams use Claude Code for large-scale code migrations and Claude Code — Common workflows ("Refactor code").
Translating between languages
Reimplementing an existing function in another language is a targeted case of the same recipe: give the source
function, name the target language and its idioms, and check the result against the same inputs. DocsAssistant
API’s Python scoring helper in ingest/build_index.py has a small Java counterpart worth keeping in sync when
the scoring logic changes:
def cosine_similarity(query_vec, page_vec):
dot = sum(q * p for q, p in zip(query_vec, page_vec))
norm_q = sum(q * q for q in query_vec) ** 0.5
norm_p = sum(p * p for p in page_vec) ** 0.5
if norm_q == 0 or norm_p == 0:
return 0.0
return dot / (norm_q * norm_p)
$ claude "translate the cosine_similarity function in @ingest/build_index.py into a Java static
method double[] -> double[] -> double in a new class
src/main/java/com/example/docsassistant/ScoreUtils.java, matching the zero-norm guard and zip()'s
stop-at-the-shorter-vector behavior, and add a
unit test under src/test/java/com/example/docsassistant/ that checks the same cases the Python
version would return 0.0 for."
package com.example.docsassistant;
public final class ScoreUtils {
private ScoreUtils() {
}
public static double cosineSimilarity(double[] queryVec, double[] pageVec) {
// zip() in the Python version stops at the shorter vector; the norms still use every element.
int n = Math.min(queryVec.length, pageVec.length);
double dot = 0.0;
for (int i = 0; i < n; i++) {
dot += queryVec[i] * pageVec[i];
}
double normQ = 0.0;
for (double q : queryVec) {
normQ += q * q;
}
double normP = 0.0;
for (double p : pageVec) {
normP += p * p;
}
if (normQ == 0.0 || normP == 0.0) {
return 0.0;
}
return dot / (Math.sqrt(normQ) * Math.sqrt(normP));
}
}
A translated function still needs the same verification as any other generated code: run it against cases the
original handles (here, the zero-norm guard and vectors of different lengths) before treating the two
implementations as equivalent. A naive port that loops over queryVec.length alone would throw
ArrayIndexOutOfBoundsException when pageVec is shorter, where the Python silently truncates, and only a
mismatched-length test case exposes that divergence. See
Claude Code — Best practices ("Give Claude a way to verify its
work").