I’m looking for some pointers on class design for a global application.
Let’s say I have to make a class structure to manage products, and the products are sold in different countries. Some of the fields for the product will have the same value across all countries (eg. product code, ERP Description) I will call these “international” fields, and some fields will be specific to a single country (eg. Local Description), lets call these “local” fields. Of course, some “local” fields will be the same for groups of countries (es. weight : 1 kilogram / 2 pounds). Also I expect that not all countries will have values for all fields.
Which fields are “international” and which fields are “local” may change from one installation to another and I am reluctant to bake this into the design as I’m sure it will bite me later on.
So, I’m trying to figure out how to structure the objects so that I can use a product at an international level and always refer to the same “product”, but also maintain and use the local information when necessary?
Just to be clear, I’m not talking about user-locale, number or date formatting etc. The source data is coming from different database schemas (one for each country). The end product will be written in C#.
I’m wondering if anyone has experience or can point me to a pattern that would provide a good solution to this before I go and reinvent the wheel?
6
I assume you have a database of some sort. I’d create a factory class that creates the objects (e.g. Product
) from the data in the database.
Let’s say your designing the Product
class. It has some of your international fields:
public class Product
{
private string ean;
}
For internationalized things like weights I’d write some structs Weight
that allows you to set the unit (pounds, kilograms). Use one consistent unit of weight in your database (for example kilograms) and use that to create the object. Then code your ToString
to return the value localized in the specified unit. This is similar to how DateTime
can take a date/time in UTC and ‘localized’ it to the user’s timezone.
When there is no weight, use for example a weight of -1 and declare this as static readonly Weight None = new Weight(-1)
.
public class Product
{
private string ean;
private Weight weight;
private DateTime availableFrom;
}
public struct Weight
{
private static readonly Weight None = new Weight(-1);
private int weightInKG;
public Weight(int weightInKG)
{
this.weightInKG = weightInKG;
}
public WeightUnit Unit
{ get; set; }
public string ToString()
{ /* Implement */ }
}
Then, if you have pieces of text that are localized (translated) then I’d just use strings. The factory class should get the appropriate localized string from the database. If there is no such string, use null
.
public class Product
{
private string ean;
private Weight weight;
private DateTime availableFrom;
private string description;
}
Lastly, if you have information that is always used together localized (for example, specs for the product) then create a class Specs
(or a hierarchy SpecsBase
MonitorSpecs
HardDiskSpecs
if there are multiple kinds) for this. When there are none, use null
. You can share these objects among multiple products if the information is the same. Again, the factory should take care of creating it.
You can also use these objects for fields that might be international or local depending on the installation.
public class Product
{
private string ean;
private Weight weight;
private DateTime availableFrom;
private string description;
private Specs specs;
}
For all objects, override the ToString
method to return the right (localized) strings.
3
I’d just create a class with all the fields you need, then delegate instance creation to a factory. The factory will create instances of the class based on whatever locale criteria you discover is best (whether you specify the locale in the create
method or specify a locale in the factory’s constructor). The factory will hold all the logic for assembling objects from whatever tables you have. You can have different strategies for handling missing or default values (if these vary based on locale), and which fields are “localized”. This concentrates all the logic in one place, yet gives you good flexibility.
1
Define a table for products which contains
ProductCode, ERP Description
and other “constant” fields.
Define a table for properties which contains only 4 columns:
PropertyId(long),
PropertyOwnerId(long),
PropertyType(int, predefined in C# code, enumeration for instance),
PropertyValue(nvarchar, will be parsed depending on PropertyType in C# code, Object in C# code)
.
This is enough to bind the properties with thier owners. Later, if the properties change, all you have to do is insert new records to the second table.
The classes should have the same structure. This design is a little uncomfortable in sence that you’ll have to parse a property read from DB or cast a property in C# code, but it is extremely flexible.
P.S if you think that some of the “constant” fields are likely to change, insert them as properties into the second table.
2
In the end I used a Product class with generic content and a detail class accessed through indexers to provide the functionality… but I’m really not sure its the right way to do it at all.
I didn’t find a specific design pattern that explicitly defines how to do this (as per the original question). Except “Factory”, but that seems bit generic…
This is how I implemented it (code has been simplified):
public class Product
{
public string Code {get;set;}
....
private Dictionary<string, ProductDetail> LocalProduct= new Dictionary<string, ProductDetail>();
public LocalDetail this[string countryCode]
{
get
{
if (LocalProducts.ContainsKey(countryCode))
return LocalProducts[countryCode];
else
throw new ApplicationException(String.Format("Local product detail does not exist for {0}", countryCode));
}
set
{
if (LocalProducts.ContainsKey(countryCode))
throw new ApplicationException(String.Format("Local product detail already exists for {0}", countryCode));
else
LocalProducts.Add(countryCode, value);
}
}
}
public class LocalDetail
{
public string Description {get; set;}
....
}
The calling code is something like this :
Product p = new Product("123456");
LocalDetail UKDetail = new LocalDetail();
UKDetail.Description = "Description in English";
p["UK"] = UKDetail;
System.Console.WriteLine("Product {0} Description in UK Detail : {1}", p.Code, p["UK"].Description);
LocalDetail ITDetail = new LocalDetail();
ITDetail.Description = "Descrizione in Italiano";
p["IT"] = ITDetail;
System.Console.WriteLine("Product {0} Description in Italian Detail : {1}", p.Code, p["IT"].Description);