Что-то вроде интерфейса или производного класса для обобщений - PullRequest
1 голос
/ 16 мая 2019

У меня есть две реализации моей бизнес-модели, которые в некоторой степени похожи на приведенный ниже скелет.

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

Первая реализация является универсальным классом, а вторая реализация имеет универсальные методы.

Я хочу убедиться, что оба класса должны иметь одинаковые свойства и методы.Разработчик может добавить метод в один класс и забыть добавить в другой, чего я хочу избежать.По мере того, как класс становится больше, это трудно обеспечить вручную, так как методы могут быть не в одном и том же порядке в обоих классах.

Интерфейс не работает из-за разницы в определении из-за обобщений (одинимеет (Of T), в то время как другой нет).

Аналогичным образом концепция производного класса не работает, потому что у каждого метода есть 2 определения, представленные с одинаковой сигнатурой.

Есть ли выход?

Ответы [ 2 ]

1 голос
/ 23 мая 2019

Спасибо всем, кто помог мне с проблемой. Я закончил тем, что создал шаблон T4. Все остальные предложенные варианты были неподходящими по разным причинам.

Для тех, кто находится в такой ситуации, как я, это то, что я сделал:

  1. Мой класс (имя класса и реализация здесь опущены), в котором реализована вся бизнес-реализация, выглядит примерно так:
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. Я добавил шаблон T4 в проект с именем Class1OfT.tt и установил его свойства Build Action = None и Custom Tool = TextTemplatingFileGenerator

  2. Добавлен следующий код в мой Class1OfT.tt файл:

<#@ 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. При сохранении файла создается новый файл с именем Class1OfT.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>
'------------------------------------------------------------------------------

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
1 голос
/ 16 мая 2019

Нет, в VB нет способа сделать это.Самое близкое было бы иметь отдельные интерфейсы для обоих, как это:

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

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

Public Class GenericMethods
    Inherits IGenericMethods

    ' ...
End Class

Public Class GenericClass(Of T)
    Inherits IGenericClass(Of T)

    ' ...
End Class

По крайней мере, тогда у вас есть только эти два интерфейса в синхронизации, но, как вы уже знаете, по-прежнему нет способа форсироватьдва интерфейса, чтобы оставаться в синхронизации.Единственный способ сделать это - добавить модульный тест, который использует отражение для сравнения двух интерфейсов и генерирует результат отказа, когда они различаются.По крайней мере, тогда вы можете автоматизировать выполнение модульных тестов, так что вы будете получать немедленные уведомления каждый раз, когда вносятся изменения в один, но не в другой.

В качестве альтернативы, поскольку оба класса, кажется, делают одно и то же,может иметь больше смысла объединить два в один класс:

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

    Public Sub Method1(Of T2)(arg1 As T2, arg2 As String)
    End Sub

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

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

Это все еще не заставляет класс иметь обе версии каждого метода, но, по крайней мере, все находится в одном классе, где он болееОчевидно, что если вы измените один, вам придется изменить другой, особенно если вы добавите несколько комментариев к этому эффекту.

Последний вариант - использовать инструмент генерации кода, такой как шаблон T4, для автоматического созданияоба класса из одного скрипта.Таким образом, когда вам нужно внести изменение, вам нужно изменить его только один раз в скрипте, и тогда оба класса будут автоматически восстановлены для соответствия.

...