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.
- 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;
- }
-
- }
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.
- 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).
- All the fields are strings! The proper type for the data should always be used like DateTimeOffset, Integer etc.
- Poor coding standards. They standard even changes from field to field!
- 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
- {
-
- if (_address1 == value)
- {
- return;
- }
-
- if (string.IsNullOrEmpty(value))
- {
- throw new ArgumentNullException(nameof(Address1),
- "Value for Address1 cannot be null or empty.");
- }
-
- 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.
- 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.
- 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.
- 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).
-
-
-
-
-
-
-
-
-
-
-
-
-
- using System;
- namespace dotNetTips.OOP.Design.Models.Article1
- {
-
-
-
-
-
- public class Person
- {
-
-
-
- private string _address1;
-
-
-
-
- private string _address2;
-
-
-
-
- private DateTimeOffset _bornOn;
-
-
-
-
- private string _cellPhone;
-
-
-
-
- private string _city;
-
-
-
-
- private string _country = "USA";
-
-
-
-
- private string _email;
-
-
-
-
- private string _firstName;
-
-
-
-
- private string _homePhone;
-
-
-
-
- private string _id;
-
-
-
-
- private string _lastName;
-
-
-
-
- private string _postalCode;
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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;
- }
- }
-
-
-
-
-
-
-
- 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.
- 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).
- 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.