Chapter 3. Creating other objects

This chapter covers

  • Instantiating other types of objects
  • Preventing objects from being incomplete
  • Protecting domain invariants
  • Using named constructors
  • Using assertions

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.

3.1. Require the minimum amount of data needed 3.1 to behave consistently

Take a look at the following Position class.

Listing 3.1. The 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.

Listing 3.2. Position has required constructor arguments for 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

  • 1 x and y have to be provided, or you won’t be able to get an instance of Position.

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.”

Exercises

  1. What’s wrong with the Money object used here?
    money = new Money()
    money.setAmount(100);
    money.setCurrency('USD');

    1. It uses setters for providing the minimum required data.
    2. It has no dependencies.
    3. Apparently it has default constructor arguments.
    4. It can exist in an inconsistent state.

3.2. Require data that is meaningful

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.

Listing 3.3. The Coordinates class
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

  • 1 Nothing stops us from creating a Coordinates object that doesn’t make any sense.

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.

Listing 3.4. Verifying the domain invariants of Coordinates
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...

  • 1 The type of the expected exception
  • 2 A keyword that should be in the exception’s message
  • 3 An anonymous function that should cause the exception to be thrown

To make these tests pass, throw an exception in the constructor as soon as something about the provided arguments looks wrong.

Listing 3.5. Throwing exceptions for invalid constructor arguments
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.

Listing 3.6. The ReservationRequest class
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:

  • There should always be at least one adult (because children can’t book a hotel room on their own).
  • Everybody can have their own room, but you can’t book more rooms than there are guests. (It wouldn’t make sense to allow people to book rooms where nobody will sleep.)

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.

Listing 3.7. Validating the meaningfulness of constructor arguments
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.

Listing 3.8. The Deal class
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.

Listing 3.9. Deal validates the sum of the amounts for both parties
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.

Listing 3.10. Removing the superfluous constructor argument
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.

Listing 3.11. The Line class
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.'
            );
        }

        // ...
    }
}

  • 1 We only care about the distance if the line is a dotted line. For solid lines, there’s no distance to be dealt with.

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.

Listing 3.12. Line now offers different ways for lines to be constructed
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;
    }
}

  • 1 No need to worry about distanceBetweenDots here!

These static methods are named constructors, and we’ll take a closer look at them in section 3.9.

Exercises

  1. PriceRange represents the minimum and maximum price in cents that a bidder would pay for a given object:
    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.

3.3. Don’t use custom exception classes for invalid argument exceptions

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.

Listing 3.13. SpecificException can be caught and dealt with
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.

3.4. Test for specific invalid argument exceptions by analyzing the ex- xception’s message

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.

Listing 3.14. The Coordinates class
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.

Listing 3.15. Tests for the domain invariants of Coordinates
// 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.

Listing 3.16. Verifying that the exception message contains a specific string
expectException(
    InvalidArgumentException.className,
    'Longitude',                         1
    function() {
        new Coordinates(-90.1, 180.1);
    }
);

  • 1 This word is supposed to be in the exception message.

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.

3.5. Extract new objects to prevent domain invariants from being verif- fied in multiple places

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.

Listing 3.17. The User class
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');
    }
);

  • 1 Validates that the provided email address is valid
  • 2 Validates it again, if it’s going to be updated
  • 3 The constructor will catch invalid email addresses.
  • 4 Creates a valid User object first
  • 5 changeEmailAddress() will also catch invalid email addresses.

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.

Listing 3.18. The EmailAddress class
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.

Exercises

  1. A country code can be represented as a two-character string, but not every two-character string will be a valid country code. Create a value object class that represents a valid country code. For now, assume that the list of known country codes is NL and GB.

3.6. Extract new objects to represent composite 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.

Listing 3.19. Amount and Currency
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 {
        // ...
    }
}

  • 1 Amount and Currency always go together.

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.

Listing 3.20. 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.

Adding more object types leads to more typing. Is that really necessary?

100 has fewer characters than new Amount(100), but all that extra typing gives you the benefits of using object types:

  1. You can be certain that the data the object wraps has been validated.
  2. An object usually exposes additional, meaningful behaviors that make use of its data.
  3. An object can keep values together that belong together.
  4. An object helps you keep implementation details away from its clients.

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.

Exercises

  1. With a Run object, you can save the distance you covered in a run:
    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.

3.7. Use assertions to validate constructor arguments

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:

1

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.

Listing 3.21. EventDispatcher uses an assertion function in its constructor
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.

Listing 3.22. Add unit tests for domain invariants
expectException(
    AssertionFailedException.className,
    'latitude',
    function() {
        new Coordinates(-90.1, 0.0)
    }
);
// and so on...
Don’t collect exceptions

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.

Exercises

  1. You can use the following assertion functions:
    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;
        }
    }

3.8. Don’t inject dependencies; optionally pass them as method arguments

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.

Listing 3.23. Money needs the ExchangeRateProvider service
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);
    }
}

  • 1 ExchangeRateProvider is a method argument, not a constructor argument.

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.

Listing 3.24. Alternative implementation: don’t pass ExchangeRateProvider
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

  • 1 We retrieve ExchangeRate up front.
  • 2 Then we use it to convert the amount we have.

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.)

Listing 3.25. Passing ExchangeRate instead of ExchangeRateProvider
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.

Listing 3.26. Alternative implementation: ExchangeService does all the work
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.

Exercises

  1. Given the following User class, how should you provide the PasswordHasher service to it?
    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
       }
    }

    • 1 Here we’d like to use the PasswordHasher service to hash the password.

    1. By adding an extra constructor argument:
      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
          );
      }
    2. By adding a setPasswordHasher(PasswordHasher passwordHasher) to the class:
      private PasswordHasher hasher;
      
      public function setPasswordHasher(PasswordHasher hasher): void
      {
          this.hasher = hasher;
      }
      
      public function setPassword(
          string plainTextPassword
      ): void {
          this.hashedPassword = this.hasher.hash(
              plainTextPassword
          );
      }
    3. By adding PasswordHasher as a method argument:
      public function setPassword(
          string plainTextPassword,
          PasswordHasher hasher
      ): void {
          this.hashedPassword = hasher.hash(
              plainTextPassword
          );
      }
    4. By making the PasswordHasher globally available:
      public function setPassword(
          string plainTextPassword
      ): void {
          this.hashedPassword = PasswordHasher.getInstance()
              .hash(
                  plainTextPassword
              );
      }

3.9. Use named constructors

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.

3.9.1. Create from primitive-type values

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.

Listing 3.27. The Date class wraps a date string
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');

  • 1 We’d still have to assert that createFromFormat() doesn’t return false.

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.

Wait, does this work?

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.

3.9.2. Don’t immediately add toString(), toInt(), etc.

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.

3.9.3. Introduce a domain-specific concept

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.

Listing 3.28. In real life, sales orders aren’t “constructed,” but “placed”
final class SalesOrder
{
    public static function place(/* ... */): SalesOrder
    {
        // ...
    }
}

salesOrder = SalesOrder.place(/* ... */);

3.9.4. Optionally use the private constructor to enforce constraints

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.

Listing 3.29. Protecting domain invariants inside a private constructor
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:

  • They can be used to offer several ways to construct an object.
  • They can be used to introduce domain-specific synonyms for creating an object.

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.

Exercises

  1. The following Date class can be instantiated by passing in a string in the right format, which will then be converted to a DateTime instance. But what if the client already has a DateTime instance available? How can we build in an option for the client to pass their instance directly to the Date object, instead of through an intermediate string representation?
    final class Date
    {
        private DateTime date;
    
        public function __construct(string date)
        {
            this.date = DateTime.createFromFormat(
                'd/m/Y',
                date
            );
        }
    }

    1. Remove the string type from the constructor’s date parameter to allow clients to pass in a DateTime instance without any type errors.
    2. Add two named constructors to the class: fromString(string date): Date and fromDateTime(DateTime dateTime): Date.
    3. Make the string date parameter optional, and add a second optional Date-Time dateTime parameter to the constructor.
    4. Create a new class that extends from Date and overrides the constructor to accept a DateTime instance instead of a string.

3.10. Don’t use property fillers

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.

Listing 3.30. Position has a property filler called fromArray()
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.

Note

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.

3.11. Don’t put anything more into an object than it needs

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.

Listing 3.31. The ProductCreated class represents an event
final class ProductCreated
{
    public function __construct(
        ProductId productId,
        Description description,
        StockValuation stockValuation,
        Timestamp createdAt,
        UserId createdBy,
        /* ... */
    ) {
        // ...
    }
}

this.recordThat(          1
    new ProductCreated(
        /* ... */         2
    )
);

  • 1 Inside the Product entity
  • 2 Passes along all the data that was available when creating the product

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.

3.12. Don’t test constructors

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.

Listing 3.32. The constructor of Coordinates
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?

Listing 3.33. A first try at testing the constructor of Coordinates
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.

Listing 3.34. Extra getters for testing the Coordinates constructor
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.

Listing 3.35. Using the new 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:

  • Only test a constructor for ways in which it should fail.
  • Only pass in data as constructor arguments when you need it to implement real behavior on the object.
  • Only add getters to expose internal data when this data is needed by some other client than the test itself.

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.

Exercises

  1. What’s wrong with the following code for the Product entity?
    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());
    }

    • 1 This is the only test for the Product class.

    1. It has getters.
    2. The getters seem to be there only to test the constructor.
    3. The properties aren’t nullable.

3.13. The exception to the rule: Data transfer objects

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):

  • A DTO can be created using a regular constructor.
  • Its properties can be set one by one.
  • All of its properties are exposed.
  • Its properties contain only primitive-type values.
  • Properties can optionally contain other DTOs, or simple arrays of DTOs.

3.13.1. Use public properties

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.

Listing 3.36. The ScheduleMeetup DTO
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.

Listing 3.37. Populating the ScheduleMeetup DTO and passing it to a service
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);

        // ...
    }
}

  • 1 Extract the form data from the request body.
  • 2 Create the command object using this data.

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.

3.13.2. Don’t throw exceptions, collect validation errors

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.

Listing 3.38. Validating the ScheduleMeetup DTO
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.

3.13.3. Use property fillers when needed

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.

Listing 3.39. The ScheduleMeetup DTO has a property filler
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;
    }
}
Exercises

  1. What type of object do you need if you want to provide a list of validation errors to the user?

    1. An entity
    2. A DTO
  2. What type of object would throw an exception if the data provided to it is incorrect?

    1. An entity
    2. A DTO
  3. What type of object would limit the amount of data that it exposes?

    1. An entity
    2. A DTO

Summary

  • Objects that are not service objects receive values or value objects, not dependencies. Upon construction, an object should require a minimum amount of data to be provided in order to behave consistently. If any of the provided constructor arguments is invalid in some way, the constructor should throw an exception about it.
  • It helps to wrap primitive-type arguments inside (value) objects. This makes it easy to reuse validation rules for these values. It also adds more meaning to the code by specifying a domain-specific name for the type (class) of the value.
  • For objects that aren’t services, constructors should be static methods, also known as named constructors, which offer yet another opportunity for introducing domain-specific names in your code.
  • Don’t provide any more data to a constructor than is needed to make the object behave as specified by its unit tests.
  • A type of object for which most of these rules don’t count is a data transfer object. DTOs are used to carry data provided by the world outside, and they expose all their internals.

Answers to the exercises

  1. Correct answers: a and d. Money is not a service, so it should not have any dependencies injected as constructor arguments. Also, based on the example, there’s no way to establish whether or not the constructor has default arguments.
  2. Suggested answer:
     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;
         }
     }
  3. Suggested answer:
     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;
         }
     }
  4. Suggested answer:
     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)
         {
             // ...
         }
     }
  5. Suggested answer:
     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;
         }
     }
  6. Correct answer: c. User is not a service, but an entity, so it should not get any dependencies injected as constructor arguments or using setter methods. Also, it should not need to reach out for dependencies. Instead, any dependency that it needs to perform a task should be provided to it as a method argument.
  7. Correct answer: b. The other options usually lead to bad design: removing types, adding multiple arguments only one of which will be used each time, and extending from a class you don’t own to add behavior to it.
  8. Correct answer: b. Getters aren’t forbidden, and it’s okay for a property to be null. The rule is not to add getters just for testing purposes.
  9. Correct answer: b. As soon as an entity recognizes that a client passes invalid data to it, it will throw an exception. This leaves no room for analyzing the available data and generating a list of validation errors.
  10. Correct answer: a. A DTO will accept any data provided to it, as long as it has the expected type. An entity will throw exceptions as soon as it receives even one piece of invalid data.
  11. Correct answer: a. A DTO by default exposes all of its data. An entity normally protects most of its internal data.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.76.218