reminds me of the way lichess creator thinks. The less code there is the less code there is to maintain and as a result it's much easier to continue working on the project. How 1 Software Engineer Outperforms 138 - Lichess Case Study was a fun video as well.
Brilliant! There are so many videos on TH-cam trashing OOP saying it requires too much boilerplate, the details are obscure, can't find the lines that actually do something and more. Their real problem is that they didn't get to the right abstractions.
When people say OOP, they usually mean inheritance. Inheritance is legitimately bad. Even though things like mixins can reduce code duplication, the bad habits it encourages isn't worth it. More often than not, an object with a unique implementation also needs a unique interface. Inheritance inhibits that and mixins often do as well.
@T1Oracle Deep inheritance is bad, it's a terrible way to reuse code. Polymorphism based on Interfaces or virtual classes is the way to go. Rarely do I go further than an Interface with alternative implementations and reuse using pure static extension methods.
If you can calculate the value of sort order DateOnly for all kinds of partial dates, you can also store that value in the db. A calculated property in code, saved to the db. If the space taken by the extra Date column is not prohibitively large :) A calculated column at SQL level in the db would not be ideal, since if you do any change on the publication date in code, won't be reflected by the sort order. But could work in a pinch, if you really can't change the code, but can create issues later. That way you can also query by this directly from the db, so you are not over-fetching just to calculate the sort order, and maybe throw away a bunch of data because you only need like the 20 latest books by some criteria.
@@AkosLukacs42 You are correct. That comes under the technique of denormalization, adding redundant information to the storage in order to speed up subsequent operations. It might amuse you to learn that I have a video in the queue that demonstrates the technique, using this very example of partial dates.
From my experience it is asking for problems if is not handled by calculated (on db server) column or trigger. But sometimes unavoidable when records numbers are high. But in this case such column could be calculated driectly in query but then we can't gain from indexes on db server.
@@zoran-horvat Didn't you use this in an earlier video on EF already, that was the first thing I thought of when you started writing the first implementation in this video.
@@grumpydeveloper69 Yes, the EF Core videos used the final, fully OO model, to which I still haven't got in this series of videos on anemic models. The idea is to cover all the steps that transform an anemic model into an OO model, which was then persisted using EF Core in those earlier videos.
Thanks Zoran, A beautiful example of polymorphism. One thing I can't let go of though is that your Year and YearMonth implementations are dynamically creating a DateOnly object with each call to Beginning and this is being accessed O(n²) times during the OrderByDescending linq method. Would this not kill performance and in the real world would you instantiate a private value for this and just return it instead? Thanks.
@@ChrisWalshZX That is a good question. First off, it is not O(n^2) but O(nlogn), as that is the worst case for modern sorting algorithms. Then, we come to the analysis of what happens inside one step. A DateOnly is just a struct, so accessing it merely means copying 4 bytes. Even creating a new instance in the YearOnly class is nothing but a few int assignments and range checks. On the other hand, the solution with property getters saves on memory access, which can often improve performance. Therefore, I don't expect a measurable impact on performance. But I do remember that this is one of the reasons we should keep property getters as small as possible and move all costly calculations into methods. That would give a hint to the callers that something might be going on and they should not let another method call the feature too many times, lest hurt performance.
@@zoran-horvat Thanks Zoran. Thanks for the O(nlogn) correction. In your example, I guess, a book never has that many authors so the amount of accesses are quite small. Sounds like some real world benchmarks might put me at ease!
@ChrisWalshZX Yes, and my guess is that the difference is negligible in most uses. When it turns out that the cost is prohibitive, and it would be in cases when the property/method is expensive, you can apply that maneuver with calculating all values once, zipping them together with the original objects, and then sorting the collection of key-value pairs without recalculating any of the keys again. In that pattern you trade memory for CPU time.
I like this. Polymorphism here prevented entropy entering the code (as a complex IF statement dealing with PartialDate), by placing the complexity closer to the source of data - in its model.
I was a bit excited to see you instantiate these polymorphic date classes, because there is where I struggle... I have had the anemic layout previously with enums to denote the type of date but then used static factory methods to instantiate them: if (partialDateType is PartialDateType.YearOnly) return PartialDate.YearOnly(); Of course I have used polymorphism for similar things as well, but one somehow has to determine which constructor to call (usually in a factory class/method). And I then wonder what the true benefits are for adding another class VS adding another static factory method. If the class holds extra data or methods, then ofc you would use a class... Any insights to share?
Having worked in C++ from 1990 and then C# from 2002, my observation is that not many new entrants know about OO inheritance and polymorphism, preferring to 'chain' functions using C# extensions (so they can 'comply' with 'O'pen for extension in SOLID) which in turn, seems to be a programming style that originated in the React/Javascript world. So we end up with devs using React-style programming with C# and ending up with un-maintainable code in both React and in C# !! Not sure how OO can be used to its full potential if people don't know the fundamentals!
The content is informative and to the point, as always. I have to say that the video editing is a bit distracting - the words jumping out and highlighting of each word focuses my attention to the word, instead on the content itself.
@@j3rmz87 They don't go together. Types in FP are much more than anemic classes - they build a rich representation through object composition. That is quite different from anemic models.
Would you be able to expand this further in any of your upcoming videos? I'm trying to wrap my head around what you've just said but doesn't seem to click in me yet
@@smatsri EF Core supports table-per-hierarchy model for polymorphic types. You can also use custom value converters where applicable, like the one I used in this demo. The PartialDate is converted to an int with zeroes in places of the missing data in this demo.
@@DaminGamerMC Not complicated at all. There are several recent videos where I have explained different tactics of persisting the fully object-oriented variant of this domain model using EF Core. It is not at all difficult because EF Core now supports working with everything I used in building that model - interfaces, records, complex object compositions, etc.
Thanks, Zoran! Lots of your materials are not some academic or wishful thinking but actual situations. Thanks to you, I see more and more flaws in my company's codebase and incoming PRs everyday... including mine😅
How would this behave when i introduce pagination as well? If i want only 10 books every time for example i still had to Order them first but with this code i would first load all books fro mthe database into the memory then sort and then take 10 books, what would be the way to sort them first and then take ten and then call toList() so that i only load the 10 books i actually need and not all?
Why not avoid all this hassle and just give the value 1st of January when it's not given? Then a new enum property specifying the type of date like: is it a year, month and year or full date?
@@ozkanb Can you implement a property Next, which returns the next partial date of the same kind? E.g. next of 2023-05 is 2023-06, and next of 2023-05-17 is 2023-05-18.
If you used 0 to represent an unspecified month/day and some large value to represent an unspecified year/date then the data could be sorted by the database without needing any of this code. Alternatively you could just add a non-polymorphic Beginning method to PartialDate that does the calculation you did in the query, also using a special value for unspecified year so you can make it not nullable.
@@shaurz You are trying to be clever where in fact your solution only works under very narrow assumptions that are not even part of the requirements. For example, you assume that the year 2023 should come before January 2023. There is no such requirement, and the actual requirement would probably be the opposite - I believe the customers would request January 2023 before year 2023 if that was ever brought up. Second issue is that you are defending a clever solution to one particular requirement, where that same idea doesn't work in any other case. Consider defining the Next property getter, which returns the next instance of a partial date within its kind. That is the problem with smart solutions - they are particular and guided by luck. A simple change in requirements turns into a torture. Why waste energy on a coding roulette when there is a regular design method, such as OOP or FP, that addresses all questions of that kind uniformly and successfully? I find your ckever ideas pointless, seriously.
@@zoran-horvat The date can be encoded as a single integer -- day in the low bits, month in the middle bits and year in the top bits. Day and month can be zero, to signify null. Then sorting just works out naturally. If you need to change the precedence of null, just adjust it so it uses the max value instead of zero. Can be done in database, in memory, or on the fly. It's compact, very efficient and easier to optimize. If you want to implement a "Next" function, you can just do it, I do not understand what your issue is. No information is lost, it's all in one place and you can transform it according to your needs. The standard Date types, my solution and your solution are all "clever" solutions that work only under narrow assumptions. There is no generic solution to all possible future problems, and believing in such thing is a fool's errand. If the requirements change, the code has to change. Except the OOP way will be harder to untangle, if it turns out that the abstraction no longer fits the problem at hand. > Why waste energy on a coding roulette when there is a regular design method, such as OOP or FP, that addresses all questions of that kind uniformly and successfully? The solution I described is the first thing that came to my mind. Why should I waste my energy on trying to force the problem into some dumb, sub-optimal design pattern?
@@mss664 I did all that in storage, where the partial date is indeed encoded in an int field. However, doing that in domain code ties your hands for no obvious benefits.
Great explanation. I feel like the next logical step after mapping such hierarchies for EF is also expressing them inside of APIs. Let's assume that there's a client requirement to be able to freely update a book's publication date. It can be Year updated to YearMonth to FullDate or any other way. And the client wants this to be exposed as an API that they could interact with. For the fun of it, let's assume we don't know what language/framework the clients wants to use for this, so if we want to use STJ for request deserialization, we can't rely on default json polymorphic tools that would require a type annotation to be present in the request. How would you approach such a requirement?
@@antonzhernosek5552 JSON is part of the contract, and so must be specified. There is already the endpoint that creates a book in this demo, and it incorporates the optional publication date as a string with some parts possibly missing. For example, that endpoint accepts these values as valid: "2023", "2023-05", "2023-05-17", all with an obvious meaning.
How about `public record struct PartialDate(int year, int month, int day);` and use zeros for month and day when they're not specified? You get sorting for free and it's easy to store in a database.
I mean you don't need polymorphism to improve the code. Most of the improved readability comes from the GetBeginning method and the Beginning getter. Also you could have used OrderByDescending from the beginning. I don't love the original PartialDate class but that is mostly because it is using a Date plus some bools. It could just have a year, an optional month, and an optional day.
@@alienm00sehunter That might work but only until the next request comes, such as calculating the next PartialDate. Such a property/method would consist of 50% copied code from the first one. And every other method would follow down that same path. Why? Do you like your code inflated by a factor of 2?
Simmilar effect defining Get Beginning method in old class will have.(Yoda speak😅). Yes it will have ifs but using subclasses those ifs(or swithches) move to the constructor or places when this class value will be initalised?
@@AK-vx4dy The caller would always have to know which form of a class it plans to instantiate, with or without a polymorphic type. The runtime branching will always be there, whether you did it with hardcoded ifs or left it to the runtime during dynamic dispatch. The difference is that, with a polymorphic type, your code will shrink by the size of the infrastructure around it. The speed would also remain pretty much the same, because branching exists either way.
@zoran-horvat Right. But code clarity is high in your version. I colud imagine franken-constructor-like procedure which generated type according to nulls in parameters, but I'm sure you would fire me instantly 😅 I rember when I first meet OOP/Inherticance in '90 I seen it as lines of code saving trick. And it worked perfectly in UI layer.
@@vincentcifello4435 Fine, let's start from a date with booleans and try to implement the rest of it. Let me give you a couple of methods the class will need to implement: Beginning, Next, Contains date, Contains partial date, OverlapsWith; then come three different implementations of IComparer. And there will be more. Here is the problem: virtually every method among these will repeat the same structure of if-elseif-else, with the same boolean tests in them. Is that the code you want to write? Code in which 30% is an identical copy of one another? You should know better than that.
@@vincentcifello4435 How can there be a date when parts are missing? In particular, I have mapped the partial date to and from an 8-digit positional int, such as 20230500. Then it all goes into one database column, which is even sortable according to one specific criterion.
If i have 10k books store in the database and i need just 20 per page It is hard to get all table data and process in memory How can i order by publication date using sql query or linq?
Either construct that complicated expression that would turn into SQL and do all the work in the database, or use a different approach, such as denormalization. Both would work fine in this case.
I probably would've just had PartialDate implement IComparer and called it a day, lol. Put the naff stuff there, then just consume it as books.OrderByDescending(b => b.PublicationDate == null).ThenByDescending(b => b.PublicationDate);
@@billy65bob Nope. There are more than one ways to compare, such as by the middle of the interval or its end. It would have to be a separate IComparer class for each criterion. BTW there exists no total implementation of IComparable on this class. Your implementation would violate the IComparable contract.
@@zoran-horvat No, the implementation would've been completely fine; build a full featured DateOnly (assuming January 1st where necessary) and return that comparison. It's a perfectly fine solution, it's only downside is that unlike your refactor, it doesn't resolve the overloaded data clump. Do you perhaps have a different idea of "contracts" here? The only guarantee I expect with comparers can be roughly summarised as: (a < b) == !(a > b)
@billy65bob You cannot make that assumption about January 1st in the class. That assumption belongs to the caller and it might not hold in a different caller. That is why I said there must be multiple implementations of IComparer. Each would implement one assumption.
@@billy65bob What? Now you're moving it to ad hominem where in fact you misplaced a class responsibility. Your proposal is plain incorrect and there is no way to fix it.
The example is not the best as you could've extracted the logic into a method and call that method right from OrderByDescending as you did for the polymorphic version. However, splitting the class into multiple ones to always have consistent state is an extremely good point. Although even when it's split (subtype polymorphism) one doesn't *have to* use the virtual methods for ad-hoc polymorphism, the same can be achieved by pattern matching/algebras. I'm not saying patmat would work better in your example, just saying it's another option and there are cases when patmat works better than virtual calls (unstable operations/interface vs unstable data/hierarchy). Two solutions to the expression problem.
Great video. The responsibility of creating the right type of Date object is now responsibility of infrastructure layer. The video would look complete if those code changes are also shown.
It is the responsibility of the UI layer, too. The data come from both sources. There are several earlier videos that speak about implementing storage and querying of these dates using EF Core.
Or you just add a computed column to your table with your "magic" publication date, and you sort on that and let the db handle it. Keeps your C# even cleaner.
@@GuidoSmeets385 That is another lesson - denormalization. The trouble is that there will be yet another column to add with the date after the interval, so it becomes a tad more complex. Anyway, that is a legitimate tradeoff - sacrificing one database column for the sake of faster queries. It is not universally applicable, but it applies in this example.
You first perform ".ToListAsync()" and then when we have data in memory you perform sorting. Are you sure this is good idea? What if you would have 10 mln books in database?
@@arkadiuszj2304 That is a good question. The reason for my decision is that I didn't want to build a very complicated lambda that would do all the transforms in the database, so I used common C# to sort the data after materializing the objects. That was a conscious tradeoff. Then, what happens when that tradeoff turns too expensive? Besides really constructing that awful lambda that would translate into an equally awful SQL, we can also denormalize the data. Besides the main representation of the partial date, which indicates a range of dates, we can also store one or more calculated dates, such as the beginning and end of the range. In that case, we could query and sort trivially by the redundant form of the field. Data denormalization trades database storage for querying speed or ease, and it is usually quite an effective tradeoff.
Yeah no this is bananas. You started with one class, 3 functions and one constructor and ended with 4 classes, 3 constructors and 3 overrides for the functional equivalent of renaming the original class's Date field to Beginning.... They need to change that Donald Knuth quote from optimization to abstraction, not like anyone does any optimization these days anyways.
Polymorphism is great, but hardly unique to OOP or DDD. You'd be hard pressed to write a non-trivial program that doesn't use ad-hoc, parametric or subtype polymorphism, even if by accident. The unique aspect of OOP is conflating inheritance and subtype polymorphism into subclasses, which is largely seen as a mistake. This is where 'prefer composition over inheritance' comes from. It turns out inheriting implementations usually causes more problems than it solves.
The bad rep comes from people using oop for logic, not for data. In most programs I have seen, data is basically DTOs or POCOs and the logic is a mess of classes inheriting from other classes overriding unintelligible methods where all sense of flow is lost.
@@unexpectedkAs That's been one of my frustrations with OOP since the start. If only ~10% actually do it right, maybe it isn't such a good tool after all.
I feel like you've explained the value of the polymorphic publication date a lot of times now. This time it's sorting, okay, but the point is the same each time.
@@KristianVidemarkParkov None of the previous three videos in the series had a single polymorphic type in it. This video is the result of many commenters asking how to turn that anemic model from previous demos into an object-oriented model.
Implementations should not be abstract. Only interfaces are truly abstract as they have no executable code. Executable code is by definition concrete. Instead of "abstract" types, we should use concrete types in composition. By using composition, you can eliminate the use of inheritance and the unnecessary rigid hierarchies they create.
Bad advice. When debugging, keeping code from multiple different files in your head is rather difficult. With the original code, it was there all in one place. What tutorials like this miss, is that real code can be heaps more complicated than these examples. There will be edgecase after edgecase. And when you need to consider all of them to fix a difficult bug, you will wish that you haven't scattered all these edgecase fixes all over the place.
@zoran-horvat I routinely edit source files that are at least 500 lines long. Just recently I finished an ECS for my game engine, that's about 2000 LoC in total. I am currently in the process of connecting that to my Vulkan backend, so that I can render meshes by attaching components to my gameobjects. including my entire Vulkan backend and the ECS, these are about 5000 LoC. It has lots of moving parts, but I am looking at what, 10-15 files maybe? Now imagine spreading these over many mini files with 70-100 LoC each. I'd be looking at 50-70 files! Imagine debugging this! Now, my engine is a comparable small hobby project with only 30000 LoC in total. But let me give you an example from my work: At work we have a repo that is 770000 LoC. I work on it daily. It uses many custom libraries we've written internally, but are stored in other repos, which I also look at from time to time. So while it's hard to estimate, I think it isn't a stretch to say that this program has at least 1 million LoC in total. Now of course you don't need to know the entire source code to implement a feature or fix a bug, as any given feature/bug touches only so much around it. But due to its age and the amount of people that worked on it in the past, it has plenty of legacy code that is written in this ideal "no file has more than 100 LoC", resulting in tons of files. Let me give you an example how this looks like: Our programm can import and display many different file formats. When importing one, the code must choose the right importer. Now, we have a ImportViewModel, which handles the call from the ui, a CommandExecuter, that fires an ImportCommand, a SceneImporter, which is called from the command, a FileProcessor, that extracts the file type, an ImporterFactory, that chooses the importer for the file, the Importer itself, which finally calls the logic to import the file. Each of these have about 100 LoC. Also, double the file count because each has an interface, because dependency injection and mocking for tests. It bloats the callstack like crazy. A junior programmer would have so much trouble figuring out how to even reach the import code. I've worked on this program for long enough, that this has become a routine. My workflow is like this: "I want to make changes to x importer. I don't know what it's called, but I know FileProcessor is within the callstack. So I open FileExplorer, click 20 times 'goto implementation' and I am there." For the most difficult bugs I've ever fixed in there, it was almost always code that is scattered between many files. Have you ever debugged code where you had to keep 30 simultaneous tabs open? It is pain i tell you. Someone throws an exception? Well, who in the 30 files could it be?Does anyone catch this? Difficult to tell when you need to observe 30 files at once. My rule of thumb is to encapsulate behaviour, not arbitrary line counts or subjective "asthetics". Prefer locality and inline private methods that are used just once. If that ends up with files that are 1000 LoC, then let it be it. You'll thank yourself when you need to debug it in 5 years.
@@zoran-horvat If you really have a 2000 line method, use defensive programming/early returns to avoid nesting. Use comments to annotate blocks; don't comment every line, but entire blocks. They guide the eye and give brief overview for the first time reader. Avoid complicated if-statements, and break them down in multiple bool-variables. Avoid side effects and keep state local. The entire point of inlining private methods is that you know a block below comes after a block above. If single use private methods are scattered up and down in the file or even other files, it requires you to have a mental map of it all. From experience I can tell you, a huge 2000 line method (which isn't terribly nested), hugely decreases the required mental capacity.
Just use the DateOnly date directly? You're already setting the month to 1 when it's "not used" and the same for the day. Also, you could have used `.OrderByDescending` from the very beginning without introducing your PartialDate abstract class. Not sure about modern Java syntax but pretty sure you could have passed `book => book.Publication.PublicationDate ?? DateOnly.MaxValue` from the very beginning.
Could you not have just done `book => book.Publication.PublicationDate?.Date ?? DateOnly.MaxValue` as the separate static create methods on PartialDate are all creating that Date property? Or to use the method on the publication, `book => book.Publication.GetBeginning(DateOnly.MaxValue)` and in there use the PartialDate's Date property? Or am I missing something that the abstract inheritance approach is adding (to this scenario)?
@@GavinForrit There is no date in the PartialDate class. It serves three cases: a date, a month and year, or just a year. Comparing these among each other required a consolidation strategy which is the choice of the caller. A different caller might make a different choice.
@@zoran-horvat @2:32 There's a property called Date on PartialDate that is a DateOnly and is set appropriately by the ctor via the three different static methods, no? Not disputing there are other benefits to the OOP approach, just pointing out the calculating of the date when getting the ordered books didn't need to happen there as that was already performed in the PartialDate class upon construction.
@GavinForrit That is before the redesign, while that type was exhibiting serious usability issues. I do not count that type as part of the solution. Look at the final design only.
Learn more at Brilliant brilliant.org/ZoranHorvat/
This link gives you a 30-day free trial and 20% off of an annual premium subscription.
"If there's anything I like more than writing code, it's deleting code" -- words to live by!
"Welcome to the tar pit of real world projects" -- more words to live by
reminds me of the way lichess creator thinks. The less code there is the less code there is to maintain and as a result it's much easier to continue working on the project.
How 1 Software Engineer Outperforms 138 - Lichess Case Study
was a fun video as well.
Deleting code is the greatest joy of programming.
As someone with no professional experience when it comes to coding, your videos are a gold mine. Thanks a lot!
Brilliant!
There are so many videos on TH-cam trashing OOP saying it requires too much boilerplate, the details are obscure, can't find the lines that actually do something and more. Their real problem is that they didn't get to the right abstractions.
@@nickbarton3191 I agree. OOP is a very good modeling method.
When people say OOP, they usually mean inheritance. Inheritance is legitimately bad. Even though things like mixins can reduce code duplication, the bad habits it encourages isn't worth it. More often than not, an object with a unique implementation also needs a unique interface. Inheritance inhibits that and mixins often do as well.
@T1Oracle Deep inheritance is bad, it's a terrible way to reuse code. Polymorphism based on Interfaces or virtual classes is the way to go.
Rarely do I go further than an Interface with alternative implementations and reuse using pure static extension methods.
If you can calculate the value of sort order DateOnly for all kinds of partial dates, you can also store that value in the db. A calculated property in code, saved to the db. If the space taken by the extra Date column is not prohibitively large :)
A calculated column at SQL level in the db would not be ideal, since if you do any change on the publication date in code, won't be reflected by the sort order. But could work in a pinch, if you really can't change the code, but can create issues later.
That way you can also query by this directly from the db, so you are not over-fetching just to calculate the sort order, and maybe throw away a bunch of data because you only need like the 20 latest books by some criteria.
@@AkosLukacs42 You are correct. That comes under the technique of denormalization, adding redundant information to the storage in order to speed up subsequent operations.
It might amuse you to learn that I have a video in the queue that demonstrates the technique, using this very example of partial dates.
From my experience it is asking for problems if is not handled by calculated (on db server) column or trigger. But sometimes unavoidable when records numbers are high.
But in this case such column could be calculated driectly in query but then we can't gain from indexes on db server.
@AK-vx4dy Denormalization is a performance trade-off.
@@zoran-horvat Didn't you use this in an earlier video on EF already, that was the first thing I thought of when you started writing the first implementation in this video.
@@grumpydeveloper69 Yes, the EF Core videos used the final, fully OO model, to which I still haven't got in this series of videos on anemic models. The idea is to cover all the steps that transform an anemic model into an OO model, which was then persisted using EF Core in those earlier videos.
"If there's anything I like more than writing code, it's deleting code" 😊
@@StephaneAmStrong That is true. It was a confession.
absolutely @@zoran-horvat
Thanks Zoran,
A beautiful example of polymorphism.
One thing I can't let go of though is that your Year and YearMonth implementations are dynamically creating a DateOnly object with each call to Beginning and this is being accessed O(n²) times during the OrderByDescending linq method. Would this not kill performance and in the real world would you instantiate a private value for this and just return it instead?
Thanks.
@@ChrisWalshZX That is a good question. First off, it is not O(n^2) but O(nlogn), as that is the worst case for modern sorting algorithms. Then, we come to the analysis of what happens inside one step. A DateOnly is just a struct, so accessing it merely means copying 4 bytes. Even creating a new instance in the YearOnly class is nothing but a few int assignments and range checks. On the other hand, the solution with property getters saves on memory access, which can often improve performance. Therefore, I don't expect a measurable impact on performance. But I do remember that this is one of the reasons we should keep property getters as small as possible and move all costly calculations into methods. That would give a hint to the callers that something might be going on and they should not let another method call the feature too many times, lest hurt performance.
@@zoran-horvat Thanks Zoran. Thanks for the O(nlogn) correction. In your example, I guess, a book never has that many authors so the amount of accesses are quite small. Sounds like some real world benchmarks might put me at ease!
@ChrisWalshZX Yes, and my guess is that the difference is negligible in most uses. When it turns out that the cost is prohibitive, and it would be in cases when the property/method is expensive, you can apply that maneuver with calculating all values once, zipping them together with the original objects, and then sorting the collection of key-value pairs without recalculating any of the keys again. In that pattern you trade memory for CPU time.
I like this. Polymorphism here prevented entropy entering the code (as a complex IF statement dealing with PartialDate), by placing the complexity closer to the source of data - in its model.
I was a bit excited to see you instantiate these polymorphic date classes, because there is where I struggle... I have had the anemic layout previously with enums to denote the type of date but then used static factory methods to instantiate them:
if (partialDateType is PartialDateType.YearOnly)
return PartialDate.YearOnly();
Of course I have used polymorphism for similar things as well, but one somehow has to determine which constructor to call (usually in a factory class/method). And I then wonder what the true benefits are for adding another class VS adding another static factory method. If the class holds extra data or methods, then ofc you would use a class...
Any insights to share?
Having worked in C++ from 1990 and then C# from 2002, my observation is that not many new entrants know about OO inheritance and polymorphism, preferring to 'chain' functions using C# extensions (so they can 'comply' with 'O'pen for extension in SOLID) which in turn, seems to be a programming style that originated in the React/Javascript world. So we end up with devs using React-style programming with C# and ending up with un-maintainable code in both React and in C# !!
Not sure how OO can be used to its full potential if people don't know the fundamentals!
C# also supports functional programming, through LINQ, records, and extension methods. Be sure not to mix that up by expecting it to be OO.
@@zoran-horvat Yes, agreed. And all of that LINQ stuff is implemented via extensions!
The content is informative and to the point, as always.
I have to say that the video editing is a bit distracting - the words jumping out and highlighting of each word focuses my attention to the word, instead on the content itself.
How do you balance functional programming and anemic domain models?
@@j3rmz87 They don't go together. Types in FP are much more than anemic classes - they build a rich representation through object composition. That is quite different from anemic models.
Would you be able to expand this further in any of your upcoming videos?
I'm trying to wrap my head around what you've just said but doesn't seem to click in me yet
@@j3rmz87 I have a few videos on functional modeling. If you watch them, you will see that the types in them are anything but anemic.
how does ef create the abstract class?
@@smatsri EF Core supports table-per-hierarchy model for polymorphic types. You can also use custom value converters where applicable, like the one I used in this demo. The PartialDate is converted to an int with zeroes in places of the missing data in this demo.
What i would love to know is how you make this classes work with EF. Configuration classes are probably really complicated.
@@DaminGamerMC Not complicated at all. There are several recent videos where I have explained different tactics of persisting the fully object-oriented variant of this domain model using EF Core. It is not at all difficult because EF Core now supports working with everything I used in building that model - interfaces, records, complex object compositions, etc.
Thanks, Zoran! Lots of your materials are not some academic or wishful thinking but actual situations. Thanks to you, I see more and more flaws in my company's codebase and incoming PRs everyday... including mine😅
How would this behave when i introduce pagination as well? If i want only 10 books every time for example i still had to Order them first but with this code i would first load all books fro mthe database into the memory then sort and then take 10 books, what would be the way to sort them first and then take ten and then call toList() so that i only load the 10 books i actually need and not all?
Why not avoid all this hassle and just give the value 1st of January when it's not given? Then a new enum property specifying the type of date like: is it a year, month and year or full date?
@@ozkanb Can you implement a property Next, which returns the next partial date of the same kind? E.g. next of 2023-05 is 2023-06, and next of 2023-05-17 is 2023-05-18.
@zoran-horvat All depends on the requirements. Good illustration on polymorphism and being strongly typed. Great video 👍.
If you used 0 to represent an unspecified month/day and some large value to represent an unspecified year/date then the data could be sorted by the database without needing any of this code. Alternatively you could just add a non-polymorphic Beginning method to PartialDate that does the calculation you did in the query, also using a special value for unspecified year so you can make it not nullable.
@@shaurz You are trying to be clever where in fact your solution only works under very narrow assumptions that are not even part of the requirements.
For example, you assume that the year 2023 should come before January 2023. There is no such requirement, and the actual requirement would probably be the opposite - I believe the customers would request January 2023 before year 2023 if that was ever brought up.
Second issue is that you are defending a clever solution to one particular requirement, where that same idea doesn't work in any other case. Consider defining the Next property getter, which returns the next instance of a partial date within its kind.
That is the problem with smart solutions - they are particular and guided by luck. A simple change in requirements turns into a torture.
Why waste energy on a coding roulette when there is a regular design method, such as OOP or FP, that addresses all questions of that kind uniformly and successfully? I find your ckever ideas pointless, seriously.
@@zoran-horvat The date can be encoded as a single integer -- day in the low bits, month in the middle bits and year in the top bits. Day and month can be zero, to signify null. Then sorting just works out naturally. If you need to change the precedence of null, just adjust it so it uses the max value instead of zero. Can be done in database, in memory, or on the fly. It's compact, very efficient and easier to optimize. If you want to implement a "Next" function, you can just do it, I do not understand what your issue is. No information is lost, it's all in one place and you can transform it according to your needs.
The standard Date types, my solution and your solution are all "clever" solutions that work only under narrow assumptions. There is no generic solution to all possible future problems, and believing in such thing is a fool's errand. If the requirements change, the code has to change. Except the OOP way will be harder to untangle, if it turns out that the abstraction no longer fits the problem at hand.
> Why waste energy on a coding roulette when there is a regular design method, such as OOP or FP, that addresses all questions of that kind uniformly and successfully?
The solution I described is the first thing that came to my mind. Why should I waste my energy on trying to force the problem into some dumb, sub-optimal design pattern?
@@mss664 I did all that in storage, where the partial date is indeed encoded in an int field. However, doing that in domain code ties your hands for no obvious benefits.
Great explanation.
I feel like the next logical step after mapping such hierarchies for EF is also expressing them inside of APIs. Let's assume that there's a client requirement to be able to freely update a book's publication date. It can be Year updated to YearMonth to FullDate or any other way.
And the client wants this to be exposed as an API that they could interact with.
For the fun of it, let's assume we don't know what language/framework the clients wants to use for this, so if we want to use STJ for request deserialization, we can't rely on default json polymorphic tools that would require a type annotation to be present in the request.
How would you approach such a requirement?
@@antonzhernosek5552 JSON is part of the contract, and so must be specified. There is already the endpoint that creates a book in this demo, and it incorporates the optional publication date as a string with some parts possibly missing. For example, that endpoint accepts these values as valid: "2023", "2023-05", "2023-05-17", all with an obvious meaning.
How about `public record struct PartialDate(int year, int month, int day);` and use zeros for month and day when they're not specified? You get sorting for free and it's easy to store in a database.
@@JeffreyRennie How would you implement Next on that record, a property that returns the next partial date of the same kind?
I mean you don't need polymorphism to improve the code. Most of the improved readability comes from the GetBeginning method and the Beginning getter. Also you could have used OrderByDescending from the beginning. I don't love the original PartialDate class but that is mostly because it is using a Date plus some bools. It could just have a year, an optional month, and an optional day.
@@alienm00sehunter That might work but only until the next request comes, such as calculating the next PartialDate. Such a property/method would consist of 50% copied code from the first one. And every other method would follow down that same path. Why? Do you like your code inflated by a factor of 2?
Simmilar effect defining Get Beginning method in old class will have.(Yoda speak😅). Yes it will have ifs but using subclasses those ifs(or swithches) move to the constructor or places when this class value will be initalised?
@@AK-vx4dy The caller would always have to know which form of a class it plans to instantiate, with or without a polymorphic type.
The runtime branching will always be there, whether you did it with hardcoded ifs or left it to the runtime during dynamic dispatch. The difference is that, with a polymorphic type, your code will shrink by the size of the infrastructure around it. The speed would also remain pretty much the same, because branching exists either way.
@zoran-horvat Right. But code clarity is high in your version.
I colud imagine franken-constructor-like procedure which generated type according to nulls in parameters, but I'm sure you would fire me instantly 😅
I rember when I first meet OOP/Inherticance in '90 I seen it as lines of code saving trick. And it worked perfectly in UI layer.
PartialDate is a Date with defaults and booleans. How could this not be part of the database?
@@vincentcifello4435 Fine, let's start from a date with booleans and try to implement the rest of it.
Let me give you a couple of methods the class will need to implement: Beginning, Next, Contains date, Contains partial date, OverlapsWith; then come three different implementations of IComparer. And there will be more.
Here is the problem: virtually every method among these will repeat the same structure of if-elseif-else, with the same boolean tests in them. Is that the code you want to write? Code in which 30% is an identical copy of one another?
You should know better than that.
@@zoran-horvat Huh? You said "the Date is not part of the database". I asked how that was possible when PartialDate has a Date field.
@@vincentcifello4435 How can there be a date when parts are missing? In particular, I have mapped the partial date to and from an 8-digit positional int, such as 20230500. Then it all goes into one database column, which is even sortable according to one specific criterion.
If i have 10k books store in the database and i need just 20 per page
It is hard to get all table data and process in memory
How can i order by publication date using sql query or linq?
Either construct that complicated expression that would turn into SQL and do all the work in the database, or use a different approach, such as denormalization. Both would work fine in this case.
I probably would've just had PartialDate implement IComparer and called it a day, lol.
Put the naff stuff there, then just consume it as books.OrderByDescending(b => b.PublicationDate == null).ThenByDescending(b => b.PublicationDate);
@@billy65bob Nope. There are more than one ways to compare, such as by the middle of the interval or its end. It would have to be a separate IComparer class for each criterion.
BTW there exists no total implementation of IComparable on this class. Your implementation would violate the IComparable contract.
@@zoran-horvat No, the implementation would've been completely fine; build a full featured DateOnly (assuming January 1st where necessary) and return that comparison.
It's a perfectly fine solution, it's only downside is that unlike your refactor, it doesn't resolve the overloaded data clump.
Do you perhaps have a different idea of "contracts" here? The only guarantee I expect with comparers can be roughly summarised as: (a < b) == !(a > b)
@billy65bob You cannot make that assumption about January 1st in the class. That assumption belongs to the caller and it might not hold in a different caller.
That is why I said there must be multiple implementations of IComparer. Each would implement one assumption.
@@zoran-horvat You are making it far more complicated than it needs to be just to win an internet argument.
@@billy65bob What? Now you're moving it to ad hominem where in fact you misplaced a class responsibility. Your proposal is plain incorrect and there is no way to fix it.
The example is not the best as you could've extracted the logic into a method and call that method right from OrderByDescending as you did for the polymorphic version. However, splitting the class into multiple ones to always have consistent state is an extremely good point. Although even when it's split (subtype polymorphism) one doesn't *have to* use the virtual methods for ad-hoc polymorphism, the same can be achieved by pattern matching/algebras. I'm not saying patmat would work better in your example, just saying it's another option and there are cases when patmat works better than virtual calls (unstable operations/interface vs unstable data/hierarchy). Two solutions to the expression problem.
Great video. The responsibility of creating the right type of Date object is now responsibility of infrastructure layer.
The video would look complete if those code changes are also shown.
It is the responsibility of the UI layer, too. The data come from both sources.
There are several earlier videos that speak about implementing storage and querying of these dates using EF Core.
"The most reliable gear is the one that is designed out of the machine." -Eliezer Yudkowsky
Fine art
Interesting video, but I don't delete code. I simply move it into a folder full of examples.
Or you just add a computed column to your table with your "magic" publication date, and you sort on that and let the db handle it. Keeps your C# even cleaner.
@@GuidoSmeets385 That is another lesson - denormalization. The trouble is that there will be yet another column to add with the date after the interval, so it becomes a tad more complex.
Anyway, that is a legitimate tradeoff - sacrificing one database column for the sake of faster queries. It is not universally applicable, but it applies in this example.
You first perform ".ToListAsync()" and then when we have data in memory you perform sorting. Are you sure this is good idea? What if you would have 10 mln books in database?
@@arkadiuszj2304 That is a good question. The reason for my decision is that I didn't want to build a very complicated lambda that would do all the transforms in the database, so I used common C# to sort the data after materializing the objects. That was a conscious tradeoff. Then, what happens when that tradeoff turns too expensive?
Besides really constructing that awful lambda that would translate into an equally awful SQL, we can also denormalize the data. Besides the main representation of the partial date, which indicates a range of dates, we can also store one or more calculated dates, such as the beginning and end of the range. In that case, we could query and sort trivially by the redundant form of the field. Data denormalization trades database storage for querying speed or ease, and it is usually quite an effective tradeoff.
Yeah no this is bananas. You started with one class, 3 functions and one constructor and ended with 4 classes, 3 constructors and 3 overrides for the functional equivalent of renaming the original class's Date field to Beginning.... They need to change that Donald Knuth quote from optimization to abstraction, not like anyone does any optimization these days anyways.
Did you miss the point? Let me see... yes, you missed the point.
Counting constructors? I'll remember that.
Polymorphism is great, but hardly unique to OOP or DDD. You'd be hard pressed to write a non-trivial program that doesn't use ad-hoc, parametric or subtype polymorphism, even if by accident.
The unique aspect of OOP is conflating inheritance and subtype polymorphism into subclasses, which is largely seen as a mistake. This is where 'prefer composition over inheritance' comes from. It turns out inheriting implementations usually causes more problems than it solves.
The bad rep comes from people using oop for logic, not for data. In most programs I have seen, data is basically DTOs or POCOs and the logic is a mess of classes inheriting from other classes overriding unintelligible methods where all sense of flow is lost.
@@unexpectedkAs That's been one of my frustrations with OOP since the start. If only ~10% actually do it right, maybe it isn't such a good tool after all.
duh, ostavi kode na github ne iza paywall
@@TankaNafaka Besplatan video nije dovoljan?
@@TankaNafaka P.S. A što se nisi prijavio na onu grupu gde besplatno dajem taj kod?
This is like the fifth video in a row with the same content.
@@KristianVidemarkParkov What do you mean the same content?
P.S. it's the fourth video in the series, not fifth.
I feel like you've explained the value of the polymorphic publication date a lot of times now. This time it's sorting, okay, but the point is the same each time.
@@KristianVidemarkParkov None of the previous three videos in the series had a single polymorphic type in it.
This video is the result of many commenters asking how to turn that anemic model from previous demos into an object-oriented model.
💐💐💐
Implementations should not be abstract. Only interfaces are truly abstract as they have no executable code. Executable code is by definition concrete. Instead of "abstract" types, we should use concrete types in composition. By using composition, you can eliminate the use of inheritance and the unnecessary rigid hierarchies they create.
@@T1Oracle Blanket statements usually ignore a bigger part of reality. Take this one as an example.
Btw, a great video would be one that demostrates writing your own Dependency Inversion class. ;)
Bad advice. When debugging, keeping code from multiple different files in your head is rather difficult. With the original code, it was there all in one place.
What tutorials like this miss, is that real code can be heaps more complicated than these examples. There will be edgecase after edgecase. And when you need to consider all of them to fix a difficult bug, you will wish that you haven't scattered all these edgecase fixes all over the place.
@@rismosch What do you keep in your head in a 10.000 LoC module?
@zoran-horvat I routinely edit source files that are at least 500 lines long. Just recently I finished an ECS for my game engine, that's about 2000 LoC in total. I am currently in the process of connecting that to my Vulkan backend, so that I can render meshes by attaching components to my gameobjects. including my entire Vulkan backend and the ECS, these are about 5000 LoC. It has lots of moving parts, but I am looking at what, 10-15 files maybe? Now imagine spreading these over many mini files with 70-100 LoC each. I'd be looking at 50-70 files! Imagine debugging this!
Now, my engine is a comparable small hobby project with only 30000 LoC in total. But let me give you an example from my work: At work we have a repo that is 770000 LoC. I work on it daily. It uses many custom libraries we've written internally, but are stored in other repos, which I also look at from time to time. So while it's hard to estimate, I think it isn't a stretch to say that this program has at least 1 million LoC in total. Now of course you don't need to know the entire source code to implement a feature or fix a bug, as any given feature/bug touches only so much around it. But due to its age and the amount of people that worked on it in the past, it has plenty of legacy code that is written in this ideal "no file has more than 100 LoC", resulting in tons of files.
Let me give you an example how this looks like: Our programm can import and display many different file formats. When importing one, the code must choose the right importer. Now, we have a ImportViewModel, which handles the call from the ui, a CommandExecuter, that fires an ImportCommand, a SceneImporter, which is called from the command, a FileProcessor, that extracts the file type, an ImporterFactory, that chooses the importer for the file, the Importer itself, which finally calls the logic to import the file. Each of these have about 100 LoC. Also, double the file count because each has an interface, because dependency injection and mocking for tests. It bloats the callstack like crazy. A junior programmer would have so much trouble figuring out how to even reach the import code. I've worked on this program for long enough, that this has become a routine. My workflow is like this: "I want to make changes to x importer. I don't know what it's called, but I know FileProcessor is within the callstack. So I open FileExplorer, click 20 times 'goto implementation' and I am there."
For the most difficult bugs I've ever fixed in there, it was almost always code that is scattered between many files. Have you ever debugged code where you had to keep 30 simultaneous tabs open? It is pain i tell you. Someone throws an exception? Well, who in the 30 files could it be?Does anyone catch this? Difficult to tell when you need to observe 30 files at once.
My rule of thumb is to encapsulate behaviour, not arbitrary line counts or subjective "asthetics". Prefer locality and inline private methods that are used just once. If that ends up with files that are 1000 LoC, then let it be it. You'll thank yourself when you need to debug it in 5 years.
@rismosch How do you vary implementation in a 2000 lines long block?
@@zoran-horvat If you really have a 2000 line method, use defensive programming/early returns to avoid nesting. Use comments to annotate blocks; don't comment every line, but entire blocks. They guide the eye and give brief overview for the first time reader. Avoid complicated if-statements, and break them down in multiple bool-variables. Avoid side effects and keep state local.
The entire point of inlining private methods is that you know a block below comes after a block above. If single use private methods are scattered up and down in the file or even other files, it requires you to have a mental map of it all. From experience I can tell you, a huge 2000 line method (which isn't terribly nested), hugely decreases the required mental capacity.
@@rismosch How do you vary the implementation? You didn't say that.
UH-OH: Teaching how to use Object-oriented principles? Now the Kotlin and Rust script-kiddies won't be able to sleep until next christmas.
You get that polymorphism is everywhere and not just OOP, right?
Bizarre comment...
Just use the DateOnly date directly? You're already setting the month to 1 when it's "not used" and the same for the day.
Also, you could have used `.OrderByDescending` from the very beginning without introducing your PartialDate abstract class. Not sure about modern Java syntax but pretty sure you could have passed `book => book.Publication.PublicationDate ?? DateOnly.MaxValue` from the very beginning.
@@hasen_judi The first paragraph is a guesswork. You shouldn't make such assumptions, but I see what made you do that.
Could you not have just done `book => book.Publication.PublicationDate?.Date ?? DateOnly.MaxValue` as the separate static create methods on PartialDate are all creating that Date property? Or to use the method on the publication, `book => book.Publication.GetBeginning(DateOnly.MaxValue)` and in there use the PartialDate's Date property? Or am I missing something that the abstract inheritance approach is adding (to this scenario)?
@@GavinForrit There is no date in the PartialDate class. It serves three cases: a date, a month and year, or just a year. Comparing these among each other required a consolidation strategy which is the choice of the caller. A different caller might make a different choice.
@@zoran-horvat @2:32 There's a property called Date on PartialDate that is a DateOnly and is set appropriately by the ctor via the three different static methods, no? Not disputing there are other benefits to the OOP approach, just pointing out the calculating of the date when getting the ordered books didn't need to happen there as that was already performed in the PartialDate class upon construction.
@GavinForrit That is before the redesign, while that type was exhibiting serious usability issues. I do not count that type as part of the solution. Look at the final design only.