Originally published on Medium · June 5, 2023
Classes are the basic unit of design in C#, but I keep running into the same problem on real codebases: a class compiles fine and does nothing to tell you how it's meant to be used. Anyone can new one up, leave half the properties unset, and pass it around — the type itself gives no hints about what "valid" looks like. Adding a bit of structure, real constructors, and validation turns a class from a passive data bag into something that actively guards its own invariants.
What's Wrong with a Plain, Implicit Class
Consider an example class:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
When working with implicit plain classes like this Person class in C#, developers often encounter a multitude of unanswered questions. The lack of explicitness and constraints within the class can leave developers uncertain about its purpose and behavior, leading to confusion and potential challenges in software development.
Here are some common questions that developers may have when encountering an implicit plain class like the Person class:
- Is the
Namefirstname, lastname, or both? - How do you calculate
Age? There is no date of birth. - When constructing the object, do I have to set all properties or just some of them?
These unanswered questions highlight the ambiguity and uncertainty that arise when dealing with implicit plain classes. Without explicitness and constraints, developers may struggle to fully understand the class's intended usage, resulting in potential inefficiencies, errors, and difficulties in maintaining the codebase.
To address these challenges, it becomes essential to transform implicit plain classes into explicit ones by introducing constraints, defining functionality, and providing clear guidelines on object creation and usage. By embracing explicitness, developers can eliminate confusion, enhance code readability, and create more robust and maintainable software systems.
Making the Person Class Explicit
By introducing explicitness and constraints into the Person class, we can enhance code robustness and improve software design. Let's delve into the key techniques for achieving this transformation:
1. Adding Functionality. We can enrich the Person class by incorporating additional methods that provide meaningful behaviors and operations. By encapsulating relevant functionality within these methods, we enhance the class's usefulness and contribute to a more cohesive codebase.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
// Additional functionality
public string GetFullName()
{
return $"Full Name: {Name}";
}
public int CalculateAgeInMonths()
{
return Age * 12;
}
public bool ValidateAddress()
{
// Add validation logic here
return !string.IsNullOrEmpty(Address);
}
}
2. Defining Concrete Constructors. Instead of relying solely on default constructors, the explicit Person class benefits from concrete constructors. These constructors allow us to enforce constraints, perform validation checks, and initialize object properties based on specific rules. This explicit approach fosters more reliable object creation and reduces the chances of inconsistent or invalid objects.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
// Concrete constructor with validation checks
public Person(string name, int age, string address)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Name cannot be empty.", nameof(name));
}
if (age < 0 || age > 120)
{
throw new ArgumentOutOfRangeException("Age must be between 0 and 120.", nameof(age));
}
if (string.IsNullOrEmpty(address))
{
throw new ArgumentException("Address cannot be empty.", nameof(address));
}
Name = name;
Age = age;
Address = address;
}
}
3. Applying Constraints. Introducing constraints to the Person class involves defining rules and limitations on how objects can be instantiated or manipulated. By imposing constraints, such as length limitations on the name or age range restrictions, we ensure that objects adhere to predetermined standards. This not only prevents errors but also guides developers in using the class correctly.
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
// Explicit constructor with constraints
public Person(string name, int age, string address)
{
if (string.IsNullOrEmpty(name) || name.Length > 50)
{
throw new ArgumentException("Name must be non-empty and have a maximum length of 50 characters.", nameof(name));
}
if (age < 0 || age > 120)
{
throw new ArgumentOutOfRangeException("Age must be between 0 and 120.", nameof(age));
}
if (string.IsNullOrEmpty(address) || address.Length > 100)
{
throw new ArgumentException("Address must be non-empty and have a maximum length of 100 characters.", nameof(address));
}
Name = name;
Age = age;
Address = address;
}
}
What This Buys You
Constraining the Person class through explicitness is a powerful approach to enhance code readability, maintainability, and software design. By adding functionality, concrete constructors, and constraints, we transform the class into a more reliable and useful component. Embracing explicitness fosters better collaboration among developers, facilitates future code modifications, and leads to the creation of higher-quality software systems.
Conclusion
Constraining the Person class through explicitness is a valuable technique to enhance code readability, maintainability, and software design. By adding explicit functionality, concrete constructors, and constraints, we improve the reliability and usefulness of the class. Embracing explicitness fosters better collaboration among developers, facilitates future code modifications, and leads to the creation of higher-quality software systems. By applying these techniques, we empower our codebase with clarity and robustness, paving the way for more efficient and scalable software development.