Chris Rudd
Chris Rudd

Reputation: 809

Powershell: Inherited classes calling Parent's empty constructors, even when passed objects

In powershell 5 I'm running into a strange inheritance problem with classes.

I want to enforce that we are passed an object during setup like [class]::new($mailbox_object), and I was intending to do this by causing [class]::new() to throw an error if it's associated object isn't assigned (say by a child constructor).

But powershell is calling empty parent constructors BEFORE calling the child constructor that was passed the object and I can't figure out if this is a bug or expected, and more importantly how to enforce that we have to be given an object at creation time

Design pattern speak: I'm trying to implement what I call a Unified Interface pattern, which is a Facade pattern to simplify/unify interactions with similar but differently typed objects, where actions for those objects are selected using a Strategy pattern and the the strategy is chosen automatically by the Facade when created (currently by trying to use an invisible Factory hidden within the Facade)

IRL Example: trying to create a unified interface for Exchange Mailbox/Group objects, and implement a MemberOf function (to return which groups it's a member of). But Mailboxes and Groups use different commands (despite matching functionality) AND 365 and On Premises versions also use different commands (get-unifiedgroup instead of get-distributiongroup) so I'm trying to hide that complexity behind a unified Facade for clarity and usability

I'm open to changing my approach, particularly if there's a better way to do this. Just keep in mind there will be at minimum the following types of disparate objects each of which will need their own implementation of .MemberOf(): Interface_Mailbox_365, Interface_Mailbox_OnPremises, Interface_Group_365, Interface_Group_OnPremises, and I may implement Offline and Generic versions eventually.

MRE below, lines with > are the output. Since I've narrowed it to the an issue with the Interface creation, I've not included the Facade or Factory, but I can add them if they end up being needed.

class Interface_MailObject
{
    $MailObject = "Interface_MailObject class - initial"
    Interface_MailObject(){write-warning "Interface_MailObject::new() MailObject: {$($this.MailObject)}"}
    static [Interface_MailObject] Build($object)
    {
        if
            ($object -eq "Mailbox Standin")
            {return [Interface_Mailbox_365]::new($object)}
        else
            {throw("we don't reach here")}
    }
}
Class Interface_Mailbox : Interface_MailObject
{
    $MailObject = "Interface_Mailbox class - initial"
    Interface_Mailbox () {write-warning "Interface_Mailbox::new() MailObject: {$($this.MailObject)}"}
    Interface_Mailbox ($MailObject) {$this.MailObject = "Interface_Mailbox class - {$($MailObject)}"}
}
Class Interface_Mailbox_365 : Interface_Mailbox
{
    $MailObject = "Interface_Mailbox_365 class - initial"
    Interface_Mailbox_365 () {write-warning "Interface_Mailbox_365::new() MailObject: {$($this.MailObject)}"}
    Interface_Mailbox_365 ($MailObject) {$this.MailObject = "Interface_Mailbox_365 class - {$($MailObject)}"}
    [object[]] MemberOf(){throw("Interface_Mailbox_365.MemberOf TBD")}
}
[Interface_MailObject]::new("Mailbox Standin")|tee -va a

> WARNING: Interface_MailObject::new() MailObject: {Interface_Mailbox_365 class - initial}
> WARNING: Interface_Mailbox::new() MailObject: {Interface_Mailbox_365 class - initial}
> 
> MailObject                                     
> ----------                                     
> Interface_Mailbox_365 class - {Mailbox Standin}

Notice that even though we called [Interface_Mailbox_365]::new("Mailbox Standin") powershell executed the grandparent's empty constructor, then the parent's empty constructor, before running the one we called.

If they executed in the other order, it would be fine. If they called parent constructors that match the same parameter qty and type, that would also be fine

But it's doing neither, and I don't know how to resolve it without using some weird acrobatics with a Singleton factory which seems like an excessive amount of micromanagement for something that should be a common need (requiring an input parameter during initialization) so I'm guessing I'm overlooking something

Upvotes: 1

Views: 2286

Answers (1)

Chris Rudd
Chris Rudd

Reputation: 809

TL:DR

Use child(object):base(object){} to declare the constructor instead of child(object){}

Thanks to @Mathias R. Jessen for helping me work it out. At first I thought I had to decouple the Facade/Factory from the Template, rather than being able to have them be the same class. Unfortunately this would mean I'm calling [MailObject_Interface]::Build($object) but not returning a [MailObject_Interface] type.

After doing some research I realized what Mathias was saying is a child constructor child(object){} is inferred to mean child(object):base(){} and you can override this by explicitly stating child(object):base(object){}

Paring that with an additional piece to verify the parent isn't called directly I was able to achieve success

Class MailObject_Interface
{
    [string] $MailObject

    MailObject_Interface ()
    {throw("You must call ::Build(`$object), because we return specialized types based on the mail object")}

    MailObject_Interface ($object) {[MailObject_Interface]::new()} # this triggers the error above

    MailObject_Interface ($object, $codephrase)
    {
        Write-Warning "calling MailObject_Interface::New($($object), $($codephrase)) {$($this.MailObject)}"
        # the Codephrase ensures 
        # either we're being called from one of our children,
        # or whomever calls us is aware of our internal workings and is taking responsibility for making sure we're handled correctly
        if
            ($codephrase -eq "Shazam!") 
            {$this.MailObject = $object}
        else
            {[MailObject_Interface]::new()} # this triggers the error above
    }
    # We run through ::Build instead of ::New because we want to return a child typed object rather than ourselves
    static [MailObject_Interface] Build($object)
    {
        if
            ($object -eq "Mailbox Standin")
            {return [Interface_Mailbox_365]::new($object)}
        else
            {throw("we don't reach here")}
    }
}
Class Interface_MailObject_Template : MailObject_Interface
{
    Interface_MailObject_Template ($object) : base ($object, "Shazam!") {Write-Warning "calling Interface_MailObject_Template::New($($object)) {$($this.MailObject)}"}
    [object[]] MemberOf(){throw(".MemberOf will be type+context specific")}
}
Class Interface_Mailbox : Interface_MailObject_Template
{
    Interface_Mailbox ($object) : base ($object) {Write-Warning "calling Interface_Mailbox::New($($object)) {$($this.MailObject)}"}
    [object[]] MemberOf(){throw("Mailbox.MemberOf will be context specific")}
}
Class Interface_Mailbox_365 : Interface_Mailbox
{
    Interface_Mailbox_365 ($object) : base ($object) {Write-Warning "calling Interface_Mailbox_365::New($($object)) {$($this.MailObject)}"}
    [object[]] MemberOf(){throw("Interface_Mailbox_365.MemberOf TBD")}
}

#\/ Rough Tests \/#

# should succeed
function Test_Correct()
    {
    Try
        {
        [MailObject_Interface]$a = [MailObject_Interface]::Build("Mailbox Standin")
        return "Succeded ($a)"
        }
    Catch
        {return "Failed"}
    }

# should fail
function Test_New_WithObject_MissingCodephrase()
    {
    Try
        {
        $a = [MailObject_Interface]::New("Mailbox Standin")
        return "Succeded: ($a)"
        }
    Catch
        {return "Failed"}
    }

# should fail
function Test_EmptyBuild()
    {
    Try
        {
        $a = [MailObject_Interface]::Build()
        return "Succeded: ($a)"
        }
    Catch
        {return "Failed"}
    }

# should fail
function Test_EmptyNew()
    {
    Try
        {
        $a = [MailObject_Interface]::New()
        return "Succeded: ($a)"
        }
    Catch
        {return "Failed"}
    }

"$(Test_Correct):`tTest_Correct (should have succeeded)"
"$(Test_New_WithObject_MissingCodephrase):`tTest_New_WithObject_MissingCodephrase (should have failed)"
"$(Test_EmptyBuild):`tTest_EmptyBuild (should have failed)"
"$(Test_EmptyNew):`tTest_EmptyNew (should have failed)"

And here are the test results

> WARNING: calling MailObject_Interface::New(Mailbox Standin, Shazam!) {}
> WARNING: calling Interface_MailObject_Template::New(Mailbox Standin) {Mailbox Standin}
> WARNING: calling Interface_Mailbox::New(Mailbox Standin) {Mailbox Standin}
> WARNING: calling Interface_Mailbox_365::New(Mailbox Standin) {Mailbox Standin}
> Succeded (Interface_Mailbox_365): Test_Correct (should have succeeded)
> Failed:   Test_New_WithObject_MissingCodephrase (should have failed)
> Failed:   Test_EmptyBuild (should have failed)
> Failed:   Test_EmptyNew (should have failed)

Also sorry about the unusual format in the Testing functions. I forgot to convert the bracket indenting to the normal standard, I use a nonstandard approach that I find more functional because it frames control logic with whitespace making it more naturally follow the perimeter walk your eyes do when skimming

Upvotes: 3

Related Questions