views:

139

answers:

3

VB 2008 .NET 3.5

Suppose we have two classes, Order and OrderItem, that represent some type of online ordering system. OrderItem represents a single line item in an Order. One Order can contain multiple OrderItems, in the form of a List(of OrderItem).

Public Class Order
    Public Property MyOrderItems() as List(of OrderItem)
    End Property
End Class

It makes sense that an OrderItem should not exist without an Order. In other words, an OrderItem class should not be able to be instantiated on its own, it should be dependent on an Order class to contain it and instantiate it. However, the OrderItem should be public in scope so that it's properties are accessible to other objects. So, the requirements for OrderItem are:

  1. Can only be instantiated by the Order class.

  2. Must be public so that any other object can access it's properties/methods through the Order object. e.g. Order.OrderItem(0).ProductID.

  3. OrderItem should be able to be passed to other subs/functions that will operate on it.

How can I achieve these goals? Is there a better approach?

+4  A: 

You can make a single constructor of OrderItem that takes an Order item.

In this constructor, you can add the OrderItem to the Order.MyOrderItems collection.

This will satisfy your requirements.

Public Class OrderItem
    Public Sub New(ByRef myOrder As Order)
        myOrder.MyOrderItems.Add(Me)
    End Sub
End Class
  1. You can only instantiate OrderItem by passing in a valid Order object to it.
  2. Still public and can be called as required (Order.OrderItem(0).ProductID).
  3. OrderItem can be passed to other subs and functions.
Oded
This is very close, and is the solution I came up with. However, I didn't state my first criteria correctly, so I revised it. The only problem with this method is that it only requires an Order to exist somewhere, not that an Order does the instantiation. Sorry for the mix up.
Casey
@Casey - You want a class that is publicly accessibly, but that can only be instantiated by a specific other class. As far as I know, this is not possible in .NET (or most other OO languages).
Oded
The only way beside's @Stephen Martin method is to use reflection to check the call stack which would be a really expensive call every time and I don't think anyone would recommend it. It would obviously only run at runtime. But I'll throw it in here for the heck of it. If New System.Diagnostics.StackTrace().GetFrame(1).GetMethod().Name <> "CreateAndAddOrder" Then Throw New ApplicationException("Invalid Caller")
Chris Haas
@Oded, @Chris Hass, Thanks for the input. I think Stephen has the best solution that meets all of the criteria, but the alternates you guys have provided are good to know as well.
Casey
+1  A: 

OrderItem's constructor should be friend (accesible only to Order) - I say friend because using friend you can limit the visibility of the OrderItem's constructor to classes in the same assembly. Note: this solution implies that Order and OrderItem are in one library/assembly and you are trying to use them from another assembly - otherwise the friend protection does not work.

The responsablity for creating OrderItem objects should belong only to Order:

Build a method like on the Order class: Public Function CreateOrderItem(whateverParametersYouNeed) as OrderItem that internally instantiates a new OrderItem, adds it to the OrderItem list and returns reference to the newly created item.

You can then do whatever you want with this OrderItem - it will always belong to the Order's list.

Ando
What do you mean by module? I was under the impression that the Friend modifier would make it visible to everything in the assembly.
Casey
assembly was the word I should have used not module.
Ando
+1  A: 

The standard way to do this is to expose the OrderItem as an interface and then implement it in a private class inside the Order class. The OrderItem class can only be instantiated inside the Order class but it can be exposed to the outside through the public IOrderItem interface. Like so:

Public Interface IOrderItem
    ReadOnly Property ItemCode() As Integer
    ReadOnly Property NumberOfItems() As Integer
    ReadOnly Property Description() As String
End Interface

Public Class Order

    Private m_Items As List(Of IOrderItem)

    Public ReadOnly Property Items() As List(Of IOrderItem)
        Get
            Return m_Items
        End Get
    End Property

    Private Class OrderItem
        Implements IOrderItem

        Private m_Code As Integer
        Private m_NumItems As Integer
        Private m_Description As String

        Public Sub New(ByVal code As Integer, ByVal numItems As Integer, ByVal desc As String)
            m_Code = code
            m_NumItems = numItems
            m_Description = desc
        End Sub

        Public ReadOnly Property Description() As String Implements IOrderItem.Description
            Get
                Return m_Description
            End Get
        End Property

        Public ReadOnly Property ItemCode() As Integer Implements IOrderItem.ItemCode
            Get
                Return m_Code
            End Get
        End Property

        Public ReadOnly Property NumberOfItems() As Integer Implements IOrderItem.NumberOfItems
            Get
                Return m_NumItems
            End Get
        End Property
    End Class
End Class
Stephen Martin
This is about the most elegant solution I have seen. I wish there was some type of language construct to do this, as I run in to it a lot with aggregate/composite classes.
Casey