[...] I want to retain collection classes for each, exposing them as properties rather than using an indexer. Does this sound right?
Yes, this sounds better than indexers, because it allows for better type safety. If you expose a normal collection, each of them can have its own element type. With an indexer, you'd be forced to choose the same collection type for all exposed collections. To illustrate this fact:
public DataTable this[string tableName] { ... }
// ^^^^^^^^^
// choose one, unspecific type for all exposed collections
vs.
public IList<Customer> Customers { get { ... } }
public IList<Supplier> Suppliers { get { ... } }
// ^^^^^^^^
// the right type for each collection
So yes, do expose single "typed" collections instead of an "untyped" collection of collections.
Side note: You say you want to allow tunnelling; some people might tell you that this violates some commonly held OO principles, such as the Single Responsibility Principle (SRP), or Separation of Concerns (SoC).
- prevent client applications from directly instantiating contained objects
public class Db
{
public IList<Customer> Customers { get { ... } }
...
}
"Contained objects" can mean two things here:
Someone can replace your whole Customers
collection. You simply prevent this by making the property read-only, ie. only provide a getter.
Someone can insert a new Customer
into your db.Customers
collection. You prevent this by choosing a (read-only) collection type for Customers
that does not allow insertion.
For example:
private List<Customer> customers = new List<Customer>();
// ^ for class-internal use only
public IList<Customer> Customers { get { return this.customers.AsReadOnly(); } }
// ^^^^^^^^^^^^^
// expose collection as read-only
- work with and expose an indefinite number of contained objects
This again can mean two things:
Your db
exposes an indefinite number of collections. Obviously, this won't work when you want each collection to be specifically typed. This only works when all your collections are exposed as having the same type (e.g. as DataTable
s, or as non-generic ICollection
s), and you will need to provide an indexer. That is, you make your many collections accessible through one collection that you expose via the indexer.
You expose a well-known and finite number of collections (e.g. Customers
, Suppliers
, etc.). Each of these, however, can then contain as many sub-objects as you want. I've assumed this scenario when I wrote the above parts of my answer. Advantages are better type safety and compile-time checking when you do tunnelling.
Concerning the last point:
db.Customers["John Smith"].BillingAddress
vs.
db.Tables["Customers"].Rows["John Smith"].Columns["BillingAddress"] as Address
In the first case, the compiler can check the types of properties such as Customers
, BillingAddress
, whereas in the second case, these properties are encoded as strings, and the compiler won't notice if you have typos in there. Also, you must indicate the correct data type yourself using type-casts.