Nitax
Nitax

Reputation: 470

What is the proper object relationship? (C#)

I had a quick question about the proper object relationship I should set up for this situation:

I have a Customer object with associated parameters and a depot object with associated parameters. Each depot serves a set of customers and the customer needs access to particular information for their respective depot.

I'm wondering what the proper relationship I should set up so that a set of customer objects all reference the same instance of a particular depot object. I wanted to be sure it wasn't creating a duplicate Depot object for each customer. Furthermore, i'd like to be able to change properties of the Depot without going through the customer itself.

I know this is probably a fairly basic question but C# has so many different "features" it gets confusing from time to time.

Thanks for your help!

Upvotes: 2

Views: 1489

Answers (4)

Damir Sudarevic
Damir Sudarevic

Reputation: 22187

Hope this is self-explanatory.

OO:

depot_model_01

ER:

depot_model_02

Upvotes: 0

Robert Paulson
Robert Paulson

Reputation: 18061

In reply to @Jason D, and for the sake of @Nitax: I'm really skimming the surface, because while it's basically easy, it also can get complicated. There's no way I'm going to re-write it better than Martin Fowler either (certainly not in 10 minutes).

You first have to sort out the issue of only 1 object in memory that refers to a specific depot. We'll achieve that with something called a Repository. CustomerRepository has a GetCustomer() method, and the DepotRepository has a GetDepot() method. I'm going to wave my hands and pretend that just happens.

Second you need to need to write some tests that indicate how you want the code to work. I can't know that, but bear with me anyways.

// sample code for how we access customers and depots
Customer customer = Repositories.CustomerRepository.GetCustomer("Bob");
Depot depot = Repositories.DepotRepository.GetDepot("Texas SW 17");

Now the hard part here is: How do you want to model the relationship? In OO systems you don't really have to do anything. In C# I could just do the following.

Customers keep a list of the depots they are with

class Customer
{
    public IList<Depot> Depots { get { return _depotList; } }
}

alternatively, Depots keep a list of the customers they are with

class Depot
{
    public IList<Customer> Customers { get { return _customerList; } }
}
// * code is very brief to illustrate.

In it's most basic form, any number of Customers can refer to any number of Depots. m:n solved. References are cheap in OO.

Mind you, the problem we hit is that while the Customer can keep a list of references to all the depot's it cares about (first example), there's not an easy way for the Depot to enumerate all the Customers.

To get a list of all Customers for a Depot (first example) we have to write code that iterates over all customers and checks the customer.Depots property:

List<Customer> CustomersForDepot(Depot depot)
{
    List<Customer> allCustomers = Repositories.CustomerRepository.AllCustomers();
    List<Customer> customersForDepot = new List<Customer>();

    foreach( Customer customer in allCustomers )
    {
        if( customer.Depots.Contains(depot) )
        {
            customersForDepot.Add(customer);
        }
    }
    return customersForDepot;
}

If we were using Linq, we could write it as

var depotQuery = from o in allCustomers
                 where o.Depots.Contains(depot)
                 select o;

return query.ToList();

Have 10,000,000 Customers stored in a database? Ouch! You really don't want to have to load all 10,000,000 customers each time a Depot needs to determine its' customers. On the other hand, if you only have 10 Depots, a query loading all Depots once and a while isn't a big deal. You should always think about your data and your data access strategy.

We could have the list in both Customer and Depot. When we do that we have to be careful about the implementation. When adding or removing an association, we need to make the change to both lists at once. Otherwise we have customers thinking they are associated with a depot, but the depot doesn't know anything about the customer.

If we don't like that, and decide we don't really need to couple the objects so tightly. We can remove the explicit List's and introduce a third object that is just the relationship (and also include another repository).

class CustomerDepotAssociation
{
    public Customer { get; }
    public Depot { get; }
}

class CustomerDepotAssociationRepository
{
    IList<Customer> GetCustomersFor(Depot depot) ...
    IList<Depot> GetDepotsFor(Customer customer) ...
    void Associate(Depot depot, Customer customer) ...
    void DeAssociate(Depot depot, Customer customer) ...
}

It's yet another alternative. The repository for the association doesn't need to expose how it associates Customers to Depots (and by the way, from what I can tell, this is what @Jason D's code is attempting to do)

I might prefer the separate object in this instance because what we're saying is the association of Customer and Depot is an entity unto itself.

So go ahead and read some Domain Driven Design books, and also buy Martin Fowlers PoEAA (Patterns of Enterprise Application Architecture)

Upvotes: 1

Jason D
Jason D

Reputation: 2303

This sounds like you've got a Many-to-many relationship going on. (Customers know about their Depots, and vice versa)

Ideally this seems best suited for a database application where you define a weak-entity table ... Of course using a database is overkill if we're talking about 10 Customers and 10 Depots...

Assuming a database is overkill, this can be modeled in code with some Dictionarys. Assuming you're using int for the unique identifiers for both Depot and Customer you could create something like the following:

// creating a derived class for readability.
public class DepotIDToListOfCustomerIDs : Dictionary<int,List<int>> {}
public class CustomerIDToListOfDepotIDs : Dictionary<int,List<int>> {}
public class DepotIDToDepotObject : Dictionary<int,Depot>{}
public class CustomerIDToCustomerObject : Dictionary<int, Customer>{}
//...
// class scope for a class that manages all these objects...
DepotIDToListOfCustomerIDs _d2cl = new DepotIDToListOfCustomerIDs();
CustomerIDToListOfDepotIDs _c2dl = new CustomerIDToListOfDepotIDs();
DepotIDToDepotObject _d2do = new DepotIDToDepotObject();
CustomerIDToCustomerObject _c2co = new CustomerIDToCustomerObject();
//...
// Populate all the lists with the cross referenced info.
//...
// in a method that needs to build a list of depots for a given customer
// param: Customer c
if (_c2dl.ContainsKey(c.ID))
{
    List<int> dids=_c2dl[c.ID];
    List<Depot> ds=new List<Depot>();
    foreach(int did in dids)
    {
        if (_d2do.ContainsKey(did))
            ds.Add(_d2do[did]);
    }
}
// building the list of customers for a Depot would be similar to the above code.

EDIT 1: note that with the code above, I've crafted it to avoid circular references. Having a customer reference a depot that also references that same customer will prevent these from being quickly garbage collected. If these objects will persist for the entirety of the applications lifespan a simpler approach certainly could be taken. In that approach you'd have two lists, one of Customer instances, the other would be a list of Depot instances. The Customer and Depot would contain lists of Depots and Customers respectively. However, you will still need two dictionaries in order to resolve the Depot IDs for the customers, and vice versa. The resulting code would be 99% the same as the above.

EDIT 2: As is outlined in others replies you can (and should) have an object broker model that makes the relationships and answers questions about the relationships. For those who have misread my code; it is by no means intended to craft the absolute and full object model for this situation. However, it is intended to illustrate how the object broker would manage these relationships in a manner that prevents circular references. You have my apologies for the confusion it caused on the first go around. And my thanks for illustrating a good OO presentation that would be readily consumed by others.

Upvotes: 1

jrista
jrista

Reputation: 32950

If I understand your question correctly, I think a solution to your problem might be an OR mapper. Microsoft provides two OR mappers at the moment, LINQ to SQL and Entity Framework. If you are using .NET 3.5, I recommend using LINQ to SQL, but if you are able to experiment with .NET 4.0, I would highly recommend looking into Entity Framework. (I discourage the use of Entity Framework in .NET 3.5, as it was released very prematurely and has a LOT of problems.)

Both of these OR mappers provide visual modeling tools that allow you to build a conceptual entity model. With LINQ to SQL, you can generate a model from your database, which will provide you with entity classes, as well as associations between those classes (representing your foreign keys from your DB schema). The LINQ to SQL framework will handle generating SQL queries for you, and will automatically map database query results into object graphs. Relationships such as the one you described, with multiple customers in a set referencing the same single department are handled automatically for you, you don't need to worry about them at all. You also have the ability to query your database using LINQ, and can avoid having to write a significant amount of stored procedures and plumbing/mapping code.

If you use .NET 4.0, Entity Framework is literally LINQ to SQL on steroids. It supports everything LINQ to SQL does, and a hell of a lot more. It supports model-driven design, allowing you to build a conceptual model from which code AND database schema are generated. It supports a much wider variety of mappings, providing a much more flexible platform. It also provides Entity SQL (eSQL), which is a text-based query language that can be used to query the model in addition to LINQ to Entities. Line LINQ to SQL, it will solve the scenario you used as an example, as well as many others.

OR mappers can be a huge time, money, and effort saver, greatly reducing the amount of effort required to interact with a relational database. They provide both dynamic querying as well as dynamic, optimistic updates/inserts/deletes with conflict resolution.

Upvotes: 1

Related Questions