Master the Design of Classes

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Zoran Horvat
Views: 23,314
Rating: undefined out of 5
Keywords: csharp, dotnet, programming, object-oriented programming, functional programming, c#, zoran, horvat, zoran horvat, c# programming, learn c#, c# tutorial
Id: jwCW446hoPk
Channel Id: undefined
Length: 11min 56sec (716 seconds)
Published: Wed Jun 12 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.