Reputation: 809
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
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