Search class based data structure like $xml.SelectNodes()

49 views Asked by At

I am refactoring some functional code to use classes, and trying to understand the "best" (most performant, easiest to read, least likely to be deprecated, etc.) way to do so. The functional code uses XML as a data structure, and I need to be able to search for nodes based on certain criteria. So, in simplified form...

$xml = [XML]@"
<Definitions>
    <Package id="A">
        <Task id="A.1">
            <DisplayName>Ignore</DisplayName>
        </Task>
        <Task id="A.2">
            <DisplayName>Find</DisplayName>
        </Task>
    </Package>
    <Package id="B">
        <Task id="B.1">
            <DisplayName>Ignore</DisplayName>
        </Task>
        <Task id="B.2">
            <DisplayName>Find</DisplayName>
        </Task>
        <Task id="B.3">
            <DisplayName>Ignore</DisplayName>
        </Task>
    </Package>
    <Package id="C">
        <Task id="C.1">
            <DisplayName>Ignore</DisplayName>
        </Task>
        <Task id="C.2">
            <DisplayName>Ignore</DisplayName>
        </Task>
        <Task id="C.3">
            <DisplayName>Find</DisplayName>
        </Task>
    </Package>
</Definitions>
"@

$target = 'Find'

$finds = $xml.SelectNodes("//Task/DisplayName[.='$target']")

foreach ($find in $finds) {
    Write-Host "$($find.ParentNode.id)"
}

I have managed to get Package and Task collections working, and I can populate a collection of Packages, containing collections of Tasks, based on the same XML, like so...

class Package {
    # Properties
    [String]$ID
    [System.Collections.Generic.List[object]]$Tasks = [System.Collections.Generic.List[object]]::New()

    # Constructor
    Package ([String]$id) {
        $this.ID = $id
    }
    # Method
    [Void] addTask([Task]$newTask) {
        $this.Tasks.Add($newTask)
    }
}

class Task {
    # Properties
    [String]$ID
    [String]$DisplayName

    # Constructor
    Task ([String]$id) {
        $this.ID = $id
    }
}

$definitions = [System.Collections.Generic.List[object]]::New()

foreach ($package in $xml.SelectNodes("//Package")) {
    $newPackage = [Package]::New($package.ID)
    foreach ($task in $package.SelectNodes("Task")) {
        $newTask = [Task]::New($task.ID)
        $newTask.DisplayName = $task.DisplayName
        $newPackage.addTask($newTask)
    }
    $definitions.Add($newPackage)
}

And I can verify that is working with some foreach loops...

foreach ($package in $definitions) {
    Write-Host "$($package.ID)"
    foreach ($task in $package.Tasks) {
        Write-Host "  $($task.ID) $($task.DisplayName)"
    }
}

Now I want to replicate $finds = $xml.SelectNodes("//Task/DisplayName[.='$target']"), and do it the "right" way. I could just iterate through the list like this.

foreach ($package in $definitions) {
    foreach ($task in $package.Tasks) {
        if ($task.DisplayName -eq $target) {
            Write-Host "$($task.ID)"
        }
    }
}

Or I could have a Find method in the Package class that takes $target as an argument, and iterates over it's own Tasks.

But I wonder if there is some Automatic Variable that already contains all objects of a particular Type, or a way to populate a variable with all objects of a particular Type, so I am iterating over a smaller list. But then I need to be able to find the Parent, and at that point this whole line of thinking seems to break down, since there is no Parent data unless I provide it.

So, what is the best way to do this search?

Also, FWIW, the reason for this exercise is that fact that there are actually about 30 different variations of Task that I will need to implement, with LOTS of shared behavior. Doing that in Functions has led to a bunch of redundant code and lots of work implementing new tasks or fixing bugs in the duplicated code. Inheritance will fix that, and a bunch of other issues that have come up, so moving to classes makes a lot of sense in the bigger picture.

1

There are 1 answers

0
Mathias R. Jessen On

As Theo alludes to in the comments, for a simple object hierarchy like your Package, you can just use Where-Object:

$package.Tasks |Where-Object { $_.DisplayName -eq $target }

Implementing a FindTasks() method is therefore just a question of wrapping Where-Object:

class Package
{
  # ...

  [Task[]]
  FindTasks([scriptblock]$filter)
  {
    return $this.Tasks |Where-Object $filter
  }
}

After which the user can do $package.FindTasks({$_.ID -eq $ID -or $_.DisplayName -like '*keyword*'}) or whatever else they want to filter on