I mentioned earlier that there are two types of objects: services and other objects. The second type of objects can be divided into more specific subtypes, namely value objects and entities (sometimes known as “models”). Services will create or retrieve entities, manipulate them, or pass them on to other services. They will also create value objects and pass them on as method arguments, or create modified copies of them. In this sense, entities and value objects are the materials that services use to perform their tasks.
In chapter 2 we looked at how a service object should be created. In this chapter, we’ll look at the rules for creating these other objects.
Take a look at the following Position class.
final class Position { private int x; private int y; public function __construct() { // empty } public function setX(int x): void { this.x = x; } public function setY(int y): void { this.y = y; } public function distanceTo(Position other): float { return sqrt( (other.x - this.x) ** 2 + (other.y - this.y) ** 2 ); } } position = new Position(); position.setX(45); position.setY(60);
Until we’ve called both setX() and setY(), the object is in an inconsistent state. We can notice this if we call distanceTo() before calling setX() or setY(); it won’t give a meaningful answer.
Since it’s crucial to the concept of a position that it have both x and y parts, we have to enforce this by making it impossible to create a Position object without providing values for both x and y.
final class Position { private int x; private int y; public function __construct(int x, int y) { this.x = x; this.y = y; } public function distanceTo(Position other): float { return sqrt( (other.x - this.x) ** 2 + (other.y - this.y) ** 2 ); } } position = new Position(45, 60); 1
This is an example of how a constructor can be used to protect a domain invariant, which is something that’s always true for a given object, based on the domain knowledge you have about the concept it represents. The domain invariant that’s being protected here is, “A position has both an x and a y coordinate.”
money = new Money() money.setAmount(100); money.setCurrency('USD');
In the previous example, the constructor would accept any integer, positive or negative and to infinity in both directions. Now consider another system of coordinates, where positions consist of a latitude and a longitude, which together determine a place on earth. In this case, not every possible value for latitude and longitude would be considered meaningful.
final class Coordinates { private float latitude; private float longitude; public function __construct(float latitude, float longitude) { this.latitude = latitude; this.longitude = longitude; } // ... } meaningfulCoordinates = new Coordinates(45.0, -60.0); offThePlanet = new Coordinates(1000.0, -20000.0); 1
Always make sure that clients can’t provide data that is meaningless. What counts as meaningless can be phrased as a domain invariant too. In this case, the invariant is, “The latitude of a coordinate is a value between –90 and 90 inclusive. The longitude of a coordinate is a value between –180 and 180 inclusive.”
When you’re designing your objects, let yourself be guided by these domain invariants. Collect more invariants as you go, and incorporate them in your unit tests. As an example, the following listing uses the expectException() utility described in section 1.10.
expectException( InvalidArgumentException.className, 1 'Latitude', 2 function() { 3 new Coordinates(90.1, 0.0); } ); expectException( InvalidArgumentException.className, 'Longitude', function() { new Coordinates(0.0, 180.1); } ); // and so on...
To make these tests pass, throw an exception in the constructor as soon as something about the provided arguments looks wrong.
final class Coordinates { // ... public function __construct(float latitude, float longitude) { if (latitude > 90 || latitude < -90) { throw new InvalidArgumentException( 'Latitude should be between -90 and 90' ); } this.latitude = latitude; if (longitude > 180 || longitude < -180) { throw new InvalidArgumentException( 'Longitude should be between -180 and 180' ); } this.longitude = longitude; } }
Although the exact order of the statements in your constructor shouldn’t matter (as we discussed earlier), it’s still recommended that you perform the checks directly above their associated property assignments. This will make it easy for the reader to understand how the two statements are related.
In some cases it’s not enough to verify that every constructor argument is valid on its own. Sometimes you may need to verify that the provided constructor arguments are meaningful together. The following example shows the ReservationRequest class, which is used to keep some information about a hotel reservation.
final class ReservationRequest { public function __construct( int numberOfRooms, int numberOfAdults, int numberOfChildren ) { // ... } }
Discussing the business rules for this object with a domain expert, you may learn about the following rules:
So it turns out that numberOfRooms and numberOfAdults are related, and can only be considered to be meaningful together. We have to make sure that the constructor takes both values and enforces the corresponding business rules, as in the following listing.
final class ReservationRequest { public function __construct( int numberOfRooms, int numberOfAdults, int numberOfChildren ) { if (numberOfRooms > numberOfAdults + numberOfChildren) { throw new InvalidArgumentException( 'Number of rooms should not exceed number of guests' ); } if (numberOfAdults < 1) { throw new InvalidArgumentException( 'numberOfAdults should be at least 1' ); } if (numberOfChildren < 0) { throw new InvalidArgumentException( 'numberOfChildren should be at least 0' ); } } }
In other cases, constructor arguments may at first sight appear to be related, but a redesign could help you avoid multi-argument validations. Consider the following class, which represents a business deal between two parties, where there’s a total amount of money that has to be divided between two parties.
final class Deal { public function __construct( int totalAmount, int amountToFirstParty, int amountToSecondParty ) { // ... } }
You should at least validate the constructor arguments separately (the total amount should be larger than 0, etc.). But there’s also an invariant that spans all the arguments: the sum of what both parties get should be equal to the total amount. The following listing shows how you could verify this rule.
final class Deal { public function __construct( int totalAmount, int amountToFirstParty, int amountToSecondParty ) { // ... if (amountToFirstParty + amountToSecondParty != totalAmount) { throw new InvalidArgumentException(/* ... */); } } }
As you may have noted, this rule could be enforced in a much simpler way. You could say that the total amount itself doesn’t even have to be provided, as long as the client provides positive numbers for amountToFirstParty and amountToSecondParty. The Deal object could figure out on its own what the total amount of the deal was by summing these values. The need to validate the constructor arguments together disappears.
final class Deal { private int amountToFirstParty; private int amountToSecondParty; public function __construct( int amountToFirstParty, int amountToSecondParty ) { if (amountToFirstParty <= 0) { throw new InvalidArgumentException(/* ... */); } this.amountToFirstParty = amountToFirstParty; if (amountToSecondParty <= 0) { throw new InvalidArgumentException(/* ... */); } this.amountToSecondParty = amountToSecondParty; } public function totalAmount(): int { return this.amountToFirstParty + this.amountToSecondParty; } }
Another example where it would seem that constructor arguments have to be validated together is the following class, which represents a line.
final class Line { public function __construct( bool isDotted, int distanceBetweenDots ) { if (isDotted && distanceBetweenDots <= 0) { 1 throw new InvalidArgumentException( 'Expect the distance between dots to be positive.' ); } // ... } }
However, this could more elegantly be dealt with by providing the client with two distinct ways of defining a line: dotted and solid. Different types of lines could be constructed with different constructors.
final class Line { private bool isDotted; private int distanceBetweenDots; public static function dotted(int distanceBetweenDots): Line { if (distanceBetweenDots <= 0) { throw new InvalidArgumentException( 'Expect the distance between dots to be positive.' ); } line = new Line(/* ... */); line.distanceBetweenDots = distanceBetweenDots; line.isDotted = true; return line; } public static function solid(): Line { line = new Line(); line.isDotted = false; 1 return line; } }
These static methods are named constructors, and we’ll take a closer look at them in section 3.9.
final class PriceRange { public function __construct(int minimumPrice, int maximumPrice) { this.minimumPrice = minimumPrice; this.maximumPrice = maximumPrice; } }The constructor currently accepts any int value for both arguments. Enhance the constructor and make it fail when these values aren’t meaningful.
If you make sure that every object has the minimum required data provided to it at construction time, and that this data is correct and meaningful, you will only encounter complete and valid objects in your application. It should be safe to assume that you can use every object as intended. There should be no surprises and no need for extra validation rounds.
So far we’ve been throwing a generic InvalidArgumentException whenever a method argument doesn’t match our expectations. We could use a custom exception class that extends from InvalidArgumentException. The advantage of doing so is that we could catch specific types of exceptions and deal with them in specific ways.
final class SpecificException extends InvalidArgumentException { } try { // try to create the object } catch (SpecificException exception) { // handle this specific problem in a specific way }
However, you should rarely need to do that with invalid argument exceptions. An invalid argument means that the client is using the object in an invalid way. Usually this will be caused be a programming mistake. In that case, you’d better fail hard and not try to recover, but fix the mistake instead.
For RuntimeExceptions on the other hand, it often makes sense to use custom exception classes because you may be able to recover from them, or to convert them into user-friendly error messages. We’ll discuss custom runtime exceptions and how to create them in section 5.2.
Even if you only use the generic InvalidArgumentException class to validate method arguments, you still need a way to distinguish between them in a unit test. Let’s take another look at the Coordinates class and constructor.
final class Coordinates { // ... public function __construct(float latitude, float longitude) { if (latitude > 90 || latitude < -90) { throw new InvalidArgumentException( 'Latitude should be between -90 and 90' ); } this.latitude = latitude; if (longitude > 180 || longitude < -180) { throw new InvalidArgumentException( 'Longitude should be between -180 and 180' ); } this.longitude = longitude; } }
We want to verify that clients can’t pass in the wrong arguments, so we can write a few tests, like the following ones.
// Latitude can't be more than 90.0 expectException( InvalidArgumentException.className, function() { new Coordinates(90.1, 0.0); } ); // Latitude can't be less than -90.0 expectException( InvalidArgumentException.className, function() { new Coordinates(-90.1, 0.0); } ); // Longitude can't be more than 180.0 expectException( InvalidArgumentException.className, function() { new Coordinates(-90.1, 180.1); } );
In the last test case, the InvalidArgumentException that gets thrown from the constructor isn’t the one we’d expect it to be. Because the test case reuses an invalid value for latitude (–90.1) from the previous test case, trying to construct a Coordinates object will throw an exception telling us that “Latitude should be between –90.0 and 90.0.” But the test was supposed to verify that the code would reject invalid values for longitude. This leaves the range check for longitude uncovered in a test scenario, even though all the tests succeed.
To prevent this kind of mistake, make sure to always verify that the exception you catch in a unit test is in fact the expected one. A pragmatic way to do this is to verify that the exception message contains certain predefined words.
expectException( InvalidArgumentException.className, 'Longitude', 1 function() { new Coordinates(-90.1, 180.1); } );
Adding this expectation about the exception message to the test in listing 3.15 will make the test fail. It will pass again once we provide the constructor with a sensible value for latitude.
You’ll often find the same validation logic repeated in the same class, or even in different classes. As an example, take a look at the following User class and how it has to validate an email address in multiple places, using a function from the language’s standard library.
final class User { private string emailAddress; public function __construct(string emailAddress) { if (!is_valid_email_address(emailAddress)) { 1 throw new InvalidArgumentException( 'Invalid email address' ); } this.emailAddress = emailAddress; } // ... public function changeEmailAddress(string emailAddress): void { if (!is_valid_email_address(emailAddress)) { 2 throw new InvalidArgumentException( 'Invalid email address' ); } this.emailAddress = emailAddress; } } expectException( 3 InvalidArgumentException.className, 'email', function () { new User('not-a-valid-email-address'); } ); user = new User('[email protected]'); 4 expectException( 5 InvalidArgumentException.className, 'email', function () use (user) { user.changeEmailAddress('not-a-valid-email-address'); } );
Although you could easily extract the email address validation logic into a separate method, the better solution is to introduce a new type of object that represents a valid email address. Since we expect all objects to be valid the moment they are created, we can leave out the “valid” part from the class name and implement it as follows.
final class EmailAddress { private string emailAddress; public function __construct(string emailAddress) { if (!is_valid_email_address(emailAddress)) { throw new InvalidArgumentException( 'Invalid email address' ); } this.emailAddress = emailAddress; } }
Wherever you encounter an EmailAddress object, you will know it represents a value that has already been validated:
final class User { private EmailAddress emailAddress; public function __construct(EmailAddress emailAddress) { this.emailAddress = emailAddress; } // ... public function changeEmailAddress(EmailAddress emailAddress): void { this.emailAddress = emailAddress; } }
Wrapping values inside new objects called value objects isn’t just useful for avoiding repeated validation logic. As soon as you notice that a method accepts a primitive-type value (string, int, etc.), you should consider introducing a class for it. The guiding question for deciding whether or not to do this is, “Would any string, int, etc., be acceptable here?” If the answer is no, introduce a new class for the concept.
You should consider the value object class itself to be a type, just like string, int, etc., are types. By introducing more objects to represent domain concepts, you’re effectively extending the type system. Your language’s compiler or runtime will be able to support you much better, because it can do type-checking for you and make sure that only the right types end up being used when passing method arguments and returning values.
When creating all these new types, you’ll find that some of them naturally belong together and always get passed together from method call to method call. For example, an amount of money always comes with the currency of the amount, as in the following listing. If a method received just an amount, it wouldn’t know how to deal with it.
final class Amount { // ... } final class Currency { // ... } final class Product { public function setPrice( 1 Amount amount, Currency currency ): void { // ... } } final class Converter { public function convert( 1 Amount localAmount, Currency localCurrency, Currency targetCurrency ): Amount { // ... } }
In this last example, the return type is actually quite confusing. An Amount will be returned, and the currency of this amount is expected to match the given target-Currency. But this is not evident by looking at the types used in this method.
Whenever you notice that values belong together (or can always be found together), wrap those values into a new type. In the case of Amount and Currency, a good name for the combination of the two could be “money,” resulting in the Money class.
final class Money { public function __construct(Amount amount, Currency currency) { // ... } }
Using this type indicates that you want to keep these values together, although if you wanted to use them separately, you still could.
100 has fewer characters than new Amount(100), but all that extra typing gives you the benefits of using object types:
If you feel like it’s a hassle to create all these objects one by one based on primitive values, you can always introduce helper methods for creating them. Here’s an example:
// Before: money = new Money(new Amount(100), new Currency('USD')); // After: money = Money.create(100, 'USD');
You will learn more about this style of creating objects in section 3.9.
final class Run { public function __construct(int distance) { // ... } }The problem with the current implementation is that there’s no way to find out what kind of value distance represents. Is it measured in meters, feet, kilometers perhaps? This requires a new value object representing both the quantity and the unit of the running distance. For your implementation, assume the distance can only be measured in meters or feet.
We’ve already seen several examples of constructors that throw exceptions when something is wrong. The general structure is always like this:
if (somethingIsWrong()) { throw new InvalidArgumentException(/* ... */); }
These checks at the beginnings of methods are called “assertions,” and they’re basically safety checks. Assertions can be used to establish the situation, examine the materials, and signal if anything is wrong. For this reason, assertions are also called “precondition checks.” Once you’re past these assertions, it should be safe to perform the task at hand with the data that has been provided.
Because you’ll often write the same kinds of checks in many different places, it’ll be convenient to use an assertion library instead.[1] Such a library contains many assertion functions that will cover almost all situations. These are some examples:
If you use PHP, take a look at the beberlei/assert or webmozart/assert package.
Assertion.greaterThan(value, limit); Assertion.isCallable(value); Assertion.between( value, lowerLimit, upperLimit ); // and so on...
The question is always, “Should you verify that these assertions work in a unit test for your object?” The guiding question is, “Would it be theoretically possible for the language runtime to catch this case?” If the answer is yes, don’t write a unit test for it.
For example, a dynamically typed language like PHP doesn’t have a way to set the type of an argument to a list of <class name>. Instead, you’d have to rely on the pretty generic array type. To verify that a given array is indeed a flat list of objects of a certain type, you would use an assertion, as in the following listing.
final class EventDispatcher { public function __construct(array eventListeners) { Assertion.allIsInstanceOf( eventListeners, EventListener.className ); // ... } }
Since this is an error condition that a more evolved type system could catch, you don’t have to write a unit test that catches the AssertionFailedException thrown by allIsInstanceOf(). However, if you have to inspect a given value and check that it’s within a certain range, or if you have to verify the number of items in a list, etc., you will have to write a unit test that shows you’ve covered the edge cases. Revisiting a previous example, the domain invariant that a given latitude is always between –90 and 90 inclusive should be verified with a test.
expectException( AssertionFailedException.className, 'latitude', function() { new Coordinates(-90.1, 0.0) } ); // and so on...
Although the tools sometimes allow it, you shouldn’t save up assertion exceptions and throw them as a list. Assertions are not meant to provide the user with a convenient list of things that are wrong. They are meant for the programmer, who needs to know that they are using a constructor or method in the wrong way. As soon as you notice anything wrong, just make the object scream.
If you want to supply the user with a list of things that are wrong about the data they provided (by submitting a form, sending an API request, etc.) you should use a data transfer object (DTO) and validate it instead. We’ll discuss this type of object at the end of this chapter.
Assertion.greaterThan(value, limit); Assertion.between( value, lowerLimit, upperLimit ); Assertion.lessThan(value, limit);Rewrite the constructor of PriceRange to use the appropriate assertion functions.
final class PriceRange { public function __construct(int minimumPrice, int maximumPrice) { if (minimumPrice < 0) { throw new InvalidArgumentException( 'minimumPrice should be 0 or more' ); } if (maximumPrice < 0) { throw new InvalidArgumentException( 'maximumPrice should be 0 or more' ); } if (maximumPrice <= minimumPrice) { throw new InvalidArgumentException( 'maximumPrice should be greater than minimumPrice' ); } this.minimumPrice = miminumPrice; this.maximumPrice = maximumPrice; } }
Services can have dependencies, and they should be injected as constructor arguments. But other objects shouldn’t get any dependencies injected, only values, value objects, or lists of them. If a value object still needs a service to perform some task, you could optionally inject it as a method argument, as in the next listing.
final class Money { private Amount amount; private Currency currency; public function __construct(Amount amount, Currency currency) { this.amount = amount; this.currency = currency; } public function convert( ExchangeRateProvider exchangeRateProvider, 1 Currency targetCurrency ): Money { exchangeRate = exchangeRateProvider.getRateFor( this.currency, targetCurrency ); return exchangeRate.convert(this.amount); } }
It may sometimes feel a bit strange to pass a service as a method argument, so it makes sense to consider alternative implementations too. Maybe we shouldn’t pass the ExchangeRateProvider service, but only the information we get from it: Exchange-Rate. This would require Money to expose both its internal Amount and Currency objects, but that may be a reasonable price to pay for not injecting the dependency. This results in a situation like the following.
final class ExchangeRate { public function __construct( Currency from, Currency to, Rate rate ) { // ... } public function convert(Amount amount): Money { // ... } } money = new Money(/* ... */); exchangeRate = exchangeRateProvider.getRateFor( 1 money.currency(), targetCurrency ); converted = exchangeRate.convert(money.amount()); 2
After moving things around one more time, we could settle for a solution that involves only exposing Money’s internal Currency object, not its Amount, as is done in the following listing. (We will get back to the topic of exposing object internals in section 6.3.)
final class Money { public function convert(ExchangeRate exchangeRate): Money { Assertion.equals( this.currency, exchangeRate.fromCurrency() ); return new Money( exchangeRate.rate().applyTo(this.amount), exchangeRate.targetCurrency() ); } } money = new Money(/* ... */); exchangeRate = exchangeRateProvider.getRateFor( money.currency(), targetCurrency ); converted = money.convert(exchangeRate);
You could argue that this solution expresses more clearly the domain knowledge we have about money and exchange rates. For example, the converted amount will be in the target currency of the exchange rate, and its “source” currency will be the same currency as the currency of the original amount.
In some cases, the need for passing around services as method arguments could be a hint that the behavior should be implemented as a service instead. In the case of converting an amount of money to a given currency, we might as well create a service and let it do the work, collecting all the relevant information from the Amount and Currency objects provided to it.
final class ExchangeService { private ExchangeRateProvider exchangeRateProvider; public function __construct( ExchangeRateProvider exchangeRateProvider ) { this.exchangeRateProvider = exchangeRateProvider; } public function convert( Money money, Currency targetCurrency ): Money { exchangeRate = this.exchangeRateProvider .getRateFor(money.currency(), targetCurrency); return new Money( exchangeRate.rate().applyTo(money.amount()), targetCurrency ); } }
Which solution you choose will depend on how close you want to keep the behavior to the data, whether or not you think it’s too much for an object like Money to know about exchange rates too, or how much you want to avoid exposing object internals.
interface PasswordHasher { public function hash(string password): string } final class User { private string username; private string hashedPassword; public function __construct(string username) { this.username = username; } public function setPassword( string plainTextPassword ): void { this.hashedPassword = /* ... */; 1 } }
private PasswordHasher hasher; public function __construct( string username, PasswordHasher hasher ) { this.hasher = hasher; } public function setPassword( string plainTextPassword ): void { this.hashedPassword = this.hasher.hash( plainTextPassword ); }
private PasswordHasher hasher; public function setPasswordHasher(PasswordHasher hasher): void { this.hasher = hasher; } public function setPassword( string plainTextPassword ): void { this.hashedPassword = this.hasher.hash( plainTextPassword ); }
public function setPassword( string plainTextPassword, PasswordHasher hasher ): void { this.hashedPassword = hasher.hash( plainTextPassword ); }
public function setPassword( string plainTextPassword ): void { this.hashedPassword = PasswordHasher.getInstance() .hash( plainTextPassword ); }
For services, it’s fine to use the standard way of defining constructors (public function __construct()). However, for other types of objects, it’s recommended that you use named constructors. These are public static methods that return an instance. They could be considered object factories.
A common case for using named constructors is constructing an object from one or more primitive-type values. This results in methods like fromString(), fromInt(), etc. As an example, take a look at the following Date class.
final class Date { private const string FORMAT = 'd/m/Y'; private DateTime date; private function __construct() { // do nothing here } public static function fromString(string date): Date { object = new Date(); DateTime = DateTime.createFromFormat( 1 Date.FORMAT, date ); object.date = DateTime; return object; } } date = Date.fromString('1/4/2019');
It’s important to add a regular, but private, constructor method, so that clients won’t be able to bypass the named constructor you offer to them, which would possibly leave the object in an invalid or incomplete state.
It may seem strange that this public static fromString() method can create a new object instance and manipulate the date property of the new instance. After all, this property is private, so that shouldn’t be allowed, right?
Scoping of methods and properties is usually class-based, not instance-based, so private properties can be manipulated by any object, as long as it’s of the exact same class. The fromString() method in this example counts as a method of the same class, which is why it can manipulate the date property directly, without the need for a setter.
When you add a named constructor that creates an object based on a primitive-type value, you may feel the need for symmetry and want to add a method that can convert the object back to the primitive-type value. For instance, having a fromString() constructor may lead you to automatically provide a toString() method too. Make sure you only do this once there is a proven need for it.
When you discuss the concept of a “sales order” with your domain expert, they would never speak about “constructing” a sales order. Maybe they would talk about “creating” a sales order, or they might use a more specific term like “placing” a sales order. Look out for these words and use them as method names for your named constructors.
final class SalesOrder { public static function place(/* ... */): SalesOrder { // ... } } salesOrder = SalesOrder.place(/* ... */);
Some objects may offer multiple named constructors, because there are different ways in which you can construct them. For example, if you want a decimal value with a certain precision, you could choose an integer value with a positive integer precision as the normalized way of representing such a number. At the same time, you may want to allow clients to use their existing values, which are strings or floats, as input for working with such a decimal value. Using a private constructor helps to ensure that whatever construction method is chosen, the object will end up in a complete and consistent state. The following listing shows an example.
final class DecimalValue { private int value; private int precision; private function __construct(int value, int precision) { this.value = value; Assertion.greaterOrEqualThan(precision, 0); this.precision = precision; } public static function fromInt( int value, int precision ): DecimalValue { return new DecimalValue(value, precision); } public static function fromFloat( float value, int precision ): DecimalValue { return new DecimalValue( (int)round(value * pow(10, precision)), precision ); } public static function fromString(string value): DecimalValue { result = preg_match('/^(d+).(d+)/', value, matches); if (result == 0) { throw new InvalidArgumentException(/* ... */); } wholeNumber = matches[1]; decimals = matches[2]; valueWithoutDecimalSign = wholeNumber . decimals; return new DecimalValue( (int)valueWithoutDecimalSign, strlen(decimals) ); } }
In summary, the using named constructors offers two main advantages:
Besides creating entities and value objects, named constructors can be used to offer convenient ways to instantiate custom exceptions. We’ll discuss these later, in section 5.2.
final class Date { private DateTime date; public function __construct(string date) { this.date = DateTime.createFromFormat( 'd/m/Y', date ); } }
Applying all the object design rules in this book will lead to objects that are in complete control of what goes into them, what stays inside, and what a client can do with them. A technique that works completely against this object design style is property filler methods, which look like the following fromArray() method.
final class Position { private int x; private int y; public static function fromArray(array data): Position { position = new Position(); position.x = data['x']; position.y = data['y']; return position; } }
This kind of method could even be turned into a generic utility that would copy values from the data array into the corresponding properties using reflection. Though it may look convenient, the object’s internals are now out in the open, so always make sure that the construction of an object happens in a way that’s fully controlled by the object itself.
At the end of this chapter, we’ll look at an exception to this rule. For data transfer objects, a property filler could be a way to map, for example, form data onto an object. Such an object doesn’t need to protect its internal data as much as an entity or a value object has to.
It’s common to start designing an object by thinking about what needs to go in. For services, you may end up injecting more dependencies than you need, so you should inject dependencies only when you need them. The same is true for other types of objects: don’t require more data than is strictly needed to implement the object’s behavior.
One type of object that often ends up carrying around more data than needed is an event object, representing something that has happened somewhere in the application. An example of such an event is the following ProductCreated class.
final class ProductCreated { public function __construct( ProductId productId, Description description, StockValuation stockValuation, Timestamp createdAt, UserId createdBy, /* ... */ ) { // ... } } this.recordThat( 1 new ProductCreated( /* ... */ 2 ) );
If you don’t know which event data will be important for yet-to-be-implemented event listeners, don’t add anything. Just add a constructor with no arguments at all, and add more data when the data is needed. This way, you will provide data on a need-to-know basis.
How do you know what data should actually go into an object’s constructor? By designing the object in a test-driven way. This means that you first have to know how an object is going to be used.
Writing tests for your objects, specifying their desired behavior, will let you figure out which data is actually needed at construction time and which data can be provided later. It will also help you figure out which data needs to be exposed later on and which data can stay behind the scenes, as implementation details of the object.
As an example, let’s take another look at the Coordinates class we saw earlier.
final class Coordinates { // ... public function __construct(float latitude, float longitude) { if (latitude > 90 || latitude < -90) { throw new InvalidArgumentException( 'Latitude should be between -90 and 90' ); } this.latitude = latitude; if (longitude > 180 || longitude < -180) { throw new InvalidArgumentException( 'Longitude should be between -180 and 180' ); } this.longitude = longitude; } }
How can we test that the constructor works? What about the following test?
public function it_can_be_constructed(): void { coordinates = new Coordinates(60.0, 100.0); assertIsInstanceOf(Coordinates.className, coordinates); }
This isn’t very informative. In fact, it’s impossible for the assertion to fail unless the constructor has thrown an exception, which is an execution flow we’re explicitly not testing here.
What is the task of the constructor? Judging from the code, it’s to assign the given constructor arguments to internal object properties. So how can we be sure that this has worked? We could add getters, which would allow us to find out what’s inside the object’s properties, as follows.
final class Coordinates { // ... public function latitude(): float { return this.latitude; } public function longitude(): float { return this.longitude; } }
The next listing shows how we could use those getters in a unit test.
public function it_can_be_constructed(): void { coordinates = new Coordinates(60.0, 100.0); assertEquals(60.0, coordinates.latitude()); assertEquals(100.0, coordinates.longitude()); }
But now we’ve introduced a way for internal data to get out of the object, for no other reason than to test the constructor.
Look back at what we’ve done here: We’ve been testing constructor code after we wrote it. We’ve been testing this code, knowing what’s going on in there, meaning the test is very close to the implementation of the class. We’ve been putting data into an object, without even knowing if we’ll ever need that data again. In conclusion, we’ve done too much, too soon, without a healthy dose of distance from the object’s implementation.
The only thing we can and should do at this point is test that the constructor doesn’t accept invalid arguments. We’ve discussed this before: you should verify that providing values for latitude and longitude outside of their acceptable ranges triggers an exception, making it impossible to construct the Coordinates object.
Further down the road, we’ll talk more about exposing data, but for now take the following advice:
Once you start adding actual behavior to the object, you will implicitly test the happy path for the constructor anyway, because when doing so you’ll need a fully instantiated object.
final class Product { private int id; private string name; public function __construct(int id, string name) { this.id = id; this.name = name; } public function id(): int { return this.id; } public function name(): string { return this.name; } } public function it_can_be_constructed(): void 1 { product = new Product(1, 'Some name'); assertEquals(1, product.id()); assertEquals('Some name', product.name()); }
The rules described in this chapter apply to entities and value objects; we care a lot about the consistency and validity of the data that ends up inside such objects. These objects can only guarantee correct behavior if the data they use is correct too.
There’s another type of object that I haven’t mentioned so far, to which most of the previous rules don’t apply. It’s a type of object that you will find at the edges of an application, where data coming from the world outside is converted into a structure that the application can work with. The nature of this process requires it to behave a little differently from entities and value objects.
This special type of object is known as a data transfer object (DTO):
Since a DTO doesn’t protect its state and exposes all of its properties, there is no need for getters and setters. This means it’s quite sufficient to use public properties for them. Because DTOs can be constructed in steps and don’t require a minimum amount of data to be provided, they don’t need constructor methods.
DTOs are often used as command objects, matching the user’s intention and containing all the data needed to fulfill their wish. An example of such a command object is the following ScheduleMeetup command, which represents the user’s wish to schedule a meetup with the given title on the given date.
final class ScheduleMeetup { public string title; public string date; }
The way you can use such an object is, for example, by populating it with the data submitted with a form, and then passing it to a service, which will schedule the meetup for the user. An example implementation can be found in the following listing.
final class MeetupController { public function scheduleMeetupAction(Request request): Response { formData = /* ... */; 1 scheduleMeetup = new ScheduleMeetup(); 2 scheduleMeetup.title = formData['title']; scheduleMeetup.date = formData['date']; this.scheduleMeetupService.execute(scheduleMeetup); // ... } }
The service will create an entity and some value objects and eventually persist them. When instantiated, these objects will throw exceptions if anything is wrong with the data that was provided to them. However, such exceptions aren’t really user-friendly; they can’t even be easily translated to the user’s language. Also, because they break the application’s flow, exceptions can’t be collected and returned as a list of input errors to the user.
If you want to allow users to correct all their mistakes in one go, before resubmitting the form, you should validate the command’s data before passing the object to the service that’s going to handle it. One way to do this is by adding a validate() method to the command, which can return a simple list of validation errors. If the list is empty, it means that the submitted data was valid.
final class ScheduleMeetup { public string title; public string date; public function validate(): array { errors = []; if (this.title == '') { errors['title'][] = 'validation.empty_title'; } if (this.date == '') { errors['date'][] = 'validation.empty_date'; } DateTime.createFromFormat('d/m/Y', this.date); errors = DateTime.getLastErrors(); if (errors['error_count'] > 0) { errors['date'][] = 'validation.invalid_date_format'; } return errors; } }
Form and validation libraries may offer you more convenient and reusable tools for validation. For instance, the Symfony Form and Validator components work really well with this kind of data transfer object.
Earlier we discussed property fillers and how they shouldn’t be used when working with most objects; they expose all the object’s internals. In the case of a DTO, this isn’t a problem because a DTO doesn’t protect its internals anyway. So, if it makes sense, you can add a property filler method to a DTO, such as to copy form data or JSON request data directly into a command object. Since filling the properties is the first thing that should happen to a DTO, it makes sense to implement the property filler as a named constructor.
final class ScheduleMeetup { public string title; public string date; public static function fromFormData( array formData ): ScheduleMeetup { scheduleMeetup = new ScheduleMeetup(); scheduleMeetup.title = formData['title']; scheduleMeetup.date = formData['date']; return scheduleMeetup; } }
final class PriceRange { public function __construct(int minimumPrice, int maximumPrice) { if (minimumPrice < 0) { throw new InvalidArgumentException( 'minimumPrice should be 0 or more' ); } if (maximumPrice < 0) { throw new InvalidArgumentException( 'maximumPrice should be 0 or more' ); } if (minimumPrice > maximumPrice) { throw new InvalidArgumentException( 'maximumPrice should be greater than minimumPrice' ); } this.minimumPrice = miminumPrice; this.maximumPrice = maximumPrice; } }
final class CountryCode { private static knownCountryCodes = ['NL', 'GB']; private string countryCode; public function __construct(string countryCode) { if (!in_array( countryCode, CountryCode.knownCountryCodes) ) { throw new InvalidArgumentException( 'Unknown country code: ' . countryCode ); } this.countryCode = countryCode; } }
final class Distance { private int distance; private string unit; public function __construct(int distance, string unit) { if (distance <= 0) { throw new InvalidArgumentException( 'distance should be greater than 0' ); } this.distance = distance; if (!in_array(unit, ['meters', 'feet'])) { throw new InvalidArgumentException( 'Unknown unit: ' unit ); } this.unit = unit; } } final class Run { public function __construct(Distance distance) { // ... } }
final class PriceRange { public function __construct(int minimumPrice, int maximumPrice) { Assertion.greaterThanOrEqual(minimumPrice, 0); Assertion.greaterThanOrEqual(maximumPrice, 0); Assertion.greaterThan(maximumPrice, minimumPrice); this.minimumPrice = miminumPrice; this.maximumPrice = maximumPrice; } }
3.17.76.218