Improve Your Model Classes With OOP - Part One - 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 1950’s 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).
 

256 Seconds with dotNetDave – Episode 6
 

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 at conference sessions from a real, in production project. This is what it looks like.
  1. public class OrderData  
  2. {  
  3.     public string ORDER;  
  4.     public string facilityID = "";  
  5.     public string OrderNumber = "";  
  6.     public string openClosed = "";  
  7.     public string transType = "";  
  8.     public string dateOpened = "";  
  9.     public string dateClosed = "";  
  10.     public string dateShop = "";  
  11.     public OrderData(string _ORDER)  
  12.     {  
  13.         this.ORDER = _ORDER;  
  14.     }  
  15.     //Remainder of code removed for brevity  
  16. }  
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. They 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:
  1. public string FacilityID { getset; }  
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.
  1. public class Person  
  2. {  
  3.     public DateTime BornOn { getset; }  
  4.     public string Address1 { getset; }  
  5.     public string Address2 { getset; }  
  6.     public string CellPhone { getset; }  
  7.     public string City { getset; }  
  8.     public string Country { getset; }  
  9.     public string Email { getset; }  
  10.     public string FirstName { getset; }  
  11.     public string HomePhone { getset; }  
  12.     public string Id { getset; }  
  13.     public string LastName { getset; }  
  14.     public string PostalCode { getset; }  
  15. }  
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.
  1. public string Address1  
  2. {  
  3.     get  
  4.     {  
  5.     return _address1;  
  6.     }  
  7.     set  
  8.     {  
  9.     // 1. Validate that the current & new value aren't the same.  
  10.     if (_address1 == value)  
  11.     {  
  12.         return;  
  13.     }  
  14.     // 2. Validate that value isn't null  
  15.     if (string.IsNullOrEmpty(value))  
  16.     {  
  17.         throw new ArgumentNullException(nameof(Address1),   
  18.                    "Value for Address1 cannot be null or empty.");  
  19.     }  
  20.     // 3. Validate that the value is within range  
  21.     if (value.Length < 10 || value.Length > 256)  
  22.     {  
  23.         throw new ArgumentOutOfRangeException(nameof(Address1),  
  24.                    "Address must be between 10 - 256 characters.");  
  25.     }  
  26.     _address1 = value;  
  27.     }  
  28. }  
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 an 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).
  1. // *******************************************************************  
  2. // Assembly         : dotNetTips.OOP.Design  
  3. // Author           : David McCarter  
  4. // Created          : 07-24-2019  
  5. //  
  6. // Last Modified By : David McCarter  
  7. // Last Modified On : 08-05-2019  
  8. // *******************************************************************  
  9. // <copyright file="PersonFixed.cs" company="dotNetTips.OOP.Design">  
  10. //     Copyright (c) McCarter Consulting. All rights reserved.  
  11. // </copyright>  
  12. // <summary>Person OOP Design for Article 1</summary>  
  13. // *******************************************************************  
  14. using System;  
  15. namespace dotNetTips.OOP.Design.Models.Article1  
  16. {  
  17.     /// <summary>  
  18.     /// Class Person with proper encapsulation and validation.  
  19.     /// Implements the <see cref="Object" />  
  20.     /// </summary>  
  21.     /// <seealso cref="Object" />  
  22.     public class Person  
  23.     {  
  24.         /// <summary>  
  25.         /// The address1  
  26.         /// </summary>  
  27.         private string _address1;  
  28.   
  29.         /// <summary>  
  30.         /// The address2  
  31.         /// </summary>  
  32.         private string _address2;  
  33.   
  34.         /// <summary>  
  35.         /// The born on  
  36.         /// </summary>  
  37.         private DateTimeOffset _bornOn;  
  38.   
  39.         /// <summary>  
  40.         /// The cell phone number  
  41.         /// </summary>  
  42.         private string _cellPhone;  
  43.   
  44.         /// <summary>  
  45.         /// The city  
  46.         /// </summary>  
  47.         private string _city;  
  48.   
  49.         /// <summary>  
  50.         /// The country  
  51.         /// </summary>  
  52.         private string _country = "USA";  
  53.   
  54.         /// <summary>  
  55.         /// The email  
  56.         /// </summary>  
  57.         private string _email;  
  58.   
  59.         /// <summary>  
  60.         /// The first name  
  61.         /// </summary>  
  62.         private string _firstName;  
  63.   
  64.         /// <summary>  
  65.         /// The home phone number  
  66.         /// </summary>  
  67.         private string _homePhone;  
  68.   
  69.         /// <summary>  
  70.         /// The unique identifier  
  71.         /// </summary>  
  72.         private string _id;  
  73.   
  74.         /// <summary>  
  75.         /// The last name  
  76.         /// </summary>  
  77.         private string _lastName;  
  78.   
  79.         /// <summary>  
  80.         /// The postal code  
  81.         /// </summary>  
  82.         private string _postalCode;  
  83.   
  84.         /// <summary>  
  85.         /// Gets or sets the Address1.  
  86.         /// </summary>  
  87.         /// <value>The Address1.</value>  
  88.         /// <exception cref="ArgumentNullException">Address1 - Value for address cannot be null or empty.</exception>  
  89.         /// <exception cref="ArgumentOutOfRangeException">Address1 - Address must be between 10 - 256 characters.</exception>  
  90.         public string Address1  
  91.         {  
  92.             get  
  93.             {  
  94.                 return this._address1;  
  95.             }  
  96.   
  97.             set  
  98.             {  
  99.                 if (this._address1 == value)  
  100.                 {  
  101.                     return;  
  102.                 }  
  103.   
  104.                 if (string.IsNullOrEmpty(value))  
  105.                 {  
  106.                     throw new ArgumentNullException(nameof(Address1),  
  107.                     "Value for address cannot be null or empty.");  
  108.                 }  
  109.   
  110.                 this._address1 = (value.Length < 10 || value.Length >
  111.                                   256) ? throw new ArgumentOutOfRangeException(nameof(Address1),   
  112.                                   "Address must be between 10 - 256 characters.") : value;  
  113.             }  
  114.         }  
  115.   
  116.         /// <summary>  
  117.         /// Gets or sets the Address2.  
  118.         /// </summary>  
  119.         /// <value>The Address2.</value>  
  120.         /// <exception cref="ArgumentNullException">Address2 - Value for address cannot be null.</exception>  
  121.         /// <exception cref="ArgumentOutOfRangeException">Address1 - Address cannot be more than 256 characters.</exception>  
  122.         public string Address2  
  123.         {  
  124.             get  
  125.             {  
  126.                 return this._address2;  
  127.             }  
  128.   
  129.             set  
  130.             {  
  131.                 if (this._address2 == value)  
  132.                 {  
  133.                     return;  
  134.                 }  
  135.   
  136.                 if (value == null)  
  137.                 {  
  138.                     throw new ArgumentNullException(nameof(Address2), "Value for address cannot be null.");  
  139.                 }  
  140.   
  141.                 this._address2 = (value.Length > 256) ? throw new ArgumentOutOfRangeException(nameof(Address1),
  142.                                    "Address cannot be more than 256 characters.") : value;  
  143.             }  
  144.         }  
  145.   
  146.         /// <summary>  
  147.         /// Gets or sets the born on date.  
  148.         /// </summary>  
  149.         /// <value>The born on date.</value>  
  150.         /// <exception cref="ArgumentOutOfRangeException">BornOn - Person cannot be born in the future.</exception>  
  151.         public DateTimeOffset BornOn  
  152.         {  
  153.             get  
  154.             {  
  155.                 return this._bornOn;  
  156.             }  
  157.   
  158.             set  
  159.             {  
  160.                 if (this._bornOn == value)  
  161.                 {  
  162.                     return;  
  163.                 }  
  164.   
  165.                 this._bornOn = value.ToUniversalTime() > DateTimeOffset.UtcNow ?
  166.                                throw new ArgumentOutOfRangeException(nameof(BornOn), 
  167.                                "Person cannot be born in the future.") : value;  
  168.             }  
  169.         }  
  170.   
  171.         /// <summary>  
  172.         /// Gets or sets the cell phone number.  
  173.         /// </summary>  
  174.         /// <value>The cell phone number.</value>  
  175.         /// <exception cref="ArgumentNullException">CellPhone - Value for phone number cannot be null.</exception>  
  176.         /// <exception cref="ArgumentOutOfRangeException">CellPhone - Phone number is limited to 50 characters.</exception>  
  177.         public string CellPhone  
  178.         {  
  179.             get  
  180.             {  
  181.                 return this._cellPhone;  
  182.             }  
  183.   
  184.             set  
  185.             {  
  186.                 if (this._cellPhone == value)  
  187.                 {  
  188.                     return;  
  189.                 }  
  190.   
  191.                 if (value == null)  
  192.                 {  
  193.                     throw new ArgumentNullException(nameof(CellPhone), "Value for phone number cannot be null.");  
  194.                 }  
  195.   
  196.                 this._cellPhone = value.Length > 50 ? throw new ArgumentOutOfRangeException(nameof(CellPhone), 
  197.                                     "Phone number is limited to 50 characters.") : value;  
  198.             }  
  199.         }  
  200.   
  201.         /// <summary>  
  202.         /// Gets or sets the city.  
  203.         /// </summary>  
  204.         /// <value>The city name.</value>  
  205.         /// <exception cref="ArgumentNullException">City - Value for City cannot be null or empty.</exception>  
  206.         /// <exception cref="ArgumentOutOfRangeException">City - City length is limited to 100 characters.</exception>  
  207.         public string City  
  208.         {  
  209.             get  
  210.             {  
  211.                 return this._city;  
  212.             }  
  213.   
  214.             set  
  215.             {  
  216.                 if (this._city == value)  
  217.                 {  
  218.                     return;  
  219.                 }  
  220.   
  221.                 if (string.IsNullOrEmpty(value))  
  222.                 {  
  223.                     throw new ArgumentNullException(nameof(City), "Value for City cannot be null or empty.");  
  224.                 }  
  225.   
  226.                 this._city = value.Length > 100 ? throw new ArgumentOutOfRangeException(nameof(City), 
  227.                                "City length is limited to 100 characters.") : value;  
  228.             }  
  229.         }  
  230.   
  231.         /// <summary>  
  232.         /// Gets or sets the country.  
  233.         /// </summary>  
  234.         /// <value>The country name.</value>  
  235.         /// <exception cref="ArgumentNullException">Country - Value for Country cannot be null or empty.</exception>  
  236.         /// <exception cref="ArgumentOutOfRangeException">Country - Country length is limited to 50 characters.</exception>  
  237.         public string Country  
  238.         {  
  239.             get  
  240.             {  
  241.                 return this._country;  
  242.             }  
  243.   
  244.             set  
  245.             {  
  246.                 if (this._country == value)  
  247.                 {  
  248.                     return;  
  249.                 }  
  250.   
  251.                 if (string.IsNullOrEmpty(value))  
  252.                 {  
  253.                     throw new ArgumentNullException(nameof(Country), "Value for Country cannot be null or empty.");  
  254.                 }  
  255.   
  256.                 this._country = value.Length > 50 ? throw new ArgumentOutOfRangeException(nameof(Country),
  257.                                   "Country length is limited to 50 characters.") : value;  
  258.             }  
  259.         }  
  260.   
  261.         /// <summary>  
  262.         /// Gets or sets the email address.  
  263.         /// </summary>  
  264.         /// <value>The email address.</value>  
  265.         /// <exception cref="ArgumentNullException">Email - Value for Email cannot be null or empty.</exception>  
  266.         /// <exception cref="ArgumentOutOfRangeException">Email - Email length is limited to 50 characters.</exception>  
  267.         public string Email  
  268.         {  
  269.             get  
  270.             {  
  271.                 return this._email;  
  272.             }  
  273.   
  274.             set  
  275.             {  
  276.                 if (this._email == value)  
  277.                 {  
  278.                     return;  
  279.                 }  
  280.   
  281.                 if (string.IsNullOrEmpty(value))  
  282.                 {  
  283.                     throw new ArgumentNullException(nameof(Email), "Value for Email cannot be null or empty.");  
  284.                 }  
  285.   
  286.                 this._email = value.Length > 50 ? throw new ArgumentOutOfRangeException(nameof(Email), 
  287.                                 "Email length is limited to 50 characters.") : value;  
  288.             }  
  289.         }  
  290.   
  291.         /// <summary>  
  292.         /// Gets or sets the first name.  
  293.         /// </summary>  
  294.         /// <value>The first name.</value>  
  295.         /// <exception cref="ArgumentNullException">FirstName - Value for name cannot be null or empty.</exception>  
  296.         /// <exception cref="ArgumentOutOfRangeException">Email - First name length is limited to 50 characters.</exception>  
  297.         public string FirstName  
  298.         {  
  299.             get  
  300.             {  
  301.                 return this._firstName;  
  302.             }  
  303.   
  304.             set  
  305.             {  
  306.                 if (this._firstName == value)  
  307.                 {  
  308.                     return;  
  309.                 }  
  310.   
  311.                 if (string.IsNullOrEmpty(value))  
  312.                 {  
  313.                     throw new ArgumentNullException(nameof(FirstName), "Value for name cannot be null or empty.");  
  314.                 }  
  315.   
  316.                 this._firstName = value.Length > 50 ? throw new ArgumentOutOfRangeException(nameof(Email), 
  317.                                     "First name length is limited to 50 characters.") : value;  
  318.             }  
  319.         }  
  320.   
  321.         /// <summary>  
  322.         /// Gets or sets the home phone number.  
  323.         /// </summary>  
  324.         /// <value>The home phone.</value>  
  325.         /// <exception cref="ArgumentNullException">HomePhone - Value for phone number cannot be null or empty.</exception>  
  326.         /// <exception cref="ArgumentOutOfRangeException">HomePhone - Home phone length is limited to 50 characters.</exception>  
  327.         public string HomePhone  
  328.         {  
  329.             get  
  330.             {  
  331.                 return this._homePhone;  
  332.             }  
  333.   
  334.             set  
  335.             {  
  336.                 if (this._homePhone == value)  
  337.                 {  
  338.                     return;  
  339.                 }  
  340.   
  341.                 if (string.IsNullOrEmpty(value))  
  342.                 {  
  343.                     throw new ArgumentNullException(nameof(HomePhone), "Value for phone number cannot be null or empty.");  
  344.                 }  
  345.   
  346.                 this._homePhone = value.Length > 50 ? throw new ArgumentOutOfRangeException(nameof(this.HomePhone), 
  347.                                     "Home phone length is limited to 50 characters.") : value;  
  348.             }  
  349.         }  
  350.   
  351.         /// <summary>  
  352.         /// Gets or sets the unique identifier.  
  353.         /// </summary>  
  354.         /// <value>The unique identifier.</value>  
  355.         /// <exception cref="ArgumentNullException">Id - Value for Id cannot be null or empty.</exception>  
  356.         /// <exception cref="ArgumentOutOfRangeException">Id - Id length is limited to 256 characters.</exception>  
  357.         public string Id  
  358.         {  
  359.             get  
  360.             {  
  361.                 return this._id;  
  362.             }  
  363.   
  364.             set  
  365.             {  
  366.                 if (this._id == value)  
  367.                 {  
  368.                     return;  
  369.                 }  
  370.   
  371.                 if (string.IsNullOrEmpty(value))  
  372.                 {  
  373.                     throw new ArgumentNullException(nameof(Id), "Value for Id cannot be null or empty.");  
  374.                 }  
  375.   
  376.                 this._id = value.Length > 256 ? throw new ArgumentOutOfRangeException(nameof(this.Id), 
  377.                              "Id length is limited to 256 characters.") : value;  
  378.             }  
  379.         }  
  380.   
  381.         /// <summary>  
  382.         /// Gets or sets the last name.  
  383.         /// </summary>  
  384.         /// <value>The last name.</value>  
  385.         /// <exception cref="ArgumentNullException">LastName - Value for name cannot be null or empty.</exception>  
  386.         /// <exception cref="ArgumentOutOfRangeException">LastName - Last name length is limited to 50 characters.</exception>  
  387.         public string LastName  
  388.         {  
  389.             get  
  390.             {  
  391.                 return this._lastName;  
  392.             }  
  393.   
  394.             set  
  395.             {  
  396.                 if (this._lastName == value)  
  397.                 {  
  398.                     return;  
  399.                 }  
  400.   
  401.                 if (string.IsNullOrEmpty(value))  
  402.                 {  
  403.                     throw new ArgumentNullException(nameof(LastName), "Value for name cannot be null or empty.");  
  404.                 }  
  405.   
  406.                 this._lastName = value.Length > 50 ? throw new ArgumentOutOfRangeException(nameof(this.LastName),
  407.                                    "Last name length is limited to 50 characters.") : value;  
  408.             }  
  409.         }  
  410.   
  411.         /// <summary>  
  412.         /// Gets or sets the postal code.  
  413.         /// </summary>  
  414.         /// <value>The postal code.</value>  
  415.         /// <exception cref="ArgumentNullException">PostalCode - Value for postal code cannot be null or empty.</exception>  
  416.         /// <exception cref="ArgumentOutOfRangeException">PostalCode - Postal code length is limited to 20 characters.</exception>  
  417.         public string PostalCode  
  418.         {  
  419.             get  
  420.             {  
  421.                 return this._postalCode;  
  422.             }  
  423.   
  424.             set  
  425.             {  
  426.                 if (this._postalCode == value)  
  427.                 {  
  428.                     return;  
  429.                 }  
  430.   
  431.                 if (string.IsNullOrEmpty(value))  
  432.                 {  
  433.                     throw new ArgumentNullException(nameof(PostalCode), "Value for postal code cannot be null or empty.");  
  434.                 }  
  435.   
  436.                 this._postalCode = value.Length > 20 ? throw new ArgumentOutOfRangeException(nameof(this.PostalCode), 
  437.                                      "Postal code length is limited to 20 characters.") : value;  
  438.             }  
  439.         }  
  440.     }  
  441. }  
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.
 
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.


Similar Articles
McCarter Consulting
Software architecture, code & app performance, code quality, Microsoft .NET & mentoring. Available!