Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to deal with a module that contains multiple interdependent classes? #185

Open
iRon7 opened this issue Apr 15, 2024 · 6 comments
Open

Comments

@iRon7
Copy link

iRon7 commented Apr 15, 2024

Related to issue #59, for my ObjectGraphTools project, I would like to separate my interdependent classes contained by a single file over multiple files to make my module better manageable.

Is there any recommended best practice for this issue?

For details, see: https://stackoverflow.com/questions/68657692/is-it-possible-to-declare-two-interdependent-classes-each-in-a-separate-file

@JustinGrote
Copy link

JustinGrote commented Apr 16, 2024

Single file is the best practice, due to all the issues involved with classes that likely will never be fixed because classes were originally intended just as a framework to make implementing DSC resources easier and not meant to be general purpose.

If you use a tool like VSCode, you have the ability to jump to definition and use the Outline view to more effectively manage this single file.

In PowerShell, you should generally use classes just for object modeling and intellisense, and only add methods that:

  • override internal behavior (e.g. ToString and op_addition)
  • implement interfaces for stuff like argument completors:
  • Provide explicit/implicit constructors/converters form other types.

Everything else should be done in functions, do not put business logic into methods. If this is what you want to do, write it in C#/F# and import it to your module as an assembly instead.

@iRon7
Copy link
Author

iRon7 commented Apr 17, 2024

@JustinGrote, thanks for you guidelines, the "class issues" and the VSCode Outline View referral.
Just for background on my specific use case: I guess it falls under points 1 (override internal behavior) and 3 (Provide explicit/implicit constructors/converters). The project (ObjectGraphTools) were I am working on doesn't concern business logic but extensive PowerShell logic. For this, I have even an additional reason why I am using classes vs. functions:

Meanwhile I have 4 ([PSSerialize], [PSDeserialize], [Xdn] and [PSNode]) (interdepended) major classes in a single "class" file (of nearly 1200 lines, and I am not done yet).

Anyways, I don't have enough knowledge of C# (and zero of F#) to do the same in one of these languages 😞.
Besides, I guess it will be more work due to the PowerShell/C# language differences (as e.g. strict vs. loosely).

@Jaykul
Copy link
Member

Jaykul commented Apr 18, 2024

Yeah, like Justin said, at runtime, any PowerShell classes that you want to expose outside your module (whether as parameters or outputs) basically have to be defined within the root module -- the main psm1 file.

You don't necessarily have to author them that way, if you merge them for publishing -- the discussion in #59 (which we moved to #171) resulted in the PoshCode/ModuleBuilder project, which works well for that.

However, the PowerShell editor in VSCode still doesn't automatically see types from other files (even when you open a folder), and the way they run PSScriptAnalyzer means it will complain about it, if your classes reference each other...

@jborean93
Copy link

jborean93 commented Apr 18, 2024

there is a lot of recursive functionally in the project due to the nature of object-graphs, therefore I am also concerned about the function/method performance difference.

While I don't have any facts to back this up I would be surprised if there was any major difference between a cmdlet and a PowerShell class method. When you define a PowerShell class the underlying .NET IL code is essentially just invoking the ScriptBlock of the method which is essentially what a function is. For example

class MyClass {
    [string]Method([string]$Foo) {
        return $Foo
    }
}

$c = [MyClass]::new()
$c.Method('test')

Calling $c.Method() here is equivalent to this psuedo code

$sbk = {
    param([string]$Foo)

    return $Foo
}
$sbk.Invoke('test')

It is definitely more complicated than that psuedo code but the general workflow applies. A function is a scriptblock so it has a very similar behaviour when it comes to invoking them.

If you have recursion you should probably want to avoid that type of logic in favour of things like stacks/queues/loops.

@iRon7
Copy link
Author

iRon7 commented Apr 18, 2024

@jborean93,

While I don't have any facts to back this up

$Iterations = 10
$MaxDepth   = 6

class MyClass {
    static $Iterations = 10
    static $MaxDepth   = 6
    Method([int]$Depth) {
        if ($Depth -ge [MyClass]::MaxDepth) { return }
        $NewDepth = $Depth + 1
        for ($i = 0; $i -lt [MyClass]::Iterations; $i++) {
            # Write-Host $i 'Depth:' $Depth
            $this.Method($NewDepth)
        }
    }
}

@{ 
    Class = (Measure-Command {
        [MyClass]::Iterations = $Iterations
        [MyClass]::MaxDepth   = $MaxDepth
        $c = [MyClass]::new()
        $c.Method(0)
    }).TotalMilliseconds
}

$sbk = {
    param([int]$Depth)
    if ($Depth -ge $MaxDepth) { return }
    $NewDepth = $Depth + 1
    for ($i = 0; $i -lt $Iterations; $i++) {
        # Write-Host $i 'Depth:' $Depth
        $sbk.Invoke($NewDepth)
    }
}

@{ 
    ScriptBlock = (Measure-Command {
        $sbk.Invoke(0)
    }).TotalMilliseconds
}
Name                           Value
----                           -----
Class                          2797.1561
ScriptBlock                    4784.1456

Both examples iterate 10 times at every level (up to 6 levels). This means that the method/function is called $Iterate^$MaxDepth (= 10^6 = 1.000.000) times.

@iRon7
Copy link
Author

iRon7 commented Apr 18, 2024

Correct, WikiPedia quote:

It follows that, for problems that can be solved easily by iteration, recursion is generally less efficient.

Meaning that for complex problems you not only need to handle the call stack but also isolate the variables in the current function scope (as the $Depth variable in the above example).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants