8

Basic Question: Given an interface: ICopiesFrom(Of In TModel) where there is no type constraint on the generic argument, can that interface be implemented more than once on the same concrete type using a different type argument without a compiler warning?

Background Info: My handle on covariance and contravariance has been increasing in recent years thanks to Mr. Eric Lippert, Google, and many hours of testing / experimenting. In a project I am working on, I have a need to separate different layers of the architecture and not expose base model / entity types to a higher layer (presentation). To accomplish this, I have been creating composite classes (MVC Models) that contain aspects of potentially multiple different base layer model types. I have a separate layer that will build these composite types from the base types (service layer). One important requirement is that the base types not be passed up via a reference, so properties must be duplicated in order to create a deep-copy of the base model class.

To remove some of the lengthy and ugly code from the service layer, I created an interface that defines a common contract for composite types that allows for the property values to be copied in the composite object. When I want to implement this interface multiple times however, the VB compiler generates a warning. The program runs just fine, but I want to understanding the specifics of why this is happening. Particularly, if this is a fragile or poor design decision, I want to know now before I get too deep.

Environment Details:

  • Language: VB.Net
  • .NET: 4.0
  • IDE: VS2010 SP1
  • Usage: Website (MVC2)

In attempting to figure this out, I have done some reasearch on SO and the internet but nothing really addresses my question specifically. Here are some of (but not all) the resources I have consulted:

Summary: Is there a better / cleaner / more flexible way to achieve what I'm trying to or do I have to live with the compiler warning?

Here is a run-able example (not the actual code) that illustrates the issue:

Public Module Materials

    Sub Main()
        Dim materials As New List(Of Composite)()
        Dim materialData As New Dictionary(Of MaterialA, MaterialB)()

        'Load data from a data source
        'materialData = Me.DataService.Load(.....'Query parameters'.....)
        Dim specificMaterial As New SpecialB() With {.Weight = 24, .Height = 12}
        Dim specificMaterialDesc As New MaterialA() With {.Name = "Silly Putty", .Created = DateTime.UtcNow.AddDays(-1)}
        Dim basicMaterial As New MaterialB() With {.Weight = 34.2, .Height = 8}
        Dim basicMaterialDesc As New MaterialA() With {.Name = "Gak", .Created = DateTime.UtcNow.AddDays(-2)}

        materialData.Add(specificMaterialDesc, specificMaterial)
        materialData.Add(basicMaterialDesc, basicMaterial)

        For Each item In materialData
            Dim newMaterial As New Composite()

            newMaterial.CopyFrom(item.Key)
            newMaterial.CopyFrom(item.Value)
            materials.Add(newMaterial)

        Next

        Console.WriteLine("Total Weight: {0} lbs.", materials.Select(Function(x) x.Weight).Sum())
        Console.ReadLine()
    End Sub

End Module


''' <summary>
''' Class that represents a composite of two separate classes.
''' </summary>
''' <remarks></remarks>
Public Class Composite
    Implements ICopiesFrom(Of MaterialA)
    Implements ICopiesFrom(Of MaterialB)

#Region "--Constants--"

    Private Const COMPOSITE_PREFIX As String = "Comp_"

#End Region

#Region "--Instance Variables--"

    Private _created As DateTime
    Private _height As Double
    Private _name As String
    Private _weight As Double

#End Region

#Region "--Constructors--"

    ''' <summary>
    ''' Creates a new instance of the Composite class.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
        _created = DateTime.MaxValue
        _height = 1D
        _name = String.Empty
        _weight = 1D
    End Sub

#End Region

#Region "--Methods--"

    Public Overridable Overloads Sub CopyFrom(ByVal model As MaterialA) Implements ICopiesFrom(Of MaterialA).CopyFrom
        If model IsNot Nothing Then
            Me.Name = model.Name
            Me.Created = model.Created
        End If
    End Sub

    Public Overridable Overloads Sub CopyFrom(ByVal model As MaterialB) Implements ICopiesFrom(Of MaterialB).CopyFrom
        If model IsNot Nothing Then
            Me.Height = model.Height
            Me.Weight = model.Weight
        End If
    End Sub

#End Region

#Region "--Functions--"

    Protected Overridable Function GetName() As String
        Dim returnValue As String = String.Empty
        If Not String.IsNullOrWhiteSpace(Me.Name) Then
            Return String.Concat(COMPOSITE_PREFIX, Me.Name)
        End If
        Return returnValue
    End Function

#End Region

#Region "--Properties--"

    Public Overridable Property Created As DateTime
        Get
            Return _created
        End Get
        Set(value As DateTime)
            _created = value
        End Set
    End Property

    Public Overridable Property Height As Double
        Get
            Return _height
        End Get
        Set(value As Double)
            If value > 0D Then
                _height = value
            End If
        End Set
    End Property

    Public Overridable Property Name As String
        Get
            Return Me.GetName()
        End Get
        Set(value As String)
            If Not String.IsNullOrWhiteSpace(value) Then
                _name = value
            End If
        End Set
    End Property

    Public Overridable Property Weight As Double
        Get
            Return _weight
        End Get
        Set(value As Double)
            If value > 0D Then
                _weight = value
            End If
        End Set
    End Property

#End Region

End Class

''' <summary>
''' Interface that exposes a contract / defines functionality of a type whose values are derived from another type.
''' </summary>
''' <typeparam name="TModel"></typeparam>
''' <remarks></remarks>
Public Interface ICopiesFrom(Of In TModel)

#Region "--Methods--"

    ''' <summary>
    ''' Copies a given model into the current instance.
    ''' </summary>
    ''' <param name="model"></param>
    ''' <remarks></remarks>
    Sub CopyFrom(ByVal model As TModel)

#End Region

End Interface

Public Class MaterialA

#Region "--Instance Variables--"

    Private _created As DateTime
    Private _name As String

#End Region

#Region "--Constructors--"

    ''' <summary>
    ''' Creates a new instance of the MaterialA class.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
        _created = DateTime.MaxValue
        _name = String.Empty
    End Sub

#End Region

#Region "--Properties--"

    Public Overridable Property Created As DateTime
        Get
            Return _created
        End Get
        Set(value As DateTime)
            _created = value
        End Set
    End Property

    Public Overridable Property Name As String
        Get
            Return _name
        End Get
        Set(value As String)
            _name = value
        End Set
    End Property

#End Region

End Class

Public Class MaterialB

#Region "--Instance Variables--"

    Private _height As Double
    Private _weight As Double

#End Region

#Region "--Constructors--"

    ''' <summary>
    ''' Creates a new instance of the MaterialB class.
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New()
        _height = 0D
        _weight = 0D
    End Sub

#End Region

#Region "--Properties--"

    Public Overridable Property Height As Double
        Get
            Return _height
        End Get
        Set(value As Double)
            _height = value
        End Set
    End Property

    Public Overridable Property Weight As Double
        Get
            Return _weight
        End Get
        Set(value As Double)
            _weight = value
        End Set
    End Property

#End Region

End Class

Public Class SpecialB
    Inherits MaterialB

    Public Overrides Property Weight As Double
        Get
            Return MyBase.Weight
        End Get
        Set(value As Double)
            MyBase.Weight = value * 2
        End Set
    End Property

End Class
Cœur
  • 37,241
  • 25
  • 195
  • 267
xDaevax
  • 2,012
  • 2
  • 25
  • 36
  • According to [Eric Lippert's post](http://stackoverflow.com/questions/8647738/multiple-generics-ambiguity/8647898#8647898) this seems to be unnecessary behaviour. You can't do anything about it. – Nico Schertler Jun 02 '14 at 14:15
  • 'not expose base model / entity types to a higher layer' and 'base types not be passed up via a reference' Just my opinion and not an answer but I think this decision is the cause of your problem. going up the layers they *should* each become more abstract but forcing the lower levels to be inaccessible means that you are effectively duplicating the code required at each level and eliminating duplication is one of the important design considerations you should be emphasising. – Carl Onager Jun 24 '14 at 08:09
  • @ClaraOnager I see your point. At the moment, I am trying to avoid both the "Brittle Base Class" problem and address SRP concerns. The 'higher level' type will be used for display / view so the other 'lower level' objects can change based on DB schema without affecting the 'display level' code. This is opposed to say, inheriting the 'lower level' class or adding it as a field / property of the 'higher level' class. Additionally, there may be some fields on the 'lower level' class that are used in other programs that I don't want accessible here. Does this make sense? – xDaevax Jun 25 '14 at 12:16
  • @NicoSchertler Do you have any suggestions on alternative methods to achieve this or should I abandon ship? – xDaevax Jun 25 '14 at 12:19
  • I see and coincidentally I've just run into a similar issue. Another thing I thought is that it might be better if the composite class contained rather than implemented. i.e. instead of being 'is a' it should be 'has a'. A class that contains instances of two other classes is conceptually much simpler than one that implements the interfaces of two other classes and thus easier to work with at a higher level. – Carl Onager Jun 25 '14 at 12:24
  • True, but wouldn't this result in exposing the instances of the other two class instances contained in the composite class to the consuming (view) code (unless you create property wrappers, which is kind of the same boat as above)? – xDaevax Jun 25 '14 at 12:28
  • Since it's a warning, I would just ignore it, knowing that I have done everything right. – Nico Schertler Jun 25 '14 at 13:07

1 Answers1

3

The program runs just fine, but I want to understanding the specifics of why this is happening

The warning is there because of (Generic) contravariance with respect to Interfaces and/or classes with an pre-existing inheritance hierarchy. It doesn't specifically apply in the case you have given in your example (may in your real code), but here is why it is warning:

suppose MaterialB Inherited MaterialA and was in turn Inherited by SpecialB then

Public Class Composite
Implements ICopiesFrom(Of MaterialA)
Implements ICopiesFrom(Of MaterialB)

combined with

Public Interface ICopiesFrom(Of In TModel)

says (due to the 'In'): Composite can be an ICopiesFrom(Of <anything Inheriting from MaterialA>) (with one implementation) AND Composite can be an ICopiesFrom(Of <anything Inheriting from MaterialB>) (with second implementation)

so if I say:

Dim broken As ICopiesFrom(Of SpecialB) = New Composite()

Which implementation should it choose, both are valid (it seems natural to choose B, but it is ambiguous)

the Situation if perhaps more clear with Interfaces:

Public Class Composite2
Implements ICopiesFrom(Of IMaterialA)
Implements ICopiesFrom(Of IMaterialB)
...
Public Class Broken
Implements IMaterialA
Implements IMaterialB
...
Dim broken As ICopiesFrom(Of Broken) = New Composite()

Which implementation should the compiler use now??

Also there is nothing in you example that Requires the In keyword (perhaps there may be in the real code). Unless you need to 'pass around' Composite AS a ICopiesFrom(Of SpecialB) for example you are gaining nothing, the ICopiesFrom(Of MaterialB) can cope with a SpecialB without (Generic) contravariance, via the normal (non-generic) mechanisms.

tolanj
  • 3,651
  • 16
  • 30