I only recently discovered your channel, Mr Horvat, but I must say -- I believe you are already my fav developer-instructor. Your content is higher-level but pragmatic, and you are EXCELENT with your presentation. You do have a strong accent which, in your case -- actually enhances your clarity. Or perhaps you are taking great care to speak deliberately and clearly? Regardless, I love how you present your topics, I appreciate the work you put into these and THANK YOU for sharing this excellent instruction and advice.
@zoran-horvat I think there were two good lessons in this video. 1) Don't obsess over primate types for structured data. 2) Small changes, otherwise you end up in a mess and can't remember what problem you're even tying to solve.
Excellent video. Looking forward to the next one. String especially can be painful. A lot of "strings" are actually "html strings", hence a + b should not necessarily be a simple concatenation. So many urls are often stored in strings, instead of a Uri. There many, many "types" of string and not understanding how to combine "types" of string is huge cause of bugs and even security issues (see "SQL strings").
I am curious about what should qualify as a "domain model", and what is adding additional unnecessary complexity to the code. By way of example; Let's say that I have a Fraction class that takes in two integer values, a Nominator and Denominator. Of course, I need the Denominator to be a non-zero value, otherwise we'll end up dividing by zero! Now, I could use a smart constructor to perform the validation on the denominator value, but I wonder if it would be more appropriate to instead have a NonZeroInt (or in C# 11+, NonZero where T : INumber, using an instance of NonZero) class, which itself has a smart constructor and performs that validation, which allows me to simply pass a normal integer value for the numerator and a NonZeroInt in the denominator. Is this approach overkill? Because I'm not sure if there is necessarily anything domain worthy about a numerical value not being equal to zero, but it would allow me to abstract away a lot of instances where I would otherwise be performing the same validation checks in other parts of the code.
Domain model is the model of the problem domain - a projection of reality to a finite set of dimensions. Anything that has a place in customer's requirements is almost certainly qualified to be part of the domain model. DDD is going one step further and introduces ubiquitous language, requesting both customers and code to name things the same.
@@metallixbrother Not necessarily - if what you are saying is the domain requirement, then it should be implemented as a domain type. Namely, if there is importance in a certain number never being zero, then that is a candidate for an explicit model.
The most annoying thing with rich domain modelling is that it almost immediately hits the wall with EF. It's much better these days compared to what we had before, but still lacking some critical features. And then we have a choice either to introduce conversion layer from domain entities to db DTOs, or constrain ourselves to what is supported.
This is great advice, and given the context of a c# application with buisness logic this seems like a good default practice in most cases. Your point about the inflexibility of them is well taken. However if applied more generally I would say primitives have some advantages depending on the circumstances. 1, Depending on the language using primitives versus objects can have performance implications (stack/heap allocations, string copying, etc). 2, Primitives are standard in the language and understood without needing to reading specific implementation details (although for a counterpoint, primitives can be more vauge - what is this string doing exactly?). 3, And they can have a less pronounced benefit in other use cases /programming paradigms (a struct of one primitive versus a primitive in a data driven application where the logic in not contained in the object). I think one could argue the addition of primitives to that struct could be anologous to extending a class but I think the main point is are you returning a value/object that is extensible to your needs. Just thought I'd share some ideas, I work on predominantly low level c++ code so that's where I'm coming from.
Great content, better than most other c# channels :) , The only small suggestion or conern I have is the usage of implict operator for Citation instead of proper static factory method, but I also understand, this is the most obvious way to not break any existing code
That was precisely the idea - to try not to cause any change in any other class before that step ends and we verify that nothing is broken. The final goal is to delete the conversion operator when the entire redesign is over.
As always, great video. One thing that might be a good content for the future video is to use more strict access modifiers for classes to communicate with a dev more clearly about the intentions but also provide some encapsulation so that classes like Citation cannot be used in, let say, Infrastructure layer.
I am not a fan of hiding models. What would be the reason not to allow persisting Citation instances, for example? When I want to indicate that some models are better used indirectly, I place them into a nested namespace. The parent namespace then provides all the means necessary to declare and create instances, but the models themselves are public like any other. That for one thing dismisses the question of how to test them.
I like the pragmatic way you've approached this problem. However, when you were splitting into segments, I would have made the backward compatible string constructor split on commas rather than treat the entire list as a single segment. Treating it all as a single segment seems prone to create UI-visible bugs if not all the sources of string initialization have been addressed.
That is the next step. I tend to make steps as small as possible, so that at every step it is more than obvious that the code before and after the change will produce the same effects. Every discrepancy means that the last change - usually no more than a couple of lines of code - is to blame. Let me give you a hint how that can help. The first time I split into multiple segments in this demo, there appeared strange white spaces in the UI. It turned out that the browser rendered whitespace around spans unless they are glued together in the HTML. I only do frontend when I must (which turns out to be in videos only...) and then I make subtle mistakes like that one. However the very fact that the only thing I did since the last time application looked fine, was to generate spans for individual segments. That helped me pinpoint the problem in a second, literally.
Thank you, an interesting lesson. But I would like to see the work of the new class (Citation class) not in a ready-made application, but in unit tests written for it. I would like to see the use of unit tests as a class design tool.
There will be a similar video soon, where I plan to show the technique of testing class postconditions. In the meanwhile, you may watch the previous video where tests are constructed against the class's contract, avoiding to depend on its implementation: th-cam.com/video/po9ziMcnAWg/w-d-xo.html
I advocate this approach, but it can create issues with serializing JSON input/output in aspnet apps. Nothing that can't be overcome, but extra work is required.
Serialization should not be performed to and from domain models. Simply, the models have other, way more complex responsibilities to think about. Lift is much simpler if you do serialization from DTOs.
If it looks like a chain of Boolean tests, then I prefer a chain of ternary operators. A personal preference. I have nothing against using switch expressions in the same situation, especially because switch expressions are matching cases in order of appearance, as documented.
The primary reason is that there are going to be multiple variants later (that is already a certainty) and therefore it cannot be a struct. Otherwise, if this were the only variant with only a single reference inside, then declaring it as a record struct would indeed be a viable performance optimization.
I still don't get the whole primitive obsession thing. It's a string. What's wrong with a string? If you want to know what it is, give it a proper name and documentation, right? The only reason why you make a new data structure is when you have values paired with behaviors or if there are some values that belong together. There is nothing wrong with primitives. There is something wrong with devs writing helper classes to infinity, not properly documenting stuff, treating values with implicit behavioral magic. Your example uses a citation. Well, by the looks of it it's not just string. You contain segments, so it can't just be a string looking at the structure of the data. I think people are to obsessed with making everything a wrapper just to get rid of primitives. Sometimes a name is a string, and age an unsigned short (for the love of Code Jesus, use date of birth). It's when you attach behaviors or rules to the value that makes it a candidate for its own type. This, however, introduces the problem of not letting data just be data. It also raises questions about stuff like validation. What logic should be in a primitive wrapper, and what shouldn't? Devs are obsessed with primitive obsession. The term is used in places where it doesn't make sense. People who just learned that a primitive should be wrapped, and that's it. Those people are wrong. What you do in the video, however, is a perfect example of proper primitive obsession and how to solve it. But I still don't get what choices you made logically outside of what I mentioned in this post. Maybe you can help me out? Disclaimer, I'm still a student, so don't take anything I say for granted.
Your experience might be skewed by being a student and/or watching a lot of videos like this. In my experience in the industry, 90% of code is way too primitive obsessed. You can for sure go too far in the other direction but I think this is less common
Thank you for this detailed analysis. I think you are right that it should not be the goal to just wrap the primitive types - and it is not. With this video, I have tried to emphasize the importance of domain modeling. When modeling is done right, primitive types will stand in the place where they belong. You will see primitive obsession when there is domain-related behavior implemented at the place where a primitive type is used. Another typical symptom is when a developer introduces ad hoc DTOs to pass multiple values together, failing to give them higher meaning. As I said in the video, there are two major consequences of misusing primitive types in a domain model. One is bugs, usually coming from a mismatched assignment or misinterpretation of a primitive value. The other is inability to implement a feature because other accompanying values were lost in the process. One should also not forget that implementing behavior at the calling site is the source of the whole range of other problems - code duplication, unwanted dependencies, rigidness and outright bugs. The bottom line is that primitive obsession is a real thing. It does happen, and it does cause damage in application development.
@@zoran-horvat "One is bugs, usually coming from a mismatched assignment or misinterpretation of a primitive value" I think that's why you shouldn't implement the implicit operator on your Citation, it's too easy to pass any string when a Citation is expected.
@@chris-pee That operator is an intermediate step while the feature is being fitted in. Once all consumers are transformed to not use strings, we remove that operator. That technique is part of iterative coding and I will explain it in greater depth in the follow-up video.
Don't be too literal. Any type that has no structure is primitive from the modeling point of view. Conversely, even a seemingly primitive type that does have a structure is not primitive. In that light, string is primitive and date is not, even though their representation might indicate the opposite.
I only recently discovered your channel, Mr Horvat, but I must say -- I believe you are already my fav developer-instructor. Your content is higher-level but pragmatic, and you are EXCELENT with your presentation. You do have a strong accent which, in your case -- actually enhances your clarity. Or perhaps you are taking great care to speak deliberately and clearly? Regardless, I love how you present your topics, I appreciate the work you put into these and THANK YOU for sharing this excellent instruction and advice.
Thanks!
Where has this video been all my life? 🎉
In my drawer :)
@zoran-horvat I think there were two good lessons in this video.
1) Don't obsess over primate types for structured data.
2) Small changes, otherwise you end up in a mess and can't remember what problem you're even tying to solve.
It shows that you are a real professional
I'm glad TH-cam suggested your videos. Instant subscribe
Excellent video. Looking forward to the next one. String especially can be painful. A lot of "strings" are actually "html strings", hence a + b should not necessarily be a simple concatenation. So many urls are often stored in strings, instead of a Uri. There many, many "types" of string and not understanding how to combine "types" of string is huge cause of bugs and even security issues (see "SQL strings").
I am curious about what should qualify as a "domain model", and what is adding additional unnecessary complexity to the code.
By way of example; Let's say that I have a Fraction class that takes in two integer values, a Nominator and Denominator. Of course, I need the Denominator to be a non-zero value, otherwise we'll end up dividing by zero!
Now, I could use a smart constructor to perform the validation on the denominator value, but I wonder if it would be more appropriate to instead have a NonZeroInt (or in C# 11+, NonZero where T : INumber, using an instance of NonZero) class, which itself has a smart constructor and performs that validation, which allows me to simply pass a normal integer value for the numerator and a NonZeroInt in the denominator.
Is this approach overkill? Because I'm not sure if there is necessarily anything domain worthy about a numerical value not being equal to zero, but it would allow me to abstract away a lot of instances where I would otherwise be performing the same validation checks in other parts of the code.
Domain model is the model of the problem domain - a projection of reality to a finite set of dimensions.
Anything that has a place in customer's requirements is almost certainly qualified to be part of the domain model.
DDD is going one step further and introduces ubiquitous language, requesting both customers and code to name things the same.
@@zoran-horvat thank you kindly for your input.
From your response, am I heading in completely the wrong direction with my example?
@@metallixbrother Not necessarily - if what you are saying is the domain requirement, then it should be implemented as a domain type. Namely, if there is importance in a certain number never being zero, then that is a candidate for an explicit model.
The most annoying thing with rich domain modelling is that it almost immediately hits the wall with EF. It's much better these days compared to what we had before, but still lacking some critical features. And then we have a choice either to introduce conversion layer from domain entities to db DTOs, or constrain ourselves to what is supported.
True, it is really demotivating ...
This is great advice, and given the context of a c# application with buisness logic this seems like a good default practice in most cases. Your point about the inflexibility of them is well taken. However if applied more generally I would say primitives have some advantages depending on the circumstances. 1, Depending on the language using primitives versus objects can have performance implications (stack/heap allocations, string copying, etc). 2, Primitives are standard in the language and understood without needing to reading specific implementation details (although for a counterpoint, primitives can be more vauge - what is this string doing exactly?). 3, And they can have a less pronounced benefit in other use cases /programming paradigms (a struct of one primitive versus a primitive in a data driven application where the logic in not contained in the object). I think one could argue the addition of primitives to that struct could be anologous to extending a class but I think the main point is are you returning a value/object that is extensible to your needs. Just thought I'd share some ideas, I work on predominantly low level c++ code so that's where I'm coming from.
Hi Zoran, do you have any video on creating a Result type? ResultOrError or something like that? I'm trying to minimise throwing exceptions. Thanks!
@@slowjocrow6451 Not now. There is a script in the queue and I will record it at one point.
Great content, better than most other c# channels :) , The only small suggestion or conern I have is the usage of implict operator for Citation instead of proper static factory method, but I also understand, this is the most obvious way to not break any existing code
That was precisely the idea - to try not to cause any change in any other class before that step ends and we verify that nothing is broken. The final goal is to delete the conversion operator when the entire redesign is over.
Really love your content!
As always, great video. One thing that might be a good content for the future video is to use more strict access modifiers for classes to communicate with a dev more clearly about the intentions but also provide some encapsulation so that classes like Citation cannot be used in, let say, Infrastructure layer.
I am not a fan of hiding models. What would be the reason not to allow persisting Citation instances, for example?
When I want to indicate that some models are better used indirectly, I place them into a nested namespace. The parent namespace then provides all the means necessary to declare and create instances, but the models themselves are public like any other. That for one thing dismisses the question of how to test them.
I like the pragmatic way you've approached this problem. However, when you were splitting into segments, I would have made the backward compatible string constructor split on commas rather than treat the entire list as a single segment. Treating it all as a single segment seems prone to create UI-visible bugs if not all the sources of string initialization have been addressed.
That is the next step. I tend to make steps as small as possible, so that at every step it is more than obvious that the code before and after the change will produce the same effects. Every discrepancy means that the last change - usually no more than a couple of lines of code - is to blame.
Let me give you a hint how that can help. The first time I split into multiple segments in this demo, there appeared strange white spaces in the UI. It turned out that the browser rendered whitespace around spans unless they are glued together in the HTML. I only do frontend when I must (which turns out to be in videos only...) and then I make subtle mistakes like that one. However the very fact that the only thing I did since the last time application looked fine, was to generate spans for individual segments. That helped me pinpoint the problem in a second, literally.
I'm very excited for the next video. Subscribed!
Thank you, an interesting lesson. But I would like to see the work of the new class (Citation class) not in a ready-made application, but in unit tests written for it. I would like to see the use of unit tests as a class design tool.
There will be a similar video soon, where I plan to show the technique of testing class postconditions.
In the meanwhile, you may watch the previous video where tests are constructed against the class's contract, avoiding to depend on its implementation: th-cam.com/video/po9ziMcnAWg/w-d-xo.html
@@zoran-horvat Thanks.
I advocate this approach, but it can create issues with serializing JSON input/output in aspnet apps. Nothing that can't be overcome, but extra work is required.
Serialization should not be performed to and from domain models. Simply, the models have other, way more complex responsibilities to think about. Lift is much simpler if you do serialization from DTOs.
Probably a dumb question but why is the Add method creating a new instance of Citation instead of just adding the segment to the current instance?
Because the design is immutable. There is no reason that would force it to be mutable, so my default decision is to keep it immutable.
Zoran, why do you not use pattern matching with switch expressions instead of nested ternaries?
If it looks like a chain of Boolean tests, then I prefer a chain of ternary operators. A personal preference. I have nothing against using switch expressions in the same situation, especially because switch expressions are matching cases in order of appearance, as documented.
From now on we should add this in every application by default 🤣
Thanks :)
Why not readonly record struct CitationSegment?
The primary reason is that there are going to be multiple variants later (that is already a certainty) and therefore it cannot be a struct.
Otherwise, if this were the only variant with only a single reference inside, then declaring it as a record struct would indeed be a viable performance optimization.
I still don't get the whole primitive obsession thing. It's a string. What's wrong with a string? If you want to know what it is, give it a proper name and documentation, right? The only reason why you make a new data structure is when you have values paired with behaviors or if there are some values that belong together.
There is nothing wrong with primitives. There is something wrong with devs writing helper classes to infinity, not properly documenting stuff, treating values with implicit behavioral magic. Your example uses a citation. Well, by the looks of it it's not just string. You contain segments, so it can't just be a string looking at the structure of the data.
I think people are to obsessed with making everything a wrapper just to get rid of primitives. Sometimes a name is a string, and age an unsigned short (for the love of Code Jesus, use date of birth). It's when you attach behaviors or rules to the value that makes it a candidate for its own type. This, however, introduces the problem of not letting data just be data. It also raises questions about stuff like validation. What logic should be in a primitive wrapper, and what shouldn't?
Devs are obsessed with primitive obsession. The term is used in places where it doesn't make sense. People who just learned that a primitive should be wrapped, and that's it. Those people are wrong. What you do in the video, however, is a perfect example of proper primitive obsession and how to solve it. But I still don't get what choices you made logically outside of what I mentioned in this post. Maybe you can help me out?
Disclaimer, I'm still a student, so don't take anything I say for granted.
Your experience might be skewed by being a student and/or watching a lot of videos like this. In my experience in the industry, 90% of code is way too primitive obsessed. You can for sure go too far in the other direction but I think this is less common
Thank you for this detailed analysis. I think you are right that it should not be the goal to just wrap the primitive types - and it is not.
With this video, I have tried to emphasize the importance of domain modeling. When modeling is done right, primitive types will stand in the place where they belong. You will see primitive obsession when there is domain-related behavior implemented at the place where a primitive type is used. Another typical symptom is when a developer introduces ad hoc DTOs to pass multiple values together, failing to give them higher meaning.
As I said in the video, there are two major consequences of misusing primitive types in a domain model. One is bugs, usually coming from a mismatched assignment or misinterpretation of a primitive value. The other is inability to implement a feature because other accompanying values were lost in the process. One should also not forget that implementing behavior at the calling site is the source of the whole range of other problems - code duplication, unwanted dependencies, rigidness and outright bugs.
The bottom line is that primitive obsession is a real thing. It does happen, and it does cause damage in application development.
You are absolutely 💯 right
@@zoran-horvat "One is bugs, usually coming from a mismatched assignment or misinterpretation of a primitive value"
I think that's why you shouldn't implement the implicit operator on your Citation, it's too easy to pass any string when a Citation is expected.
@@chris-pee That operator is an intermediate step while the feature is being fitted in. Once all consumers are transformed to not use strings, we remove that operator.
That technique is part of iterative coding and I will explain it in greater depth in the follow-up video.
Joke's on you, strings aren't primitive in my language
Don't be too literal. Any type that has no structure is primitive from the modeling point of view. Conversely, even a seemingly primitive type that does have a structure is not primitive. In that light, string is primitive and date is not, even though their representation might indicate the opposite.