Rubberduck
Rubberduck copied to clipboard
Convert to Class Refactoring
Standard Modules and Classes with PredeclaredId = True behave in a similar manner (i.e., global namespace, no assembly required). However, there are more software design options and opportunities if a Standard Module's code is implemented as a PredeclaredId Class (Interfaces, Events, etc). Moreover, once a Standard Module's code is relocated to a PredeclaredId Class, the path-of-resistance to other OO opportunities is likely reduced.
(AFAIK) The only conditions where a PredeclaredId Class cannot directly replace a Standard Module and its references is when the Standard Module declares:
- Entry-point macros or,
PublicUser Defined Type declarations.PublicConstants
When none of the above conditions exist in a Standard Module, I often find myself wishing it was a PredeclaredId Class (YMMV). This refactoring would simplify that conversion process and so, would facilitate the refactoring of legacy projects that are primarily composed of Standard Modules.
The refactoring process would probably be something like:
- Create a new target Class Module with
PredeclaredId = True - Replicate the contents of the source Standard Module in the target Class Module, replicate
Publicconstant declarations asPublicread-only properties of the same name. - Module qualify all Standard Module external references with the name of the new
PredeclaredIdClass name - Remove the source Standard Module Or...
4a. If the Standard Module declares public UDTs or contains procedures that could be entry points, then retain the Standard Module with all public UDT declarations, and forward all public parameter-less subroutines calls to the
PredeclaredIdClass.
Step 4 is where most of the work is. Steps 1-3 largely exist in one form or another.
This refactoring would probably become unnecessary once #892 is implemented.
One more consideration: the standard module must not contain any procedure that is referenced by an AddressOf operator.
Or Application.Run which is the same as a macro entry point but stringly typed so maybe harder to observe.
@bclothier @Greedquest Yes, thanks...both are excellent points.
I'm unfamiliar with using the Address Of operator..but what I've found about it so far is that it can only be used within the same project as the referenced procedure. If that is true, then I would think it possible to reliably find all procedure references preceded by the operator in the Parser results.
On the other hand, references within Application.Run calls could reside within other projects that may or may not be loaded in the VBE. This possibility would seem to drive a solution that always retains the Standard Module and all Public procedures - with or without parameters (broader than 4a above). The retained procedures' logic would be replaced with code that forwards all calls to the generated PredeclaredId Class. That would leave the final the migration decisions (what to keep/delete) with the developer while still moving the procedural-vs-OO code 'needle' a little further in the OO direction.
Well the principle for other projects is that it's not RD's concern e.g refactor->rename on some member of the public API will only modify call sites RD knows about, if there exists a 3rd piece of code which references your project as an addin or calls it with application.run there's really nothing you can do to protect it. That's why you design your public interface once and don't change it if you know other code might reference it.
The concern is more that stringly typed calls can be generated dynamically so short of evaluating your vba code there's really nothing the duck can do to work out the argument to Application.Run except for special cases like consts or literals. But maybe this is just caveat emptor - you can include the refactoring but be aware it may break dynamic calls, just like renaming a procedure of a class would if it is being called with late bound unresolvable code.
Understood. I think this refactoring gets a 'pass' regarding concerns of stringly typed parameters since (as currently proposed) the legacy Public API would be retained by the refactoring. Unlike refactor->rename, this refactoring is uninterested in changing the API identifiers...it would just change where the API's concrete implementation resides.