تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Value Objects Overview0:00

Hey there. Welcome back. It is finally time for us to talk a little bit about design patterns. And the first one we're going to talk about are value objects. To be fair value objects are super, super simple. They have a few qualities. The first quality is that they are immutable. Once we have instantiated a value object, we cannot change its contents.

Once we have instantiated a value object, we cannot change its contents. The second quality is they do not have an identity. Think about something like an Invoice or a Payment as Eloquent models. They have an id, right? That's their identifier, and you can correlate them using that id. And you can also check whether Invoice A is Invoice B by checking whether they have the same id.

And you can also check whether invoiceA is invoiceB by checking whether they have the same identifier value. Value objects do not have identity. Instead, we compare them using values. So valueObjectA is equal to valueObjectB if they have the same values. Additionally, value objects should be both small and self-validating. That is, we should not be able to instantiate a value object with invalid state.

That is we should not be able to instantiate a value object with invalid state. We've spoken about that a little bit on the first lessons. And finally, value objects can have behavior that operates on their own data. A good simple example of a value object is an email. You have roles when it comes to instantiating an email, for example, it might require an at sign or a domain extension. They do not have identity.

or a domain extension. They do not have identity. That is emailA is equal to emailB if they hold the same internal value and you can have some behavior on top of that. Maybe you just want to get the username or you just want to get the domain. Those can be methods in your value object. Another good example is having a Money object. It's a very small object.

Creating Money Value Object1:45

Another good example is having a Money object. It's a very small object. It can be full of behavior, you can add money on top of it, and it is also self validating. So let's jump into the code and see how we could create our very own value object. Okay, so I went ahead and I created a Money class and also a MoneyTask. And Money is a good example because it is pretty common first of all.

And money is a good example because it is pretty common first of all. And second, we deal with money quite frequently on this application. So let's go with that. Well, the first thing we know is that those objects are immutable, right? So let's go ahead and add the readonly keyword here. Second, we know they must hold some internal state, and in this case we want to hold some amount of money. For this example, I'm gonna go with an integer

and in this case we want to hold some amount of money. For this example, I'm gonna go with an integer and we're gonna call this amount in cents. So we are always going to store any monetary values in cents, at least internally. Now, if we go to our Invoice model, you're gonna notice that we're storing monetary amounts as decimals, right? So this is the major unit, for example, dollars. So we would pass something like 10.15, uh, $10 and 15 cents.

Adding fromMajor Constructor2:49

So we would pass something like 10.15, uh, $10.15. And this is not compatible with what we have as our money value object as we are actually storing that data as cents internally. So let's go ahead and do a static construction. We're gonna call this fromDecimal or fromMajor. Maybe that's better. And we're gonna take two types here. We're gonna take either an int, a float, or a string, and we're gonna call this a nonMajor.

We're gonna take either a int, a float, or a string, and we're gonna call this a non major. And this is going to return an instance of itself. So here's what I'm gonna do. I'm gonna go to moneyTest and I'm going to add a couple of task cases. I'm going to use the power of editing to come back in a few seconds. There we go. So here I have a couple of task scenarios. On the first one, we are creating a Money instance from the 10.

On the first one, we are creating a Money instance from 10. On the second one, we're creating a Money instance from 10.50, which is a float. And on the third one we have a string 10.19. And the reason we're taking multiple types here is just because this is an example in real life, I recommend you to narrow this down to as little types as possible. Okay, let's run our tests. Failing, yes, this needs to be a static method.

Okay, let's run our tests. Failing, yes, this needs to be a static method. Let's rerun our tests, obviously failing. So let's get this started. We're gonna say we're going to return a new instance of itself. And for now, we're just gonna do a mount in major. We're gonna cast this to a string and we're gonna do a many major times a hundred. Let's say if this passes, it does, let's go ahead.

and we're gonna do a many major times a hundred. Let's say if this passes, it does, let's go ahead and add a new test case. I'm gonna say that I want to create money from major, and here's what I'm gonna pass. I'm gonna say it's a string and I'm gonna say 10.994. So this is where modeling becomes really important. How do we want to represent such a monetary amount internally? If you have a value object, a money object, for example,

amount internally? If you have a ValueObject, a Money object, for example, any context where two decimals is enough, for example, if you're displaying that to the customer, you don't need to display all of the decimals, then you should go with that. But if you have a context where you want to store as many decimals as possible, for example, to deal with interest internally, then you should have a ValueObject that incorporates

with interest internally, then you should have a value object that incorporates that structure internally, that holds the data internally in the correct way. And in some cases you might have a value option with the same name, for example, money in different places within your application. And they might have slightly different behavior and they might also store data slightly different. That's because value objects should be scoped

and they might also store data slightly different. That's because value objects should be scoped out to context. In many cases, it is fine to have a global ValueObject, for example, a Money object, but in some cases you need very precise, very specific value objects and it is totally okay to have more than one. With that said, let's move on with this. For our example, even though we're dealing with interest,

With that said, let's move on with this. For our example, even though we're dealing with interest, this is just an example application, we're good to go with two decimals. So we can expect this to equal 10.99, and this would be moneyAmountInCents. Let's run this test and it is still passing. The reason why this is still passing is because we are converting the entire thing into an integer. So if we have 10.994

because we are converting the entire thing into an integer. So if we have 10.994 and we multiply that by 100, we get 10 99 0.4. So this value is incorrect. But since we're converting that into an integer, then we are removing this remainder right here. So we're good to go even with that edge case. Cool. Now remember we said that value objects should be self validating, so let's implement that.

Enforcing Self-Validation6:26

that value objects should be self validating, so let's implement that. Let's add a new test. We're gonna say test. It requires the amount to be greater than zero. And now we can test a couple of things. For example, we could test that we cannot instant something like 0. Let's try that. And although we don't have any assertions, the task is still passing.

although we don't have any assertions, the task is still passing. So let's do something. Let's go into our constructor and we can say something like, if the amount in cents is equal to zero, we want to throw an InvalidArgumentException and say amount needs to be greater than zero. Thank you, copilot. There we go. Let's rerun our test and we get an InvalidArgumentException.

Let's rerun our test and we get an InvalidArgumentException. So we can tell php union that we expect an InvalidArgumentException here, let's rerun a task and now it's passing. And then let's also add a task to make sure we cannot pass a negative value task. It does not accept negative values. So we can do the same thing. We're gonna expect an exception,

So we can do the same thing. We're gonna expect an exception, and we can see money from major, we can pass -1. Let's run this task. It is not failing. That is, it is failing because this exception should be thrown. So now we can go here and we can say, let's suggest that let's say less than or equal to 0. And now our tasks are all passing. Very, very cool.

Adding Formatting Behavior7:35

that let's say less than or equal to zero. And now our tasks are all passing. Very, very cool. Even though this is pretty simple with this look of code, we guarantee that our money object is always valid. This way we're ensuring that if we try to instantShape that we're just going to crash. Okay, just wrap this up. Let's add some very simple behavior to this object. Since we're storing values internally, a sense, which is the minor amount for now, when we show something

Since we're storing values internally, a sense, which is the minor amount for now, when we show something to the user, we, we don't wanna show it a sense, right? We want to show it as the major amounts dollars in this case. And we also did not want to have a bunch of logic scattered throughout the app when it comes to formatting those monetary amounts. So let's add some behavior to our money object because it belongs there.

So let's add some behavior to our Money object because it belongs there. Let's, let's go ahead. Let's jump into the code. So here we have a very simple task. We're constructing a Money object, $10.99, and then when calling major, we expect 10.99 is a float. And when calling formattedMajor, we expect this string. So it's all signed, 10.99, let's run this test. That is going to fail. We don't have that method. So let's go ahead and add the major method.

That is going to fail. We don't have that method. So let's go ahead and add the major method and the major method's gonna return a float for now. And we can say something pretty simple, right? We can say that we want to cast this to a float and then say, amountInCents divided by a hundred, let's rerun our tests. And that is passing, but now it's failing on this line. We don't have the formatted major method. So let's go ahead and add this method.

We don't have the formatted major method. So let's go ahead and add this method. This is going to return a string, and now I'm going to rely on page number_format. So let's say $formatter is going to be a new NumberFormatter and under $this is the correct locale, and this is a currency formatter. And then we're gonna say that we want to format a currency and the amount is going to be the $amount in major and the currency is going to be USD.

and the amount is going to be the amount in major and the currency is going to be USD. So let's try this, and it is passing. So if we go ahead and just dump and die, what do we get here? We get 10.99 with the dollar sign. Now, if I were to replace this with BRL, which is the Brazil currency, and we rerun the task, you can see that we now have it formatted specifically for Brazil.

and we rerun the task, you can see that we now have it formatted specifically for Brazil. So let's do this. Let's go back to USD. Let's remove this jumpAndDie statement and rerun our tasks. And there we go. So now we have a formatter. Now something that you might have noticed, we have this amountingSense property, but it is public, which means that even though it cannot be changed

Encapsulating Internal State10:03

but it is public, which means that even though it cannot be changed because the class, if we only, we can still have consumers depending on it. So if we ever want to change how we store money internally, for example, if we want to move from storing a cents of just two decimals to storing maybe 10 decimals or even storing major, we cannot afford to make the change easily. So what I'm gonna do is I'm actually going

to make the change easily. So what I'm gonna do is I'm actually going to make this private, and then I'm going to have a method called minor, which is going to give me or even sense, which going to give me the amount in cents, and this should going to return amounting cents. We can go to our test and we can update every place.

We can go to our test and we can update every place where we call amount to this. And if we rerun the tasks, they are all passing. The benefit in making internal, state private is that we now have some freedom into how this class stores data internally. If we want to maybe store the major amount, we can do that. And as long as sents returns to the same thing, it's not a break and change to any of its consumers.

And as long as sense returns to the same thing, it's not a break and change to any of its consumers. That doesn't mean that all of your value objects have to do this. If you have very simple value objects and you're fine with consuming the value directly, then yeah, sure that can be public. It's not a big problem. Okay, I think this is a good place to stop this lesson. We're talking about value objects.

I think this is a good place to stop this lesson. We're talking about value objects. We've created our own value object. We hand out self validation, and we've also added some behavior to it. On the next lesson, let's continue to bid. On top of this, add some extra behavior and let's also see how we can plug those value objects into our Eloquent models.

how we can plug those value objects into our Eloquent models. And here's the sneak week. Laravel actually supports all of that first party, so it's really, really easy to plug our value objects into our Eloquent models. See you in the next lesson. Bye.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟