Search code examples
.netvb.netgenericsinterfacederived-class

Something like interface or derived class for generics


I have two implementations of my business model, which is somewhat like the skeleton below.

Public Class Class1(Of T)
    Public Property Property1 As String
    Public Property Property2 As String
    Public Property Property3 As Decimal

    Public Sub Method1(arg1 As T, arg2 As String)

    End Sub
    Public Sub Method2(arg1 As T, arg2 As String)

    End Sub
    Public Sub Method3(arg1 As T, arg2 As String, arg3 as Integer)

    End Sub
End Class

Public Class Class1
    Public Property Property1 As String
    Public Property Property2 As String
    Public Property Property3 As Decimal

    Public Sub Method1(Of T)(arg1 As T, arg2 As String)

    End Sub
    Public Sub Method2(Of T)(arg1 As T, arg2 As String)

    End Sub
    Public Sub Method3(Of T)(arg1 As T, arg2 As String, arg3 as Integer)

    End Sub
End Class

The first implementation is a generic class while the second implementation has generic methods.

I want to ensure that both classes should have same properties and methods. A developer may add a method in one class and forget to add in the other, which I want to avoid. As the class grows bigger, it is hard to manually ensure this, as the methods may not be in the same order in both classes.

An interface doesn't work because of difference in definition due to generics (one has (Of T) while the other doesn't).

Similarly derived class concept doesn't work because each method will have 2 definitions exposed with same signature.

Is there a way out?


Solution

  • Thanks to all who helped me with the problem. I ended up creating a T4 template. All other options suggested were unsuitable for various reasons.

    For anyone in situation like me, this is what I did:

    1. My class (class name and implementation omitted here) which has all the business implementation is somewhat like this.
    Public Class Class1
        Public Property Property1 As String
        Public Property Property2 As String
        Public Property Property3 As Decimal
    
        Public Sub Method1(Of T)(arg1 As T, arg2 As String)
            Throw New NotImplementedException
        End Sub
        Public Sub Method2(Of T)(arg1 As T, arg2 As String)
            Throw New NotImplementedException
        End Sub
        Public Sub Method3(Of T)(arg1 As T, arg2 As String, arg3 As Integer)
            Throw New NotImplementedException
        End Sub
        Public Function Function1(Of T)(arg1 As T, arg2 As String) As String
            Throw New NotImplementedException
        End Function
        Public Function Function2(Of T)(arg1 As T, arg2 As String) As List(Of T)
            Throw New NotImplementedException
        End Function
        Public Function Function3(Of T)(arg1 As T, arg2 As String, arg3 As Integer) As Decimal
            Throw New NotImplementedException
        End Function
    End Class
    
    1. I added a T4 Template to the project named Class1OfT.tt and set its properties Build Action = None and Custom Tool = TextTemplatingFileGenerator

    2. Added the following code to my Class1OfT.tt file:

    <#@ template debug="true" hostspecific="true" language="VB" #>
    <#@ assembly name="System.Core" #>
    <#@ import namespace="System.IO" #>
    <#@ import namespace="System.Linq" #>
    <#@ import namespace="System.Text" #>
    <#@ import namespace="System.Text.RegularExpressions" #>
    <#@ import namespace="System.Collections.Generic" #>
    <#@ import namespace="Microsoft.VisualBasic" #>
    <#@ output extension=".vb" #>
    
    '------------------------------------------------------------------------------
    ' <auto-generated>
    '     This code was generated from a template.
    '
    '     Manual changes to this file may cause unexpected behavior in your application.
    '     Manual changes to this file will be overwritten if the code is regenerated.
    ' </auto-generated>
    '------------------------------------------------------------------------------
    <#
      Const ClassName = "Class1"
    #>
    
    Option Strict On
    Option Compare Text
    Imports System.Data.SqlClient
    
    Public Class <#= ClassName #>(Of T)
        Private ClsObj As <#= ClassName #>
    <#
     Dim reProperty As New Regex("Public Property (?<name>\w+)(?<sig>(?: As [^=]+))", RegexOptions.Compiled Or RegexOptions.IgnoreCase)
     Dim reFunction As New Regex("Public Function (?<name>\w+)(?<of>\(Of [^\)]*\))?(?<sig>\(.*\)(?: As .+)?)", RegexOptions.Compiled Or RegexOptions.IgnoreCase)
     Dim reSub As New Regex("Public Sub (?<name>\w+)(?<of>\(Of [^\)]*\))?(?<sig>\(.*\))", RegexOptions.Compiled Or RegexOptions.IgnoreCase)
     Dim absolutePath As String = Host.ResolvePath(ClassName & ".vb")
     Dim contents As String = IO.File.ReadAllText(absolutePath)
     contents = contents.Substring(contents.IndexOf("Public Class " & ClassName))
     contents = contents.Substring(0, contents.IndexOf("End Class"))
     contents = contents.Replace(vbTab, " ")
     For Each line As String In Split(contents, vbNewLine)
      line = Trim(line)
      If line Like "Public ReadOnly Property *" Then
    #>
    
        <#= line #>
    <#
      ElseIf line Like "Public Property *" Then
        Dim groups = reProperty.Match(line).Groups
    #>
    
        Public Property <#= groups("name").Value #><#= groups("sig").Value.TrimEnd #>
            Get
                Return ClsObj.<#= groups("name").Value #>
            End Get
            Set(value<#= groups("sig").Value.TrimEnd #>)
                ClsObj.<#= groups("name").Value #> = value
            End Set
        End Property
    <#
      ElseIf line Like "Public Function *" Then
        Dim groups = reFunction.Match(line).Groups
    #>
    
        Public Function <#= groups("name").Value #><#= groups("sig").Value #>
            Return ClsObj.<#= GetFnDef(groups("name").Value, groups("of").Value, groups("sig").Value) #>
        End Function
    <#
      ElseIf line Like "Public Sub *" AndAlso Not line Like "Public Sub New(*" Then
        Dim groups = reSub.Match(line).Groups
    #>
    
        Public Sub <#= groups("name").Value #><#= groups("sig").Value #>
            ClsObj.<#= GetFnDef(groups("name").Value, groups("of").Value, groups("sig").Value) #>
        End Sub
    <#
      End If
     Next
    #>
    End Class
    
    <#+
        Function GetFnDef(name As String, ofPart As String, fnArgs As String) As String
            Static reArgName As New RegularExpressions.Regex("^\w+$", RegularExpressions.RegexOptions.Compiled)
            If fnArgs.StartsWith("(") Then fnArgs = fnArgs.SubString(1)
            Dim parts() As String = Split(fnArgs)
            Dim args = Enumerable.Range(0, parts.Length).Where(Function(n) parts(n) = "As").Select(Function(n) parts(n - 1)).ToList
            For i As Integer = args.Count - 1 To 0 Step -1
                If args(i) Like "*()" Then args(i) = args(i).Substring(0, args(i).Length - 2)
                If Not reArgName.IsMatch(args(i)) Then args.RemoveAt(i)
            Next
            If String.IsNullOrEmpty(ofPart) Then
                Return String.Concat(name, "(", Join(args.ToArray, ", "), ")")
            Else
                Return String.Concat(name, "(Of T)(", Join(args.ToArray, ", "), ")")
            End If
        End Function
    #>
    
    1. On saving the file, a new file named Class1OfT.vb is generated that has following code. Exactly what I need.
    '------------------------------------------------------------------------------
    ' <auto-generated>
    '     This code was generated from a template.
    '
    '     Manual changes to this file may cause unexpected behavior in your application.
    '     Manual changes to this file will be overwritten if the code is regenerated.
    ' </auto-generated>
    '------------------------------------------------------------------------------
    
    Option Strict On
    Option Compare Text
    Imports System.Data.SqlClient
    
    Public Class Class1(Of T)
        Private ClsObj As Class1
    
        Public Property Property1 As String
            Get
                Return ClsObj.Property1
            End Get
            Set(value As String)
                ClsObj.Property1 = value
            End Set
        End Property
    
        Public Property Property2 As String
            Get
                Return ClsObj.Property2
            End Get
            Set(value As String)
                ClsObj.Property2 = value
            End Set
        End Property
    
        Public Property Property3 As Decimal
            Get
                Return ClsObj.Property3
            End Get
            Set(value As Decimal)
                ClsObj.Property3 = value
            End Set
        End Property
    
        Public Sub Method1(arg1 As T, arg2 As String)
            ClsObj.Method1(Of T)(arg1, arg2)
        End Sub
    
        Public Sub Method2(arg1 As T, arg2 As String)
            ClsObj.Method2(Of T)(arg1, arg2)
        End Sub
    
        Public Sub Method3(arg1 As T, arg2 As String, arg3 As Integer)
            ClsObj.Method3(Of T)(arg1, arg2, arg3)
        End Sub
    
        Public Function Function1(arg1 As T, arg2 As String) As String
            Return ClsObj.Function1(Of T)(arg1, arg2)
        End Function
    
        Public Function Function2(arg1 As T, arg2 As String) As List(Of T)
            Return ClsObj.Function2(Of T)(arg1, arg2)
        End Function
    
        Public Function Function3(arg1 As T, arg2 As String, arg3 As Integer) As Decimal
            Return ClsObj.Function3(Of T)(arg1, arg2, arg3)
        End Function
    End Class