Composing polymorphic objects in ASP.NET MVC3 project
Asked Answered
L

2

6

The essence of my question is how to compose these objects (see below) in a sensible way with MVC3 and Ninject (though I am not sure DI should be playing a role in the solution). I can't disclose the real details of my project but here is an approximation which illustrates the issue/question. Answers in either VB or C# are appreciated!

I have several different products with widely varying properties yet all of them need to be represented in a catalog. Each product class has a corresponding table in my database. A catalog entry has a handful of properties specific to being a catalog entry and consequently have their own table. I have defined an interface for the catalog entries with the intent that calling the DescriptionText property will give me very different results based on the underlying concrete type.

Public Class Clothing
    Property Identity as Int64
    Property AvailableSizes As List(Of String)
    Property AvailableColor As List(Of String)
End Class

Public Class Fasteners
    Property Identity as Int64
    Property AvailableSizes As List(Of String)
    Property AvailableFinishes As List(Of String)
    Property IsMetric As Boolean
End Class

Public Interface ICatalogEntry
    Property ProductId as Int64
    Property PublishedOn As DateTime
    Property DescriptionText As String
End Interface

Given that the DescriptionText is a presentation layer concern I don't want to implement the ICatalogEntry interface in my product classes. Instead I want to delegate that to some kind of formatter.

Public Interface ICatalogEntryFormatter
    Property DescriptionText As String
End Interface

Public Class ClothingCatalogEntryFormatter
    Implements ICatalogEntryFormatter

    Property DescriptionText As String
End Class

Public Class FastenerCatalogEntryFormatter
    Implements ICatalogEntryFormatter

    Property DescriptionText As String
End Class

In a controller somewhere there will be code like this:

Dim entries As List(Of ICatalogEntry)
                   = catalogService.CurrentCatalog(DateTime.Now)

In a view somewhere there will be code like this:

<ul>
@For Each entry As ICatalogEntry In Model.Catalog
    @<li>@entry.DescriptionText</li>
Next
</ul>

So the question is what do the constructors look like? How to set it up so the appropriate objects are instantiated in the right places. Seems like generics or maybe DI can help with this but I seem to be having a mental block. The only idea I've come up with is to add a ProductType property to ICatalogEntry and then implement a factory like this:

Public Class CatalogEntryFactory
    Public Function Create(catEntry as ICatalogEntry) As ICatalogEntry
        Select Case catEntry.ProductType
        Case "Clothing"
            Dim clothingProduct = clothingService.Get(catEntry.ProductId)
            Dim clothingEntry = New ClothingCatalogEntry(clothingProduct)
            Return result
        Case "Fastener"
            Dim fastenerProduct = fastenerService.Get(catEntry.ProductId)
            Dim fastenerEntry = New FastenerCatalogEntry(fastenerProduct)
            fastenerEntry.Formatter = New FastenerCatalogEntryFormatter
            Return fastenerEntry
    ...     
    End Function
End Class

Public ClothingCatalogEntry
    Public Sub New (product As ClothingProduct)
        Me.Formatter =  New ClothingCatalogEntryFormatter(product)
    End Sub

    Property DescriptionText As String
        Get
            Return Me.Formatter.DescriptionText
        End Get
    End Property
End Class

...FastenerCatalogEntry is omitted but you get the idea...

Public Class CatalogService
    Public Function CurrentCatalog(currentDate as DateTime)
        Dim theCatalog As List(Of ICatalogEntry)
                                  = Me.repository.GetCatalog(currentDate)

        Dim theResult As New List(Of ICatalogEntry)

        For Each entry As ICataLogEntry In theCatalog
            theResult.Add(factory.Create(entry))
        Next

        Return theResult
    End Function
End Class

IMHO, I am not really getting any smells off this code other than having to change the factory for every new product class that comes along. Yet, my gut says that this is the old way of doing things and nowadays DI and/or generics can do this better. Suggestions on how to handle this are much appreciated (as are suggestions on a better title...)

Lowelllowenstein answered 17/4, 2012 at 23:4 Comment(0)
L
1

So making a few small changes I got this to work using the Ninject Factory extension. Biggest change is that my entities have enough info to display either type (clothes or fasteners in my contrived example) if the item is actually clothes then the fastener specific properties will be null and vice versa.

Public Interface IDescribable
    ReadOnly Property DescriptionText As String
End Interface

Public Enum ProductType
    CLOTHING
    FASTENER
End Enum

Public Interface ICatalogEntry
    Inherits IDescribable
    ReadOnly Property ProductId As Int64
    ReadOnly Property PublishedOn As DateTime
    ReadOnly Property ProductType As ProductType
End Interface

Public Class CatalogEntryEntity
    Public Property ProductId As Long
    Public Property ProductType As ProductType
    Public Property PublishedOn As Date
    Public Property DescriptionText As String
    Public Property Color As String
    Public Property Finish As String
    Public Property IsMetric As Boolean
End Class

Then with this in place I can define my catalog service as follows:

Public Class CatalogService
    Private ReadOnly _factory As ICatalogEntryFactory
    Private ReadOnly _repository As CatalogRepository

    Public Sub New(entryFactory As ICatalogEntryFactory, repository As CatalogRepository)
        Me._factory = entryFactory
        Me._repository = repository
    End Sub

    Public Function CurrentCatalog(currentDate As DateTime) As List(Of ICatalogEntry)
        Dim items = Me._repository.GetCatalog()
        Return (From item In items Select _factory.Create(item.ProductType.ToString(), item)).ToList()
    End Function
End Class

Public Interface ICatalogEntryFactory
    Function Create(bindingName As String, entity As CatalogEntryEntity) As ICatalogEntry
End Interface

Ninject will provide the factory (which is awesome!) assuming I setup the bindings like this:

theKernel.Bind(Of ICatalogEntry)().To(Of ClothingCatalogEntry)().Named("CLOTHING")
theKernel.Bind(Of ICatalogEntry)().To(Of FastenerCatalogEntry)().Named("FASTENER")
theKernel.Bind(Of ICatalogEntryFactory)().ToFactory(Function() New UseFirstParameterAsNameInstanceProvider())

I've omitted the FastenerCatalogEntry for brevity; the ClothingCatalogEntry is like this:

Public Class ClothingCatalogEntry   
    Public Sub New(ByVal entity As CatalogEntryEntity)
...

It was this post that helped me the most to figure this out. I used UseFirstParameterAsNameInstanceProvider exactly as shown there.

Lowelllowenstein answered 24/4, 2012 at 15:45 Comment(0)
L
1

I like to just use the default constructor on models for the view and populate them via Automapper.

I would have a view model like this:

public interface IHasDescription
{
    public string DescriptionText { get; set; }
}

public class ViewModelType : IHasDescription
{
    [DisplayName("This will be rendered in the view")]
    public string SomeText { get; set; }

    public string DescriptionText { get; set; }
}

And I have a model from the DAL like this:

public class DALModelType
{
    public string SomeText { get; set; }
}

So you have something like this in your controller:

var dalModel = someRepository.GetAll();
var viewModel = Mapper.Map<DALModelType, ViewModelType>(dalModel);

And you have the Automapper setup code in some file. This way you only have the conversion code in one place instead of in multiple methods/controllers. You have a custom resolver which uses dependency injection (instead of () => new CustomResolver()) and this will house your logic for getting the display text.

Mapper.CreateMap<IHasDescription, ViewModelType>()
    .ForMember(dest => dest.DescriptionText, 
               opt => opt.ResolveUsing<CustomResolver>().ConstructedBy(() => new CustomResolver()));

Not sure if this works with your workflow but it should be able to get you what you want.

Lefthand answered 17/4, 2012 at 23:50 Comment(3)
So the custom resolver handles the fact that more than one class maps to the same interface? That is what is pushing me to the factory class in my question and that is what I am most uncomfortable about and completely unclear how to get rid of.Lowelllowenstein
Ok, now that I read the link you provided I've concluded that I should reasonably expect my DI container (Ninject) to have some answer for this scenario. Furthermore it looks like it does and I need contextual binding github.com/ninject/ninject/wiki/Contextual-Binding and I probably need this as well: github.com/ninject/ninject.extensions.factory/wikiLowelllowenstein
Yeah, that should actually solve your problem. In your ninject initialization you should be able to load from a resource file.Lefthand
L
1

So making a few small changes I got this to work using the Ninject Factory extension. Biggest change is that my entities have enough info to display either type (clothes or fasteners in my contrived example) if the item is actually clothes then the fastener specific properties will be null and vice versa.

Public Interface IDescribable
    ReadOnly Property DescriptionText As String
End Interface

Public Enum ProductType
    CLOTHING
    FASTENER
End Enum

Public Interface ICatalogEntry
    Inherits IDescribable
    ReadOnly Property ProductId As Int64
    ReadOnly Property PublishedOn As DateTime
    ReadOnly Property ProductType As ProductType
End Interface

Public Class CatalogEntryEntity
    Public Property ProductId As Long
    Public Property ProductType As ProductType
    Public Property PublishedOn As Date
    Public Property DescriptionText As String
    Public Property Color As String
    Public Property Finish As String
    Public Property IsMetric As Boolean
End Class

Then with this in place I can define my catalog service as follows:

Public Class CatalogService
    Private ReadOnly _factory As ICatalogEntryFactory
    Private ReadOnly _repository As CatalogRepository

    Public Sub New(entryFactory As ICatalogEntryFactory, repository As CatalogRepository)
        Me._factory = entryFactory
        Me._repository = repository
    End Sub

    Public Function CurrentCatalog(currentDate As DateTime) As List(Of ICatalogEntry)
        Dim items = Me._repository.GetCatalog()
        Return (From item In items Select _factory.Create(item.ProductType.ToString(), item)).ToList()
    End Function
End Class

Public Interface ICatalogEntryFactory
    Function Create(bindingName As String, entity As CatalogEntryEntity) As ICatalogEntry
End Interface

Ninject will provide the factory (which is awesome!) assuming I setup the bindings like this:

theKernel.Bind(Of ICatalogEntry)().To(Of ClothingCatalogEntry)().Named("CLOTHING")
theKernel.Bind(Of ICatalogEntry)().To(Of FastenerCatalogEntry)().Named("FASTENER")
theKernel.Bind(Of ICatalogEntryFactory)().ToFactory(Function() New UseFirstParameterAsNameInstanceProvider())

I've omitted the FastenerCatalogEntry for brevity; the ClothingCatalogEntry is like this:

Public Class ClothingCatalogEntry   
    Public Sub New(ByVal entity As CatalogEntryEntity)
...

It was this post that helped me the most to figure this out. I used UseFirstParameterAsNameInstanceProvider exactly as shown there.

Lowelllowenstein answered 24/4, 2012 at 15:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.