Lukasz Lenart created WW-5661:
---------------------------------

             Summary: Refactor DefaultActionInvocation into smaller 
collaborators
                 Key: WW-5661
                 URL: https://issues.apache.org/jira/browse/WW-5661
             Project: Struts 2
          Issue Type: Improvement
          Components: Core
            Reporter: Lukasz Lenart
             Fix For: 7.4.0


h3. Trigger

SonarCloud reports {{java:S6539}} against {{DefaultActionInvocation.java:59}}:

{quote}
Split this "Monster Class" into smaller and more specialized ones to reduce its 
dependencies
on other classes from 22 to the maximum authorized 20 or less.
{quote}

The rule is {{INFO}} severity and the metric itself is not the point. The class 
crossed the
threshold because WW-5659 added two imports, but the underlying condition long 
predates that
change and is worth addressing on its own terms rather than as a metric chase.

h3. Current state

{{core/src/main/java/org/apache/struts2/DefaultActionInvocation.java}}

* 570 lines, 28 imports
* 18 instance fields, 15 of them {{protected}}
* ~30 methods
* implements {{ActionInvocation}}, which declares only 10 methods - the public 
contract is
  small relative to the implementation behind it

The class currently owns several unrelated concerns:

* *invocation state* - {{action}}, {{proxy}}, {{result}}, {{explicitResult}}, 
{{resultCode}},
  {{executed}}, {{stack}}, {{invocationContext}}
* *dependency injection surface* - seven {{@Inject}} setters
  ({{unknownHandlerManager}}, {{valueStackFactory}}, {{objectFactory}}, 
{{container}},
  {{actionEventListener}}, {{ognlUtil}}, {{asyncManager}})
* *interceptor dispatch* - {{invoke}}, {{mergedParams}}, {{executeConditional}},
  {{createInterceptors}}, {{prepareLazyParamInjector}}
* *action instantiation* - {{createAction}}
* *context map assembly* - {{createContextMap}}
* *result lifecycle* - {{createResult}}, {{getResult}}, {{executeResult}}, 
{{saveResult}}
* *action method invocation and exception handling* - {{invokeAction}}
* *async coordination* - {{asyncManager}} interplay inside {{invoke}}
* *pre-result listener registry* - {{preResultListeners}}, 
{{addPreResultListener}}

{{invoke()}} alone spans roughly 90 lines and mixes interceptor iteration, 
lazy-params
dispatch, conditional-interceptor dispatch, async short-circuiting and result 
execution.

h3. The binding constraint

{{DefaultActionInvocation}} is not effectively sealed. {{RestActionInvocation}}
({{plugins/rest}}) extends it, overrides {{invoke}} and {{saveResult}}, and 
reads or writes the
inherited {{protected}} fields {{explicitResult}}, {{resultCode}}, {{proxy}}, 
{{stack}},
{{result}} and {{container}} directly.

Any refactoring that relocates those fields breaks that subclass, and very 
likely third-party
subclasses too, since the {{protected}} surface has been stable for a long 
time. The class is
constructed in exactly one place ({{DefaultActionProxyFactory.java:65}}), so 
the *construction*
path is easy to change; it is the *inheritance* surface that is expensive.

This is why the issue targets 8.0.0 rather than a minor release.

h3. Candidate seams

Offered as starting points for a design discussion, not as a settled plan:

* *result lifecycle* - {{createResult}} / {{getResult}} / {{executeResult}} / 
{{saveResult}}
  form a coherent group with a clear boundary. {{RestActionInvocation}} 
overrides
  {{saveResult}}, so this seam needs a migration story before it can move.
* *action instantiation* - {{createAction}} depends on {{objectFactory}},
  {{actionEventListener}} and {{unknownHandlerManager}} and is otherwise 
self-contained.
  Probably the cheapest extraction.
* *context map assembly* - {{createContextMap}} is pure construction and 
touches little state.
* *interceptor dispatch* - the most valuable extraction and the hardest, 
because {{invoke()}}
  is re-entrant: interceptors call {{invocation.invoke()}} recursively, so any 
split must
  preserve that.
* *async coordination* - currently interleaved with interceptor dispatch inside 
{{invoke()}};
  worth separating so each is readable alone.

h3. Non-goals

* Not a behaviour change. This is structural work and should be covered by the 
existing test
  suite passing unchanged, not by new assertions about new behaviour.
* Not a fix for {{mergedParams}}' inherited name-based lookup quirk - that is 
tracked
  separately.
* Not an attempt to reach a specific Sonar number. If a split lands the metric 
under 20, good;
  if the honest decomposition leaves it at 21, that is an acceptable outcome 
and the rule
  should be marked accordingly.

h3. Suggested approach

Given the inheritance constraint, extract collaborators incrementally and keep
{{DefaultActionInvocation}} as a thin delegating facade that preserves its 
current
{{protected}} surface for as long as compatibility requires. That allows the 
work to proceed in
reviewable steps rather than one large break, with the facade thinned out once 
subclasses have
migrated.

h3. References

* SonarCloud rule {{java:S6539}} on {{DefaultActionInvocation.java:59}}
* WW-5659 - added the two imports that crossed the threshold; did not create 
the condition
* WW-4759 - {{struts2-api}} extraction, also targeted at 8.0.0; worth 
sequencing against




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to