views:

160

answers:

3

Hi Everyone,

How to design classes involving collections of other classes?

General Example:

A Workspace contains number of Projects .
A Project contains large number of Resources .
Each Resource may contain large number of Files.

So here the classes identified can be Workspace,Project,Resource and File. Workspace will have list of Project.Project will have list of Resources and Resource will have list of Files. Of course each class has its related settings.

Now the basic questions are :
a) Who creates and adds a class to a particular collection? Another class or the class containing the collection?
b) Also how to keep track of a particular collection and how to store same?
c) Who audits changes to a particular collection?
d) What are the different design patterns that could be applied in such situations?

Basically I want to reduce the coupling between the different classes .

Thanks Everyone

+5  A: 

There are many kinds of relationships - consider

  • Automobile and Wheels
  • Automobile and Driver
  • Automobile and Registered Owner
  • Customer and Order and Order Line
  • School and Class and instance of Class

If you look at UML modelling you'll see concepts such as Cardinality and Direction and distictions between Aggegration and Composition and questions relating to the life-cycle of the related objects.

It's then unsurprising that we need a range of techniques and patterns to deal with different kinds of relationships.

Concerning d). There's one overriding principle Law of Demeter or principle of least knowledge.

One important technique is then, Encapsulation decrease coupling by hiding information. Automobiles probably have little interest in many details of people, so we might have a IDriver interface on our Person class, IDriver offers the particular methods that Automobile cares about. The general principle being to favour programming to interfaces.

Following that through, we can think about a). Creation. As we're tending to use Interfaces, it often makes sense to use Factory patterns. That does leave the question of who calls the factory. Do we prefer:

   IPerson aPerson = myAutomobile.createDriver( /* params */);  

or

  IPerson aPerson = aPersonFactory.create( /* params */);
  myAutomobile.addDriver(aPerson);

Here I think it's pretty clear that Automobiles don't know much about people, and therefore the second is better division of responsibilities. However maybe Orders could reasonably create OrderLines, Classes create ClassInstances?

b). Keeping track? That's why we have rich sets of Collection classes. Which ones to use depend upon the nature of the relationship (one-one, one-many; etc.) and how we use it. So we pick Arrays and HashMaps etc. according to need. For a Car/Wheel we might even use names attributes of the Car - after all a Car has exactly six wheels (frontLeft, frontRight, backLeft, backRight, spare and steering). If by "store" you mean persist, then we're looking at techniques such foreign keys in a relational database. Mapping between RDBMS and in-memory objects is increasingly managed by nice persistence mechanisms such as JPA.

c). Audit? I've not seen auditing applied specifically at a relationship level. Clearly the automobile.addDriver() method may be arbitrarily complex. If there's a business requirement to audit this action, then it's pretty clear that this a decent place to do it. This is just a standard OO design question revolving around who owns the information. General principle: "Do Not Repeat Yourself" so pretty clearly we don't want every object that calls addDriver() to need to remember to audit, hence it's Auto's job.

djna
+1  A: 

Creating good OO design is a bit of an artform, but there are some principals to keep in mind.

Think about how your users will use your interface. Who is your user? Is it just you, and for a specific purpose, or are you designing something for different clients. Interface design is hard, but think about real examples of how your users will actually want to use your interface. One good way to do this is by writing tests, which forces you to use your own stuff. Another way is to look at existing libraries that do the same thing and see how they are used.

Keep things simple, stupid. That is, keep them simple from the perspective of your callers. This applies to encapsulation, where you should only expose the internals of your implementation as needed. It applies to creating a consistent interface that is easy to understand. And it means you should avoid the trap of making an object for every conceivable class, making tons of attributes, and being as general as possible. This last point is an especially important one; what makes us good as developers is that we have the ability to speculate and abstract, but because of this, we often fall into the trap of making systems massively more complicated than they need to be.

Think about ownership. Each object will either be owned by another, meaning that it should be created and destroyed as part of that object, or it will be referenced by another. You should design your interface accordingly. Ownership is a form of coupling, but it is often the correct design. It certainly simplifies your interface, as well as the burden on your callers to maintain their own objects.

Though shalt know and use thine collection libraries. Know the collection libraries of whatever language / framework you are using, and use them. Also, try to make your own interfaces consistent with those libraries in terms of naming and behavior.

Concerning auditing. You can do some auditing (logging, keeping statistics) from within your classes, but if your auditing needs are complex (needing to know when your data changes), it might be better to use an Observer pattern.

Alex M.
+2  A: 

When designing software I find it useful to look at things from a type theoretic point of view and see where it leads me.

A WorkSpace is of type Project + Project^2 + Project^3... (meaning whatever is true of a list of projects is true of a WorkSpace)

Similarly,

A Project is of type Resource + Resource^2 + Resource^3...

A Resource is of type File + File^2 + File^3 ...

So in a language like C#† you might define you WorkSpace class thusly:

public class WorkSpace : IList<Project> //the important thing here is that you're declaring that things that are true for a list of Projects is true for a WorkSpace. The WorkSpace class may in fact do other stuff too...
{

}

and similarly for the other classes.

Then in your client code you'd use it like this:

foreach (var project in WorkSpace)
{
    //do stuff
}

or

Projects.Add(new Resource() { file1, file2, file3, file4, /* etc */});

Think about the types and their relationships first. Encapsulation is a bit of a low-level housekeeping concept. The principle there is to keep related code close together and unrelated code far apart (where far apart means separated by or behind some kind of boundary, e.g. a function or a class or whatever other boundary concept a language might offer).

†From your posting history I surmise that you are familiar with C# but this applies to other languages.

Rodrick Chapman
+1 for `WorkSpace : IList<Project>` approach. Never imagined do it this way.
MainMa
Prefer containment to inheritance here. A workspace isn't really a list of projects in the sense that you would want to be able to substitute a workspace every place you use a list of projects; and furthermore, a workspace is probably a list of lots of other stuff too, like a list of project settings, and a list of user preferences. Using containment means better encapsulation and a simpler interface for your users.
Alex M.
@nstoertz You can make WorkSpace an IList<ProjectSetting> and IList<UserPreferences> as well since you're implementing an interface (though I probably wouldn't implement those other interfaces). All you're saying is that everything that is true of a list of projects is also true of a WorkSpace but things true of a WorkSpace may not be true of a list of projects. Also, while implementation inheritance can be dangerous (and I do wish non abstract classes were sealed by default) interface inheritance is pretty safe and uncontroversial.
Rodrick Chapman
@noblethrasher: IList<T> is derived from the non-generic-IEnumerable interface. If a class implements two IList<ProjectSetting> and IList<UserPreferences>, what should the non-generic IEnumerable-implementation do? Either way, it's not what a client of IList<ProjectSetting> or IList<UserPreferences> would expect.
nikie
@nikie You would implement them as explicit interfaces but, like I said, I personally would not have the class implement those interfaces.
Rodrick Chapman