In a previous article, Designing Data Objects in C# and F#, I gave some recommendations on how to design data objects.
In this article, I am going to go through some examples of data object designs that have some issues. I will discuss the issues with such designs and propose solutions for them.
The issues I discuss here are based on real issues that I have seen in the code bases that I work with. This does not mean that the code in the examples here is real code. The problem domains I use in the article are different. The examples have also been simplified to fit into the article.
Example #1 - Two related optional properties
public sealed class Customer
{
public string Name { get; }
public Maybe<DateTime> LastPurchaseDate { get; }
public Maybe<decimal> LastPurchaseAmount { get; }
//Constructor...
}
This class represents a customer in some shopping application. The LastPurchaseDate property is optional because its type is Maybe<DateTime>. The LastPurchaseAmount property is also optional.
In a nutshell, Maybe<T> can either have no value (NoValue) or have a specific value of type T.
For more information about Maybe, see the Designing Data Objects in C# and F# article.
The class also has a constructor that simply allows us to specify a value for each of the class properties.
My issue with this design is that the customer object is modeled in a way that allows us to specify a value for LastPurchaseDate and specify a NoValue for LastPurchaseAmount, and vice versa.
In reality, either both should have values, or both should have no values. If this customer has at least purchased one item, both will have a value. If the customer has purchased nothing, both will have a NoValue.
Consider this updated design:
public sealed class Customer
{
public string Name { get; }
public Maybe<LastPurchaseDetails> LastPurchaseDetails { get; }
//Constructor...
}
public sealed class LastPurchaseDetails
{
public DateTime LastPurchaseDate { get; }
public decimal LastPurchaseAmount { get; }
//Constructor...
}
Now, the invalid states are no longer representable.
Example #2 – Convenient but bad Data Object reuse
Consider this interface that represents a service in an application that allows an administrator to manage a set of machines (e.g. PCs):
public interface IService
{
ImmutableArray<Machine> GetMachines();
void UpdateMachine(Machine machine);
}
The Machine class represents a single machine. It has some properties defined inside it.
public sealed class Machine
{
public string Name { get; }
public ImmutableArray<User> UsersLoggedInTheLast24Hours { get; }
public bool GamingIsEnabled { get; }
public bool PowerSavingIsEnabled { get; }
public bool TurboSpeedIsEnabled { get; }
//Constructor...
}
The GetMachines method is called by some GUI application to get details about the machines so that it can show them to the administrator. It displays the name of each machine, the list of users logged into that machine in the last 24 hours, and whether some features (gaming, power saving, and turbo speed) are enabled on that machine.
The UpdateMachine method is invoked by the GUI application when the administrator wishes to update certain settings on a specific machine. It takes a Machine object as a parameter.
The developer who designed this contract saw that it was convenient to use the same Data Object (i.e., the Machine class) to allow the administrator to view the machine information, and to also update machine settings.
However, such convenience comes at a cost.
For one, the reader of this contract can become confused about the UsersLoggedInTheLast24Hours property. What does it mean to update a specific machine with an updated value of this property? We sure can’t change history and change the users who logged into the machine in the last 24 hours (we don’t have a time machine). Maybe updating this value will change the logs recorded on that machine that contain the list of logged in users? Or maybe this property is ignored by the implementor of the UpdateMachine method?
After the developer who is reading the code goes to the implementation of the UpdateMachine method, they find out that this property is completely ignored. The developer who wrote the code just used the same Data Object for convenience.
What about GamingIsEnabled, PowerSavingIsEnabled, and TurboSpeedIsEnabled? After reading the code, the developer finds out that only the first two of these properties can be changed, and the third one is simply ignored. Again, the developer who wrote this code used the same Data Object for convenience.
Although it means more code, the following design is better:
public interface IService
{
ImmutableArray<Machine> GetMachines();
void UpdateMachine(string machineName, MachineChanges changes);
}
public sealed class Machine
{
//Machine class members here...
}
public sealed class MachineChanges
{
public bool GamingIsEnabled{ get; }
public bool PowerSavingIsEnabled { get; }
//Constructor...
}
Example #3 – Using an empty byte array to model a no value
Consider this User class:
public sealed class User
{
public string Name { get; }
public ImmutableArray<byte> Image { get; }
public bool HasImage => Image.Length > 0;
//Other properties...
//Constructor...
}
One of the properties of the User class is the Image property which is of type ImmutableArray<byte>. This property contains a JPEG image of the user. Such image is displayed beside the user name in the application.
Notice the HasImage property. It returns true if the Image array is non-empty. Reading this code, one can understand that the image is optional. When the user has no image, the Image property would contain an empty byte array.
Although the HasImage property can help make the reader understand this design, it would be better if we use the Maybe type here to model the fact that the image of the user is optional:
public sealed class Customer
{
public string Name { get; }
public Maybe<ImmutableArray<byte>> Image { get; }
//Other properties...
//Constructor...
}
Note: The Image property design can still be enhanced. For example, currently the property does not tell us that the contents of the byte array is actually a JPEG image. One enhancement for example is to encapsulate the byte array inside a JpegImage class.
Example #4 – A property that has two slightly different meanings
Consider this data object:
public sealed class MobileDevice
{
public bool IsOnline { get; }
public GpsLocation GpsLocation { get; }
public Brand Brand { get; }
public string Name { get; }
//Constructor...
}
The MobileDevice class models a mobile device in an application that allows users to monitor mobile devices. For example, employers can use the application to monitor special mobile devices they gave to their employees. Or parents can use the application to monitor the mobile devices of their children.
The IsOnline property is true if the mobile device is reachable, i.e., the application can communicate with the device. The GpsLocation property contains the location of the device based on the GPS device inside the mobile.
The Brand property contains information about the maker of the mobile device (e.g. Samsung, Apple, etc.).
The Name property is used by the user of the application to identify the device.
When reviewing the class, a code reviewer asked the developer who wrote the class, the following question:
Reviewer: “If the device is online, the GpsLocation property would return the location of the device. That is understandable. However, when the mobile device is offline (IsOnline is false), what would be the value of the GpsLocation property? Would it be null? If so, shouldn’t you use the Maybe type to model the fact that it is optional?”
Developer: “No, it would contain the last known GPS location of the device. Do you think I should rename the property to CurrentOrLastKnownGpsLocation?”.
Reviewer: “That would be better. Still, this name does not tell us when this property would contain the current location and when it would contain the last known location. In other words, it doesn’t tell the reader about the relationship between this property and the IsOnline property. I have a better idea”.
The reviewer suggest the following changes to the code:
public sealed class MobileDevice
{
public ReachabilityStatus ReachabilityStatus { get; }
public Brand Brand { get; }
public string Name { get; }
//Constructor...
}
public abstract class ReachabilityStatus
{
private ReachabilityStatus(){ }
public sealed class Online : ReachabilityStatus
{
public GpsLocation GpsLocation { get; }
//Constructor...
}
public sealed class Offline : ReachabilityStatus
{
public GpsLocation LastKnownGpsLocation { get; }
//Constructor...
}
}
The IsOnline and the GpsLocation properties were removed. And a ReachabilityStatus property was added. This property contains a Sum type.
I explained Sum types in the Designing Data Objects in C# and F# article. In a nutshell a Sum type is a type whose values can be of one of a fixed number of types. For example, the values of the ReachabilityStatus type can either be of type Online or of type Offline. The Online type contains a single property called GpsLocation that contains the current location of the device. The Offlinetype’s single property is correctly named LastKnownGpsLocation.
One could argue that the current code is verbose. This is true because C# does not have a way to model Sum types in a concise manner. In the Designing Data Objects in C# and F# article, I talked about how you can model Sum types concisely in F#. I also talked about how developers can model their data objects in F# and then write the behavior code that uses these data objects in C#. I leave the F# implementation of MobileDevice and ReachabilityStatus for the reader to write as an exercise. Refer to the mentioned article for help.
Example #5 – Using DateTime to model a Date
This is a very simple example to explain. The DateTime struct is in the Base Class Library (BCL), and it models both a date and a time. Sometimes, we want to have a property in a Data Object to hold just a date. Consider this example:
public sealed class Member
{
public DateTime SubscriptionDate { get; }
//Other properties and constructor...
}
The member class represents a member in some club. The SubscriptionDate property represents the date when the member joined the club. In a particular application where this class exists, only the date when the member subscribed is important. Users of the application don’t care if the member joined the club at 10 AM or 5 PM, they only care about the date.
The type of the property however consists of both a date and a time. What would the time part be in this case? 00:00? Or did the developer who developed this application just in case also included the time when the member joined?
It is better in this case to have a Date type to represent a date like this:
public sealed class Date
{
public int Year { get; }
public int Month { get; }
public int Day { get; }
//Constructor..
}
Conclusion:
In this article I gave examples of some Data Object designs and discussed some issues with them. I also provided sample solutions.
This article was technically reviewed by Damir Arh.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Yacoub Massad is a software developer and works mainly on Microsoft technologies. Currently, he works at Zeva International where he uses C#, .NET, and other technologies to create eDiscovery solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at
criticalsoftwareblog.com. He is also the creator of DIVEX(
https://divex.dev), a dependency injection tool that allows you to compose objects and functions in C# in a way that makes your code more maintainable. Recently he started a
YouTube channel about Roslyn, the .NET compiler.