Is it possible to declare two interdependent classes each in a separate file?
Asked Answered
C

1

6

I have two PowerShell classes that are inter-dependent:

Class A:

class A {

   [int] hidden $i
   [B]   hidden $b_

   A([int] $i) {
     $this.i = $i
   }

   [void] set_b([B] $b) {
      $this.b_ = $b
   }

   [B] get_b() {
      return $this.b_
   }

   [int] get_i() {
      return $this.i
   }
}

and class B:

class B {

   [int] hidden $j
   [A]   hidden $a_

   B([int] $j) {
     $this.j = $j
   }


   [void] set_a([A] $a) {
      $this.a_ = $a
   }

   [A] get_a() {
      return $this.a_
   }

   [int] get_j() {
      return $this.j
   }
}

If I put both classes into the same source file, A-and-B.ps1, and dot-source the file:

. .\A-and-B.ps`

I am able to use these classes:

[A] $a = new-object A 42
[B] $b = new-object B 99

$b.set_a($a)
$a.set_b($b)

$a.get_b().get_a().get_i()

However, I want to put each class into its own file: A.ps1 and B.ps1. Unfortunately, I cannot dot-source A.ps1 because I receive Unable to find type [B]. error messages (which I understand).

So I tried to forward-declare class B in A.ps1:

class B {}

class A {
   ...

which allowed to dot-source both files, A.ps1 and B.ps1.

However, trying to use these classes results in the error message

Cannot convert argument "b", with value: "B", for "set_b" to type "B": "Cannot convert the "B" value of type "B" to type "B"."

So, is there a way to put interdependent classes into their own source file in PowerShell?

Capello answered 4/8, 2021 at 20:28 Comment(4)
I can think of a terrible way that we shouldn't use -> invoke-expression ((gc A.ps1 -Raw)+(gc B.ps1 -Raw))Wrangle
@Wrangle lol I was thinking of another awful one . ([scriptblock]::Create((Get-Content .\class* -Raw)))Californium
Maybe this is something PSDepend could help with. Seems like it would be inline with the intent of the module.Usanis
@TheMadTechnician, fyi: I have opened an PSDepend issue to verify your suggestion.Steiger
D
3

I tried to do a hands-on test to find a solution to your issue.

FYI: I have used PowerShell 7.4.1

Let's define Module A in a.psm1 file -

class A {
    [int] hidden $i
    hidden $b_

    A([int]$i) {
        $This.i = $i
    }
    
    [Void] set_b([Object]$b) {
        $This.b_ = $b
    }

    [Object] get_b() {
        return $this.b_
    }
}

Where Class A is dependent on Class B, however, to avoid a circular reference, I used the type [Object], which works fine.


Now, let's define Module B in b.psm1 file -

class B {
    [int] hidden $j
    hidden $a_

    B([int]$j) {
        $This.j = $j
    }

    [Void] set_a([Object]$a) {
        $This.a_ = $a
    }

    [Object] get_a() {
        return $this. a_
    }
}

Again, Class B here depends on Class A, so we have used Object as a type to avoid direct import.


Finally, I tested the modules [A] & [B] in a file test.ps1 -

using module ./a.psm1
using module ./b.psm1

$a = [A]::new(42)
$b = [B]::new(99)


$b.set_a($a)
$a.set_b($b)

$a.get_b().get_a().get_i()

# Outputs: 42

My experiment (screenshots) File structure
Files structure Code structure
Codes
Result
Execution

To clarify your concern...

"What about the interdependency where class A uses class [B] and vice versa (see the example in the question) and developing and debugging (a module) in, e.g., VSCode?"

In the solution, I have avoided importing/referencing [A] and [B] dependencies. I focused on the expected outcome, not the interdependency. If this is precisely your concern, then please confirm. By the way, I developed the modules [A] and [B] using VS Code.


Updated answer:

With working code samples [no import issues] FYI: With the Export-ModuleMember -* * parts being commented out, it seems to be working fine. However, explicitly saying Export-ModuleMember -* * caused me issues. BTW, I am using PowerShell v7.4.2 in MacOS + VSCOde

  1. Let's define class A in file a.ps1
# a.ps1

class A {
    [int] hidden $i
    [System.Object] hidden $b_

    A([int] $int) {
        $This.i = $int
    }
    
    [void] set_b([Object] $b) {
        $This.b_ = $b
    }

    [System.Object] get_b() {
        return $this.b_
    }
}


# Export-ModuleMember -Class A
  1. Class B in file b.ps1
# b.ps1

class B {
    [int] hidden $j
    [System.Object] hidden $a_

    B([int]$j) {
        $This.j = $j
    }

    [Void] set_a([Object]$a) {
        $This.a_ = $a
    }

    [System.Object] get_a() {
        return $this.a_
    }
}


# Export-ModuleMember -Class B
  1. Now define the function that leverages Class A & B in main.ps1
# main.ps1

Import-Module .\a.ps1 -Force
Import-Module .\b.ps1 -Force


function Test {
    $a = [A]::new(42)
    $b = [B]::new(99)
    
    $b.set_a($a)
    $a.set_b($b)
    
    return $a.get_b().get_a().i
}

# Export-ModuleMember -Function Test

Screenshots

Codes & file structure PowerShell in Action

Degrade answered 18/4 at 14:0 Comment(4)
Please elaborate on the answer as best practices, pros and cons and everything that is related as: What about the interdependecy where class A uses class [B] and vice versa (see the example in the question) and developing and debugging (a module) in e.g. VSCode?Steiger
@iRon, I have updated my answer. Please take a look at it and let me know if it was helpful. Thanks.Degrade
I have removed my downvote seen you answer and investigation, yet it will leave a lot of open issues implied with the question as the PROBLEMS you should see in the VSCode Panel when doing this. Besides, how do I deploy this as a module? E.g. your .\Test.ps1 script might contain a Test function that uses both classes, how do you export that function (Export-ModuleMember) without errors? I guess I have to live with the fact that the only best practice in here is to merge the classes in a single file.Steiger
@Steiger Thanks for the clarification. I have updated my answer with another experiment that solves the Export-ModuleMember issue. Please take a look at it, probably try and see if this is something useful for you case. Thanks.Degrade

© 2022 - 2024 — McMap. All rights reserved.