DDT means dont-do-that packaging.
This skill is used to constrain a common bad smell: when the code is clearly very short, its semantics are very direct, and its call relationships are very simple, yet it still insists on splitting out an extra layer of functions and an extra layer of types, ultimately making the human reading path longer and the comprehension cost higher.
When to trigger
When the user expresses the following kinds of meaning, prioritize using this skill.
- Saying the code is over-encapsulated, over-abstracted, or split too finely
- Pointing out that a function has only 2 to 5 lines and is called from only one place
- Pointing out that a type that would have been enough as one piece has been split into multiple thin types that jump back and forth
- Requesting that a helper be inlined back into the main flow
- Requesting that thin types be merged to reduce reading jumps
- Requesting that the code be closer to human reading, rather than being split for the sake of formal layering
Core principles
First determine whether the split has real benefits, then decide whether to keep it or take it back.
If a function has only a few lines, has only one caller, and its function name does not provide new business semantics, by default this is over-encapsulation and it should be inlined directly back into the call site. Do not keep this kind of thin function just to look more modular.
If a type merely slices a thin layer out of another type, does not form stable reuse, and does not significantly reduce comprehension cost, by default this is over-splitting and it should be merged back into a more direct type definition. Only keep the split when a certain subtype is truly used independently in multiple places, or when splitting it out can clearly reduce the complexity of the main type.
Code should first serve human reading, not formal decoupling. Local slight coupling is usually more readable than one extra jump.
Function judgment rules
When encountering a small function, judge in this order.
First check whether it is simple logic of only 2 to 5 lines. Things like a simple assignment, a single branch, a single pass-through, a single map, or a single string check are by default not worth extracting into a separate function.
Then check whether it has only one caller. If there is only one caller, and the call site is very close to the definition site, prefer inlining.
Then check whether the function name adds new business semantics. If the function name merely restates the implementation, such as thin wrapper names like parseRound, rememberRound, or buildPayload, while the code itself is already understandable at a glance, prefer inlining.
Then check whether extracting it truly reduces the cognitive burden of the main flow. If the main flow still must be clicked into to see the details, then this layer of extraction usually has no value.
Only the following cases are recommended for keeping a separate function.
The function is reused in multiple places. The function carries clear, stable, non-negligible business semantics. The function's internal logic has already grown long enough to crush the readability of the main flow. Or the function needs independent testing, independent comments, or independent reuse.
Type judgment rules
When encountering type splitting, judge in this order.
First check whether one type can directly express the current problem clearly. If it can, do not continue splitting for the sake of a sense of hierarchy.
Then check whether the subtype serves only one parent type and is consumed in only one place. If so, prefer merging it back.
Then check whether the benefits after splitting truly exist. If after splitting there are only more names, more jumps, and a longer comprehension path, without forming reuse or clearly compressing complexity, then it is over-splitting.
Only the following cases are recommended for splitting out a subtype.
Subtypes are shared by multiple modules. A nested structure itself has stable semantics. The main type has grown to the point of hurting readability, and extracting it would clearly improve readability. Or the subtype itself is an independent boundary, such as an API response body, a database row structure, or a message body structure.
Actions to take
When over-encapsulation is confirmed, do these things directly.
Inline small functions with a single caller back into the call site. Delete helpers that provide no benefit. Merge overly thin types back into the main type. Delete intermediate layers that merely pass things through. Keep necessary comments, but only explain key intent, not restate the surface behavior of the code.
If the user is reviewing code rather than asking for direct modifications, clearly point out which functions or types are over-encapsulated, and give a one-sentence reason, focusing on explaining why this layer of separation makes reading worse.
Output requirements
For the suggestions or modifications given, follow these expression habits by default.
Directly point out what should be inlined and what should be merged. Do not speak vaguely about levels of abstraction. Do not introduce new helpers just to round things out. Do not take logic you just pulled back and wrap it again under a different name.
If you need to explain the reason, one short sentence is enough, for example: there is only one caller and the logic is too thin, so it reads better after inlining. Or: this subtype has no independent reuse value, so merging it makes the reading path shorter.
Anti-pattern checklist
Treat all of the following as dont-do-that packaging by default.
A thin function of 2 to 5 lines with only one caller.
A helper that only does one field copy, one simple check, one string match, or one map wrapper.
Thin wrapper functions like parseX, buildX, normalizeX, rememberX, toX that exist just for the sake of splitting.
Getter, selector, mapper, and convert functions that only wrap one layer of value retrieval, renaming, or pass-through.
When a single type could express it clearly, but it is split into a main type plus a subtype plus an alias type.
Forcibly extracting a nested object even though it has no reuse value, just to look neat.
Examples
Example one.
The following style is a typical case of thin-function over-encapsulation.handleMessageEvent() It has only two lines of real logic, and it is called in only one case .
case AgentFlowEnum.MESSAGE: {
this.handleMessageEvent(chunk, messageData);
break;
}
private handleMessageEvent(chunk: DifyMsgSSERes, messageData: MessagePayload) {
messageData.data.answer = chunk.answer ?? "";
this.writeMessagePayload(messageData);
}
A more appropriate style is to inline it directly back into the switch branch, making the reading path shorter.
case AgentFlowEnum.MESSAGE: {
messageData.data.answer = chunk.answer ?? "";
this.writeMessagePayload(messageData);
break;
}
Example two.
The following style is also a typical case of splitting for the sake of splitting.parseRound() It only has a regex match and parseInt(), and it is used only once by resolveRound() .
private resolveRound(logData: AgentMessageData): number {
const round = this.parseRound(logData.label);
if (round !== null) {
return round;
}
if (logData.parent_id !== null) {
const parentRound = this.roundMap.get(logData.parent_id);
if (parentRound !== undefined) {
return parentRound;
}
}
throw new Error(`agent_log round not found: ${logData.label}`);
}
private parseRound(label: string | undefined): number | null {
if (!label) return null;
const matched = label.trim().match(/^ROUND (\d+)$/u);
if (!matched) {
return null;
}
return Number.parseInt(matched[1], 10);
}
A more appropriate style is to pull these few lines of logic directly back into resolveRound()。
private resolveRound(logData: AgentMessageData): number {
const matched = logData.label.trim().match(/^ROUND (\d+)$/u);
if (matched) {
return Number.parseInt(matched[1], 10);
}
if (logData.parent_id !== null) {
const parentRound = this.roundMap.get(logData.parent_id);
if (parentRound !== undefined) {
return parentRound;
}
}
throw new Error(`agent_log round not found: ${logData.label}`);
}
Example three.
The following type split is also excessive. Someone reading the code needs to first look at DifyMsgSSERes, then jump to DifyWorkflowEventData, then jump to AgentMessageData, but in reality no stable reuse boundary is formed here.
export type AgentMessageData = {
id: string;
parent_id: string | null;
label: string;
status: string;
};
export type DifyWorkflowEventData = AgentMessageData & {
title?: string;
outputs?: {
structured_output?: Record<string, any>;
[key: string]: any;
};
[key: string]: any;
};
export type DifyMsgSSERes = {
event: string;
data: DifyWorkflowEventData;
};
const logData = chunk.data as AgentMessageData;
A more appropriate style is to use a single type to make the current boundary clear directly, and no extra assertion is needed when consuming it.
export type AgentMessageData = {
id: string;
parent_id: string | null;
label: string;
status: string;
title?: string;
outputs?: {
structured_output?: Record<string, any>;
[key: string]: any;
};
[key: string]: any;
};
export type DifyMsgSSERes = {
event: string;
data: AgentMessageData;
};
const logData = chunk.data;
Example four.
The following getter, selector, mapper, and convert style methods also count as over-encapsulation if they essentially just wrap one layer of value retrieval, renaming, and pass-through.
const labels = toolList.map((tool) => this.mapToolLabel(tool));
private mapToolLabel(tool: AgentTool) {
return tool.label;
}
A more appropriate style is to directly remove this thin wrapper layer.
const labels = toolList.map((tool) => tool.label);
Decision preference
When unsure, prefer one fewer layer.
If it can be understood directly in the current context, do not add extra encapsulation.
If you can explain it clearly with one type, don't split it first and then circle back.