Как стать автором
Обновить

Книга «Объекты. Стильное ООП»

Время на прочтение9 мин
Количество просмотров9.4K
image Привет, Хаброжители!

Хороший объектно-ориентированный код удобно читать, изменять и исправлять. Универсальные практики проектирования объектов, собранные в этой книге, позволят улучшить ваш стиль кодирования. Эти правила подойдут к любому объектно-ориентированному языку, они делают код максимально понятным и надежным, а также повышают производительность как индивидуальных разработчиков, так и команд.

Книга «Объекты. Стильное ООП» познакомит вас с профессиональными техниками написания ОО-кода. Маттиас Нобак раскрывает правила создания объектов, определения методов, изменения и извлечения состояний и многое другое. Все примеры написаны на простом псевдокоде, который легко перевести в любой язык программирования. Кейс за кейсом вы изучите ключевые сценарии и задачи проектирования объектов, а затем шаг за шагом создадите простое веб-приложение, которое покажет, как должны взаимодействовать объекты разных типов.

Для кого эта книга
Эта книга предназначена для программистов, владеющих хотя бы базовыми знаниями объектно-ориентированных языков программирования. Вам нужно понимать возможности языка в области классов. Это подразумевает, что вы имеете представление о том, как создать класс, инстанцировать его, расширить, обозначить как abstract, задать метод, вызвать его, задать параметры и их типы, задать возвращаемые типы, задать свойства и их типы и т. д.
Я также рассчитываю, что у вас есть опыт работы со всем этим, пусть даже небольшой. Вы наверняка сможете освоить эту книгу, если прошли базовый курс программирования или давно работаете с классами.


3.2. ЗАПРАШИВАЙТЕ ТОЛЬКО ДАННЫЕ, КОТОРЫЕ ИМЕЮТ СМЫСЛ


В примере выше конструктор принимает любое целое число, положительное или отрицательное, вплоть до бесконечности в обоих направлениях. А теперь рассмотрим другую систему координат, где расположение задается широтой и долготой, определяющими положение объекта на земле. В таком случае не каждое значение для широты и долготы будет считаться осмысленным, см. листинг 3.3.

image

Всегда следите, чтобы клиент предоставлял только осмысленные значения. Все, что считается бессмысленным, теоретически можно также назвать инвариантом предметной области. Но в данном случае инвариантом будет то, что «широта— это значение между –90 и 90 включительно, а долгота — значение между –180 и 180 включительно».

При проектировании объектов ориентируйтесь на инварианты предметной области. Собирайте больше информации об инвариантах и используйте их при создании модульных тестов. Например, ниже представлен листинг, в котором используется утилита expectException(), описанная в разделе 1.10.

image

Чтобы тесты проходили успешно, выдавайте исключение в конструкторе, как только любой аргумент будет неверным, см. листинг 3.5.

Листинг 3.5. Выдача исключений при недопустимых аргументах конструктора

final class Coordinates
{
    // ... 

    public function __construct(float latitude, float longitude)
    {

        if (latitude > 90 || latitude < -90) {
            throw new InvalidArgumentException(
                'Широта должна быть в пределах от -90 до 90'
            );
        }
        this.latitude = latitude;

        if (longitude > 180 || longitude < -180) {
            throw new InvalidArgumentException(
                'Долгота должна быть в пределах от -180 до 180'
            );
        }
        this.longitude = longitude;
    }
}

Хотя порядок выражений в конструкторе неважен (как мы обсудили выше), рекомендуется все же проводить проверку аргументов непосредственно перед присвоением значения свойству. Это облегчит чтение и понимание того, как связаны выражения.

В некоторых случаях проверки допустимости аргументов по отдельности недостаточно и нужно проверить, что все вместе взятые аргументы конструктора имеют смысл. В примере ниже показан класс ReservationRequest, который используется, чтобы хранить информацию о бронировании номеров в отеле.

Листинг 3.6. Класс ReservationRequest

final class ReservationRequest
{

    public function __construct(
        int numberOfRooms,
        int numberOfAdults,
        int numberOfChildren
    ) {
        // ...
    }
}

Обсуждая бизнес-правила для этого объекта с отраслевым экспертом, вы узнаете следующие ограничения:
  • Всегда должен быть хотя бы один взрослый (так как дети не могут бронировать номера в отеле самостоятельно).
  • Любой может забронировать для себя отдельный номер, но количество бронирований не должно превышать количества гостей (нет смысла бронировать номер, в котором никто не будет ночевать).

Поэтому получается, что numberOfRooms (число номеров) и numberOfAdults (число взрослых) связаны и должны проверяться на допустимость вместе. Нужно убедиться, что конструктор принимает оба значения и применяет соответствующие правила, как в листинге ниже.

Листинг 3.7. Проверка аргументов конструктора на допустимость

final class ReservationRequest
{

    public function __construct(
        int numberOfRooms,
        int numberOfAdults,
        int numberOfChildren
    ) {

        if (numberOfRooms > numberOfAdults + numberOfChildren) {
            throw new InvalidArgumentException(
                'Количество номеров не должно превышать количества гостей'
            );
        } 

        if (numberOfAdults < 1) {
            throw new InvalidArgumentException(
                'numberOfAdults должен иметь значение 1 или больше'
            );
        } 

        if (numberOfChildren < 0) {
            throw new InvalidArgumentException(
                'numberOfChildren должен иметь значение 0 или больше'
            );
        }
    }
}

В другом случае аргументы конструктора могут поначалу показаться связанными, но если класс перепроектировать, то консолидированной проверки аргументов удастся избежать. Рассмотрим следующий класс, представляющий бизнес-сделку, в ходе которой необходимо разделить некоторое количество денежных средств между двумя сторонами.

Листинг 3.8. Класс Deal

final class Deal
{

    public function __construct(
        int totalAmount,
        int amountToFirstParty,
        int amountToSecondParty
    ) {
        // ...
    }
}

Необходимо хотя бы по отдельности проверить аргументы конструктора (например, общее количество (totalAmount) должно быть больше 0 и т. д.). Но здесь также присутствует инвариант, который затрагивает все аргументы: сумма средств, получаемых каждой из сторон, должна быть равна общему их количеству. В листинге ниже показано, как проверить это правило.

Листинг 3.9. Deal проверяет сумму средств сторон

final class Deal
{

    public function __construct(
        int totalAmount,
        int amountToFirstParty,
        int amountToSecondParty
    ) {
        // ... 

        if (amountToFirstParty + amountToSecondParty
            != totalAmount) {
            throw new InvalidArgumentException(/* ... */);
        }
    }
}

Как вы, возможно, заметили, это правило можно реализовать намного эффективнее. Общую сумму саму по себе можно и не сообщать, если клиент предоставляет положительные числа для amountToFirstParty и amountToSecondParty. Объект Deal может сам узнать общую сумму сделки, складывая эти значения. Консолидированной проверки аргументов конструктора не требуется.

Листинг 3.10. Удаление лишних аргументов конструктора

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;
    }
}

Другой пример, в котором может показаться, что аргументы конструктора необходимо проверять вместе, — класс Line в листинге ниже, представляющий линию.

image

Реализация будет эффективнее, если предоставить клиенту возможность создавать линии двух видов: пунктирные и сплошные. Различные типы линий можно создавать, вызывая различные конструкторы.

image

Эти статические методы называют именованными конструкторами, и мы рассмотрим их подробнее в разделе 3.9.

УПРАЖНЕНИЕ

2 PriceRange представляет минимальную и максимальную цены в центах, которые покупатель может заплатить за некий объект:

final class PriceRange
{

    public function __construct(int minimumPrice, int maximumPrice)
    {
        this.minimumPrice = minimumPrice;
        this.maximumPrice = maximumPrice;
    }
}

Конструктор здесь принимает любое значение int для обоих аргументов. Улучшите конструктор и сделайте так, чтобы он выдавал ошибку, если эти значения не имеют смысла.

Если предоставить каждому объекту минимально необходимые верные и имеющие смысл данные во время вызова конструктора, приложение будет содержать только полные и действительные объекты, поведение которых будет соответствовать ожиданиям. Никаких сюрпризов или дополнительных проверок.

3.3. НЕ ИСПОЛЬЗУЙТЕ СОБСТВЕННЫЕ КЛАССЫ ИСКЛЮЧЕНИЙ ПРИ ПРОВЕРКЕ НЕДОПУСТИМЫХ АРГУМЕНТОВ


До сих пор мы выдавали общее исключение InvalidArgumentException, если аргумент метода не соответствовал нашим ожиданиям. Можно использовать собственный класс исключений, который расширяется от InvalidArgumentException. Преимущество такого подхода в том, что можно получать специфичные типы исключений и обрабатывать их определенным образом.

Листинг 3.13. SpecificEsception можно получать и обрабатывать

final class SpecificException extends InvalidArgumentException
{
} 

try {
    // попытка создать объект
} catch (SpecificException exception) {
    // обработка специфичной задачи определенным образом
}

Тем не менее при проверке аргументов это обычно не требуется. Недопустимый аргумент означает, что клиент использует объект не по назначению. Обычно это вызвано ошибкой программирования. В таком случае лучше остановить работу программы, не пытаясь восстановиться, и исправить ошибку.

В то же время в случае с RuntimeExceptions зачастую имеет смысл использовать собственные исключения, так как после них есть возможность восстановиться или преобразовать их в сообщения об ошибках, понятные пользователю. Мы подробнее рассмотрим исключения времени исполнения и то, как их создавать, в разделе 5.2.

3.4. ПРОВЕРЯЙТЕ СПЕЦИФИЧЕСКИЕ ИСКЛЮЧЕНИЯ ДЛЯ НЕДОПУСТИМЫХ АРГУМЕНТОВ, АНАЛИЗИРУЯ СООБЩЕНИЯ ИСКЛЮЧЕНИЙ


Даже если вы используете общий класс исключений InvaliArgumentException для проверки аргументов методов, эти аргументы необходимо различать во время выполнения модульных тестов. Еще раз посмотрим на конструктор класса Coordinates.

Листинг 3.14. Класс Coordinates

final class Coordinates
{
    // ... 

    public function __construct(float latitude, float longitude)
    {
        if (latitude > 90 || latitude < -90) {
            throw new InvalidArgumentException(
                'Широта должна иметь значение от -90 до 90'
            );
        }
        this.latitude = latitude; 

        if (longitude > 180 || longitude < -180) {
            throw new InvalidArgumentException(
                'Долгота должна иметь значение от -180 до 180'
            );
        }
        this.longitude = longitude;
    }
}

Нам нужно проверить, что клиенты не могут передавать неверные аргументы. Для этого напишем несколько тестов, как показано ниже.

Листинг 3.15. Тестирование инвариантов предметной области в классе Coordinates

// Широта не может быть больше 90.0
expectException(
    InvalidArgumentException.className,
    function() {
        new Coordinates(90.1, 0.0);
    }
); 

// Широта не может быть меньше -90.0
expectException(
    InvalidArgumentException.className,
    function() {
        new Coordinates(-90.1, 0.0);
    }
);

// Долгота не может быть больше 180.0
expectException(
    InvalidArgumentException.className,
    function() {
        new Coordinates(-90.1, 180.1);
    }
);

В последнем тесте из конструктора выдается исключение InvalidArgumentException, но это не то, что мы ожидали. Так как в этом случае используется недопустимое значение для широты из предыдущего теста, при попытке создания объекта Coordinates будет выдаваться исключение, которое сообщит, что «широта должна иметь значение от –90.0 до 90.0». Но тест должен проверять, что код отклонит недопустимые значения долготы. Это значит, что долгота не будет проверяться в этом тестовом сценарии, даже если все тесты пройдут успешно.

Чтобы предотвратить такого рода ошибки, старайтесь проверять, что выдаваемые в модульном тесте исключения соответствуют ожидаемым. Удобнее всего проверять, что сообщение об исключении содержит определенные слова.

image

При добавлении в тест ожидания подобного сообщения об исключении, как в листинге 3.15, тест будет выдавать ошибку. И он снова будет проходить успешно, если в конструктор будет передано верное значение широты.

3.5. СОЗДАВАЙТЕ НОВЫЕ ОБЪЕКТЫ, ЧТОБЫ ИЗБЕЖАТЬ МНОГОКРАТНОЙ ПРОВЕРКИ ИНВАРИАНТОВ ПРЕДМЕТНОЙ ОБЛАСТИ


На практике вы обнаружите, что одна и та же логика проверки повторяется в одном классе или даже в разных классах. Например, взгляните на следующий класс User, в котором при помощи функции из стандартной библиотеки многократно проверяется email-адрес.

image

И хотя можно просто передать логику проверки email-адреса в отдельный метод, более грамотное решение — создать отдельный класс объектов, представляющий собой допустимый email-адрес. Так как мы ожидаем, что все объекты создаются допустимыми, уберем часть «valid» из имени класса и реализуем решение следующим образом.

Листинг 3.18. Класс EmailAddress

final class EmailAddress
{

    private string emailAddress; 

    public function __construct(string emailAddress)
    {
        if (!is_valid_email_address(emailAddress)) {
            throw new InvalidArgumentException(
                'Недопустимый email'
            );
        }
        this.emailAddress = emailAddress;
    }
}

Когда вам встретится объект EmailAddress, вы всегда будете знать, что значение email-адреса для него уже проверено:

final class User
{

    private EmailAddress emailAddress;

     public function __construct(EmailAddress emailAddress)
    {
        this.emailAddress = emailAddress;
    } 

    // ... 

    public function changeEmailAddress(EmailAddress emailAddress): void
    {
        this.emailAddress = emailAddress;
    }
}

Оборачивание значений в новые объекты под названием объекты-значения полезно не только, чтобы избегать повторения логических конструкций. Как только вы заметите, что метод принимает примитивные значения (string, int и т. д.), стоит задуматься о том, чтобы создать для них класс. Чтобы принять решение, ответьте на вопрос: будут ли здесь приемлемы значения string, int, и т. д.? Если ответ — нет, создавайте отдельный класс.

Объекты-значения сами по себе стоит рассматривать как типы наравне со string, int и т. п. Создавая новые объекты для представления понятий, вы расширяете систему типов. Компилятор языка или среда исполнения могут проводить проверку соответствия типов, чтобы только правильные типы использовались при передаче значений через аргументы методов и в возвращаемых значениях.

УПРАЖНЕНИЕ

3 Код страны может быть представлен строкой из двух символов, но не каждая такая строка может быть кодом страны. Создайте класс объекта-значения, который будет представлять код страны. Будем считать, что список известных кодов страны состоит из NL и GB.
Об авторе
Маттиас Нобак — профессиональный веб-разработчик (с 2003 года). Он живет в городе Зейст в Нидерландах со своей второй половинкой и детьми: сыном и дочерью.

Маттиас — основатель собственной компании в области веб-разработки, обучения и консультирования под названием Noback’s Office. Он вплотную занимается бэкенд-разработкой и программной архитектурой и всегда ищет возможности для улучшения методов проектирования ПО.

С 2011 года ведет блог о программировании на matthiasnoback.nl. Маттиас — автор и других книг: «A Year with Symfony» (Leanpub, 2013), «Microservices for Everyone» (Leanpub, 2017) и «Principles of Package Design»1 (Apress, 2018). Связаться с Маттиасом можно по электронной почте (info@matthiasnoback.nl) или в Twitter (@matthiasnoback).

Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Объекты
Теги:
Хабы:
Всего голосов 12: ↑11 и ↓1+10
Комментарии5

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия