In
part 1 of this series, I discussed building model classes properly with Object-Oriented Programming (OOP), specifically encapsulation that
must include data validation. In this article, I’m going to show you constructors, interfaces and more that you should implement for your model classes. We will build upon the
Person.cs type from part 1 and end up with a new Person type as shown to the right. Some of what I will show can speed up the performance of Person during sorting.
Constructors
Types in .NET require a constructor so that they can be created. When a class is created using Visual Studio, it does not add it (for some reason). If you don’t implement one, then the compiler will add an empty constructor for you during the build process. This is called the “magic constructor”, since it’s added for you. If you want to use a constructor to set data, then that can be done like this.
- public Person(string id, string email)
- {
- Email = email;
- Id = id;
- }
My rule for constructors is that the data being set is required for the type. For Person, I am making Id and Email required. I also use this for other types of classes such as data context classes where the connection string is required.
I’d like to point out that I am using the properties to set the data, not directly setting the private fields. This way the data set in the constructor goes through the same validation as outside code setting the properties.
When you create a constructor like this, the compiler won’t add the “magic constructor” anymore. The empty constructor is required for serialization. You need to add it yourself like this.
If this empty constructor is not added, then deserialization for the type will not work. I never add constructors to make it easy for someone to set data. Since we now have object initialization in .NET, there isn’t’ a reason to. Here is an example.
- var person = new Person("ASODIDADLVOD109A", "[email protected]")
- {
- FirstName = "David",
- LastName = "McCarter"
- };
When I design types like this, I want to require users of the type to send in the email address and id and not use the empty constructor. There is a way to hide from Intellisense, the empty constructor like this.
- [EditorBrowsable(EditorBrowsableState.Never)]
- public Person()
- {}
Using the EditorBrowsable attribute will hide the constructor, or any other method, from types outside of the assembly that the model is located in.
By default, when objects are compared in .NET, reflection is used, which can affect the performance of that type. The next three topics will tackle this issue. Make sure to benchmark the performance of your type.
Implementing IComparable & IComparable<>
To make the ordering of object better, it’s recommended to implement the IComparable and
IComparable<> interfaces. Now the definition of the type looks like this.
- public class Person : IEquatable<Person>, IComparable,
- IComparable<Person>
For Person, I am implementing the two methods like this.
- public int CompareTo(object obj)
- {
- if (obj == null)
- {
- return 1;
- }
-
- var other = obj as Person;
- if (other == null)
- {
- throw new ArgumentException(nameof(obj) +
- " is not a " + nameof(Person));
- }
-
- return CompareTo(other);
- }
-
- public int CompareTo(Person other)
- {
- if (other == null)
- {
- return 1;
- }
-
- int result = 0;
-
- result = _email.CompareTo(other._email);
- if (result != 0)
- {
- return result;
- }
-
- return result;
- }
As you can see in the second CompareTo() method, I am only showing comparing the email address. All the properties are compared in the
actual class.
Performance Increase
Implementing these two interfaces can increase the performance when sorting a collection of that type in .NET Core 3 and the .NET Framework 4.8. In the chart below, the performance is benchmarked by comparing a Person class without these interfaces (Mean in the chart) against a Person class that implement these interfaces (Overloaded in the chart).
As you can see, implanting these two interfaces can cause a 17-26% performance increase. So why not implement them?
Overloading GetHashCode()
Overloading the GetHashCode() method, could be a performance increase and you can choose what data will used to create the hash code. Here is how I did it for Person.
-
-
-
-
-
- public override int GetHashCode()
- {
- HashCode.Combine(Email, Id);
- }
As you can see, I am only using the data for the email address and id for the hash code. Overriding GetHashCode() is simple when using refactoring tools.
Making Classes Easier to Debug
Finally, for this article, is making your type easier to use when debugging. Here is why let’s say I have a collection of Person and I want to look at its values. This is the default experience in Visual Studio.
As you can see, by default it shows the full type name. Not very useful is it? Well, there is a new attribute called
DebuggerDisplay that you can use to fix this.
- [DebuggerDisplay("{Email}")]
- public class Person : IEquatable<Person>, IComparable,
- IComparable<Person>
Using DebuggerDisplay, I can set what properties that will be used for the display while debugging.
As you can see, this is much more useful and anyone using this type will thank you!
Override ToString()
While you are at it, you should override ToString() too. As with debugging, ToString() will just return the type name that again isn’t very useful. Here is how I did it for Person.
- public override string ToString()
- {
- return $"{Id} - {Email}";
- }
Summary
As you can see, there is much more to building good model classes than just implementing properties. But we are not done yet. In the next article, I will tackle serialization. Most models get serialized and deserialized when going through a web API (service) or something similar.
The Person class is now over 600 lines of code and documentation, too large to post in this article. You can view it by going to
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.