rubberduck-vba / Rubberduck

Every programmer needs a rubberduck. COM add-in for the VBA & VB6 IDE (VBE).
https://rubberduckvba.com
GNU General Public License v3.0
1.92k stars 302 forks source link

@Inject annotation to mark members for property injection #5463

Open retailcoder opened 4 years ago

retailcoder commented 4 years ago

Justification Write-only properties are a code smell, but so is having a useless get accessor just to shut off a warning about it (and then have an @Ignore annotation to shut off a warning about that accessor being useless). When a setter is only intended to be used for property injection from a factory method that's in the same class module,

Description Let's introduce an @Inject annotation that tells the WriteOnlyProperty inspection to ignore this member. The annotation should only be valid on PropertyLet and PropertySet class members... and at least one Function member in the class.

Additional context This annotation would make several new inspection opportunities to warn about misuse of these members. I can think of one that suggests using the Friend access modifier (for properties anyway), and another that warns of any usage outside the class module it's defined in (or outside @Inject functions, if there are any). Function members with an @Inject annotation can be validated to ensure they return one of the interfaces the class module can be used with (i.e. the class' default interface, or any Implements) - and then there can even be an inspection to suggest using an abstract interface (marked with @Interface) when a factory method (the function marked with @Inject) is returning the class' default/"concrete" interface.

retailcoder commented 4 years ago

Side note, should there be a ObjectOrientedOpportunities inspection type?

retailcoder commented 4 years ago

This annotation would also enable automatically generating factory methods:

  1. Validate that no function has an @Inject annotation (factory method already present)
  2. Grab all @Inject properties in the module
  3. Figure out a parameter name for each property (need a convention here, perhaps append Value to the property name)
  4. Generate the factory method signature and body

The generated factory method could look like this:

'@Inject
Public Function Create({parameters}) As {ClassName}
    If Me Is {ClassName} Then Err.Raise 5, TypeName(Me), "Factory method should only be invoked from default instance."
    Dim result As {ClassName}
    Set result = New {ClassName}
    result.{Property} = {corresponding parameter value}
    Set Create = result
End Function

The Extract Interface refactoring could then be enhanced to leverage this: when extracting an interface from a class module that has an @Inject annotation on a Function (i.e. has a factory method), Extract Interface could generate the Get accessors for the @Inject properties (optionally also generating the Let accessors, but not by default), and make the factory method return the new interface instead of {ClassName}. Or is that too much for Extract Interface to be doing and should be a separate thing?

pchemguy commented 3 years ago

Let me propose an alternative approach. A common OOP approach to object creation involves a combination of a 1) factory responsible for creation of an uninitialized object invoked as a "class method" (as in Python, with the closest VBA analog being default predeclared object instance) and 2) constructor, invoked as an "instance method" (as in Python, with the closest VBA analog being non-default object instance), which has the same signature as the corresponding factory, is invoked by the factory, and has access to private properties for proper initialization. Similarly to defining a "Create" factory, an "init" constructor method with the same signature can be defined and used in place of those setters, whose sole purpose is initialization of private instance properties.

A9G-Data-Droid commented 3 years ago

I would want a decorator called @Constructor because VBA doesn't allow for Constructor Overloading with different number of parameters. What are the uses for property injection outside of a constructor?

BZngr commented 3 years ago

@A9G-Data-Droid

What are the uses for property injection outside of a constructor?

Setting aside the VBA language for a moment...Property Injection is an alternative to Constructor Injection. An object could use both for a single dependency - though I suspect that the need to use both forms of injection would be rare. Generally speaking, if a dependency can be injected via a constructor - it should be...and there is no need for Property Injection.

Property Injection can be used when there is already an acceptable Local Default implementation to fulfill the dependency. Consequently, the Local Default is not Constructor Injected. Dependencies injected using Property Injection are sometimes considered optional dependencies since they are not required to construct a valid instance. Property Injection is one mechanism of implementing the Open/Closed Principle.

Example:

Assume there is an IAudibleFeedback interface with a single method called 'MakeNoise'. Further assume that a 'Feedback' class implements this interface and offers a Private Sub IAudibleFeedback_MakeNoise implementation that uses the built-in Beep Statement. In order to support optional IAudibleFeedback implementations, the 'Feedback' object can use Property Injection. Internally, the 'Feedback' object might look something like:

Implements IAudibleFeedback

Private feedback As IAudibleFeedback

Public Property Set AudibleFeedback(ByVal RHS As IAudibleFeedback)
    Set feedback = RHS
End Property

Private Sub IAudibleFeedback_MakeNoise()
    If feedback Is Nothing then
        Beep 
        Exit Sub
    End If

    feedback.MakeNoise
End Sub 

Now assume someone has written a 'KlaxonHorn' object that also implements IAudibleFeedback where calling IAudibleFeedback_MakeNoise generates a klaxon horn sound. The 'KlaxonHorn' object's IAudibleFeedback interface can be property injected using Property Set AudibleFeedback to change the sound generated by the 'Feedback' object. The key point here is that, with Property Injection, the 'Feedback' object's default behavior can be changed without altering its source code.