ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Better, Faster, Lighter Programming in .NET and Java
Pages: 1, 2, 3

Principle 3. Strive for Transparency

The heart of any enterprise application is a domain model that represents the actual business problem that your customers face. It is this domain model that provides the actual value to your customers; it is what is unique about their business and your software. Everything else is just a tool in service to the business model. Things like transactional support, persistence, messaging, security -- all of those things are generic services that exist in many applications.



In order to keep the business model light, simple, and easy to maintain, you want to keep it as uncluttered with ancillary code as possible. The services provided to the domain model should be transparent, meaning that you shouldn't have to add anything to your domain objects to get the benefit of those services. At the very least, you shouldn't have to add anything to them that you can't safely ignore when you need to, such as at unit-test time.

Take, for example, an invoice processing system. When a user wants to edit an existing invoice, they will generally choose that invoice, modify some fields on a form (Web- or Win-) and submit the changes. A very non-transparent web application might look like this (showing only the code-behind, not the .aspx file):

public class EditInvoice : Page 
{
    // series of fields with invoice data
    protected TextBox invoiceNumber;
    protected TextBox invoiceAmount;
    protected DropDown invoiceCurrency;
    // etc.

    public btnSubmit_OnClick(object sender, EventArgs e)
    {
        try
        {
            // fetch invoice data from database
            SqlConnection conn = new SqlConnection(CONNSTRING);
            SqlCommand comm = new SqlCommand("select * from 
invoice where invoiceNumber = " + invoiceNumber.Text, conn);
            SqlDataAdapter da = new SqlDataAdapter(comm);
            DataSet ds = new DataSet();
            conn.open();
            da.fill(ds);
        } catch (Exception ex) {
            // log the exception 
        } finally {
            conn.close();
        }

        // perform business logic
        Double amount = Convert.ToDouble(invoiceAmount.Text);
        if(amount > 10000) throw new InvoiceAmountTooHighException();

        // edit the data
        DataTable t = ds.Tables[0];
        DataRow r = t.rows[0];
        // etc.

        // update the database
        try 
        {	
            conn.open();
            da.update(ds);
        } catch (Exception ex2) {
            // log exception 
        } finally {
            conn.close();
        }
    }
}

This is the worst kind of code imaginable. Your domain model (the invoice itself and the business rules applying to it) is embedded in a file that is responsible for displaying the data, as well as for persisting it. When things go wrong, or the model needs to change, it will be extremely difficult to find exactly where to go, and changes are likely to have ripple effects all over your code. Not to mention that unit testing something like this is a nightmare: you have to mock up a browser, and trace all the way from display to the database to see if your business logic (trapping for amounts < 10000) is working correctly.

A better strategy would be to create an official domain object for your business code, and separate everything else into transparent layers around it. We'll start with a new Invoice class:

public class Invoice
{
    // private data 
    private long invoiceNumber;
    private double invoiceAmount;
    private Currency invoiceCurrency;
    // etc.  

    // public properties
    public double Amount
    {
        get { return invoiceAmount; }
        set 	
        {
            if (value > 10000)
            throw new InvoiceAmountTooHighException();
            invoiceAmount = value;
        }
    }
    // etc.
}

This domain class doesn't know anything about how it will be displayed to the user or how it will be persisted to the database. It is entirely focused on representing the code business abstraction invoice, and the rules that apply to its data. Nothing else clutters the code.

We will separate out the other two portions of our example, the view and the persistence, into their own classes. We'll make a class called PersistenceManager, which we'll come back to in a minute, but for now, assume it has a method called saveInvoice that takes a single instance of Invoice as a parameter.

The display class can then focus on managing the display of information from the invoice:

public class EditInvoice : Page 
{
    private Invoice invoice;
    private PersistenceManager persistenceManager;

    // series of fields with invoice data
    protected TextBox invoiceNumber;
    protected TextBox invoiceAmount;
    protected DropDown invoiceCurrency;
    // etc.

    public btnSubmit_OnClick(object sender, EventArgs e)
    {
        try 
        {
            invoice.setAmount(
            Convert.ToDouble(invoiceAmount.Text));
        } catch (Exception ex) {
            // notify user of exception
        }

        try 
        {
            persistenceManager.saveInvoice(invoice);
        } catch (Exception ex) {
            // notify user of data exception
        }
    }
}

Now, the presentation layer is totally transparent to your business model. If your business model needs to change, there is a single codebase to look at, and not a lot of extraneous code to get in the way. Changes to the model do not necessarily need to affect the presentation layer, and vice versa.

The persistence layer is the same way. By separating out your persistence logic into another class (PersistenceManager) you abstract away the common, non-business- specific functionality of interacting with a database to a transparent part of the code. To implement PersistenceManager, you have a variety of choices. You can implement the transactional script pattern, which is essentially what we did in the first page above. That is to say, you use the native data interaction objects provided by .NET (DataSet, SqlDataAdapter, etc.) to manually pull values out of the Invoice instance and modify the database, manually controlling any transactional semantics yourself.

On the other hand, you could use an object/relational (O/R) mapping framework to handle the work for you. O/R mappers are frameworks that, given a mapping file that relates class definitions with database schema, can auto-generate the SQL scripts needed to perform persistence activities. These kinds of frameworks simplify your code greatly by eliminating all of the hard-coded SQL scripts used by the transactional script pattern, and free you to work in terms of your domain model only. Currently, the major O/R mapping framework available to .NET developers is NHibernate, a .NET port of the popular Java Hibernate framework. NHibernate is an open source project found at nhibernate.sourceforge.net. Microsoft has its own O/R mapping technology called ObjectSpaces that is going to be released with Longhorn and/or Whidbey, but unless you are a beta tester for Microsoft, you can only get NHibernate at present.

NHibernate makes the persistence effort totally transparent to your code. You start by providing mapping entries for your domain model. You create one mapping file per persistent class, name it classname.hbm.xml, and put it next to your .cs file. The mapping file for Invoice (invoice.hbm.xml) might look like this:

<?xml version="1.0" encoding="utf-8" ?> 

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0">

    <class name="com.relevance.transparency.Invoice, 
com.relevance.transparency" table="invoices">
                
        <id name="invoiceId" column="ID" type="int">
            <generator class="assigned" />
        </id>
                
        <property name="Amount" column="amount" type="double"/>
        <property name="Currency" type="char(3)"/>
        <!-- etc. -->
    </class>
        
</hibernate-mapping>

When mapping properties to columns, be sure to use the name of the public property, not the private field. If the property isn't publicly accessible, NHibernate won't be able to access the values.

Finally, you need to configure NHibernate itself so it can find the datasource used for persisting all these classes. You can add that configuration information to your application's .config file, like so:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
        <section name="nhibernate" 
type="System.Configuration.NameValueSectionHandler, System, 
Version=1.0.3300.0,Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </configSections>
        
    <nhibernate>
        <add 
          key="hibernate.connection.provider"          
          value="NHibernate.Connection.DriverConnectionProvider" 
          />
        <add 
          key="hibernate.dialect"                      
          value="NHibernate.Dialect.MsSql2000Dialect" 
          />
        <add 
          key="hibernate.connection.driver_class"          
          value="NHibernate.Driver.SqlClientDriver" 
          />
        <add 
          key="hibernate.connection.connection_string" 
          value="Server=dataserver;initial 		 
          catalog=transparency;User ID=webuser;Password=webpwd" 
          />
    </nhibernate>
</configuration>

The last step is to implement the saveInvoice method of PersistenceManager. The PersistenceManager class must reference the nhibernate assembly. Then, the class looks like this:

public class PersistenceManager
{
    private Configuration config;
    private IsessionFactory factory;

    public PersistenceManager()
    {
        Configuration config = new Configuration();
        config.addAssembly("com.relevance.transparency");
        IsessionFactory factory = config.BuildSessionFactory();
    }

    public void saveInvoice(Invoice invoice)
    {
        ISession session = factory.OpenSession();
        ITransaction tx = session.BeginTransaction();
        session.save(invoice);
        tx.Commit();
        session.Close();
    }
}

Furthermore, adding a Load method to the PersistenceManager is just as easy.

public Invoice loadInvoice(string invoiceId)
{
    ISession session = factory.OpenSession();
    Invoice result = (Invoice)session.Load(typeof(Invoice),invoiceId);
    session.Close();
    return result;
}

By employing an O/R mapper, you can make persistence totally transparent to the business model and can take the extra step of simplifying your code by eliminating hand- coded SQL scripts. The drawback to a scheme like this is that it is next to impossible to tune the SQL scripts, since they are auto-generated. If your application is dependent on highly customized and optimized SQL scripts, then you will want to either stick with the transactional script pattern, or use a modified NHibernate implementation where you pass the queries in manually using HSQL dialect (which is outside of the scope of this article).

Pages: 1, 2, 3

Next Pagearrow