Marco Dorantes' WebLog

"Computer science is no more about computers than astronomy is about telescopes" -Edsger W. Dijkstra

Designing distributed server-side Windows applications that scale very well

Designing distributed server-side Windows applications that scale very well

  • Comments 1

In my view of object-oriented design, what makes it different from other software design schools of thought is its dependency management facilities, almost all other object-oriented mechanisms have been already known and used conceptually by other design paradigms. So, if any benefit is going to be drawn from object design, then use its dependency management facilities to make your design as simple and efficient as possible. By dependency management I mean, who knows who between design elements.

Processor objects (also known as Business Services) are not part of any industry standard, as far as I know, but it is a classic Microsoft design since Windows DNA. The rationale behind Business Services component design is very well presented in Transactional COM+ by Tim Ewald, in the book Business Services are known as Processor objects. This very notion and the surrounding implications are the cornerstone of the benchmark results at the Transaction Processing Performance Council where for many years Microsoft SQL Server 2000 was top number 1 (see recent results). New and augmented design strategies should take that into account so they stand on the shoulders of giants.

The Processor pattern is not even documented as is (as far as I know) in any catalog of object-oriented design patterns. One reason could be that it does not follow object-orientation as many people tend to understand objects, nevertheless, it is an object design; another reason is that simplest implementations of Processors resemble functional decomposition, because that is precisely what it is (and typical object zealots hate functional decomposition eagerly, sadly the same zealots fall in errant architectures with distributed objects, see Errant Architectures by Martin Fowler). They hate functional decomposition because the dependency management nightmare it usually provokes, so it is important to be aware and watch out from the beginning this potential problem.

In class-based object orientation, a class represent a collection of objects sharing common structure or behavior, e.g. Customer class contain a private data structure and public operations, in the case of Processor classes they contain only operations, a name for a Processor class could be: CustomerOperation, with static operations like:

CustomerOperation.Register(CustomerInfo customer_info);
IEnumerable<CustomerInfo> CustomerOperation.FilteredListOf(string zipcode);
IEnumerable<CustomerInfo> CustomerOperation.TopBuyersInYear(int year, decimal amount);
CustomerOperation.MarkAsDeleted(string customer_id);
The point here is that processors represent relevant business operations that affect or provide the current business state out there. These processors are deployed as components which are exposed with a variety of façades and accessed via a variety of communication protocols. Processor objects are at the heart of the architectural style known as connected systems.

As is expected, each Processor has the business logic, represent a business transaction and the whole operation should be executed in the context of a transaction with all ACID properties.

An object has structure, behavior and identity. As Processors are business operations, the physical object identity (based-on memory address) is not relevant because for Processors what is important is the state of the objects not their physical identity, instead Processors rely on logical identity for objects, that logical identity must be distributed and shared so it is common that this identity is stored in a common resource, a SQL Server database is a choice for that, a common choice for distributed enterprise applications.

In contrast with the design in this other page, where the BusinessObject represent the client code, our Processors or Business Services represent the business operation that is invoked by the client code which sends a request for the invocation of that logic, and only cares about that and doesn’t care about nothing extra like transactions, factories, tables, stored procedures, or any other implementation detail of the like.

Let’s see the implementation of a Processor’s operation with local transactions in a next level of detail:

namespace BusinessServices
{

class CustomerOperation
{
      // Used by the boundary façade used by clients (e.g. Web Service layer) or by client code directly.
      // assumes a new transactional context should be created and propagated across all possible Processor or data objects
      // involved in the operation

      public static Register(CustomerInfo customer_info)
      {
            TransactionalContext tx=new TransactionalContext();
            try
            {
                  Register(tx, customer_info)
                  tx.Transaction.Commit();
            }
            catch(SqlException err)
            {
                  //proper design for operations: log and instrumentation event firing
                  tx.Transaction.Rollback();
                  throw;
            }
            finally
            {
                  tx.Connection.Close();
            }
      }

      // Used by other Processor objects or other TransactionalContext-controlling objects –like unit tests ;-)
      
      public static void Register(TransactionalContext tx, CustomerInfo customer_info)
      {
            // Here, the Processor object invokes other Processors or Data Services (aka Data Objects) in a true
            // functional decomposition way, propagating the very same TransactionalContext 
            // which encompasses the same business transaction.

            DataAccessLayer.CustomerStore.Insert(tx,customer_info);
      }
}


class CustomerInfo : IDataContainer
{
      public string CustomerID;
      public string Name;

      public void SetDataTo(IDataParameterCollection parameters)
      {
            // copy object field values as object of IDataParameterCollection
      }

      public void LoadDataFrom(IDataReader reader)
      {
            CustomerID=(string)reader["CustomerID"];
            Name=(string)reader["Name"];
      }
}

public class CustomerCollection : CollectionBase, IDataContainer
{
      public void SetDataTo(IDataParameterCollection parameters){}

      public void LoadDataFrom(IDataReader reader)
      {
            while(reader.Read())
            {
                  CustomerInfo acustomer=new CustomerInfo();
                  acustomer.LoadDataFrom(reader);
                  this.InnerList.Add(acustomer);
            }
      }
}

} // namespace BusinessServices

// DataAccessLayer hides information about the DB objects, no other part of the design knows anything about any DB object,
// that is, no other part has a dependency on any DB object
namespace DataAccessLayer
{

// This interface helps to decouple from the BusinessServices layer, avoiding dependency cycles
public interface IDataContainer
{
      void LoadDataFrom(System.Data.IDataReader reader);
      void SetDataTo(System.Data.IDataParameterCollection pars);
}

public class TransactionalContext {} //owns and manages initialization of SqlConnection and SqlTransaction objects

public static class CustomerStore
{
      public static void Insert(TransactionalContext tx, IDataContainer customer_info)
      {
            SqlCommand cmd=tx.Connection.CreateCommand();
            cmd.Transaction=tx.Transaction;
            cmd.CommandText="<stored procedure name>";
            cmd.CommandType=CommandType.StoredProcedure;
            cmd.Parameters.Add("@<paramenter_name>", <null or default value>);
            // other stored procedure parameters...

            // The IDataContainer customer_info knows its state (data), so this part is delegated to it
            customer_info.SetDataTo(cmd.Parameters);

            cmd.ExecuteNonQuery();
      }

      public static void SelectByZipCode(TransactionalContext tx, string zipcode, IDataContainer collection)
      {
            SqlCommand cmd=tx.Connection.CreateCommand();
            cmd.Transaction=tx.Transaction;
            cmd.CommandText="<stored procedure name>";
            cmd.CommandType=CommandType.StoredProcedure;
            cmd.Parameters.Add("@zipcode",zipcode);
            using(IDataReader reader=cmd.ExecuteReader())
            {
                  collection.LoadDataFrom(reader);
            }
      }
}

} // namespace DataAccessLayer
In my experience, designing distributed, server-side Windows applications that scale very well implies a pattern like the above, plus many other things of course.

With .Net Framework 2.0 and above, this detailed design must be updated with a proper use of System.Transactions namespace.
Leave a Comment
  • Please add 4 and 2 and type the answer here:
  • Post