次の方法で共有


Using Decorators For Inter-Layer Communication

While I hope that my previous post made it clear that Data Transfer Objects are not my first choice for transferring complex data between layers, I still owe my faithful reader(s) an outline of a better alternative.

One option is to define an abstraction (interface or base class) and pass objects implementing the abstraction between layers, using the Decorator design pattern to transform or enrich the implementation along the way. While I'm not saying this is the only, or even the best way, this approach at least has the benefit that it saves you from writing a lot of transformation and mapping code if your layer hierarchy is deep and you perform a fair amount of content enrichment along the way.

Using the same scenario as my former post as a scenario, we start out by defining an abstract Product class (it could also have been an interface):

 public abstract class Product
 {
     protected Product()
     {
     }
  
     public abstract int ProductId { get;}
  
     public abstract string Name { get;set;}
  
     public abstract decimal ListPrice { get;set;}
  
     public abstract decimal Discount { get;set;}
  
     public abstract int InventoryCount { get;set;}
 }

One thing I aleady like better than the DTO option is that now I can model the Product with a read-only ProductId property. Creating a root Product instance in the data access layer is easy:

 public Product ReadProduct(int productId)
{
    using (IDataReader r = this.GetProductReader(productId))
    {
        if (!r.Read())
        {
            throw new ArgumentException("No such product.", "productId");
        }
 
        return new DataProduct(r);
    }
}

The DataProduct class simply derives from Product. The constructor reads the data off the data reader and into private fields that backs the properties of the class:

 internal DataProduct(IDataReader productReader)
{
    this.productId_ = (int)productReader["ProductId"];
    this.name_ = (string)productReader["Name"];
    this.listPrice_ = (decimal)productReader["ListPrice"];
    this.discount_ = (decimal)productReader["Discount"];
    this.inventoryCount_ = (int)productReader["InventoryCount"];
}

The DataProduct class is an internal class created and returned by the data access component. It constitutes the root of a potential Decorator hierarchy. As the Product passes up through application layers, we can add more features in each layer, and instead of writing cloning or wrapper code each time, we can define a single, basic Decorator:

 public abstract class ProductDecorator : Product
{
    private Product innerProduct_;
 
    protected ProductDecorator(Product p)
        : base()
    {
        this.innerProduct_ = p;
    }
 
    public override int ProductId
    {
        get { return this.innerProduct_.ProductId; }
    }
 
    public override string Name
    {
        get { return this.innerProduct_.Name; }
        set { this.innerProduct_.Name = value; }
    }
 
    public override decimal ListPrice
    {
        get { return this.innerProduct_.ListPrice; }
        set { this.innerProduct_.ListPrice = value; }
    }
 
    public override decimal Discount
    {
        get { return this.innerProduct_.Discount; }
        set { this.innerProduct_.Discount = value; }
    }
 
    public override int InventoryCount
    {
        get { return this.innerProduct_.InventoryCount; }
        set { this.innerProduct_.InventoryCount = value; }
    }
}

The nice thing about this class is that you only have to write it once, and then you can keep reusing it to add more and more features. 

In the next layer up from the data access layer (the business logic layer), we can just consume the Product instance served by the data access component. To add features to a Product instance, we can define another abstraction:

 public abstract class BusinessProduct : ProductDecorator
 {
     protected BusinessProduct(Product p)
         : base(p)
     {
     }
  
     public abstract decimal DiscountedPrice { get;}
  
     public abstract bool InStock { get;}
 }

Creating an implementation of BusinessProduct is easy:

 public class SimpleBusinessProduct : BusinessProduct
 {
     public SimpleBusinessProduct(Product p)
         : base(p)
     {
     }
  
     public override decimal DiscountedPrice
     {
         get { return this.ListPrice - this.Discount; }
     }
  
     public override bool InStock
     {
         get { return this.InventoryCount > 0; }
     }
 }

Notice that this is the same business logic as in my former post, but instead of dealing with arbitrary 'companion' classes or predefined DTOs, this approach is simply an extension of the Product class.

Since I defined BusinessProduct as an abstract class, I can use the abstraction in the next layer (e.g. the User Inteface Process layer) without referencing any particular implementation, effectively decoupling the UIP layer from the business logic layer:

 public class ProductView : BusinessProduct
 {
     private BusinessProduct innerProduct_;
  
     public ProductView(BusinessProduct bp)
         : base(bp)
     {
         this.innerProduct_ = bp;
     }
  
     public override decimal DiscountedPrice
     {
         get { return this.innerProduct_.DiscountedPrice; }
     }
  
     public override bool InStock
     {
         get { return this.innerProduct_.InStock; }
     }
  
     public virtual Color Color
     {
         get
         {
             if (this.InStock)
             {
                 return Color.Black;
             }
             return Color.Red;
         }
     }
 }

Notice that this is simply a new Decorator that decorates BusinessProduct instead of simply decorating Product, but it still does this by leveraging the original ProductDecorator class (BusinessProduct derives from ProductDecorator).

Each time you want to extend the functionality of an abstract data object, you simple create a new abstract Decorator that inherits from a simpler Decorator. The more application layers and the more features you need to add along the way, the more you save on this approach, since you only have to write the basic plumbing code once.

While this is certainly not the only alternative to DTOs, it's at least an attractive alternative for applications with many layers and a lot of features added along the way.

Comments

  • Anonymous
    February 07, 2008
    Brilliant post. Really inspiring. I am currently working with my own DTO approach which works OK - but I am tired of writing "converters" and "wrappers" to suit my needs in higher layers. I am working across a process boundaries (WCF). Should I continue to use my DTO's there or would your approach also fit there? Best regards, Anders

  • Anonymous
    February 07, 2008
    Hi Anders Glad you liked the post :) When working across boundaries, the third tenet of service-orientation states that "services share schema and contract, not class". Even if you don't believe in the four tenets of service-orientation, there's always Fowlers first law of distributed objects: Don't distribute your objects :) It all boils down to the same result: The default approach in WCF is to use message contracts for inter-process communication. A message contract is essentially a DTO (particularly if you use Fowler's definition), so the default approach in WCF corresponds to DTOs. Unless you really understand the ramifications of sharing classes across boundaries, you should continue using message contracts/DTOs when it comes to crossing process boudaries. There's a good reason this is the default in WCF :) HTH