I'm on my way to my first OOP-for-real project in C#, and having some big
trouble with code architeture.
Well, as both SP and Dale have said, you are starting off on the wrong foot
if you are trying to design classes out of tables
I have four namespaces:
- Layout
- Info - Where I hold empty structs with business objects
- DB - Where I fill my Info objects
- Classes - Where I do all the business stuff with the return from DB
I would disagree with your 'namespaces'. In OO architecture we often talk
about layered architecture and certainly you could represent those layers in
namespaces; but you would not separate what you have put in your Info
namespace from your classes namespace. Rather try these layers:
Business
Persistence
Presentation
The Business layer should include classes that not only describe youor data
but also the related logic inside those classes. You will sometimes find
*some* classes that are 'data-only' classes; also *some* classes that are
'logic-only'; but the majority of your classes will include both data and
logic in the same place.
A snapshot from my database:
- Table Orders, Fields: ID, Date, ClientID
- Table OrdersDetails, Fields: OrderID, ProductID, Price, Quant,
Discount
So here is your first problem with designing from a database to a class; but
to be fair, you will get the same problem in reverse without the solution
that the Persistence layer should provide.
A good OPF (Object Persistence Framework) should allow you to translate
between the OO model and the RDB model, as there is what is known as an
'impedance mismatch'
If we take your Order scenario as a starting point, you should have classes
that look like this :
Order
{
ID
Client
Date
Details
Total
}
OrderDetail
{
ID
Quantity
Product
Price
Discount
Total
}
Now, if you compare this to your table definitions, you will se some
differences :
Where in a table you would talk about storing IDs of foreign keys, in
classes you should always use references to 'real objects'; so in Order you
have a Client property which holds an instance of the Client class, not a
ClientID integer property.
You will notice that there is no reference to the Order in the OrderDetail
class, even though this exists in the database. This is very important as
classes should reflect the real world, not a relational interpretation of
it.
OrderDetail instances should not be instantiable outside of the Order class
as the relationship between the two classes is said to be that of
Composition.
<side note>
There are two ways of describing containment of objects inside other
objects: Aggreagation and Composition.
Aggregation describes a scenario where an object contains a list of other
objects, but those contained objects have a life of their own outside of the
containing object; essentially the containing object has a list of
references to external objects.
Composition describes a scenario where the objects contained by another
object are owned by that object and live and die with the containing object;
this is the case with the concept of an Order.
</side note>
One way of expressing Composition in your Order class is to ensure that the
only way to access the OrderDetail items is by way of a read-only list or
perhaps even just an enumerator. Either way do not rely on exisiting classes
like ArrayList to be the type of a list, but rather create your own typesafe
lists. This can be tedious in .NET 1.1, but generics in .NET 2.0 make it a
real breeze.
Or you can just implement the list functionality in the Order class in a way
that it is impossible to create Details that are not part of the Order.
public class OrderDetail
{
private int id;
private int quantity;
private Product product;
private decimal price;
public int ID
{
get { return id; }
set { id = value; }
}
public int Quantity
{
get { return quantity; }
set { quantity = value; }
}
public Product Product
{
get { return product; }
set { product = value; }
}
public decimal Price
{
get { return price; }
set { price = value; }
}
public decimal Total
{
get { return quantity * price; }
}
}
public class Order
{
private int id;
private string number;
private Customer customer;
private DateTime date;
private ArrayList details = new ArrayList();
public int ID
{
get { return id; }
set { id = value; }
}
public string Number
{
get { return number; }
set { number = value; }
}
public Customer Customer
{
get { return customer; }
set { customer = value; }
}
public DateTime Date
{
get { return date; }
set { date = value; }
}
public OrderDetail Add()
{
OrderDetail newItem = new OrderDetail();
details.Add(newItem);
return newItem;
}
public void Remove(OrderDetail item)
{
details.Remove(item);
}
public void Remove(int idx)
{
details.RemoveAt(idx);
}
public OrderDetail this[int Idx]
{
get { return (OrderDetail) details[Idx]; }
}
public decimal Total
{
get
{
decimal total = 0.0M;
foreach(OrderDetail d in details)
total += d.Total;
return total;
}
}
}
What I need, is to compute the total of an individual OrderDetail, and of
the entire order. I create this total fields with sum in sql, the fastest
way, and it's working nice.
But... This calc is bussiness logic, and it should be in my "Classes"
namespace, isn't that right? Cause if I have some special discount to apply,
for example, I'm in trouble. But, the only way to do this logic in the
"Classes" namespace, is looping through each object of the collection I have
already created - this sucks.
What is wrong with iterating a small list ?
If you look at my example Order class, you will see how to place the logic
for calculating the Order Total. Now this can be stored in a database if you
want, but the time taken to calculate it in memory is usually less than the
time taken to read it from disk
What concept I am misleading? What should I do?
The translation between the OO ideas and the RDB ideas is the 'missing
link'. That missing link is best handled in th Persistence layer by a
hierarchy of mapping classes that allow you to translate the property layout
of classes to the column layout of tables.
Take a look at this paper on OPFs by Scott Ambler,
http://www.ambysoft.com/persistenceLayer.pdf
I have written several OPFs based on some of the ideas in this paper, with
some modifications. To cope with the difference between referencing Details
in Order and OrderID in OrderDetail, look at the concept he talks about of
Association Maps. This is the piece of magic that allows you to keep your
object model free from RDB concepts.
Also you might like to look at some of the articles on Persistence on my
website; they are written for a Delphi audience, but the principles are the
same, whatever the language
www.carterconsulting.org.uk
Joanna