Layered Application Design (Part 2): The Basics

While the pursuit of cake in gaming may be a poor choice, a layered cake is your best friend in software development.

In the last post, I attempted to convince you of why you should care about implementing a layered approach to application design through an example.  Hopefully you bought it.  If not, skip reading this and go knock out some of your Christmas shopping.

When we discuss layered application design, one of the first questions is: how many layers do I need and what does each layer do?  While there’s no single answer to this question, the most often-used approach is to implement a three layer design consisting of the following:

  • User Interface Layer (UI) – Provides the interface to the end user.
  • Business Logic Layer (BLL) – Provides the “brains” of the application.
  • Data Access Layer (DAL) – Provides access to reading and writing the application’s persistent data.

Naturally, each of these layers must connect to and communicate with each of the layers above/below it.  Just like a layered cake, the DAL resides at the bottom of the stack with the BLL sitting on top of it.  The BLL requests to read and write data to/from the DAL in order to carry out its tasks.  Sitting on top of the BLL is the UI layer, which provides the interfaces that interact with the end user, allow him/her to drive the application.  The UI then commands the BLL to carry out the requested actions and displays the resulting information to the user.  If we recall our earlier example, we can see how there’s logic from all three layers contained within this single block of code.

protected void addProduct_Click(object sender, EventArgs e)
{
    SqlConnection con = null;
    SqlDataReader reader = null;
    if (String.IsNullOrEmpty(this.sku.Text))
        // Display error message that SKU is required.
        return;

    if (String.IsNullOrEmpty(this.name.Text))
        // Display error message that Name is required.
        return;

    if (String.IsNullOrEmpty(this.description.Text))
        // Display error message that Description is required.
        return;

    try
    {
        con = new SqlConnection(@"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Catalog.mdf;Integrated Security=True;User Instance=True");
        con.Open();
        SqlCommand cmd = new SqlCommand();
        cmd.Connection = con;
        cmd.CommandText = "SELECT * FROM Users WHERE ID = '" + User.Identity.Name + "'";
        reader = cmd.ExecuteReader();

        bool isAdmin = false;
        if (reader.HasRows)
            if (reader.Read())
                isAdmin = reader.GetBoolean(1);

        reader.Close();

        if (isAdmin == false)
        {
            // Display an error message that this requires admin rights.
            return;
        }

        cmd.CommandText = @"INSERT INTO PRODUCTS(SKU, Name, Description) SELECT '" + this.sku.Text + "','" + this.name.Text + "', '" + this.description.Text + "'" + @" WHERE NOT EXISTS(SELECT 1 FROM PRODUCTS WHERE SKU=" + this.sku.Text + ")";
        if (cmd.ExecuteNonQuery() != 1)
            // Display an error message that the SKU already exists and cannot be added.
            return;

        // Display a confirmation message.
    }
    finally
    {
        if (reader != null)
            reader.Close();
        if (con != null)
            con.Close();
        }
    }

Our goal with layered application design is to optimize our ability to adapt to changes in requirements over time.  These changes tend to fall into one of two types: functional change requirements and environment change requirements.  Functional change requirements deal with the required feature set of the application and how it changes over time.  This is what typically drives the need to release new versions of software Microsoft Word (By now I think we can all agree that Microsoft Word has FAR more features than anyone will ever use).  In our earlier example, we saw a functional change requirement in which we needed to add a restriction preventing non-admins from adding new products to our product catalog.

Environment change requirements deal with changes to the hardware/software environment in which the application must run.  Again, referring to our earlier example, we saw an environment change requirement in which we needed to make our application switch from SQL Server to Oracle.

A good layered design provides the best defense against these types of inevitable changes.  If these types of changes AREN’T inevitable where you work, please call me as I’d like to work there because your workplace is one is a million.

Given these layer concepts and why they make our lives earlier, let’s talk about HOW they make our lives easier…

If you’ve been programming long enough, you’ve probably read books or articles discussing the basic concepts of Object Orientation.  With layered application design, we’re going to borrow from these concepts and boil them down to two:

  • Abstraction – Making sure each layer knows as little as possible about the internals of the layers with which it communicates.
  • Don’t Repeat Yourself (DRY) – Avoiding the duplication of logic.

In our example, we can see that these concepts are not present.  Because there is no layering, there is no opportunity for abstraction.  The UI layer knows everything…UI, business logic and data access.  In a layered design with good abstraction, the UI layer and business logic layer wouldn’t know anything about how the data is stored and what technology/product (SQL Server, Oracle, etc.) is being used.

We can also see that our example doesn’t follow the DRY concept either.  Although we don’t see any duplication in this snippet, we can see that everywhere where our application is reading/writing data, it is likely creating a SQLConnection and passing in our connection string…major duplication!

Let’s apply some layered design and dance this mess around to see what we can come up with…

protected void addProduct_Click(object sender, EventArgs e)
{
    try
    {
        UserManager userManager = new UserManager();
        User user = userManager.GetUser(User.Identity.Name);
        if (user.IsAdmin == false)
        {
            // Display an error message.
            // ...
            return;
        }

        ProductManager productManage = new ProductManager();
        productManage.AddProduct(this.sku.Text, this.name.Text, this.description.Text);

        // Display a confirmation that the product was added.
        // ...

        return;
    }
    catch (Exception ex)
    {
        // Display an error message.
        // ...
        return;
    }
}

Ah, this feels better.  So what’s different about this code?  Well, for one, there’s less of it, which makes it easier to read.  Secondly, it’s easier to understand what the application is trying to accomplish:

  1. See if the user requesting to add a product is an admin and prevent this action is s/he is not.
  2. Add the product.
  3. Display a happy message that the product has been added.

The reason that this feels better is because this code has implemented abstraction and the DRY concept to create a layered design.  We’ve introduced abstraction with the new UserManager and ProductManager classes and we’ve introduced DRY by eliminating the need to repeat our database connection string throughout the application (see, it’s gone!).  Abstraction + DRY = Happy layered design.

But this new code raises a question: Where the heck are the UserManager and ProductManager classes and what do they do?  They’re in the new business logic layer, silly…

public class UserManager
{
    private DataRepository repository = new DataRepository();
    public User GetUser(string id)
    {
        UserDTO userDTO = repository.GetUser(id);
        return new User(userDTO.ID, userDTO.IsAdmin);
    }
}

public class ProductManager
{
    private DataRepository repository = new DataRepository();
    public Product AddProduct(string sku, string name, string description)
    {
        if (String.IsNullOrEmpty(sku))
            throw new ArgumentNullException("sku");
        if (String.IsNullOrEmpty(name))
            throw new ArgumentNullException("name");
        if (String.IsNullOrEmpty(description))
            throw new ArgumentNullException("description");

        ProductDTO productDTO = repository.AddProduct(sku, name, description);
        return new Product(productDTO.SKU, productDTO.Name, productDTO.Description);
    }
}

Admittedly, there’s not a lot of “business logic” in these business logic layer classes and methods, but I think you get the general idea.  Notice that the AddProduct method verifies that we have values for all of the product properties (SKU, Name, Description)…that’s a business rule!

Bueller But wait, the methods in these business logic layer classes are talking to a DataRepository class.  Where is that and what does it do? Anyone, anyone?  Bueller?

That’s right, it’s in the Data Access Layer!

public class DataRepository
{
    private readonly string connectionString = @"Data Source=.\SQLEXPRESS;" + @"AttachDbFilename=|DataDirectory|\Catalog.mdf;" + @"Integrated Security=True;" + @"User Instance=True";

    public UserDTO GetUser(string id)
    {
        SqlDataReader reader = null;
        SqlConnection con = null;
        try
        {
            con = new SqlConnection(this.connectionString);
            con.Open();
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = con;
            cmd.CommandText = "SELECT * FROM Users WHERE ID = '" + id + "'"; 

            reader = cmd.ExecuteReader();
            if (reader.HasRows)
            {
                if (reader.Read())
                {
                    return new UserDTO(reader.GetString(0), reader.GetBoolean(1));
                }
            }

            return null;
        }
        finally
        {
            if (reader != null)
                reader.Close();
            if (con != null)
                con.Close();
        }
    }

    public ProductDTO AddProduct(string sku, string name, string description)
    {
        SqlDataReader reader = null;
        SqlConnection con = null;
        try
        {
            con = new SqlConnection(this.connectionString);
            con.Open();
            SqlCommand cmd = new SqlCommand();
            cmd.Connection = con;
            cmd.CommandText = "INSERT INTO PRODUCTS " + "(SKU, Name, Description) SELECT '" + sku + "','" + name + "', '" + description + "'" + " WHERE NOT EXISTS(SELECT 1 FROM PRODUCTS" + "WHERE SKU=" + sku + ")";

            if (cmd.ExecuteNonQuery() != 1)
            {
                throw new Exception(String.Format("Product SKU '{0}' already exists" + " and cannot be added.", sku));
            }

            return new ProductDTO(null, name, description);
        }
        finally
        {
            if (reader != null)
                reader.Close();
            if (con != null)
                con.Close();
        }
    }
}

We now have the beginnings of a nice layered design using the concepts of abstraction and DRY.  If we’ve done a good job, this design should be much better at standing up to changes in business and environment requirements and allow us to have more time playing our Xbox.  The PITA It guy wants to change from SQL Server to Oracle?  Fine.  We only have to touch the data access layer code.  Snap!

Is this code perfect?  No.  There’s more that could be done and we’ll investigate those next time.

If you were a whiny skeptic, are you now convinced of the importance of baking a nice layered application cake yet or are you still holding out?  I’d love to hear your thoughts.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s