How to manage many to many relationships without creating too many classes

How to manage many to many relationships without creating too many classes

Old forum URL: forums.lhotka.net/forums/t/8005.aspx


alef posted on Tuesday, November 17, 2009

I've the following use case:
    An employee can choose from a countrylist the countries he wants to work in. In the UI this is typically done with a dual list. (two listboxes and some buttons to move item(s) from one listbox to the other)

I was thinking to create the following objects following the projecttracker example:
 - CountryList (ReadOnlyListBase)
      - CountryInfo (ReadOnlyBase)
 - Employee (BusinessBase)
       - SelectedCountries (BusinessListBase)
           - SelectedCountry (BusinessBase)

When modeling the application like this, I can create a fully functional application.
But I'm not so happy with the design.
I find that too much classes are created.
I'm more in favor of the following model:
 - CountryList (ReadOnlyListBase)
      - CountryInfo (ReadOnlyBase)
 - Employee (BusinessBase)
       - List<CountryInfo> (just a property)

But in this case I don't see how this must be saved to the database. When reading the book I've noticed that only BusinessBase has the methods(fetch,insert,update and create). How do I need to code this. So the employee object needs to save the list of selected countries.











Fintanv replied on Tuesday, November 17, 2009

alef:

I was thinking to create the following objects following the projecttracker example:
 - CountryList (ReadOnlyListBase)
      - CountryInfo (ReadOnlyBase)
 - Employee (BusinessBase)
       - SelectedCountries (BusinessListBase)
           - SelectedCountry (BusinessBase)

When modeling the application like this, I can create a fully functional application.
But I'm not so happy with the design.
I find that too much classes are created.
I'm more in favor of the following model:
 - CountryList (ReadOnlyListBase)
      - CountryInfo (ReadOnlyBase)
 - Employee (BusinessBase)
       - List<CountryInfo> (just a property)

In the example you have given you have eliminated two classes at the expense of increased complexity.  One of the classes you eliminated (SelectedCountries) is actually quite trivial to implement since the vast majority of the functionality you need is encapsulated in the base class.  The goal is to increase maintainability of the code, not necessarily to reduce the total number of classes. While there is a balance to be struck, your second design does not buy you much, and indeed may cost you (IMHO).  By going with the first design, you will enable your child objects to save themselves to the database, again with minimal code.

 

ajj3085 replied on Tuesday, November 17, 2009

I'd go with your first design.

CountryList / CountryInfo is meant to display information about a country.

Employee / SelectedCountries / SelectedCountry is meant to allow you to edit an employee (Employee), and define a relationship between zero or more countries (SelectedCountries / SelectedCountry). 

Since there are two different roles there (display of information vs. representing a relationship between Employee and Country), you should have seperate classes.

HTH

Andy

alef replied on Wednesday, November 18, 2009

Thanks for the reactions.
So can I conclude that with CSLA it is NOT possible to implement the second design or maybe it is possible but the framework is not taken into account this design and that is the reason it will be more complex.
Suppose that for the employee you want to choose also the languages he can speak, the courses he want to follow, ....

Also suppose you have another editable root object which also needs a country list.
So we'll need to add a lot of extra classes. So I don't known that this will increase the maintainability.


On top of that the code in the UI for the duallist is not so easy. It is not possible to bind just a couple of objects. Because the left side of the duallist has objects of type CountryInfo and the right side of the duallist has objects of the type SelectedCountry. So these are two different kind of object types.
So I've the following code in the UI:


    public PisteWedstrijdEdit(Employee employee)
    {
      InitializeComponent();

      // store object reference
      _employee = employee;

      // set up binding      
      BindUI();

      // check authorization
      ApplyAuthorizationRules();
    }


    private void BindUI()
    {

      gridControlAvailableCountry.DataSource = null;
      gridControlSelectedCountry.DataSource = null;

      DataTable dtAvailable = new DataTable();
      dtAvailable.Columns.Add("Id", typeof(int));
      dtAvailable.Columns.Add("Value", typeof(string));
      DataTable dtSelected = dtAvailable.Clone();

      SelectedCountries selectedCountries = _employee.SelectedCountries;
      if (selectedCountries == null) return;
      
      foreach (CountryInfo countryInfo in CountryList.GetCountryList())
      {
        if (selectedCountries.Contains(countryInfo.Id))
        {
          dtSelected.Rows.Add(new object[] { countryInfo.Id, countryInfo.Name});
        }
        else
        {
          dtAvailable.Rows.Add(new object[] { countryInfo.Id, countryInfo.Name});
        }
      }

      gridControlAvailableCountry.DataSource = dtAvailable;
      gridControlSelectedCountry.DataSource = dtSelected;
    }

    private void dualListAdd_AfterAction(object sender, DualListActionEventArgs e)
    {
        SelectedCountry selectedCountry = _employee.SelectedCountries.AddNew();
        selectedCountry.CountryId = (int)gridViewSelectedCountry.GetDataRow(e.ToIndex)["Id"];

    }

    private void dualListDelete_AfterAction(object sender, DualListActionEventArgs e)
    {
        _employee.SelectedCountries.Remove((int)gridViewAvailableCountry.GetDataRow(e.ToIndex)["Id"]);
    }



So because we have two different kind of object types (SelectedCountry, CountryInfo) and a duallist needs two of the same kind in the UI we need to implement a workaround.
In the BindUI() method we'll provide the two listboxes the same type of object, these are two datatables which we'll create on the fly.

And in the events (dualListAdd_AfterAction, dualListDelete_AfterAction) we need to synchronize our business objects with the reality.

So as you can see we don't have anymore the simple binding.





ajj3085 replied on Wednesday, November 18, 2009

Oh, its entirely possible to create the second design in csla... its just that more than likely you'll wind up regretting it down the road.

Fintanv replied on Wednesday, November 18, 2009

For what its worth here is how I would approach the problem:

- ICountry (Interface that exposes the country properties)

-  CountryList (ReadOnlyListBase)
      - CountryInfo (ReadOnlyBase) : ICountry

- CountryCollection (BusinessListBase)
     - Country (BusinessBase) : ReadOnly properties : ICountry


 - Employee (BusinessBase)
       - SelectedCountries (CountryCollection ): Property that may initially be empty or populated with already saved employee choices.  This is the list of countries that will be persisted when you 'save' your employee business object.

       - NonSelectedCountries (CountryCollection ): Property that may be populated by the values stored in CountryList or from a direct db call, minus those countries found in the SelectedCountries property (could be passed in as an 'exclusion' list to the factory method).

You can then encapsulate the functionality to move the countries from selected list to non-selected list within your Employee root class.

Your UI listboxes bind to the two collections exposed by your root object.  Your UI buttons determine what countries are selected and then pass this info to the method in your root that handles the move between the lists.

No need for workarounds hosted in the UI, and encapsulates the use case into a single business object (Employee).  This is more maintainable at the use case level of granularity.


         

alef replied on Wednesday, November 25, 2009


I've created a full working example of this like the description of Fintanv.
Maybe at the end Lhotka can add it to the examples.
For the GUI using a business model like this is a dream. It is really simple, just two lines of code :
      private void cmdAdd_Click(object sender, EventArgs e)
      {
        _employee.AddCountry(((ICountry)AvailableCountriesListBox.SelectedItem));
       
      }

      private void cmdDelete_Click(object sender, EventArgs e)
      {
        _employee.RemoveCountry(((ICountry)SelectedCountriesListBox.SelectedItem));
      }


In the business layer some things to pay attention.
The DataPortal_Update method of the class Employee can't use the following:
        FieldManager.UpdateChildren(cn, this);
I've replaced this with the following
        //Update Child object(s)
        DataPortal.UpdateChild(ReadProperty(SelectedCountriesProperty),cn, this);  
Reason : employee has two collections SelectedCountries and NonSelectedCountries. Only the SelectedCountries must be saved to the database.
If we use FieldManager.UpdateChildren then CSLA will try to save both collections to the database.



For the rest everything is normal code.


But still I'm not convinced of the design (the business layer)

In the database we have an intermediate table but in the OO model we don't always match up. If we don't have extra information on the relationship between Employee and Country (i.e.. a property to express his favor in a percentage for that country) I don't see a reason to have objects like CountryCollection and Country.
When doing some research I stumbled on an old posting of yours: http://www.lhotka.net/Article.aspx?id=ff226256-903f-4aee-a921-8b09ef40901b. In the post, you seem to be saying that the intermediate table concept doesn't fit into OOD/OOP.

ajj3085 is saying it is possible to do this. Is it possible to show me how to do this?
You can a find full working example in attachment.

ajj3085 replied on Wednesday, November 25, 2009

It's ok that your business object design doesn't match your database design.  It usually shouldn't.  The reason is that your business layer is driven by use cases, or what the user is trying to do.  This rarely directly matches the goals of database design, which is a relational model designed to reduce duplicate data and normalize it.  Those goals are different, so its no suprise that they produce different models.  That's why you do your mapping between the two in your DataPortal_XYZ methods, or ObjectFactory.

 

It wouldn't be hard to bulid the other model you suggest, and I"m sure you can figure out how to do this.  The pain will come in though ifyour data model changes, or if one use case changes and the other doesn't.  you'll either end up violating the Single responsiblity principal, or you'll have to do the work to create the design you now have anyway.

alef replied on Friday, November 27, 2009


I'm a bit confused now.

The OO model like Fintanv is describing is a one to one mapping with the database tables.
The country objects represents the bridge table. The dataportal methods of Country will update the bridge table.

In the contrary the OO model

 - CountryList (ReadOnlyListBase)
      - CountryInfo (ReadOnlyBase)
 - Employee (BusinessBase)
       - List<CountryInfo> (just a property)

directly represent the many-to-many relationship, without the need to create composite entities.

I must admit to have a consistent design, we should always keep a collection of the object mapped to the bridge table. That way, if you decide to add attributes to the bridge table later, you won't have completely redo your object model and hence your application. You could simply add properties to the object mapped to the bridge table.


When reading the article
http://www.lhotka.net/Article.aspx?id=ff226256-903f-4aee-a921-8b09ef40901b
Lhotka is saying "What you've hit on here is an example of where relational models and OO models don't always match up".
So the model he is proposing doesn't correspond to the one of Fintanv
We can say that in my use case an Employee not owns 'Countries'. Lhotka says "To effectively implement this type of scenario you almost always need to use a technique called lazy loading."
In my use case I prefer not the lazy loading (when getting an employee and I want immediately also the countries where the employee wants to work), but the OO model described in the article gets more my favor.


It is nice to hear that CSLA is supporting both models. But I regret I can't see how to do this. Please is it possible to implement this in my example. I've put some effort to create this example so that we have a good starting point.

peteisace replied on Saturday, November 28, 2009

Er, I wouldn't bother with the first way, just make the second way do what you actually want.

What are the objects here? it's only Employee and Country (and the list that ties them together). You should read ALL the countries into your BusinessListBase class, irrespective of whether they have been selected or not. You will need a public settable property on your Country object, called "Selected". You don't need to model the selected country different from one that's not, it's the same object, just a slightly different state. Then just expose 2 properties on your list class: "Selected" and "Available". Implement like this:

public IEnumerable Selected
{
get { return this.Where(p => p.Selected); }
}

then bind the appropriate grid to the appropriate property on you list class. When the user hits the a button just grab the country instance and reverse the selected status:

private void CountrySelectedClick(object sender, RoutedEventArgs e)
{
Button b = (Button)sender;
Country country = (Country)sender.DataContext;
Country.Selected = !country.Selected;
}

you may need to override OnChildChanged on the list class and fire PropertyChanged events for the available / selected properties to get the grids updating in the UI cleanly.

This way you persist every country so it can do the relevant insert / delete based on it's selected status.

Easy :)

Copyright (c) Marimer LLC