Design Patterns in Practice - C# Design Pattern Essentials (2012)

C# Design Pattern Essentials (2012)

Part VI. Design Patterns in Practice

This part consists of a single chapter which shows a small, cut-down application that demonstrates example uses of certain common design patterns as described in this book. The patterns illustrated are:

· Layers

· Singleton

· Façade

· Factory

· Observer

· Strategy

29. Sample 3-Tier Application

This chapter develops a small, sample 3-tier graphical application that makes use of a selection of commonly used design patterns. The application displays a list of engines that have come off the Foobar Motor Company production line (this means that the list may show the same engine type and size more than once), and provides facilities to build new engines and to save & restore the data to persistent storage. Please note that this is not intended to be production level software; the code has been greatly simplified in order to concentrate on the patterns involved.

The finished application will look as follows:

Figure 29.1 : Manage Engines form

Each time the Build Engine button is clicked a dialogue will appear enabling you to create a new engine of your chosen type and size to be added to the list:

Figure 29.2 : Build Engine dialogue

The Save button will store the listed data to a file on your disk and the Restore button will replace the listed data with the values of the most recent save.

The application will be designed using a 3-tier architecture using the Layers pattern comprising a user interface layer, a business model layer, and a database layer. The classes for each layer will be stored in the namespaces Business and Database with the root namespace for the user interface classes. Each layer communicates only with the layer one level below it, as shown in the following figure:

Figure 29.3 : 3-tier Layers pattern as namepaces

The database tier

Starting with the Database namespace, we will refer to an object as an Entity. Since it is common for database tables to have a primary key we will define the class EntityKeyGenerator using the Singleton pattern:

namespace Database
{
[Serializable]

class EntityKeyGenerator

{
// static members

private static volatile EntityKeyGenerator instance;

public static EntityKeyGenerator Instance

{

get

{

if (instance == null)

{

instance = new EntityKeyGenerator();

}

return instance;

}

}

// instance variables
private int nextKey;

// private constructor

private EntityKeyGenerator()

{

}

// instance methods
virtual int NextKey

{
return ++nextKey;
}

}
}

To generate the next unique key for any IEngine entity just needs a call to:

EntityKeyGenerator.Instance.NextKey;

We will now define a simple EntityTable class that can store a dictionary of objects keyed by a sequential numeric id:

namespace Database

{
[Serializable]
public class EntityTable

{
private EntityKeyGenerator keyGenerator;
private IDictionary<int?, object> entities;
[field:NonSerialized] public event EventHandler EntityTableItemAdded;
[field:NonSerialized] public event EventHandler EntityTableRestored;

internal EntityTable(EntityKeyGenerator keyGenerator)

{
this.keyGenerator = keyGenerator;
entities = new Dictionary<int?, object>();

}

internal virtual object GetByKey(int? key)

{
return entities[key];
}

internal virtual ICollection<object> All()

{
return entities.Values;
}

internal virtual int? AddEntity(object value)

{
int? key = keyGenerator.NextKey;
entities[key] = value;

EntityAddedEventArgs args = new EntityAddedEventArgs();

args.objectAdded = value;

if (EntityTableItemAdded != null)

{

EntityTableItemAdded(this, args);

}
return key;
}

internal virtual void Restore(EntityTable restoredTable)

{
entities.Clear();

foreach (KeyValuePair<int?, object> pair in restoredTable.entities)

{

entities.Add(pair.Key, pair.Value);

}
}

// Nested class for entity added eventargs
public class EntityAddedEventArgs : EventArgs

{
public object objectAdded;
}

}
}

Note the following:

· The constructor requires an EntityKeyGenerator object which it can use to generate the next key for this particular entity type;

· The entities are stored in a dictionary keyed by an integer (to represent the primary key) and where the value will be an object. This allows the class to store any object type and therefore promotes loose-coupling;

· Methods are provided to return all entities or just one if the key is provided;

· The AddEntity() method generates the next primary key which it returns, after adding the entity to the dictionary;

· The Restore() method replaces the data with that provided in the argument;

Saving the data to disk will be accomplished either by object serialization or by creating a CSV formatted text file. This suggests the Strategy pattern so that either approach can easily be switched in. The AbstractEntityPersistenceStrategy class provides the base class to define this:

namespace Database

{

abstract class AbstractEntityPersistenceStrategy

{

internal virtual string GetFileName(EntityTable table)

{

return table.GetType().Name;

}

internal abstract string FileSuffix { get; }

internal abstract void Save(EntityTable table);

internal abstract EntityTable Restore(EntityTable table);

}

}

The EntitySerializationStrategy class extends the above to implement the required methods:

namespace Database

{

class EntitySerializationStrategy : AbstractEntityPersistenceStrategy

{

internal override string FileSuffix

{

get

{

return ".ser";

}

}

internal override void Save(EntityTable table)

{

Stream stream = File.Open(GetFileName(table) + FileSuffix, FileMode.Create);

BinaryFormatter formatter = new BinaryFormatter();

formatter.Serialize(stream, table);

stream.Close();

}

internal override EntityTable Restore(EntityTable table)

{

Stream stream = File.Open(GetFileName(table) + FileSuffix, FileMode.Open);

BinaryFormatter formatter = new BinaryFormatter();

EntityTable restoredTable = (EntityTable)formatter.Deserialize(stream);

stream.Close();

return restoredTable;

}

}

}

The EntityCSVStrategy class likewise could be coded to use a CSV formatted file, although the code is omitted here:

namespace Database

{

class EntityCSVStrategy : AbstractEntityPersistenceStrategy

{

internal override string FileSuffix

{

get

{

return ".csv";

}

}

internal override void Save(EntityTable table)

{

// code to save table data in CSV format (omitted)

}

internal override EntityTable Restore(EntityTable table)

{

// code to restore table data from CSV format (omitted)

return table;

}

}

}

In order to simplify the job of any namepace that needs to make use of the database (which will be the Business namepace in our case) there will be only a single point of access to all database functionality. This will provide a high-level view of the database which hides the internal structure and so also promotes loose-coupling. The Facade pattern used in conjunction with the Singleton pattern provides a means of defining a single point of access, as shown in the DatabaseFacade enum below:

namespace Database

{

[Serializable]

public class DatabaseFacade

{

// static members

private static volatile DatabaseFacade instance;

public static DatabaseFacade Instance

{

get

{

if (instance == null)

{

instance = new DatabaseFacade();

}

return instance;

}

}

// instance variables

private EntityTable engines;

private AbstractEntityPersistenceStrategy persistenceStrategy;

public event EventHandler EngineAdded;

public event EventHandler EnginesRestored;

// private constructor

private DatabaseFacade()

{

engines = new EntityTable(EntityKeyGenerator.Instance);

engines.EntityTableItemAdded += HandleEngineAdded;

engines.EntityTableRestored += HandleEnginesRestored;

// Set which persistence strategy to use

// (maybe get from configuration settings somewhere)

persistenceStrategy = new EntitySerializationStrategy();

}

// instance methods

public virtual object[] AllEngines

{

get

{

return engines.All.ToArray();

}

}

public virtual object GetEngine(int? key)

{

return engines.GetByKey(key);

}

public virtual int? AddEngine(object engine)

{

return engines.AddEntity(engine);

}

public virtual void SaveEngines()

{

persistenceStrategy.Save(engines);

}

public virtual void RestoreEngines()

{

EntityTable restoredEngines = persistenceStrategy.Restore(engines);

engines.Restore(restoredEngines);

}

public void HandleEngineAdded(Object sender, EventArgs args)

{

if (EngineAdded != null)

{

EngineAdded(sender, args);

}

}

public void HandleEnginesRestored(Object sender, EventArgs args)

{

if (EnginesRestored != null)

{

EnginesRestored(sender, args);

}

}

}

}

Note the following:

· The class is a Singleton, since the calling namepace should only use one Facade object;

· The class holds the EntityTable object to store the engines and methods to get all or one of them, as well as adding a new engine. If your system also managed vehicles then there would be equivalent variables and methods for this, too;

· The serialization persistence strategy is assumed, but you can see how easy it would be to use alternative strategies;

The layer diagram can now be shown with the classes of the Database namespace included:

Figure 29.4 : Database namepace with Facade class

Note how the business tier communicates only through the DatabaseFacade object. This has the effect of hiding the Database namepace complexity behind the facade.

The business tier

Moving on to the Business namepace, this will consist primarily of the IEngine hierarchy as used throughout this book:

Figure 29.5 : IEngine hierarchy

In order to facilitate engine objects being serialised, the class definition of AbstractEngine needs to specify that objects are serializable:

namespace Business

{

[Serializable]

public abstract class AbstractEngine : IEngine

{

... remainder of class omitted ...

The other classes are unchanged, except that they of course now reside in a namepace called Business.

Because there are two types of engine that can exist (standard and turbo), it will be useful to create a Factory class that creates objects of the correct type depending upon the supplied arguments. To this end, define a new class EngineFactory in the Business namespace:

namespace Business

{

class EngineFactory

{

public enum Type

{

Standard,

Turbo

}

public static IEngine Create(EngineFactory.Type type, int size)

{

if (type == Type.Standard)

{

return new StandardEngine(size);

}

else

{

return new TurboEngine(size);

}

}

public static IEngine Create(int size, bool turbo)

{

return EngineFactory.Create(turbo ? Type.Turbo : Type.Standard, size);

}

private EngineFactory()

{

}

}

}

Note how the Create() method is static and is overloaded so that client objects can either supply the enum Type value or a boolean.

Just as was done for the database layer, the Business namepace will have its own facade object, in this case the BusinessFacade singleton:

namespace Business

{

[Serializable]

public class BusinessFacade

{

// static members

private static volatile BusinessFacade instance;

public static BusinessFacade Instance

{

get

{

if (instance == null)

{

instance = new BusinessFacade();

}

return instance;

}

}

// instance variables

public event EventHandler EngineAdded;

public event EventHandler EnginesRestored;

// private constructor

private BusinessFacade()

{

DatabaseFacade.Instance.EngineAdded += HandleEngineAdded;

DatabaseFacade.Instance.EnginesRestored += HandleEnginesRestored;

}

// instance methods

public virtual string[] EngineTypes

{

get

{

return Enum.GetNames(typeof(EngineFactory.Type));

}

}

public virtual object[] AllEngines

{

get

{

return DatabaseFacade.Instance.AllEngines;

}

}

public virtual object AddEngine(object type, int size)

{

EngineFactory.Type engineType;

if (type is string)

{

engineType = (EngineFactory.Type)Enum.Parse(typeof(EngineFactory.Type), (string)type);

}

else

{

engineType = (EngineFactory.Type)type;

}

IEngine engine = EngineFactory.Create(size, (engineType == EngineFactory.Type.Turbo));

DatabaseFacade.Instance.AddEngine(engine);

return engine;

}

public virtual void SaveEngines()

{

DatabaseFacade.Instance.SaveEngines();

}

public virtual void RestoreEngines()

{

DatabaseFacade.Instance.RestoreEngines();

}

public void HandleEngineAdded(Object sender, EventArgs args)

{

if (EngineAdded != null)

{

EngineAdded(sender, args);

}

}

public void HandleEnginesRestored(Object sender, EventArgs args)

{

if (EnginesRestored != null)

{

EnginesRestored(sender, args);

}

}

}

}

Note the following:

· The methods delegate to the appropriate DatabaseFacade methods;

· The AllEngines getter and AddEngine() method return type is object rather than IEngine. This means that the Business layer will be loosely-coupled with the user interface layer so that the latter does not depend upon the former's implementation details. The user interface can make use of theToString() method of the object class to obtain the information to show in its list.

The layer diagram can now be shown with the classes of the Business namepace included:

Figure 29.6 : Business namepace with Facade class

Note how the user interface communicates only through the BusinessFacade object. This has the effect of hiding the business layer's complexity behind the facade.

The user interface tier

The user interface layer includes a ManageEngines class which shows a scrollable list of engines and some buttons:

public partial class ManageEngines : Form

{

public ManageEngines()

{

InitializeComponent();

BusinessFacade.Instance.EngineAdded += HandleEngineAdded;

BusinessFacade.Instance.EnginesRestored += HandleEnginesRestored;

}

private void ManageEngines_Load(object sender, EventArgs e)

{

// Create some sample data (will be added to list through events)

BusinessFacade.Instance.AddEngine(EngineFactory.Type.Standard, 1300);

BusinessFacade.Instance.AddEngine(EngineFactory.Type.Standard, 1600);

BusinessFacade.Instance.AddEngine(EngineFactory.Type.Standard, 2000);

BusinessFacade.Instance.AddEngine(EngineFactory.Type.Turbo, 2500);

}

private void buildEngineButton_Click(object sender, EventArgs e)

{

Form buildEngineForm = new BuildEngineForm();

buildEngineForm.ShowDialog(this);

}

public void HandleEngineAdded(Object sender, EventArgs args)

{

EntityTable.EntityAddedEventArgs entityArgs = (EntityTable.EntityAddedEventArgs)args;

enginesList.Items.Add(entityArgs.objectAdded.ToString());

}

public void HandleEnginesRestored(Object sender, EventArgs args)

{

enginesList.Clear();

foreach (object engine in BusinessFacade.Instance.AllEngines)

{

enginesList.Items.Add(engine.ToString());

}

}

private void saveButton_Click(object sender, EventArgs e)

{

BusinessFacade.Instance.SaveEngines();

}

private void restoreButton_Click(object sender, EventArgs e)

{

BusinessFacade.Instance.RestoreEngines();

}

}

The Build Engine button creates and displays a BuildEngineForm object, which is as follows:

public partial class BuildEngineForm : Form

{

public BuildEngineForm()

{

InitializeComponent();

}

private void BuildEngineForm_Load(object sender, EventArgs e)

{

// Load engine types combo

object[] engineTypes = BusinessFacade.Instance.EngineTypes;

foreach (object obj in engineTypes)

{

typeCombo.Items.Add(obj.ToString());

}

typeCombo.SelectedIndex = 0;

// Load engine size combo

sizeCombo.Items.Add(1300);

sizeCombo.Items.Add(1600);

sizeCombo.Items.Add(2000);

sizeCombo.Items.Add(2500);

sizeCombo.SelectedIndex = 0;

}

private void cancelButton_Click(object sender, EventArgs e)

{

this.Close();

}

private void okButton_Click(object sender, EventArgs e)

{

BusinessFacade.Instance.AddEngine(typeCombo.SelectedItem, (int)sizeCombo.SelectedItem);

this.Close();

}

}

The OK button invokes the appropriate BusinessFacade method to add an engine with your selected criteria. On the main list panel the Save and Restore buttons also make appropriate calls to the BusinessFacade object methods so that the currently displayed data can be saved or restored.