0

I am trying to create a class with a shared function that will return a class based on the value of a parameter, instead of a class instance (basically a class factory.) Then, I will call the class' constructor to get the instance I need.

For example, I have these three classes:

class Test
  sub New(byval anID as Integer)
  end sub
end class

class Test_A
  inherits Test

  sub New(byval anID as Integer)
  end sub
end class

class Test_B
  inherits Test

  sub New(byval anID as Integer)
  end sub
end class

I want something like (I know it does not work):

class TestFactory
  shared function Create(byval aParam as Integer) as Test
    select aParam
      case 0
        return Test_A
      case 1
        return Test_B
      ...
    End Select
  end function
end class

to later use it in the following way:

dim aTest as Test = New TestFactory.Create(0)(anID:=100)

I searched for similar question, and they all seem to suggest using generics. But in my case i think they are not suitable, since I need to know the class beforehand (see this answer.)

Any suggestions?

johnnyRose
  • 7,310
  • 17
  • 40
  • 61
  • Every class could hold a [single](http://en.wikipedia.org/wiki/Singleton_pattern) instance of itself which is returned through a factory method(`GetInstance)`, then you can return the right according to the parameter. – Tim Schmelter Mar 17 '15 at 16:42
  • I am sorry Tim, but I do not understand what you are saying. Which class has the GetInstance method? Can you provide an example? – Angelos Arampatzis Mar 17 '15 at 16:47
  • Why not use generic? `Public Shared Function Create(Of T As Test)(anID as Integer) As T` – Bjørn-Roger Kringsjå Mar 17 '15 at 17:07
  • The function would still return an instance of a class, not a class. I cannot have something like this: return Type_A – Angelos Arampatzis Mar 17 '15 at 17:11
  • You can `Return GetType(Type_A)`, and you return type from `Create` will be `Type`. You would then have to create an instance using reflection, and the method signature does not guarantee that the returned type is a subclass of `Test`. I'll add an example to my answer. – Mark Mar 17 '15 at 17:18
  • I was already working on this: Public Class TestFactory2 Shared Function DecideClass(ByVal aKind As Integer) As Type Select Case aKind Case 1 Return GetType(Test_A) Case 2 Return GetType(Test_B) Case Else Return GetType(Test) End Select End Function End Class But the only way I could find to create an instance from that is Activator.CreateInstance which requires a constructor with zero arguments. – Angelos Arampatzis Mar 17 '15 at 17:26

2 Answers2

2

I was surprised that I couldn't make this work with the constructor directly, but you can at least return a delegate/lambda expression that calls into the constructor you want:

Class TestFactory
  Public Shared Function Create(byval aParam as Integer) as Func(Of Integer, Test)
    Select aParam
      Case 0
        return Function(id) New Test_A(id)
      Case 1
        Return Function(id) New Test_B(id)
      ...
    End Select
  End Function
End Class

And then you can call it almost exactly as you described:

Dim aTest As Test = TestFactory.Create(0)(100)

What we are doing now is effectively Currying your Test factory.

My experience, though, is that this is the wrong direction in the first place. Create() methods generally return instances, not Types, and right or wrong, there will be an expectation for this among other programmers who use the type and are familiar with the pattern. If you do go down this path, at least consider a different name for the method.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • Huh, I don't think I realized you could actually get a hold of the constructor in VB. Is there anything comparable in C#? – TyCobb Mar 17 '15 at 17:50
  • @TyCobb Read closely... I'm not sure you can, either, as I couldn't make this work. I posted the answer more as a sign-post, than a complete solution. – Joel Coehoorn Mar 17 '15 at 17:50
  • Sorry. Just saw your bigger edit after I posted. Didn't realize your first sentence was related to the code you posted. It's probably because `New` is a `Sub` and doesn't actually return you the instance/type. – TyCobb Mar 17 '15 at 17:51
  • You could `Return Function(x) New Test_A(x)`, which isn't too bad. – Mark Mar 17 '15 at 18:16
  • @Mark Yes, I think I'll do that. I guess that we just have use a `New` operator somewhere to actually call a constructor method. – Joel Coehoorn Mar 17 '15 at 18:20
  • It looks like constructor delegates are [not](http://stackoverflow.com/q/1600712/2278086) [possible](http://stackoverflow.com/q/10593630/2278086) in .NET - and the answers to those questions also point out that the return from the constructor method is `void` (they are instance methods, not some sort of factory), so it wouldn't help anyway. Fascinating stuff! :-) – Mark Mar 17 '15 at 18:32
1

In the example case, you could just pass in your constructor parameter to your factory method:

class TestFactory
  shared function Create(byval aParam as Integer, byval anID as Integer) as Test
    select aParam
      case 0
        return New Test_A(anID)
      case 1
        return New Test_B(anID)
      ...
    End Select
  end function
end class

Otherwise, I think you would have to return a Type and call the constructor using reflection, which is not so nice because, as mentioned in the comments, the method signature does not guarantee that the returned type is a subtype of Test.

class TestFactory
  shared function Create(byval aParam as Integer) as Type
    select aParam
      case 0
        return GetType(Test_A)
      case 1
        return GetType(Test_B)
      ...
    End Select
  end function
end class

' Use something like this...
Dim t As Type = TestFactory.Create(0)
' Probably need a DirectCast here...
Dim myTest As Test = t.GetConstructor(New Type() {GetType(Integer)}) _
    .Invoke(New Object() {anID})
Mark
  • 8,140
  • 1
  • 14
  • 29
  • This was my initial though, but I would prefer not to "polute" the factory's signature with parameters needed by the constructor. – Angelos Arampatzis Mar 17 '15 at 16:51
  • 1
    I would think that pollution is pretty common, if the factory needs more information in order to create the instance... at least more common than having a factory that doesn't actually create anything. ;-) – Mark Mar 17 '15 at 16:58
  • @AngelosArampatzis a factory is usually there to [create](http://en.wikipedia.org/wiki/Factory_method_pattern). If you want to return a class it'll have to be a [Type](https://msdn.microsoft.com/en-us/library/system.type.aspx). – the_lotus Mar 17 '15 at 17:01
  • Fair enough, the class "factory" does not create anything. It is just a way of hiding the decision making process. – Angelos Arampatzis Mar 17 '15 at 17:05
  • If you do not want to have to pass the argument for the constructor to your factory, perhaps you could remove the argument from the constructor and move the processing of that argument to a new "Initialise" method in your class. You would probably need to add checks to other methods in your class to make sure the Initialise method has been called, so this may turn out to be too much trouble. – Blackwood Mar 17 '15 at 17:10
  • 1
    Returning a `Type` by itself feels wrong, to me, because you can't constrain it in the method signature that it must inherit from `Test`. That Type could be _anything_, and that means you'd have to give up type safety or have a very nasty cast somewhere down the road. Probably "polluting" the method signature is the way to go. – Joel Coehoorn Mar 17 '15 at 18:03
  • @JoelCoehoorn Yep, totally agree, and mentioned that in a comment on the question before adding that example, but I'll put it in the answer too. – Mark Mar 17 '15 at 18:08