lundi 14 novembre 2016

Design pattern for composition with good performance

I am looking for a design pattern that allows me to composite multiple functions/variables into a single object, with some specific requirements:

  • The entire object definition should be de-/serializable to XML.
  • Based on this initial definition, instances of an object with the specified variables/functions should be created. These instances should be serializable and de-/serializable to XML as well, but it doesn't have to be the same format as the definition.
  • The system should be easy to extend in the future, if more functions are added.
  • Performance should be at an acceptable level. At runtime, there will be approximately 2000 objects that each have their own collection of these compositable objects, and each collection could hold 10-20 composite instances on average (some of which may be created and deleted frequently while others stay around permanently). These lists will be iterated rather frequently to look for composite objects that provide a specific functionality for the given situation.

I'll illustrate it with an example. This approach defines an interface for each function that I want to provide in the composition and then defines individual classes that implement one or more interfaces into composite objects. This assumes that the serialization will be done automatically with an XMLSerializer, hence why some of the properties are declared as public despite having a getter.

'This is an abstract base class, so all composite objects can be held in the same list
'(as well as sharing some common properties)
Public MustInherit Class ModifierBase
    Public Property Name As String
End Class

'This is a function that should be provided.
Public Interface IMod_MinBound
    Function GetMin() As Double
End Interface

'Another function.
Public Interface IMod_MaxBound
    Function GetMax() As Double
End Interface

Next up are the implementations of these functions.

'Implements only one function.
Public Class MinBoundImpl
    Inherits ModifierBase
    Implements IMod_MinBound

    Public Property MinValue As Double

    Public Sub New()
        'Empty ctor for serialization
    End Sub

    Public Sub New(name As String, min As Double)
        Me.Name = name
        MinValue = min
    End Sub

    Public Function GetMin() As Double Implements IMod_MinBound.GetMin
        Return MinValue
    End Function
End Class

'Implements the other function.
Public Class MaxBoundImpl
    Inherits ModifierBase
    Implements IMod_MaxBound

    Public Property MaxValue As Double

    Public Sub New()
        'Empty ctor for serialization
    End Sub

    Public Sub New(name As String, max As Double)
        Me.Name = name
        MaxValue = max
    End Sub

    Public Function GetMax() As Double Implements IMod_MaxBound.GetMax
        Return MaxValue
    End Function
End Class

'Implements both functions.
Public Class ClampBoundImpl
    Inherits ModifierBase
    Implements IMod_MaxBound, IMod_MinBound

    Public Property MaxValue As Double
    Public Property MinValue As Double

    Public Sub New()
        'Empty ctor for serialization
    End Sub

    Public Sub New(name As String, min As Double, max As Double)
        Me.Name = name
        MinValue = min
        MaxValue = max
    End Sub

    Public Function GetMax() As Double Implements IMod_MaxBound.GetMax
        Return MaxValue
    End Function

    Public Function GetMin() As Double Implements IMod_MinBound.GetMin
        Return MinValue
    End Function
End Class

And last but not least some test code that would put it to use. This is just an example. There are other usages in praxis, where more than just one of the modifiers may apply at a time.

Public Sub ModifierTest()
    Dim modList As New List(Of ModifierBase)

    'Usually the workflow would be, that the original definitions
    'come from an XML file and get stored with a unique name in a hash map.
    'Then instances would be created as copies of those original definitions.
    modList.Add(New MinBoundImpl("Test 1", 10))
    modList.Add(New MinBoundImpl("Test 2", 5))
    modList.Add(New MaxBoundImpl("Test 3", 80))
    modList.Add(New ClampBoundImpl("Test 4", 8, 75))

    GetModifiedValue(0, modList) 'Should return 5
    GetModifiedValue(100, modList) 'Should return 80
End Sub

'This is just one of many possible applications.
Private Function GetModifiedValue(val As Double, modList As List(Of ModifierBase)) As Double
    Dim min = Double.MaxValue
    Dim max = Double.MinValue

    'Iterate through all possible modifiers and only consider the functionality
    'that is relevant to us in the current scenario.
    For Each m In modList
        If TypeOf m Is IMod_MinBound Then
            min = Math.Min(min, DirectCast(m, IMod_MinBound).GetMin())
        End If
        If TypeOf m Is IMod_MaxBound Then
            max = Math.Max(max, DirectCast(m, IMod_MaxBound).GetMax())
        End If
    Next

    Return Clamp(val, min, max)
End Function

The drawback of this approach is that, as the number of interfaces increases, I'd have to provide an exponential amount of implementations if I wanted to cover all the possible permutations of functionality that the user could wish to define in the XML.

Another (perhaps better) approach might be to let ModifierBase hold a hash map of actual implementations (rather than interfaces), and then retrieve the implementation for a desired key (which might as well just be the class name) instead of checking whether a specific interface is being implemented. Not sure how this compares performance-wise, though, since there may be more object instances this way. Is GC pressure a thing in .NET?

Or is there another design pattern that might be suitable here and which I am completely overlooking?

Aucun commentaire:

Enregistrer un commentaire