Improve Your Model Classes with OOP – Part 1: The Basics

Ever since I have been speaking and writing, I have talked about proper class design using Object-Oriented Programming. OOP has been around since the 1950s and to me is still the best way to properly design classes, for now, and the future. Many of the projects I see fail are due to not using OOP or doing it wrong. I still see senior-level developers not implementing OOP properly.

Now, I am seeing beginner developers using poor practices by others as a guide and they are learning OOP wrong. So, I want to write a series of articles on how to implement classes with proper OOP. For the first series, I’m going to focus on business classes or what most developers now call, model classes. These types of classes are widely used, especially in ASP.NET Model-View-Controller websites. There is more to OOP than just model classes, but I will tackle that in a different series of articles. So, check back often for a new article (usually every two weeks).

Poor Class Design

I have many examples of poor class design, but the worse one that I can remember is the one below that I use conference sessions from a real, in production project. This is what it looks like.

public class OrderData
{
    public string ORDER;
    public string facilityID = "";
    public string OrderNumber = "";
    public string openClosed = "";
    public string transType = "";
    public string dateOpened = "";
    public string dateClosed = "";
    public string dateShop = "";

    public OrderData(string _ORDER)
    {
        this.ORDER = _ORDER;
    }

    //Remainder of code removed for brevity
}

The real class had 198 public string fields to hold the data. They used no other types, which is just bad! Let’s go over the main issues.

  1. All the data for the class was held in those 198 public fields. This completely breaks encapsulation, the first pillar of any OOP class design (discussed below).
  2. All the fields are strings! The proper type for the data should always be used like DateTimeOffset, Integer, etc.
  3. Poor coding standards. The standard even changes from field to field!
  4. No documentation.

Number 1 & 2, of course, are the most important, but so are the other two.

Encapsulation – The 1st Pillar of OOP

There are three main pillars of OOP and they are encapsulation, inheritance, and polymorphism. If you are new to OOP, and so we are all on the same page, this is the definition of it on Wikipedia:

Object-oriented programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain data, in the form of fields (often known as attributes), and code, in the form of procedures (often known as methods). A feature of objects is an object’s procedures that can access and often modify the data fields of the object with which they are associated (objects have a notion of “this” or “self”). In OOP, computer programs are designed by making them out of objects that interact with one another. OOP languages are diverse, but the most popular ones are class-based, meaning that objects are instances of classes, which also determine their types.

In this series of articles about model classes, I will mostly just focus on encapsulation. Wikipedia then defines encapsulation as this:

Encapsulation is an object-oriented programming concept that binds together the data and functions that manipulate the data, and that keeps both safe from outside interference and misuse. Data encapsulation led to the important OOP concept of data hiding.

If a class does not allow calling code to access internal object data and permits access through methods only, this is a strong form of abstraction or information hiding known as encapsulation. Some languages (Java, for example) let classes enforce access restrictions explicitly, for example denoting internal data with the private keyword and designating methods intended for use by code outside the class with the public keyword. Encapsulation prevents external code from being concerned with the internal workings of an object. This facilitates code refactoring, for example allowing the author of the class to change how objects of that class represent their data internally without changing any external. It also encourages programmers to put all the code that is concerned with a certain set of data in the same class, which organizes it for easy comprehension by other programmers. Encapsulation is a technique that encourages decoupling.

Since encapsulation is the first pillar of OOP, if that isn’t done correctly, then to me, good OOP design is not being practiced. And I always say if you aren’t practicing good OOP design, you will build a house of cards, and they all eventually fall.

So, if we get rid of the fields, and replace them with properties or auto-properties like this:

public string FacilityID { get; set; }

Is this good OOP design? The short answer is no.

Data Hiding

By practicing good OOP design, it’s your job as the developer to properly implement encapsulation, which is all about data hiding. The only way that code outside of the class or type can get or set the data is to go through methods and properties. For the rest of the articles, I will be using an example type called Person (as seen below) and you will see how it changes as we go along.

public class Person
{
    public DateTime BornOn { get; set; }
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string CellPhone { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string HomePhone { get; set; }
    public string Id { get; set; }
    public string LastName { get; set; }
    public string PostalCode { get; set; }
}

This example class closely follows the way that I see how over 90% of model classes being designed. But this still is not proper OOP design since there isn’t any data validation. The whole purpose of encapsulation is not only hiding the data but making sure it’s correct in the first place! Never allow bad data into the class, ever! I always say, “Bad data in, bad data out!”. If that bad data gets into the database, then it’s very difficult and very costly to fix.

Unless your model classes will take any length of string, or incorrect date, incorrect number values, etc. (and I’m sure they shouldn’t do this), then you need to validate it! Never rely on the database to validate your data since that would be a big performance issue. Also, new document-based databases like Cosmos DB do not have typed columns, so there isn’t really a way to do it unless you want to write a lot of back-end scripts.

Validating the Data

Unless your code really does not care about the value of the data (for example I won’t do this for Boolean values), then the first lines of any property or method must validate the data. Here is how I would write the Address1 property in Person.

public string Address1
{
  get
  {
    return _address1;
  }
  set
  {
    // 1. Validate that the current & new value aren't the same.
    if (_address1 == value)
    {
      return;
    }

    // 2. Validate that value isn't null
    if (string.IsNullOrEmpty(value))
    {
      throw new ArgumentNullException(nameof(Address1),
        "Value for Address1 cannot be null or empty.");
    }

    // 3. Validate that the value is within range
    if (value.Length < 10 || value.Length > 256)
    {
      throw new ArgumentOutOfRangeException(nameof(Address1),
        "Address must be between 10 - 256 characters.");
    }

    _address1 = value;
  }
}

Let’s go through the validation and why it’s important.

  1. The first validation is to make sure that the new value isn’t the same as the current value. Why set the same value twice? This can be a performance issue. Also, many model classes used in apps, also throw change events when data is modified, so this is very important for those types of classes.
  2. Validate that we have a string! As you should know, a null value in a string can crash the application if code in the class calls a String method like Length. This is to make sure the string is not null or is empty.
  3. Last but not lease is the length of the string itself. Most databases set the string length size, so you need to mimic this in the code before it gets to the database. This is even important for databases like Cosmos DB since each document has a size limit. Sending any length of string could cause an error.

Of course, your validation for #3 will be different based on business rules. If you follow what I recently wrote in my Reuse, Reuse and More Code Reuse! article, these model classes should always but put in a reusable assembly (away from the database context) so they can be used by any layer of your application. That way any code that uses it will have the exact same validation… no code duplication!

The Final Person Class

There is more to talk about, but I want to wrap up this first article. Below is the final Person class with proper naming standards and documentation (both are a must in any OOP design).

// ***********************************************************************
// Assembly         : dotNetTips.OOP.Design
// Author           : David McCarter
// Created          : 07-24-2019
//
// Last Modified By : David McCarter
// Last Modified On : 07-24-2019
// ***********************************************************************
// <copyright file="PersonFixed.cs" company="dotNetTips.OOP.Design">
//     Copyright (c) McCarter Consulting. All rights reserved.
// </copyright>
// <summary>Person OOP Design for Article 1</summary>
// *********************************************************************** 
using System; 
namespace dotNetTips.OOP.Design.Models.Article1
{
    /// <summary>
    /// Class Person with proper encapsulation and validation.
    /// Implements the <see cref="Object" />
    /// </summary>
    /// <seealso cref="Object" />
    public class PersonFixed
    {
        /// <summary>
        /// The address1
        /// </summary>
        private string _address1;
 
        /// <summary>
        /// The address2
        /// </summary>
        private string _address2;
 
        /// <summary>
        /// The born on
        /// </summary>
        private DateTimeOffset _bornOn;
 
        /// <summary>
        /// The cell phone number
        /// </summary>
        private string _cellPhone;
 
        /// <summary>
        /// The city
        /// </summary>
        private string _city;
 
        /// <summary>
        /// The country
        /// </summary>
        private string _country = "USA";
 
        /// <summary>
        /// The email
        /// </summary>
        private string _email;
 
        /// <summary>
        /// The first name
        /// </summary>
        private string _firstName;
 
        /// <summary>
        /// The home phone number
        /// </summary>
        private string _homePhone;
 
        /// <summary>
        /// The unique identifier
        /// </summary>
        private string _id;
 
        /// <summary>
        /// The last name
        /// </summary>
        private string _lastName;
 
        /// <summary>
        /// The postal code
        /// </summary>
        private string _postalCode;
 
        /// <summary>
        /// Gets or sets the Address1.
        /// </summary>
        /// <value>The Address1.</value>
        /// <exception cref="ArgumentNullException">Address1 - 
        ///  Value for address cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Address1 - 
        ///  Address must be between 10 - 256 characters.</exception>
        public string Address1
        {
            get
            {
                return this._address1;
            }
 
            set
            {
                if (this._address1 == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(Address1), 
                      "Value for address cannot be null or empty.");
                }
 
                this._address1 = (value.Length < 10 || value.Length > 256) 
                  ? throw new ArgumentOutOfRangeException(nameof(Address1), 
                  "Address must be between 10 - 256 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the Address2.
        /// </summary>
        /// <value>The Address2.</value>
        /// <exception cref="ArgumentNullException">Address2 - 
        ///   Value for address cannot be null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Address1 - 
        ///   Address cannot be more than 256 characters.</exception>
        public string Address2
        {
            get
            {
                return this._address2;
            }
 
            set
            {
                if (this._address2 == value)
                {
                    return;
                }
 
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(Address2), 
                      "Value for address cannot be null.");
                }
 
                this._address2 = (value.Length > 256) ? 
                  throw new ArgumentOutOfRangeException(nameof(Address1), 
                  "Address cannot be more than 256 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the born on date.
        /// </summary>
        /// <value>The born on date.</value>
        /// <exception cref="ArgumentOutOfRangeException">BornOn - 
        ///   Person cannot be born in the future.</exception>
        public DateTimeOffset BornOn
        {
            get
            {
                return this._bornOn;
            }
 
            set
            {
                if (this._bornOn == value)
                {
                    return;
                }
 
                this._bornOn = value.ToUniversalTime() > 
                  DateTimeOffset.UtcNow ? throw new 
                  ArgumentOutOfRangeException(nameof(BornOn), 
                  "Person cannot be born in the future.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the cell phone number.
        /// </summary>
        /// <value>The cell phone number.</value>
        /// <exception cref="ArgumentNullException">CellPhone - 
        ///   Value for phone number cannot be null.</exception>
        /// <exception cref="ArgumentOutOfRangeException">CellPhone - 
        ///   Phone number is limited to 50 characters.</exception>
        public string CellPhone
        {
            get
            {
                return this._cellPhone;
            }
 
            set
            {
                if (this._cellPhone == value)
                {
                    return;
                }
 
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(CellPhone), 
                      "Value for phone number cannot be null.");
                }
 
                this._cellPhone = value.Length > 50 ? 
                  throw new ArgumentOutOfRangeException(nameof(CellPhone), 
                  "Phone number is limited to 50 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the city.
        /// </summary>
        /// <value>The city name.</value>
        /// <exception cref="ArgumentNullException">City - Value for 
        ///   City cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">City - City 
        ///   length is limited to 100 characters.</exception>
        public string City
        {
            get
            {
                return this._city;
            }
 
            set
            {
                if (this._city == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(City), 
                      "Value for City cannot be null or empty.");
                }
 
                this._city = value.Length > 100 ? 
                  throw new ArgumentOutOfRangeException(nameof(City), 
                  "City length is limited to 100 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the country.
        /// </summary>
        /// <value>The country name.</value>
        /// <exception cref="ArgumentNullException">Country - 
        ///   Value for Country cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Country - 
        ///   Country length is limited to 50 characters.</exception>
        public string Country
        {
            get
            {
                return this._country;
            }
 
            set
            {
                if (this._country == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(Country), 
                      "Value for Country cannot be null or empty.");
                }
 
                this._country = value.Length > 50 ? 
                  throw new ArgumentOutOfRangeException(nameof(Country), 
                  "Country length is limited to 50 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the email address.
        /// </summary>
        /// <value>The email address.</value>
        /// <exception cref="ArgumentNullException">Email - Value for 
        ///   Email cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Email - Email 
        ///   length is limited to 50 characters.</exception>
        public string Email
        {
            get
            {
                return this._email;
            }
 
            set
            {
                if (this._email == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(Email), 
                      "Value for Email cannot be null or empty.");
                }
 
                this._email = value.Length > 50 ? 
                  throw new ArgumentOutOfRangeException(nameof(Email), 
                  "Email length is limited to 50 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the first name.
        /// </summary>
        /// <value>The first name.</value>
        /// <exception cref="ArgumentNullException">FirstName - Value 
        ///   for name cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Email - 
        ///   First name length is limited to 50 characters.</exception>
        public string FirstName
        {
            get
            {
                return this._firstName;
            }
 
            set
            {
                if (this._firstName == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(FirstName), 
                      "Value for name cannot be null or empty.");
                }
 
                this._firstName = value.Length > 50 ? 
                  throw new ArgumentOutOfRangeException(nameof(Email), 
                  "First name length is limited to 50 characters.") 
                  : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the home phone number.
        /// </summary>
        /// <value>The home phone.</value>
        /// <exception cref="ArgumentNullException">HomePhone - Value 
        ///   for phone number cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">HomePhone - 
        ///   Home phone length is limited to 50 characters.</exception>
        public string HomePhone
        {
            get
            {
                return this._homePhone;
            }
 
            set
            {
                if (this._homePhone == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(HomePhone), 
                      "Value for phone number cannot be null or empty.");
                }
 
                this._homePhone = value.Length > 50 ? 
                  throw new ArgumentOutOfRangeException(
                  nameof(this.HomePhone), "Home phone length is limited 
                  to 50 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the unique identifier.
        /// </summary>
        /// <value>The unique identifier.</value>
        /// <exception cref="ArgumentNullException">Id - Value for 
        ///   Id cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">Id - Id 
        ///   length is limited to 256 characters.</exception>
        public string Id
        {
            get
            {
                return this._id;
            }
 
            set
            {
                if (this._id == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(Id), 
                      "Value for Id cannot be null or empty.");
                }
 
                this._id = value.Length > 256 ? 
                  throw new ArgumentOutOfRangeException(nameof(this.Id), 
                  "Id length is limited to 256 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the last name.
        /// </summary>
        /// <value>The last name.</value>
        /// <exception cref="ArgumentNullException">LastName - Value 
        ///   for name cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">LastName - 
        ///   Last name length is limited to 50 characters.</exception>
        public string LastName
        {
            get
            {
                return this._lastName;
            }
 
            set
            {
                if (this._lastName == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(LastName), 
                      "Value for name cannot be null or empty.");
                }
 
                this._lastName = value.Length > 50 ? 
                  throw new ArgumentOutOfRangeException(
                  nameof(this.LastName), "Last name length is limited to 
                  50 characters.") : value;
            }
        }
 
        /// <summary>
        /// Gets or sets the postal code.
        /// </summary>
        /// <value>The postal code.</value>
        /// <exception cref="ArgumentNullException">PostalCode - Value 
        ///   for postal code cannot be null or empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">PostalCode - 
        ///   Postal code length is limited to 20 characters.</exception>
        public string PostalCode
        {
            get
            {
                return this._postalCode;
            }
 
            set
            {
                if (this._postalCode == value)
                {
                    return;
                }
 
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException(nameof(PostalCode), 
                      "Value for postal code cannot be null or empty.");
                }
 
                this._postalCode = value.Length > 20 ? 
                  throw new ArgumentOutOfRangeException(
                  nameof(this.PostalCode), "Postal code length is 
                  limited to 20 characters.") : value;
            }
        }
    }
}

Before checking in any source code, two more things should be done.

  1. Document your class and methods! To make this easier, you can use the FREE Visual Studio extension GhostDoc from Submain.com. If you use proper naming standards, then writing this documentation is very quick and easy! GhostDoc is what I used to document the Person class (above).
  2. Run the FREE Visual Studio extension called StyleCop. StyleCop was originally written by Microsoft and they have used it ever since version 1.0 of .NET to ensure their classes all look consistent and looks like it’s all written by the same person. You should do the same in all your projects, especially if you are using contractors.

There you have it for the first article in this series. In the next article, I will show what else should be implemented for proper model class design.

Summary

There are many OOP books out there, so if you are new to it or need a refresher, please get one and read it from cover to cover a few times. OOP is something you can learn and use the rest of your programming career.

You can follow how the Person class changes over the writing of these articles by going here: https://gist.github.com/RealDotNetDave/d635ce8eab4b20ac1b8a4cededb0fb46

Do you practice good OOP design? Well, let’s see what you think at the end of this series. I’d be interested to know. Do have any tips you’d like to share? Please make a comment below.

2 thoughts on “Improve Your Model Classes with OOP – Part 1: The Basics

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

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

Google photo

You are commenting using your Google 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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.