Designing object models is hard. That's it.
Big words, they keep coming. But these words won't do it for you.
What you need instead is what someone poetically named the universal problem solver. Your brain.
I will apply all those big words, yes, but they will only be the tools, not the goal of my design.
Let's start with doing it the wrong way so that you can learn about the issues
the object-oriented design fights. This is just a class. It will model a book.
Object-oriented design teaches us to keep an object's state private and
initialize it during construction. Fine. Do the same with all the
pieces of state in a class. Another core principle of object-oriented
design is to ensure every object is constructed in a valid state.
We usually throw on validation errors. The next stage is to introduce C#
properties. Objects in a traditional object-oriented design are encapsulating
state and controlling all mutations. We distinguish accessors, which may
expose the state, from mutators, which update the private state after validation.
This terribly verbose form is only meant to guarantee the encapsulation of the object's state.
Still, encapsulation is a good thing. Use it. Here is one nice little detail
- encapsulating a collection. We typically keep data structures private and
expose their read-only variants, such as a read-only list or an IEnumerable.
But with the power of protecting the state comes great responsibility.
How do we apply complex mutations if all the data are private? The class must do it all here.
Adding a new author. Removing an author. Turning all authors to uppercase.
The object of this class is the only one that possesses the access
required to implement these operations. This is the time to familiarize yourself with the
first law of customer engagement: Customers know no boundaries. They will always ask for more.
Like, moving an author by one step up or down in the list of authors.
Related, but not the same, moving an author to the front or the back of the list of authors.
Did you hear about the second law of customer engagement? The rules of logic do not
bind them. They can ask for anything. In the blink of an eye, my class has
grown to almost a hundred lines long, and I am still nowhere near the end of writing it.
The principal issue I notice in this design is primitive obsession. This class is obsessed
with primitive types: string, int, DateOnly. And a particular form of primitive
obsession - stringification. Everything appears to be a string.
Let me show you how bad the stringification can be.
What string comparison am I using in the RemoveAuthor method?
But of course! Let me use invariant culture case-insensitive comparison. Any better?
Of course not. How did I get that invariant culture idea? Well, honestly, I had
no better idea, so... invariant. The same problem appears in the
AuthorsToUppercase method a few lines below. You get the point. My data have no
structure where structure matters. That is where most object-oriented
programmers make a tragic mistake. They keep walking brainlessly through that mud.
I have just added the culture to the book. Books come in all sorts of languages, don't they?
I could use this culture in the methods below, but that only binds my class to
the string type even stronger. Primitive obsession goes much
further than just string comparison. What is this integer representing
the book's edition? How do I represent summer 2025 with an int?
The publication date works the same. What if the publisher only said the month and
the year but not the day within the month? This class is inflated,
unreadable, complex, and wrong. Strap up your seatbelt.
The actual demo you came for starts right now. One of the greatest impediments to good
object-oriented design is the lack of cohesion. All these methods ignore all the
properties but one - the list of authors. The Book class is not cohesive. It looks
like two unrelated classes squeezed into one. The natural course of action is to move part of
the state and related behavior into a new class. Let me remind you that you can download the
entire source code from my Patreon page, including parts I will not show in the video.
Every registered patron helps hundreds of other programmers watch all the programming
videos on my YouTube channel for free. So, please visit me on Patreon and
help me keep this channel free forever. This is the new class that represents
the list of authors. The actual data structure is still private, so the word List
in the class's name has a symbolic meaning. However, after copying all those methods
here, this class exhibits a different issue. It speaks at two levels of abstractness.
Some of its operations look cryptic because they essentially belong to the list collection.
Why not pull list manipulation into extensions of the list type, then?
This new class only contains extension methods on the generic list type.
There is quite a lot of code here, but these methods only focus on manipulating
the generic list and nothing else. They don't even know how their caller
will find the desired element of the list. The new implementation focuses on domain-specific
operations such as adding and removing authors, swapping their positions, and the like.
You will not find list-specific logic in this class anymore.
That is the lesson about abstractness. Let each class speak at one level of abstractness.
That leaves us with this class's authentic design flaws: the question of string comparison
and the question of strings themselves. The authors list can contain a culture info
object as a dependency. That is another important concept in object-oriented design.
The culture info is a dependency. The authors are the data.
Comparing authors within the specified culture outlines another
essential design concept: parameterization. Turning strings to uppercase will show another
important design tactic - using a dependency as a strategy, as in the Strategy design pattern.
I am also using first-class functions here. I am passing a method to a method.
Another valuable design tactic is writing intention-revealing code. I don't
have to explain what these methods do. Now, to the question of stringified authors.
Different authors of one book could come from different cultures.
Turning all of them to uppercase using the same culture is wrong, but
I don't have per-author culture info! I don't have that because
authors are stupid strings. Objects to the rescue! This is the
model of an author. It only wraps a string representing the full name, as before.
But now comes the substantial improvement. I plan to prove that the
culture info truly belongs here. Turning the author to uppercase
will use this culture info. Matching the author against a string
will also use the culture info. This results from responsibility
segregation, with each responsibility implemented where it belongs.
There will be no more strings in the list of authors. It will be
- guess what! - a list of authors. There is nothing to validate there.
Validation is part of each individual author. To cut a long story short, here is
the authors list implementation, which wraps strongly typed author objects.
Look how neatly the cultures fit the new implementation. It is now an entire list
of cultures pulled out from the authors, where they belong.
This entire redesign will undoubtedly significantly impact
the Book class from which it all started. The book was obsessed with managing a
proper list of strings representing authors. It's time to say goodbye to stringified authors
and that entire mismatched responsibility. I have removed around 50
lines of code from this class. Here is the class diagram at the
beginning of this demo - one class. I extracted the list of authors
from it, then transferred the list operations to another class and the author
operations to the third class in a row. It begins to look like a
class diagram now, does it? Another fundamental object-oriented
design principle is to group related objects and values into a separate object.
The publisher, edition, publication date, and even the culture - those pieces of
information comprise the book's release. Welcome the new class that wraps
these pieces of information. Four constructor arguments are a tad above what
I usually tolerate, but I can still live with it. The book's constructor is intolerable.
Don't be surprised to see it melt down along with all that property validation
after I pulled the book's release out. I can now focus on putting the last nail
in the primitive obsession's coffin. Why is the publisher a string and not a class?
Why is the edition an int and not an interface? Why is the publication date a
DateOnly and not a class or a struct? What do you say about this class's
simplicity, safety, and extendibility now? Remember the Open-Closed Principle? Open
for extension, closed for modification? Yes, I can extend this class by writing new
polymorphic implementations of each type it depends on, not by changing this class here.
Let me show you how that works on the example of the edition type.
What is an edition? I don't know. I really don't know. So, I leave its definition open-ended.
It could be that integer we had already. It could be identified by a season of the year.
You can add whatever you like later, even in other assemblies, which makes any class
that depends on the IEdition interface quite extendible. Without modification, of course.
Interfaces also define their own behavior. For example, we might need to compare editions
of the same book and sort them in a time order. Implementing comparison on an open-ended
hierarchy of types is one of the craziest problems in object-oriented programming.
Without solving that problem, I still want to point out that it is now confined to the
IEdition interface and its implementations alone. No other class in the model will know how
that complex feature works. Responsibility segregation at its finest.
Each problem, hard or easy, only appears in one place.
How about publishing dates? That should be easier because there are
only a few possible definitions there. That is why I have opted for an abstract
record. It could be a full date, a month within a year, or just a year.
You can learn about C# records from other videos on my channel.
I want to show you what the class diagram looks like now.
The book depends on some polymorphic types now. There are a couple of implementations
of the edition interface, and all three possibilities for the publication date.
The highest-valued principle I applied several times in this demo is the separation of concerns.
With concerns separated, now you can play with this model and implement whatever behavior
you might need in future development.