by Zoran Horvat
One drawback often emphasized about constructors is that they don’t have their own names. Constructors are named after a class, and that doesn’t necessarily communicate their logic well.
As an example, look at the very simple Student class implementation, which is currently only responsible to keep track of the student’s name and to let her enroll for a semester. The latter feature will impact the way we instantiate this class.
public class Student
{
public string Name { get; }
public Semester Enrolled { get; private set; }
public Student(string name)
{
this.Name = name
?? throw new ArgumentNullException(nameof(name));
}
public void Enroll(Semester semester) { ... }
}
And then comes the domain logic. Students can enroll a semester and then take exams at some later time. The trick is that students must enroll semesters in order. Any attempt to jump over semesters will result in an exception.
Here is the implementation:
public class Student
{
public string Name { get; }
public Semester Enrolled { get; private set; }
...
public void Enroll(Semester semester)
{
if (semester == null || semester.Predecessor != this.Enrolled)
throw new ArgumentException();
this.Enrolled = semester;
}
}
And just after successfully unrolling this change, the new requirement comes to the table, telling that some students may come from a student exchange program. These students don’t have to enroll semesters in order. We get an idea (not a bright one!) to constrain Student instances by keeping a Boolean flag, which tells whether the student comes from exchange or not. The new flag has suddenly become a constructor argument.
public class Student
{
public string Name { get; }
public Semester Enrolled { get; private set; }
private bool ComesFromExchange { get; }
public Student(bool comesFromExchange, string name)
{
this.ComesFromExchange = comesFromExchange;
this.Name = name;
}
...
}
Don’t judge my design yet, I’m just trying to find shortest path out from this new requirement. On the bright side, this flag is private, and nobody can really tell its value from just looking at the object of this class. Why exposing it through the public constructor then?
One common technique to improve communication skills of the constructor is to replace it with one or more static factory methods. We can turn the constructor private, as the first step. Nobody will see it anymore, and the only way to construct the Student instance will be from the inside.
In that light, I will allow the caller to construct the object through static factory methods, but I will only offer limited options. Static factories will communicate special meanings about the construction process. Like having a static Create method, which communicates nothing special, and therefore makes us believe that it will construct a common student. Another method, CreateFromExchange, will clearly communicate additional information through its name. The name says that it will create the student coming from an exchange program.
public class Student
{
...
// Mind the private constructor
private Student(bool comesFromExchange, string name) { ... }
public static Student Create(string name) =>
new Student(false, name);
public static Student CreateFromExchange(string name) =>
new Student(true, name);
}
I am showing you this as an example how you can make your code more readable and more understandable. You should never underestimate the power of code readability. Knowing precisely what the function will do by just looking at its signature, and without having to look at its source to confirm your expectations, is a great defensive tool. These two factory functions are telling their purpose quite obviously.
On the other hand, this solution with static factory methods is bringing troubles on another plane. If you have multiple methods to construct a single object, then it may look like your object is doing different things, depending on how it was constructed. And that is true for this Student class. Boolean flag will affect the way it executes the Enroll method, for example:
public class Student
{
...
public void Enroll(Semester semester)
{
if (semester == null ||
(!this.ComesFromExchange &&
semester.Predecessor != this.Enrolled))
throw new ArgumentException();
this.Enrolled = semester;
}
}
A Boolean flag inside an object is a bad coding practice, and it calls for defensive code in all other places. Callers will probably need to think how to deal with an object depending on the method which was used to construct it. In this case, caller would need to know whether the student comes from exchange program or not, before attempting to enroll her for a semester. Failing to check conditions up-front will lead to an exception thrown back from the Enroll method.
Beware of that coding style. If you catch yourself making more than one factory method for one class, you better ask yourself is it really one class you’re talking about? Maybe you’re talking about two classes, only merged together into one piece of code.
My advice is to have exactly one factory function per class, and have no discrete parameters, such as Booleans and enums. In the remainder of this article, I will show you one way of resolving this issue with students. It is based on class derivation. There are other ways, based on object composition, strategies, etc. and I will suggest you investigate alternatives as an exercise.
If I wanted to have two kinds of students, regular ones and those coming from student exchange program, then I could make two derived classes, RegularStudent and ExchangeStudent, both deriving from the base Student. Each of these new classes will expose only one factory function, in form of a single parameterized constructor. And then the base type will be relieved from having to know where the student is coming from.
public abstract class Student
{
public string Name { get; }
public Semester Enrolled { get; set; }
protected Student(string name)
{
this.Name = name;
}
public abstract bool CanEnroll(Semester semester);
public void Enroll(Semester semester)
{
if (!this.CanEnroll(semester))
throw new ArgumentException();
this.Enrolled = semester;
}
}
public class RegularStudent : Student
{
public RegularStudent(string name) : base(name) { }
public override bool CanEnroll(Semester semester) =>
semester != null && semester.Predecessor == base.Enrolled;
}
public class ExchangeStudent : Student
{
public ExchangeStudent(string name) : base(name) { }
public override bool CanEnroll(Semester semester) =>
semester != null;
}
Not only that the base type doesn’t know the details of the student, but it will not be responsible to construct objects at all. Both static factory functions that used to reside in the Student class are now gone. Information is encoded in the subtype itself.
And then we come to the feature which used to depend on construction method. Enroll method has to check the rules before proceeding. That is where abstract CanEnroll method comes to the picture, letting the derived classes to implement precise domain logic in each of the cases separately.
In particular, regular student will only allow enrolling the immediately following semester. And student coming from exchange will be allowed to enroll any semester with no constraints (other than the trivial request to have a non-null semester reference, of course.)
This completes implementation of the feature which used to depend on the way in which student instances are constructed. I will strongly advise you to avoid multiple creation methods for any class. Instead, you will probably want to construct several related classes, each covering one usage scenario, and each instantiated in one and only one way.
Allowing multiple construction methods may reflect on stability in negative ways, and that is the primary issue I have tried to fight by introducing derived classes in this example. On the other hand, if you decide to keep multiple constructors in place, then you might opt to replace them with static factory functions, for the sake of improved code readability.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.