kollabor 0.4.9__py3-none-any.whl → 0.4.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +2 -0
- agents/coder/__init__.py +0 -0
- agents/coder/agent.json +4 -0
- agents/coder/api-integration.md +2150 -0
- agents/coder/cli-pretty.md +765 -0
- agents/coder/code-review.md +1092 -0
- agents/coder/database-design.md +1525 -0
- agents/coder/debugging.md +1102 -0
- agents/coder/dependency-management.md +1397 -0
- agents/coder/git-workflow.md +1099 -0
- agents/coder/refactoring.md +1454 -0
- agents/coder/security-hardening.md +1732 -0
- agents/coder/system_prompt.md +1448 -0
- agents/coder/tdd.md +1367 -0
- agents/creative-writer/__init__.py +0 -0
- agents/creative-writer/agent.json +4 -0
- agents/creative-writer/character-development.md +1852 -0
- agents/creative-writer/dialogue-craft.md +1122 -0
- agents/creative-writer/plot-structure.md +1073 -0
- agents/creative-writer/revision-editing.md +1484 -0
- agents/creative-writer/system_prompt.md +690 -0
- agents/creative-writer/worldbuilding.md +2049 -0
- agents/data-analyst/__init__.py +30 -0
- agents/data-analyst/agent.json +4 -0
- agents/data-analyst/data-visualization.md +992 -0
- agents/data-analyst/exploratory-data-analysis.md +1110 -0
- agents/data-analyst/pandas-data-manipulation.md +1081 -0
- agents/data-analyst/sql-query-optimization.md +881 -0
- agents/data-analyst/statistical-analysis.md +1118 -0
- agents/data-analyst/system_prompt.md +928 -0
- agents/default/__init__.py +0 -0
- agents/default/agent.json +4 -0
- agents/default/dead-code.md +794 -0
- agents/default/explore-agent-system.md +585 -0
- agents/default/system_prompt.md +1448 -0
- agents/kollabor/__init__.py +0 -0
- agents/kollabor/analyze-plugin-lifecycle.md +175 -0
- agents/kollabor/analyze-terminal-rendering.md +388 -0
- agents/kollabor/code-review.md +1092 -0
- agents/kollabor/debug-mcp-integration.md +521 -0
- agents/kollabor/debug-plugin-hooks.md +547 -0
- agents/kollabor/debugging.md +1102 -0
- agents/kollabor/dependency-management.md +1397 -0
- agents/kollabor/git-workflow.md +1099 -0
- agents/kollabor/inspect-llm-conversation.md +148 -0
- agents/kollabor/monitor-event-bus.md +558 -0
- agents/kollabor/profile-performance.md +576 -0
- agents/kollabor/refactoring.md +1454 -0
- agents/kollabor/system_prompt copy.md +1448 -0
- agents/kollabor/system_prompt.md +757 -0
- agents/kollabor/trace-command-execution.md +178 -0
- agents/kollabor/validate-config.md +879 -0
- agents/research/__init__.py +0 -0
- agents/research/agent.json +4 -0
- agents/research/architecture-mapping.md +1099 -0
- agents/research/codebase-analysis.md +1077 -0
- agents/research/dependency-audit.md +1027 -0
- agents/research/performance-profiling.md +1047 -0
- agents/research/security-review.md +1359 -0
- agents/research/system_prompt.md +492 -0
- agents/technical-writer/__init__.py +0 -0
- agents/technical-writer/agent.json +4 -0
- agents/technical-writer/api-documentation.md +2328 -0
- agents/technical-writer/changelog-management.md +1181 -0
- agents/technical-writer/readme-writing.md +1360 -0
- agents/technical-writer/style-guide.md +1410 -0
- agents/technical-writer/system_prompt.md +653 -0
- agents/technical-writer/tutorial-creation.md +1448 -0
- core/__init__.py +0 -2
- core/application.py +343 -88
- core/cli.py +229 -10
- core/commands/menu_renderer.py +463 -59
- core/commands/registry.py +14 -9
- core/commands/system_commands.py +2461 -14
- core/config/loader.py +151 -37
- core/config/service.py +18 -6
- core/events/bus.py +29 -9
- core/events/executor.py +205 -75
- core/events/models.py +27 -8
- core/fullscreen/command_integration.py +20 -24
- core/fullscreen/components/__init__.py +10 -1
- core/fullscreen/components/matrix_components.py +1 -2
- core/fullscreen/components/space_shooter_components.py +654 -0
- core/fullscreen/plugin.py +5 -0
- core/fullscreen/renderer.py +52 -13
- core/fullscreen/session.py +52 -15
- core/io/__init__.py +29 -5
- core/io/buffer_manager.py +6 -1
- core/io/config_status_view.py +7 -29
- core/io/core_status_views.py +267 -347
- core/io/input/__init__.py +25 -0
- core/io/input/command_mode_handler.py +711 -0
- core/io/input/display_controller.py +128 -0
- core/io/input/hook_registrar.py +286 -0
- core/io/input/input_loop_manager.py +421 -0
- core/io/input/key_press_handler.py +502 -0
- core/io/input/modal_controller.py +1011 -0
- core/io/input/paste_processor.py +339 -0
- core/io/input/status_modal_renderer.py +184 -0
- core/io/input_errors.py +5 -1
- core/io/input_handler.py +211 -2452
- core/io/key_parser.py +7 -0
- core/io/layout.py +15 -3
- core/io/message_coordinator.py +111 -2
- core/io/message_renderer.py +129 -4
- core/io/status_renderer.py +147 -607
- core/io/terminal_renderer.py +97 -51
- core/io/terminal_state.py +21 -4
- core/io/visual_effects.py +816 -165
- core/llm/agent_manager.py +1063 -0
- core/llm/api_adapters/__init__.py +44 -0
- core/llm/api_adapters/anthropic_adapter.py +432 -0
- core/llm/api_adapters/base.py +241 -0
- core/llm/api_adapters/openai_adapter.py +326 -0
- core/llm/api_communication_service.py +167 -113
- core/llm/conversation_logger.py +322 -16
- core/llm/conversation_manager.py +556 -30
- core/llm/file_operations_executor.py +84 -32
- core/llm/llm_service.py +934 -103
- core/llm/mcp_integration.py +541 -57
- core/llm/message_display_service.py +135 -18
- core/llm/plugin_sdk.py +1 -2
- core/llm/profile_manager.py +1183 -0
- core/llm/response_parser.py +274 -56
- core/llm/response_processor.py +16 -3
- core/llm/tool_executor.py +6 -1
- core/logging/__init__.py +2 -0
- core/logging/setup.py +34 -6
- core/models/resume.py +54 -0
- core/plugins/__init__.py +4 -2
- core/plugins/base.py +127 -0
- core/plugins/collector.py +23 -161
- core/plugins/discovery.py +37 -3
- core/plugins/factory.py +6 -12
- core/plugins/registry.py +5 -17
- core/ui/config_widgets.py +128 -28
- core/ui/live_modal_renderer.py +2 -1
- core/ui/modal_actions.py +5 -0
- core/ui/modal_overlay_renderer.py +0 -60
- core/ui/modal_renderer.py +268 -7
- core/ui/modal_state_manager.py +29 -4
- core/ui/widgets/base_widget.py +7 -0
- core/updates/__init__.py +10 -0
- core/updates/version_check_service.py +348 -0
- core/updates/version_comparator.py +103 -0
- core/utils/config_utils.py +685 -526
- core/utils/plugin_utils.py +1 -1
- core/utils/session_naming.py +111 -0
- fonts/LICENSE +21 -0
- fonts/README.md +46 -0
- fonts/SymbolsNerdFont-Regular.ttf +0 -0
- fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
- fonts/__init__.py +44 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
- kollabor-0.4.15.dist-info/RECORD +228 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
- plugins/agent_orchestrator/__init__.py +39 -0
- plugins/agent_orchestrator/activity_monitor.py +181 -0
- plugins/agent_orchestrator/file_attacher.py +77 -0
- plugins/agent_orchestrator/message_injector.py +135 -0
- plugins/agent_orchestrator/models.py +48 -0
- plugins/agent_orchestrator/orchestrator.py +403 -0
- plugins/agent_orchestrator/plugin.py +976 -0
- plugins/agent_orchestrator/xml_parser.py +191 -0
- plugins/agent_orchestrator_plugin.py +9 -0
- plugins/enhanced_input/box_styles.py +1 -0
- plugins/enhanced_input/color_engine.py +19 -4
- plugins/enhanced_input/config.py +2 -2
- plugins/enhanced_input_plugin.py +61 -11
- plugins/fullscreen/__init__.py +6 -2
- plugins/fullscreen/example_plugin.py +1035 -222
- plugins/fullscreen/setup_wizard_plugin.py +592 -0
- plugins/fullscreen/space_shooter_plugin.py +131 -0
- plugins/hook_monitoring_plugin.py +436 -78
- plugins/query_enhancer_plugin.py +66 -30
- plugins/resume_conversation_plugin.py +1494 -0
- plugins/save_conversation_plugin.py +98 -32
- plugins/system_commands_plugin.py +70 -56
- plugins/tmux_plugin.py +154 -78
- plugins/workflow_enforcement_plugin.py +94 -92
- system_prompt/default.md +952 -886
- core/io/input_mode_manager.py +0 -402
- core/io/modal_interaction_handler.py +0 -315
- core/io/raw_input_processor.py +0 -946
- core/storage/__init__.py +0 -5
- core/storage/state_manager.py +0 -84
- core/ui/widget_integration.py +0 -222
- core/utils/key_reader.py +0 -171
- kollabor-0.4.9.dist-info/RECORD +0 -128
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
- {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
<!-- Refactoring skill - safe code transformation while keeping tests green -->
|
|
2
|
+
|
|
3
|
+
refactoring mode: CHANGE STRUCTURE, NOT BEHAVIOR
|
|
4
|
+
|
|
5
|
+
when this skill is active, you follow disciplined refactoring practices.
|
|
6
|
+
this is a comprehensive guide to safe code transformation.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PHASE 0: ENVIRONMENT VERIFICATION
|
|
10
|
+
|
|
11
|
+
before doing ANY refactoring, verify the safety net exists.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
verify testing framework exists
|
|
15
|
+
|
|
16
|
+
<terminal>python -m pytest --version</terminal>
|
|
17
|
+
|
|
18
|
+
if pytest not installed:
|
|
19
|
+
<terminal>pip install pytest pytest-cov</terminal>
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
verify tests exist for code you will refactor
|
|
23
|
+
|
|
24
|
+
<terminal>find . -name "test_*.py" -type f | head -20</terminal>
|
|
25
|
+
|
|
26
|
+
<terminal>python -m pytest tests/ --collect-only</terminal>
|
|
27
|
+
|
|
28
|
+
if no tests exist for the target code:
|
|
29
|
+
[warn] cannot refactor safely without tests
|
|
30
|
+
[warn] either write tests first, or refuse to refactor
|
|
31
|
+
the discipline: no tests, no refactoring, no exceptions
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
verify current test status
|
|
35
|
+
|
|
36
|
+
<terminal>python -m pytest tests/ -v</terminal>
|
|
37
|
+
|
|
38
|
+
all tests must pass BEFORE starting refactoring.
|
|
39
|
+
if tests are failing:
|
|
40
|
+
[1] fix failing tests first
|
|
41
|
+
[2] or write reproduction tests for known issues
|
|
42
|
+
[3] never refactor while tests are red
|
|
43
|
+
|
|
44
|
+
document current state:
|
|
45
|
+
|
|
46
|
+
<terminal>python -m pytest tests/ --cov -v | tee test_status_before.log</terminal>
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
verify git status
|
|
50
|
+
|
|
51
|
+
<terminal>git status</terminal>
|
|
52
|
+
|
|
53
|
+
<terminal>git diff --stat</terminal>
|
|
54
|
+
|
|
55
|
+
ensure working directory is clean or changes are committed.
|
|
56
|
+
refactoring on top of uncommitted work is risky.
|
|
57
|
+
|
|
58
|
+
best practice:
|
|
59
|
+
<terminal>git checkout -b refactor/safe-cleanup-description</terminal>
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
check for IDE refactoring support
|
|
63
|
+
|
|
64
|
+
<terminal>code --list-extensions | grep -i refactor</terminal>
|
|
65
|
+
|
|
66
|
+
optional but helpful:
|
|
67
|
+
- VS Code: Python extension with refactoring
|
|
68
|
+
- PyCharm: built-in refactoring tools
|
|
69
|
+
- vim/emacs: with lsp or rope plugin
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
PHASE 1: WHAT IS REFACTORING
|
|
73
|
+
|
|
74
|
+
definition:
|
|
75
|
+
|
|
76
|
+
refactoring is changing code structure WITHOUT changing behavior.
|
|
77
|
+
the external behavior stays exactly the same.
|
|
78
|
+
the internal structure becomes cleaner, clearer, more maintainable.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
what refactoring is NOT:
|
|
82
|
+
|
|
83
|
+
[x] adding new features
|
|
84
|
+
[x] fixing bugs (behavior changes)
|
|
85
|
+
[x] performance optimization (usually changes behavior)
|
|
86
|
+
[x] updating dependencies
|
|
87
|
+
[x] changing API contracts
|
|
88
|
+
|
|
89
|
+
these are different activities.
|
|
90
|
+
do them separately from refactoring.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
the two hats:
|
|
94
|
+
|
|
95
|
+
hat 1: adding function
|
|
96
|
+
- adding new capabilities
|
|
97
|
+
- fixing bugs
|
|
98
|
+
- changing behavior
|
|
99
|
+
- tests will change/expand
|
|
100
|
+
|
|
101
|
+
hat 2: refactoring
|
|
102
|
+
- restructuring existing code
|
|
103
|
+
- behavior stays exactly the same
|
|
104
|
+
- tests stay exactly the same
|
|
105
|
+
|
|
106
|
+
never wear both hats at once.
|
|
107
|
+
switch clearly between them.
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
when to refactor
|
|
111
|
+
|
|
112
|
+
[ok] when you need to add a feature and the code resists
|
|
113
|
+
[ok] when code is duplicated in multiple places
|
|
114
|
+
[ok] when names dont clearly express intent
|
|
115
|
+
[ok] when methods are too long to understand
|
|
116
|
+
[ok] when conditionals are deeply nested
|
|
117
|
+
[ok] when you need to understand how code works
|
|
118
|
+
|
|
119
|
+
[warn] when deadlines are tight and tests are missing
|
|
120
|
+
[warn] when code works but you dont like the style
|
|
121
|
+
[warn] when youre trying to learn a new pattern
|
|
122
|
+
|
|
123
|
+
the rule of three:
|
|
124
|
+
1. first time - just do it
|
|
125
|
+
2. second time - wince at the duplication
|
|
126
|
+
3. third time - refactor
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
when NOT to refactor
|
|
130
|
+
|
|
131
|
+
[x] production is down and youre panicked
|
|
132
|
+
[x] no tests exist
|
|
133
|
+
[x] tests are failing
|
|
134
|
+
[x] youre about to release
|
|
135
|
+
[x] the code is about to be deleted
|
|
136
|
+
[x] you dont understand what the code does
|
|
137
|
+
|
|
138
|
+
first understand, then refactor.
|
|
139
|
+
never refactor mysterious code.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
PHASE 2: THE REFACTORING CYCLE
|
|
143
|
+
|
|
144
|
+
the core rhythm:
|
|
145
|
+
|
|
146
|
+
[1] verify tests pass
|
|
147
|
+
[2] identify the smell
|
|
148
|
+
[3] choose the refactoring
|
|
149
|
+
[4] make the SMALL change
|
|
150
|
+
[5] run tests
|
|
151
|
+
[6] if tests pass, commit
|
|
152
|
+
[7] if tests fail, revert and try smaller step
|
|
153
|
+
[8] repeat
|
|
154
|
+
|
|
155
|
+
this cycle repeats for every refactoring.
|
|
156
|
+
never batch multiple refactorings without testing.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
step 1: verify tests pass
|
|
160
|
+
|
|
161
|
+
<terminal>python -m pytest tests/ -v</terminal>
|
|
162
|
+
|
|
163
|
+
get to green before changing anything.
|
|
164
|
+
document the baseline:
|
|
165
|
+
<terminal>python -m pytest tests/ --cov > coverage_baseline.txt</terminal>
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
step 2: identify the smell
|
|
169
|
+
|
|
170
|
+
common code smells:
|
|
171
|
+
|
|
172
|
+
- duplicated code
|
|
173
|
+
- long method
|
|
174
|
+
- large class
|
|
175
|
+
- long parameter list
|
|
176
|
+
- divergent change
|
|
177
|
+
- shotgun surgery
|
|
178
|
+
- feature envy
|
|
179
|
+
- data clumps
|
|
180
|
+
- primitive obsession
|
|
181
|
+
- switch statements
|
|
182
|
+
- temporary fields
|
|
183
|
+
- refused bequest
|
|
184
|
+
- comments
|
|
185
|
+
|
|
186
|
+
name the smell before fixing it.
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
step 3: choose the refactoring
|
|
190
|
+
|
|
191
|
+
match smell to refactoring:
|
|
192
|
+
- duplicated code -> extract method
|
|
193
|
+
- long method -> decompose conditional, extract method
|
|
194
|
+
- large class -> extract class
|
|
195
|
+
- long parameter list -> introduce parameter object
|
|
196
|
+
- feature envy -> move method
|
|
197
|
+
- primitive obsession -> replace primitive with object
|
|
198
|
+
- switch statements -> replace conditional with polymorphism
|
|
199
|
+
|
|
200
|
+
know your refactoring catalog.
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
step 4: make the small change
|
|
204
|
+
|
|
205
|
+
how small?
|
|
206
|
+
- one extraction at a time
|
|
207
|
+
- one rename at a time
|
|
208
|
+
- one move at a time
|
|
209
|
+
|
|
210
|
+
if you hesitate, make the change smaller.
|
|
211
|
+
you can always make another small change after.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
step 5: run tests
|
|
215
|
+
|
|
216
|
+
<terminal>python -m pytest tests/ -v</terminal>
|
|
217
|
+
|
|
218
|
+
every single change gets tested.
|
|
219
|
+
no exceptions.
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
step 6: commit if green
|
|
223
|
+
|
|
224
|
+
<terminal>git add -A</terminal>
|
|
225
|
+
<terminal>git commit -m "refactor: extract user validation to separate method"</terminal>
|
|
226
|
+
|
|
227
|
+
small commits are your friend.
|
|
228
|
+
they make rollback easy.
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
step 7: revert if red
|
|
232
|
+
|
|
233
|
+
<terminal>git checkout -- .</terminal>
|
|
234
|
+
|
|
235
|
+
or
|
|
236
|
+
<terminal>git reset --hard HEAD</terminal>
|
|
237
|
+
|
|
238
|
+
never try to fix tests that broke during refactoring.
|
|
239
|
+
revert and think.
|
|
240
|
+
make a smaller change.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
PHASE 3: EXTRACT METHOD
|
|
244
|
+
|
|
245
|
+
the most fundamental refactoring.
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
when to extract
|
|
249
|
+
|
|
250
|
+
signs you need extract method:
|
|
251
|
+
- method is longer than 10-15 lines
|
|
252
|
+
- method has multiple levels of indentation
|
|
253
|
+
- comments explain what a block does
|
|
254
|
+
- code is duplicated
|
|
255
|
+
- a group of lines forms a single concept
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
basic extraction
|
|
259
|
+
|
|
260
|
+
before:
|
|
261
|
+
|
|
262
|
+
def print_report(self, users):
|
|
263
|
+
# calculate totals
|
|
264
|
+
total_age = 0
|
|
265
|
+
for user in users:
|
|
266
|
+
total_age += user.age
|
|
267
|
+
average = total_age / len(users)
|
|
268
|
+
|
|
269
|
+
# print header
|
|
270
|
+
print("=" * 50)
|
|
271
|
+
print("USER REPORT")
|
|
272
|
+
print("=" * 50)
|
|
273
|
+
|
|
274
|
+
# print users
|
|
275
|
+
for user in users:
|
|
276
|
+
print(f"{user.name}: {user.age} years old")
|
|
277
|
+
|
|
278
|
+
# print footer
|
|
279
|
+
print("=" * 50)
|
|
280
|
+
print(f"Average age: {average}")
|
|
281
|
+
print("=" * 50)
|
|
282
|
+
|
|
283
|
+
after:
|
|
284
|
+
|
|
285
|
+
def print_report(self, users):
|
|
286
|
+
average = self._calculate_average_age(users)
|
|
287
|
+
self._print_header()
|
|
288
|
+
self._print_users(users)
|
|
289
|
+
self._print_footer(average)
|
|
290
|
+
|
|
291
|
+
def _calculate_average_age(self, users):
|
|
292
|
+
total_age = sum(user.age for user in users)
|
|
293
|
+
return total_age / len(users)
|
|
294
|
+
|
|
295
|
+
def _print_header(self):
|
|
296
|
+
print("=" * 50)
|
|
297
|
+
print("USER REPORT")
|
|
298
|
+
print("=" * 50)
|
|
299
|
+
|
|
300
|
+
def _print_users(self, users):
|
|
301
|
+
for user in users:
|
|
302
|
+
print(f"{user.name}: {user.age} years old")
|
|
303
|
+
|
|
304
|
+
def _print_footer(self, average):
|
|
305
|
+
print("=" * 50)
|
|
306
|
+
print(f"Average age: {average}")
|
|
307
|
+
print("=" * 50)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
extraction with parameters
|
|
311
|
+
|
|
312
|
+
before:
|
|
313
|
+
|
|
314
|
+
def process_order(self, order):
|
|
315
|
+
if order.total > 1000:
|
|
316
|
+
discount = order.total * 0.1
|
|
317
|
+
order.total -= discount
|
|
318
|
+
if order.customer.is_vip:
|
|
319
|
+
discount = order.total * 0.05
|
|
320
|
+
order.total -= discount
|
|
321
|
+
|
|
322
|
+
after:
|
|
323
|
+
|
|
324
|
+
def process_order(self, order):
|
|
325
|
+
self._apply_volume_discount(order)
|
|
326
|
+
self._apply_vip_discount(order)
|
|
327
|
+
|
|
328
|
+
def _apply_volume_discount(self, order):
|
|
329
|
+
if order.total > 1000:
|
|
330
|
+
discount = order.total * 0.1
|
|
331
|
+
order.total -= discount
|
|
332
|
+
|
|
333
|
+
def _apply_vip_discount(self, order):
|
|
334
|
+
if order.customer.is_vip:
|
|
335
|
+
discount = order.total * 0.05
|
|
336
|
+
order.total -= discount
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
extraction with return value
|
|
340
|
+
|
|
341
|
+
before:
|
|
342
|
+
|
|
343
|
+
def send_notification(self, user, message):
|
|
344
|
+
formatted = f"Dear {user.name},\n\n{message}\n\nBest regards"
|
|
345
|
+
email = self._get_email_address(user)
|
|
346
|
+
# ... send email ...
|
|
347
|
+
return formatted
|
|
348
|
+
|
|
349
|
+
after:
|
|
350
|
+
|
|
351
|
+
def send_notification(self, user, message):
|
|
352
|
+
formatted = self._format_message(user, message)
|
|
353
|
+
email = self._get_email_address(user)
|
|
354
|
+
# ... send email ...
|
|
355
|
+
return formatted
|
|
356
|
+
|
|
357
|
+
def _format_message(self, user, message):
|
|
358
|
+
return f"Dear {user.name},\n\n{message}\n\nBest regards"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
testing extract method
|
|
362
|
+
|
|
363
|
+
<terminal>python -m pytest tests/test_report.py -v</terminal>
|
|
364
|
+
|
|
365
|
+
tests should pass without modification.
|
|
366
|
+
if tests fail, the extraction changed behavior.
|
|
367
|
+
revert and try again.
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
PHASE 4: RENAME VARIABLES AND FUNCTIONS
|
|
371
|
+
|
|
372
|
+
good names are critical for readability.
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
when to rename
|
|
376
|
+
|
|
377
|
+
signs you need to rename:
|
|
378
|
+
- you have to think to understand what a variable means
|
|
379
|
+
- the name is misleading
|
|
380
|
+
- the name is too generic (data, info, value, temp)
|
|
381
|
+
- abbreviations require decoding
|
|
382
|
+
- the name describes implementation, not intent
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
naming principles
|
|
386
|
+
|
|
387
|
+
[ok] user, order, calculate_total, is_valid
|
|
388
|
+
[ok] fetch_user_data, render_html_content
|
|
389
|
+
[warn] u, o, calc, check
|
|
390
|
+
[x] data, info, value, temp, thing, stuff
|
|
391
|
+
|
|
392
|
+
intent > brevity.
|
|
393
|
+
clarity > cleverness.
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
rename variables
|
|
397
|
+
|
|
398
|
+
before:
|
|
399
|
+
|
|
400
|
+
def proc(self, d):
|
|
401
|
+
for i in d:
|
|
402
|
+
print(i['n'])
|
|
403
|
+
return len(d)
|
|
404
|
+
|
|
405
|
+
after:
|
|
406
|
+
|
|
407
|
+
def display_users(self, users):
|
|
408
|
+
for user in users:
|
|
409
|
+
print(user['name'])
|
|
410
|
+
return len(users)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
rename methods
|
|
414
|
+
|
|
415
|
+
before:
|
|
416
|
+
|
|
417
|
+
def get(self, id):
|
|
418
|
+
return self.db.find(id)
|
|
419
|
+
|
|
420
|
+
after:
|
|
421
|
+
|
|
422
|
+
def find_user_by_id(self, user_id):
|
|
423
|
+
return self.db.find(user_id)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
rename boolean variables
|
|
427
|
+
|
|
428
|
+
use is/has/should/can prefixes:
|
|
429
|
+
|
|
430
|
+
[ok] is_valid, has_permission, should_retry, can_delete
|
|
431
|
+
[x] valid, permission, retry, delete
|
|
432
|
+
|
|
433
|
+
rename with IDE:
|
|
434
|
+
|
|
435
|
+
# VS Code
|
|
436
|
+
F2 on the name, type new name, Enter
|
|
437
|
+
|
|
438
|
+
# PyCharm
|
|
439
|
+
Shift+F6 on the name, type new name, Enter
|
|
440
|
+
|
|
441
|
+
# command line (manual, risky)
|
|
442
|
+
<terminal>grep -r "old_name" src/</terminal>
|
|
443
|
+
# then careful find and replace
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
rename checklist
|
|
447
|
+
|
|
448
|
+
[ ] search for all usages of the name
|
|
449
|
+
[ ] verify each usage still makes sense with new name
|
|
450
|
+
[ ] run tests to ensure nothing broke
|
|
451
|
+
[ ] commit the rename
|
|
452
|
+
|
|
453
|
+
<terminal>python -m pytest tests/ -v</terminal>
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
PHASE 5: INLINE TEMP / INLINE VARIABLE
|
|
457
|
+
|
|
458
|
+
reverse of extract method.
|
|
459
|
+
when a variable is only used once, inline it.
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
when to inline
|
|
463
|
+
|
|
464
|
+
signs you need to inline:
|
|
465
|
+
- temp variable is used only once
|
|
466
|
+
- the expression is clearer than the variable name
|
|
467
|
+
- the variable doesnt add meaningful abstraction
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
basic inline
|
|
471
|
+
|
|
472
|
+
before:
|
|
473
|
+
|
|
474
|
+
def calculate_discount(self, order):
|
|
475
|
+
base_price = order.quantity * order.item_price
|
|
476
|
+
if base_price > 1000:
|
|
477
|
+
return base_price * 0.9
|
|
478
|
+
return base_price
|
|
479
|
+
|
|
480
|
+
after:
|
|
481
|
+
|
|
482
|
+
def calculate_discount(self, order):
|
|
483
|
+
base_price = order.quantity * order.item_price
|
|
484
|
+
if base_price > 1000:
|
|
485
|
+
return order.quantity * order.item_price * 0.9
|
|
486
|
+
return base_price
|
|
487
|
+
|
|
488
|
+
inline the second usage since it adds nothing.
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
inline with explanation
|
|
492
|
+
|
|
493
|
+
before:
|
|
494
|
+
|
|
495
|
+
def is_eligible_for_discount(self, customer):
|
|
496
|
+
is_important = customer.tier == "VIP" and customer.years > 5
|
|
497
|
+
if is_important:
|
|
498
|
+
return True
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
after:
|
|
502
|
+
|
|
503
|
+
def is_eligible_for_discount(self, customer):
|
|
504
|
+
return customer.tier == "VIP" and customer.years > 5
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
inline to simplify
|
|
508
|
+
|
|
509
|
+
before:
|
|
510
|
+
|
|
511
|
+
def process(self, data):
|
|
512
|
+
result = self._transform(data)
|
|
513
|
+
return result
|
|
514
|
+
|
|
515
|
+
after:
|
|
516
|
+
|
|
517
|
+
def process(self, data):
|
|
518
|
+
return self._transform(data)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
PHASE 6: EXTRACT CLASS
|
|
522
|
+
|
|
523
|
+
when a class does too much, split it.
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
when to extract class
|
|
527
|
+
|
|
528
|
+
signs you need extract class:
|
|
529
|
+
- class has more than ~300 lines
|
|
530
|
+
- class has unrelated responsibilities
|
|
531
|
+
- class changes for multiple reasons
|
|
532
|
+
- subset of methods uses subset of fields
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
identify natural boundaries
|
|
536
|
+
|
|
537
|
+
group related:
|
|
538
|
+
- fields
|
|
539
|
+
- methods that use those fields
|
|
540
|
+
- responsibilities
|
|
541
|
+
|
|
542
|
+
these form the new class.
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
basic extraction
|
|
546
|
+
|
|
547
|
+
before:
|
|
548
|
+
|
|
549
|
+
class Person:
|
|
550
|
+
def __init__(self, name, email, phone, street, city, zip_code):
|
|
551
|
+
self.name = name
|
|
552
|
+
self.email = email
|
|
553
|
+
self.phone = phone
|
|
554
|
+
self.street = street
|
|
555
|
+
self.city = city
|
|
556
|
+
self.zip_code = zip_code
|
|
557
|
+
|
|
558
|
+
def get_full_address(self):
|
|
559
|
+
return f"{self.street}, {self.city} {self.zip_code}"
|
|
560
|
+
|
|
561
|
+
def get_email_domain(self):
|
|
562
|
+
return self.email.split('@')[1]
|
|
563
|
+
|
|
564
|
+
after:
|
|
565
|
+
|
|
566
|
+
class Person:
|
|
567
|
+
def __init__(self, name, contact_info):
|
|
568
|
+
self.name = name
|
|
569
|
+
self.contact_info = contact_info
|
|
570
|
+
|
|
571
|
+
class ContactInfo:
|
|
572
|
+
def __init__(self, email, phone, street, city, zip_code):
|
|
573
|
+
self.email = email
|
|
574
|
+
self.phone = phone
|
|
575
|
+
self.address = Address(street, city, zip_code)
|
|
576
|
+
|
|
577
|
+
def get_email_domain(self):
|
|
578
|
+
return self.email.split('@')[1]
|
|
579
|
+
|
|
580
|
+
class Address:
|
|
581
|
+
def __init__(self, street, city, zip_code):
|
|
582
|
+
self.street = street
|
|
583
|
+
self.city = city
|
|
584
|
+
self.zip_code = zip_code
|
|
585
|
+
|
|
586
|
+
def get_full_address(self):
|
|
587
|
+
return f"{self.street}, {self.city} {self.zip_code}"
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
extraction with delegation
|
|
591
|
+
|
|
592
|
+
before:
|
|
593
|
+
|
|
594
|
+
class OrderProcessor:
|
|
595
|
+
def __init__(self):
|
|
596
|
+
self.inventory = {}
|
|
597
|
+
self.pricing = {}
|
|
598
|
+
|
|
599
|
+
def process_order(self, order):
|
|
600
|
+
if self._check_inventory(order):
|
|
601
|
+
price = self._calculate_price(order)
|
|
602
|
+
self._update_inventory(order)
|
|
603
|
+
return price
|
|
604
|
+
|
|
605
|
+
def _check_inventory(self, order):
|
|
606
|
+
# inventory logic
|
|
607
|
+
pass
|
|
608
|
+
|
|
609
|
+
def _calculate_price(self, order):
|
|
610
|
+
# pricing logic
|
|
611
|
+
pass
|
|
612
|
+
|
|
613
|
+
def _update_inventory(self, order):
|
|
614
|
+
# inventory update logic
|
|
615
|
+
pass
|
|
616
|
+
|
|
617
|
+
after:
|
|
618
|
+
|
|
619
|
+
class OrderProcessor:
|
|
620
|
+
def __init__(self, inventory_manager, pricing_calculator):
|
|
621
|
+
self.inventory = inventory_manager
|
|
622
|
+
self.pricing = pricing_calculator
|
|
623
|
+
|
|
624
|
+
def process_order(self, order):
|
|
625
|
+
if self.inventory.check_available(order):
|
|
626
|
+
price = self.pricing.calculate(order)
|
|
627
|
+
self.inventory.update(order)
|
|
628
|
+
return price
|
|
629
|
+
|
|
630
|
+
class InventoryManager:
|
|
631
|
+
def __init__(self):
|
|
632
|
+
self.stock = {}
|
|
633
|
+
|
|
634
|
+
def check_available(self, order):
|
|
635
|
+
# inventory logic
|
|
636
|
+
pass
|
|
637
|
+
|
|
638
|
+
def update(self, order):
|
|
639
|
+
# inventory update logic
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
class PricingCalculator:
|
|
643
|
+
def __init__(self):
|
|
644
|
+
self.price_list = {}
|
|
645
|
+
|
|
646
|
+
def calculate(self, order):
|
|
647
|
+
# pricing logic
|
|
648
|
+
pass
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
testing extract class
|
|
652
|
+
|
|
653
|
+
<terminal>python -m pytest tests/test_order_processor.py -v</terminal>
|
|
654
|
+
|
|
655
|
+
interface tests should pass.
|
|
656
|
+
internal structure changed, behavior unchanged.
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
PHASE 7: MOVE METHOD / MOVE FUNCTION
|
|
660
|
+
|
|
661
|
+
when a method is in the wrong class, move it.
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
when to move
|
|
665
|
+
|
|
666
|
+
signs a method should move:
|
|
667
|
+
- method uses more data from another class
|
|
668
|
+
- method has no real use in current class
|
|
669
|
+
- a class has feature envy (uses another class more than itself)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
move to where data is
|
|
673
|
+
|
|
674
|
+
before:
|
|
675
|
+
|
|
676
|
+
class Order:
|
|
677
|
+
def __init__(self, customer):
|
|
678
|
+
self.customer = customer
|
|
679
|
+
self.items = []
|
|
680
|
+
|
|
681
|
+
def get_customer_address(self):
|
|
682
|
+
return f"{self.customer.street}, {self.customer.city}"
|
|
683
|
+
|
|
684
|
+
def get_customer_discount(self):
|
|
685
|
+
if self.customer.tier == "VIP":
|
|
686
|
+
return 0.1
|
|
687
|
+
return 0.0
|
|
688
|
+
|
|
689
|
+
after:
|
|
690
|
+
|
|
691
|
+
class Order:
|
|
692
|
+
def __init__(self, customer):
|
|
693
|
+
self.customer = customer
|
|
694
|
+
self.items = []
|
|
695
|
+
|
|
696
|
+
def get_customer_address(self):
|
|
697
|
+
return self.customer.get_address()
|
|
698
|
+
|
|
699
|
+
def get_customer_discount(self):
|
|
700
|
+
return self.customer.get_discount()
|
|
701
|
+
|
|
702
|
+
class Customer:
|
|
703
|
+
def __init__(self, street, city, tier):
|
|
704
|
+
self.street = street
|
|
705
|
+
self.city = city
|
|
706
|
+
self.tier = tier
|
|
707
|
+
|
|
708
|
+
def get_address(self):
|
|
709
|
+
return f"{self.street}, {self.city}"
|
|
710
|
+
|
|
711
|
+
def get_discount(self):
|
|
712
|
+
if self.tier == "VIP":
|
|
713
|
+
return 0.1
|
|
714
|
+
return 0.0
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
move to parameter object
|
|
718
|
+
|
|
719
|
+
before:
|
|
720
|
+
|
|
721
|
+
def calculate_shipping(order):
|
|
722
|
+
if order.customer.address.state == order.warehouse.address.state:
|
|
723
|
+
return 5.0
|
|
724
|
+
else:
|
|
725
|
+
distance = calculate_distance(
|
|
726
|
+
order.customer.address,
|
|
727
|
+
order.warehouse.address
|
|
728
|
+
)
|
|
729
|
+
return distance * 0.1
|
|
730
|
+
|
|
731
|
+
after:
|
|
732
|
+
|
|
733
|
+
def calculate_shipping(order):
|
|
734
|
+
return order.shipping_calculator.calculate()
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
PHASE 8: REPLACE CONDITIONAL WITH POLYMORPHISM
|
|
738
|
+
|
|
739
|
+
when switch statements duplicate, use polymorphism.
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
when to use polymorphism
|
|
743
|
+
|
|
744
|
+
signs you need polymorphism:
|
|
745
|
+
- same switch appears in multiple places
|
|
746
|
+
- adding new type requires changing many places
|
|
747
|
+
- switch on type codes
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
before: type codes
|
|
751
|
+
|
|
752
|
+
class Employee:
|
|
753
|
+
def __init__(self, type_code):
|
|
754
|
+
self.type_code = type_code
|
|
755
|
+
|
|
756
|
+
def calculate_pay(self):
|
|
757
|
+
if self.type_code == "ENGINEER":
|
|
758
|
+
return self.salary * 1.0
|
|
759
|
+
elif self.type_code == "MANAGER":
|
|
760
|
+
return self.salary * 1.2
|
|
761
|
+
elif self.type_code == "SALES":
|
|
762
|
+
return self.salary * 0.9 + self.commission
|
|
763
|
+
|
|
764
|
+
def get_bonus(self):
|
|
765
|
+
if self.type_code == "ENGINEER":
|
|
766
|
+
return 5000
|
|
767
|
+
elif self.type_code == "MANAGER":
|
|
768
|
+
return 10000
|
|
769
|
+
elif self.type_code == "SALES":
|
|
770
|
+
return self.sales * 0.05
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
after: polymorphism
|
|
774
|
+
|
|
775
|
+
from abc import ABC, abstractmethod
|
|
776
|
+
|
|
777
|
+
class Employee(ABC):
|
|
778
|
+
def __init__(self, salary):
|
|
779
|
+
self.salary = salary
|
|
780
|
+
|
|
781
|
+
@abstractmethod
|
|
782
|
+
def calculate_pay(self):
|
|
783
|
+
pass
|
|
784
|
+
|
|
785
|
+
@abstractmethod
|
|
786
|
+
def get_bonus(self):
|
|
787
|
+
pass
|
|
788
|
+
|
|
789
|
+
class Engineer(Employee):
|
|
790
|
+
def calculate_pay(self):
|
|
791
|
+
return self.salary * 1.0
|
|
792
|
+
|
|
793
|
+
def get_bonus(self):
|
|
794
|
+
return 5000
|
|
795
|
+
|
|
796
|
+
class Manager(Employee):
|
|
797
|
+
def calculate_pay(self):
|
|
798
|
+
return self.salary * 1.2
|
|
799
|
+
|
|
800
|
+
def get_bonus(self):
|
|
801
|
+
return 10000
|
|
802
|
+
|
|
803
|
+
class Sales(Employee):
|
|
804
|
+
def __init__(self, salary, commission, sales):
|
|
805
|
+
super().__init__(salary)
|
|
806
|
+
self.commission = commission
|
|
807
|
+
self.sales = sales
|
|
808
|
+
|
|
809
|
+
def calculate_pay(self):
|
|
810
|
+
return self.salary * 0.9 + self.commission
|
|
811
|
+
|
|
812
|
+
def get_bonus(self):
|
|
813
|
+
return self.sales * 0.05
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
factory creation
|
|
817
|
+
|
|
818
|
+
class EmployeeFactory:
|
|
819
|
+
@staticmethod
|
|
820
|
+
def create(type_code, **kwargs):
|
|
821
|
+
if type_code == "ENGINEER":
|
|
822
|
+
return Engineer(kwargs.get("salary", 0))
|
|
823
|
+
elif type_code == "MANAGER":
|
|
824
|
+
return Manager(kwargs.get("salary", 0))
|
|
825
|
+
elif type_code == "SALES":
|
|
826
|
+
return Sales(
|
|
827
|
+
kwargs.get("salary", 0),
|
|
828
|
+
kwargs.get("commission", 0),
|
|
829
|
+
kwargs.get("sales", 0)
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
PHASE 9: DECOMPOSE CONDITIONAL
|
|
834
|
+
|
|
835
|
+
complex conditionals are hard to read.
|
|
836
|
+
break them into named methods.
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
when to decompose
|
|
840
|
+
|
|
841
|
+
signs you need to decompose:
|
|
842
|
+
- conditional logic is hard to understand
|
|
843
|
+
- comments are needed to explain the conditional
|
|
844
|
+
- same condition appears multiple times
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
decompose and/or
|
|
848
|
+
|
|
849
|
+
before:
|
|
850
|
+
|
|
851
|
+
def calculate_shipping_cost(self, order):
|
|
852
|
+
if (order.weight > 10 and order.destination.country != "US") or \
|
|
853
|
+
(order.weight > 20 and order.destination.country == "US"):
|
|
854
|
+
return order.weight * 2.0
|
|
855
|
+
else:
|
|
856
|
+
return order.weight * 1.0
|
|
857
|
+
|
|
858
|
+
after:
|
|
859
|
+
|
|
860
|
+
def calculate_shipping_cost(self, order):
|
|
861
|
+
if self._is_express_shipping(order):
|
|
862
|
+
return order.weight * 2.0
|
|
863
|
+
else:
|
|
864
|
+
return order.weight * 1.0
|
|
865
|
+
|
|
866
|
+
def _is_express_shipping(self, order):
|
|
867
|
+
return self._is_heavy_international(order) or \
|
|
868
|
+
self._is_very_heavy_domestic(order)
|
|
869
|
+
|
|
870
|
+
def _is_heavy_international(self, order):
|
|
871
|
+
return order.weight > 10 and order.destination.country != "US"
|
|
872
|
+
|
|
873
|
+
def _is_very_heavy_domestic(self, order):
|
|
874
|
+
return order.weight > 20 and order.destination.country == "US"
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
consolidate conditional fragments
|
|
878
|
+
|
|
879
|
+
before:
|
|
880
|
+
|
|
881
|
+
def send_email(self, user, message):
|
|
882
|
+
if user.email:
|
|
883
|
+
print(f"Sending to {user.email}")
|
|
884
|
+
print(f"Subject: {message.subject}")
|
|
885
|
+
print(f"Body: {message.body}")
|
|
886
|
+
self.sent_count += 1
|
|
887
|
+
else:
|
|
888
|
+
print("No email provided")
|
|
889
|
+
|
|
890
|
+
after:
|
|
891
|
+
|
|
892
|
+
def send_email(self, user, message):
|
|
893
|
+
if not user.email:
|
|
894
|
+
print("No email provided")
|
|
895
|
+
return
|
|
896
|
+
|
|
897
|
+
self._send_email_message(user.email, message)
|
|
898
|
+
self.sent_count += 1
|
|
899
|
+
|
|
900
|
+
def _send_email_message(self, email, message):
|
|
901
|
+
print(f"Sending to {email}")
|
|
902
|
+
print(f"Subject: {message.subject}")
|
|
903
|
+
print(f"Body: {message.body}")
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
replace nested conditional with guard clauses
|
|
907
|
+
|
|
908
|
+
before:
|
|
909
|
+
|
|
910
|
+
def get_pay_amount(self):
|
|
911
|
+
result = 0
|
|
912
|
+
if self.is_dead:
|
|
913
|
+
result = self.dead_amount()
|
|
914
|
+
else:
|
|
915
|
+
if self.is_separated:
|
|
916
|
+
result = self.separated_amount()
|
|
917
|
+
else:
|
|
918
|
+
if self.is_retired:
|
|
919
|
+
result = self.retired_amount()
|
|
920
|
+
else:
|
|
921
|
+
result = self.normal_amount()
|
|
922
|
+
return result
|
|
923
|
+
|
|
924
|
+
after:
|
|
925
|
+
|
|
926
|
+
def get_pay_amount(self):
|
|
927
|
+
if self.is_dead:
|
|
928
|
+
return self.dead_amount()
|
|
929
|
+
if self.is_separated:
|
|
930
|
+
return self.separated_amount()
|
|
931
|
+
if self.is_retired:
|
|
932
|
+
return self.retired_amount()
|
|
933
|
+
return self.normal_amount()
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
PHASE 10: INTRODUCE PARAMETER OBJECT
|
|
937
|
+
|
|
938
|
+
when parameters always travel together, group them.
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
when to group parameters
|
|
942
|
+
|
|
943
|
+
signs you need parameter object:
|
|
944
|
+
- same parameters appear together repeatedly
|
|
945
|
+
- parameters form a coherent concept
|
|
946
|
+
- number of parameters exceeds 3-4
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
before: grouped parameters
|
|
950
|
+
|
|
951
|
+
def draw_graph(self, data, x_min, x_max, y_min, y_max, color, width):
|
|
952
|
+
# drawing logic
|
|
953
|
+
pass
|
|
954
|
+
|
|
955
|
+
def analyze_data(self, data, x_min, x_max, y_min, y_max):
|
|
956
|
+
# analysis logic
|
|
957
|
+
pass
|
|
958
|
+
|
|
959
|
+
def export_data(self, data, x_min, x_max, y_min, y_max, format):
|
|
960
|
+
# export logic
|
|
961
|
+
pass
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
after: parameter object
|
|
965
|
+
|
|
966
|
+
class DataRange:
|
|
967
|
+
def __init__(self, x_min, x_max, y_min, y_max):
|
|
968
|
+
self.x_min = x_min
|
|
969
|
+
self.x_max = x_max
|
|
970
|
+
self.y_min = y_min
|
|
971
|
+
self.y_max = y_max
|
|
972
|
+
|
|
973
|
+
def contains(self, x, y):
|
|
974
|
+
return self.x_min <= x <= self.x_max and \
|
|
975
|
+
self.y_min <= y <= self.y_max
|
|
976
|
+
|
|
977
|
+
def draw_graph(self, data, range, style):
|
|
978
|
+
# drawing logic
|
|
979
|
+
pass
|
|
980
|
+
|
|
981
|
+
def analyze_data(self, data, range):
|
|
982
|
+
# analysis logic
|
|
983
|
+
pass
|
|
984
|
+
|
|
985
|
+
def export_data(self, data, range, format):
|
|
986
|
+
# export logic
|
|
987
|
+
pass
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
benefits:
|
|
991
|
+
|
|
992
|
+
- fewer parameters
|
|
993
|
+
- can add behavior to the object
|
|
994
|
+
- easier to add new related data
|
|
995
|
+
- clearer intent
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
PHASE 11: REMOVE DEAD CODE
|
|
999
|
+
|
|
1000
|
+
dead code has no tests and is not used.
|
|
1001
|
+
remove it without mercy.
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
find dead code
|
|
1005
|
+
|
|
1006
|
+
check for unused imports:
|
|
1007
|
+
<terminal>python -m flake8 src/ --select=F401</terminal>
|
|
1008
|
+
|
|
1009
|
+
check for unused variables:
|
|
1010
|
+
<terminal>python -m pylint src/ --disable=all --enable=unused-variable</terminal>
|
|
1011
|
+
|
|
1012
|
+
check for unreachable code:
|
|
1013
|
+
<terminal>python -m vulture src/</terminal>
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
grep for potential dead code
|
|
1017
|
+
|
|
1018
|
+
<terminal>grep -r "TODO.*remove" src/</terminal>
|
|
1019
|
+
<terminal>grep -r "FIXME.*deprecated" src/</terminal>
|
|
1020
|
+
<terminal>grep -r "def.*_old\|class.*_old" src/</terminal>
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
check for unused functions
|
|
1024
|
+
|
|
1025
|
+
<terminal>grep -r "def " src/ | while read line; do
|
|
1026
|
+
func=$(echo $line | grep -o "def [a-z_]*" | cut -d' ' -f2)
|
|
1027
|
+
if [ -n "$func" ]; then
|
|
1028
|
+
count=$(grep -r "$func(" src/ tests/ | wc -l)
|
|
1029
|
+
if [ $count -le 1 ]; then
|
|
1030
|
+
echo "Possibly unused: $func"
|
|
1031
|
+
fi
|
|
1032
|
+
fi
|
|
1033
|
+
done</terminal>
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
before removing dead code
|
|
1037
|
+
|
|
1038
|
+
[1] verify no tests reference it
|
|
1039
|
+
[2] search codebase for references
|
|
1040
|
+
[3] check git history for why it exists
|
|
1041
|
+
|
|
1042
|
+
<terminal>grep -r "function_name" src/ tests/</terminal>
|
|
1043
|
+
|
|
1044
|
+
<terminal>git log --all --source --full-history -S "function_name"</terminal>
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
remove safely
|
|
1048
|
+
|
|
1049
|
+
<read><file>src/module.py</file></read>
|
|
1050
|
+
|
|
1051
|
+
<edit>
|
|
1052
|
+
<file>src/module.py</file>
|
|
1053
|
+
<find>
|
|
1054
|
+
# Old implementation, kept for reference
|
|
1055
|
+
def old_function():
|
|
1056
|
+
pass
|
|
1057
|
+
</find>
|
|
1058
|
+
<replace>
|
|
1059
|
+
</replace>
|
|
1060
|
+
</edit>
|
|
1061
|
+
|
|
1062
|
+
<terminal>python -m pytest tests/ -v</terminal>
|
|
1063
|
+
|
|
1064
|
+
tests should pass.
|
|
1065
|
+
dead code by definition has no tests.
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
PHASE 12: SIMPLIFY CONDITIONAL LOGIC
|
|
1069
|
+
|
|
1070
|
+
complex conditionals indicate missing concepts.
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
consolidate duplicate conditions
|
|
1074
|
+
|
|
1075
|
+
before:
|
|
1076
|
+
|
|
1077
|
+
if customer.is_vip:
|
|
1078
|
+
discount = 0.1
|
|
1079
|
+
if customer.tier == "VIP":
|
|
1080
|
+
discount = 0.1
|
|
1081
|
+
|
|
1082
|
+
after:
|
|
1083
|
+
|
|
1084
|
+
if customer.is_vip:
|
|
1085
|
+
discount = 0.1
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
use De Morgan's laws
|
|
1089
|
+
|
|
1090
|
+
before:
|
|
1091
|
+
|
|
1092
|
+
if not (customer.is_vip or customer.is_new):
|
|
1093
|
+
apply_standard_pricing()
|
|
1094
|
+
|
|
1095
|
+
after:
|
|
1096
|
+
|
|
1097
|
+
if not customer.is_vip and not customer.is_new:
|
|
1098
|
+
apply_standard_pricing()
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
before:
|
|
1102
|
+
|
|
1103
|
+
if not customer.is_vip and not customer.is_new:
|
|
1104
|
+
apply_standard_pricing()
|
|
1105
|
+
|
|
1106
|
+
after:
|
|
1107
|
+
|
|
1108
|
+
if not (customer.is_vip or customer.is_new):
|
|
1109
|
+
apply_standard_pricing()
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
reverse conditionals for clarity
|
|
1113
|
+
|
|
1114
|
+
before:
|
|
1115
|
+
|
|
1116
|
+
def process_order(self, order):
|
|
1117
|
+
if order.is_valid:
|
|
1118
|
+
if order.has_items:
|
|
1119
|
+
if order.customer.can_pay:
|
|
1120
|
+
self._process(order)
|
|
1121
|
+
else:
|
|
1122
|
+
return "Payment failed"
|
|
1123
|
+
else:
|
|
1124
|
+
return "No items"
|
|
1125
|
+
else:
|
|
1126
|
+
return "Invalid order"
|
|
1127
|
+
|
|
1128
|
+
after:
|
|
1129
|
+
|
|
1130
|
+
def process_order(self, order):
|
|
1131
|
+
if not order.is_valid:
|
|
1132
|
+
return "Invalid order"
|
|
1133
|
+
if not order.has_items:
|
|
1134
|
+
return "No items"
|
|
1135
|
+
if not order.customer.can_pay:
|
|
1136
|
+
return "Payment failed"
|
|
1137
|
+
|
|
1138
|
+
self._process(order)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
PHASE 13: COMMON ANTI-PATTERNS AND FIXES
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
anti-pattern: god method
|
|
1145
|
+
|
|
1146
|
+
signs:
|
|
1147
|
+
- method over 50 lines
|
|
1148
|
+
- multiple levels of nesting
|
|
1149
|
+
- does many different things
|
|
1150
|
+
|
|
1151
|
+
fix:
|
|
1152
|
+
- extract methods for each responsibility
|
|
1153
|
+
- decompose conditionals
|
|
1154
|
+
- use guard clauses
|
|
1155
|
+
|
|
1156
|
+
before:
|
|
1157
|
+
|
|
1158
|
+
def process(self, data):
|
|
1159
|
+
if data:
|
|
1160
|
+
for item in data:
|
|
1161
|
+
if item.type == "A":
|
|
1162
|
+
if item.value > 0:
|
|
1163
|
+
result = self._calculate_a(item.value)
|
|
1164
|
+
self._save(result)
|
|
1165
|
+
else:
|
|
1166
|
+
self._log("Invalid value")
|
|
1167
|
+
elif item.type == "B":
|
|
1168
|
+
if item.value > 0:
|
|
1169
|
+
result = self._calculate_b(item.value)
|
|
1170
|
+
self._save(result)
|
|
1171
|
+
else:
|
|
1172
|
+
self._log("Invalid value")
|
|
1173
|
+
return "Done"
|
|
1174
|
+
|
|
1175
|
+
after:
|
|
1176
|
+
|
|
1177
|
+
def process(self, data):
|
|
1178
|
+
if not data:
|
|
1179
|
+
return "Done"
|
|
1180
|
+
|
|
1181
|
+
for item in data:
|
|
1182
|
+
self._process_item(item)
|
|
1183
|
+
return "Done"
|
|
1184
|
+
|
|
1185
|
+
def _process_item(self, item):
|
|
1186
|
+
if item.value <= 0:
|
|
1187
|
+
self._log("Invalid value")
|
|
1188
|
+
return
|
|
1189
|
+
|
|
1190
|
+
if item.type == "A":
|
|
1191
|
+
self._process_type_a(item)
|
|
1192
|
+
elif item.type == "B":
|
|
1193
|
+
self._process_type_b(item)
|
|
1194
|
+
|
|
1195
|
+
def _process_type_a(self, item):
|
|
1196
|
+
result = self._calculate_a(item.value)
|
|
1197
|
+
self._save(result)
|
|
1198
|
+
|
|
1199
|
+
def _process_type_b(self, item):
|
|
1200
|
+
result = self._calculate_b(item.value)
|
|
1201
|
+
self._save(result)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
anti-pattern: feature envy
|
|
1205
|
+
|
|
1206
|
+
signs:
|
|
1207
|
+
- method uses more data from another class
|
|
1208
|
+
- method "belongs" in another class
|
|
1209
|
+
|
|
1210
|
+
fix: move the method
|
|
1211
|
+
|
|
1212
|
+
before:
|
|
1213
|
+
|
|
1214
|
+
class Order:
|
|
1215
|
+
def __init__(self, customer):
|
|
1216
|
+
self.customer = customer
|
|
1217
|
+
|
|
1218
|
+
def get_discounted_total(self):
|
|
1219
|
+
if self.customer.tier == "VIP":
|
|
1220
|
+
return self.total * 0.9
|
|
1221
|
+
elif self.customer.tier == "LOYAL":
|
|
1222
|
+
return self.total * 0.95
|
|
1223
|
+
return self.total
|
|
1224
|
+
|
|
1225
|
+
after:
|
|
1226
|
+
|
|
1227
|
+
class Order:
|
|
1228
|
+
def __init__(self, customer):
|
|
1229
|
+
self.customer = customer
|
|
1230
|
+
|
|
1231
|
+
def get_discounted_total(self):
|
|
1232
|
+
return self.customer.get_discounted_price(self.total)
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
anti-pattern: primitive obsession
|
|
1236
|
+
|
|
1237
|
+
signs:
|
|
1238
|
+
- using primitives instead of small objects
|
|
1239
|
+
- related primitives travel together
|
|
1240
|
+
|
|
1241
|
+
fix: extract class
|
|
1242
|
+
|
|
1243
|
+
before:
|
|
1244
|
+
|
|
1245
|
+
def calculate_shipping(self, street, city, state, zip_code):
|
|
1246
|
+
# shipping logic using all these parameters
|
|
1247
|
+
pass
|
|
1248
|
+
|
|
1249
|
+
def validate_address(self, street, city, state, zip_code):
|
|
1250
|
+
# validation logic
|
|
1251
|
+
pass
|
|
1252
|
+
|
|
1253
|
+
after:
|
|
1254
|
+
|
|
1255
|
+
class Address:
|
|
1256
|
+
def __init__(self, street, city, state, zip_code):
|
|
1257
|
+
self.street = street
|
|
1258
|
+
self.city = city
|
|
1259
|
+
self.state = state
|
|
1260
|
+
self.zip_code = zip_code
|
|
1261
|
+
|
|
1262
|
+
def validate(self):
|
|
1263
|
+
# validation logic
|
|
1264
|
+
pass
|
|
1265
|
+
|
|
1266
|
+
def get_shipping_cost(self):
|
|
1267
|
+
# shipping logic
|
|
1268
|
+
pass
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
PHASE 14: IDE REFACTORING TOOLS
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
VS Code refactoring
|
|
1275
|
+
|
|
1276
|
+
install python extension:
|
|
1277
|
+
<terminal>code --install-extension ms-python.python</terminal>
|
|
1278
|
+
|
|
1279
|
+
refactoring shortcuts:
|
|
1280
|
+
- F2: rename symbol
|
|
1281
|
+
- Ctrl+Shift+R (Mac: Cmd+Shift+R): refactor preview
|
|
1282
|
+
- Ctrl+. (Mac: Cmd+.): quick fix
|
|
1283
|
+
- F12: go to definition
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
PyCharm refactoring
|
|
1287
|
+
|
|
1288
|
+
refactoring menu:
|
|
1289
|
+
- Shift+F6: rename
|
|
1290
|
+
- Ctrl+Alt+M: extract method
|
|
1291
|
+
- Ctrl+Alt+V: extract variable
|
|
1292
|
+
- Ctrl+Alt+P: extract parameter
|
|
1293
|
+
- F6: move
|
|
1294
|
+
- Ctrl+Alt+N: inline
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
command line tools
|
|
1298
|
+
|
|
1299
|
+
rope (python refactoring library):
|
|
1300
|
+
<terminal>pip install rope</terminal>
|
|
1301
|
+
|
|
1302
|
+
<terminal>rope refactor.py --extract-method extract_user_validation</terminal>
|
|
1303
|
+
|
|
1304
|
+
rope can:
|
|
1305
|
+
- extract method
|
|
1306
|
+
- rename
|
|
1307
|
+
- move
|
|
1308
|
+
- inline
|
|
1309
|
+
- extract variable
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
bowler (code refactoring tool):
|
|
1313
|
+
<terminal>pip install bowler</terminal>
|
|
1314
|
+
|
|
1315
|
+
# create a refactoring script
|
|
1316
|
+
<terminal>cat > refactor_fixme.py << 'EOF'
|
|
1317
|
+
import bowler
|
|
1318
|
+
|
|
1319
|
+
def rename_old_function(command):
|
|
1320
|
+
(
|
|
1321
|
+
command
|
|
1322
|
+
.capture("old_func = 'old_function_name'")
|
|
1323
|
+
.rename("new_function_name")
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
bowler.Query(".py").modify(rename_old_function).execute()
|
|
1327
|
+
EOF
|
|
1328
|
+
|
|
1329
|
+
<terminal>python refactor_fixme.py --diff</terminal>
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
safe refactoring with git
|
|
1333
|
+
|
|
1334
|
+
create refactoring branch:
|
|
1335
|
+
<terminal>git checkout -b refactor/extract-validation</terminal>
|
|
1336
|
+
|
|
1337
|
+
after each small refactor:
|
|
1338
|
+
<terminal>git add -A && git commit -m "refactor: extract user validation"</terminal>
|
|
1339
|
+
|
|
1340
|
+
view progress:
|
|
1341
|
+
<terminal>git log --oneline refactor/extract-validation</terminal>
|
|
1342
|
+
|
|
1343
|
+
when done:
|
|
1344
|
+
<terminal>git checkout main</terminal>
|
|
1345
|
+
<terminal>git merge refactor/extract-validation --squash</terminal>
|
|
1346
|
+
<terminal>git commit -m "refactor: extract user validation to separate methods"</terminal>
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
PHASE 15: REFACTORING RULES (STRICT MODE)
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
while this skill is active, these rules are MANDATORY:
|
|
1353
|
+
|
|
1354
|
+
[1] NEVER refactor without tests
|
|
1355
|
+
tests are your safety net
|
|
1356
|
+
no tests, no refactoring, no exceptions
|
|
1357
|
+
|
|
1358
|
+
[2] run tests after EVERY change
|
|
1359
|
+
every single change, no matter how small
|
|
1360
|
+
<terminal>python -m pytest tests/ -v</terminal>
|
|
1361
|
+
|
|
1362
|
+
[3] commit when tests pass
|
|
1363
|
+
small commits are your friend
|
|
1364
|
+
make rollback trivial
|
|
1365
|
+
<terminal>git commit -m "refactor: description"</terminal>
|
|
1366
|
+
|
|
1367
|
+
[4] revert if tests fail
|
|
1368
|
+
dont try to fix broken tests
|
|
1369
|
+
revert immediately
|
|
1370
|
+
try a smaller change
|
|
1371
|
+
|
|
1372
|
+
[5] one refactoring at a time
|
|
1373
|
+
dont batch multiple changes
|
|
1374
|
+
test between each
|
|
1375
|
+
commit often
|
|
1376
|
+
|
|
1377
|
+
[6] never change behavior
|
|
1378
|
+
structure only
|
|
1379
|
+
if behavior changes, its not refactoring
|
|
1380
|
+
tests prove behavior unchanged
|
|
1381
|
+
|
|
1382
|
+
[7] keep changes small
|
|
1383
|
+
if you hesitate, make it smaller
|
|
1384
|
+
you can always make another change
|
|
1385
|
+
|
|
1386
|
+
[8] refactor only when green
|
|
1387
|
+
fix tests first
|
|
1388
|
+
never refactor broken tests
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
PHASE 16: REFACTORING SESSION CHECKLIST
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
before starting:
|
|
1395
|
+
|
|
1396
|
+
[ ] tests exist and pass
|
|
1397
|
+
[ ] working directory clean or on branch
|
|
1398
|
+
[ ] baseline coverage recorded
|
|
1399
|
+
[ ] refactoring goal identified
|
|
1400
|
+
|
|
1401
|
+
during refactoring:
|
|
1402
|
+
|
|
1403
|
+
[ ] tests pass before each change
|
|
1404
|
+
[ ] change is small and focused
|
|
1405
|
+
[ ] tests pass after each change
|
|
1406
|
+
[ ] change is committed
|
|
1407
|
+
[ ] progress toward goal
|
|
1408
|
+
|
|
1409
|
+
after completing:
|
|
1410
|
+
|
|
1411
|
+
[ ] all tests pass
|
|
1412
|
+
[ ] coverage unchanged or improved
|
|
1413
|
+
[ ] code is clearer
|
|
1414
|
+
[ ] no behavior changes
|
|
1415
|
+
[ ] commit message is clear
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
FINAL REMINDERS
|
|
1419
|
+
|
|
1420
|
+
|
|
1421
|
+
refactoring is discipline
|
|
1422
|
+
|
|
1423
|
+
it requires patience.
|
|
1424
|
+
it requires tests.
|
|
1425
|
+
it requires small steps.
|
|
1426
|
+
|
|
1427
|
+
the discipline pays off in:
|
|
1428
|
+
- code you can understand
|
|
1429
|
+
- features you can add quickly
|
|
1430
|
+
- bugs you can fix safely
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
the golden rule
|
|
1434
|
+
|
|
1435
|
+
tests must pass before and after.
|
|
1436
|
+
if tests fail, you broke it.
|
|
1437
|
+
revert and try again.
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
when in doubt
|
|
1441
|
+
|
|
1442
|
+
make the change smaller.
|
|
1443
|
+
you can always make another small change.
|
|
1444
|
+
small changes are safe changes.
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
the goal
|
|
1448
|
+
|
|
1449
|
+
code that is easy to understand.
|
|
1450
|
+
code that is easy to change.
|
|
1451
|
+
code that does what it says.
|
|
1452
|
+
no more, no less.
|
|
1453
|
+
|
|
1454
|
+
now go clean up some code.
|