Статьи

От процедурного к объектно-ориентированному PHP

Этот урок был вдохновлен речью Роберта К. Мартина, которую я посмотрел год назад. Основная тема его выступления — о возможности выбора «Последнего языка программирования» . Он затрагивает такие темы, как, почему такой язык должен существовать? И как это должно выглядеть? Однако, если вы читаете между строк, была еще одна интересная идея, которая привлекла мое внимание: ограничения, которые каждая парадигма программирования накладывает на нас, программистов. Поэтому, прежде чем мы перейдем к вопросу о том, как мы можем преобразовать PHP-приложение, основанное на процедурах, в объектно-ориентированное, я хотел бы сначала немного рассказать о теории.


Таким образом, каждая парадигма программирования ограничивает нашу способность делать все, что мы хотим. Каждый из них что-то отнимает и предоставляет альтернативу для достижения того же результата. Модульное программирование убирает неограниченный размер программы. Это заставляет программиста использовать модули максимального размера, и каждый модуль заканчивается оператором перехода к другому модулю. Итак, первое ограничение по размеру. Затем структурированное программирование и процедурное программирование убирают оператор «перехода» и ограничивают программиста последовательностью, выбором и итерацией. Последовательности — это присвоения переменных, выборки — это решения if-else, а итерации — циклы do-while. Это строительные блоки большинства языков программирования и современных парадигм.

Объектно-ориентированное программирование убирает указатели на функции и вводит полиморфизм. PHP не использует указатели так, как это делает C, но вариант этих указателей на функции можно наблюдать в переменных функциях . Это позволяет программисту использовать значение переменной в качестве имени функции, чтобы можно было получить нечто подобное:

01
02
03
04
05
06
07
08
09
10
11
12
13
function foo() {
    echo «This is foo»;
}
 
function bar($param) {
    echo «This is bar saying: $param»;
}
 
$function = ‘foo’;
$function();
 
$function = ‘bar’;
$function(‘test’);

Это может не выглядеть важным на первый взгляд. Но подумайте, чего мы можем достичь с помощью такого мощного инструмента. Мы можем отправить переменную в качестве параметра функции, а затем позволить этой функции вызвать другую, на которую ссылается значение параметра. Это потрясающе. Это позволяет нам изменять функциональность функции, не зная об этом. Без функции даже не замечая никакой разницы.

На самом деле мы можем делать полиморфные вызовы с помощью этой техники.

Теперь вместо того, чтобы думать о том, что предоставляют указатели на функции, подумайте о том, как они работают. Разве они не скрытые заявления о переходе? На самом деле, они, или, по крайней мере, они очень похожи на косвенные «переход». Что не очень хорошо. То, что мы имеем здесь, на самом деле является умным способом «перейти», не используя его напрямую. Я должен признать, что в PHP, как показано в примере выше, это довольно легко понять, но это может привести к путанице в больших проектах и ​​множестве различных функций, передаваемых из одной функции в другую. В Си это еще более непонятно и ужасно трудно понять.

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

dependency_comparison

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

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


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


Google предоставляет клиент API для PHP. Мы будем использовать его для подключения к нашей учетной записи Google, чтобы мы могли управлять там календарями. Если вы хотите запустить код, вы должны настроить свою учетную запись Google для приема запросов календаря.

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

Клиентский код PHP Google API включен в каждый проект из примера кода, прилагаемого к этому руководству. Я рекомендую вам использовать это. Кроме того, если вам интересно, как установить его самостоятельно, ознакомьтесь с официальной документацией .

Затем следуйте инструкциям и заполните информацию в файле apiAccess.php . Этот файл потребуется как процедурным, так и объектно-ориентированным примером, поэтому вам не нужно его повторять. Я оставил свои ключи там, чтобы вы могли легче идентифицировать и заполнить свои.

Если вы используете NetBeans, я оставил файлы проекта в папках, содержащих различные примеры. Таким образом, вы можете просто открыть проекты и сразу же запустить их на локальном PHP-сервере (требуется PHP 5.4), просто выбрав Run / Run Project .

Клиентская библиотека для подключения к Google API является объектно-ориентированной. Ради нашего функционального примера я написал небольшой набор функций, которые в них заключают, те функции, которые нам нужны. Таким образом, мы можем использовать процедурный уровень, написанный над объектно-ориентированной клиентской библиотекой, чтобы нашему коду не приходилось использовать объекты.

Если вы хотите быстро проверить, работает ли ваш код и подключение к API Google, просто используйте приведенный ниже код в качестве файла index.php . В нем должны быть перечислены все календари, которые есть в вашем аккаунте. Должен быть хотя бы один календарь с полем сводки, являющимся вашим именем. Если у вас есть календарь с днями рождения вашего контакта, он может не работать с этим API Google, но не паниковать, просто выберите другой.

01
02
03
04
05
06
07
08
09
10
require_once ‘./google-api-php-client/src/Google_Client.php’;
require_once ‘./google-api-php-client/src/contrib/Google_CalendarService.php’;
require_once __DIR__ .
require_once ‘./functins_google_api.php’;
require_once ‘./functions.php’;
session_start();
 
$client = createClient();
if(!authenticate($client)) return;
listAllCalendars($client);

Этот файл index.php будет точкой входа в наше приложение. Мы не будем использовать веб-фреймворк или что-то необычное. Мы просто выведем HTML-код.


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

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

  • index.php — единственный файл, к которому мы обращаемся непосредственно из браузера и передаем ему параметры GET.
  • functions_google_api.php — оболочка для API Google, о которой мы говорили выше.
  • functions.php — где все происходит.

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

procedural_schema

У нас есть единственная функция doUserAction() , которая решает с помощью длинного оператора if-else , какие другие методы вызывать на основе параметров в переменной GET . Затем методы подключаются к календарю Google с помощью API и выводят на экран все, что мы хотим запросить.

01
02
03
04
05
06
07
08
09
10
function printCalendarContents($client) {
    putTitle(‘These are you events for ‘ . getCalendar($client, $_GET[‘showThisCalendar’])[‘summary’] . ‘ calendar:’);
    foreach (retrieveEvents($client, $_GET[‘showThisCalendar’]) as $event) {
        print(‘<div style=»font-size:10px;color:grey;»>’ . date(‘Ymd H:m’, strtotime($event[‘created’])));
        putLink(‘?showThisEvent=’ . htmlentities($event[‘id’]) .
                ‘&calendarId=’ .
        print(‘</div>’);
        print(‘<br>’);
    }
}

Этот пример, вероятно, самая сложная функция в нашем коде. Он вызывает вспомогательную функцию с именем putTitle() , которая просто печатает некоторый форматированный HTML- putTitle() для заголовка. Заголовок будет содержать название нашего календаря, которое можно получить, вызвав getCalendar() из functions_google_api.php . Возвращенный календарь будет массивом, содержащим summary поле. Это то, что мы после.

Переменная $client передается повсеместно во всех наших функциях. Требуется для подключения к Google API. Мы будем иметь дело с этим позже.

Далее мы перебираем все события в текущем календаре. Этот список массивов получается путем запуска вызова API, инкапсулированного в retrieveEvents() . Для каждого события мы печатаем дату его создания, а затем его название.

events_in_a_calendar

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


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

Первое, что меня раздражает в нашем неорганизованном коде, это то, что мы передаем эту переменную $client в качестве аргумента повсюду, на нескольких уровнях глубоко внутри вложенных функций. У процедурного программирования есть умный способ решить эту проблему — глобальная переменная. Поскольку $client определен в index.php и в глобальной области видимости, все, что нам нужно изменить, — это то, как наши функции используют его. Поэтому вместо ожидания параметра $client мы можем использовать:

1
2
3
4
5
6
7
8
function printCalendars() {
    global $client;
    putTitle(‘These are your calendars:’);
    foreach (getCalendarList($client)[‘items’] as $calendar) {
        putLink(‘?showThisCalendar=’ . htmlentities($calendar[‘id’]), $calendar[‘summary’]);
        print(‘<br>’);
    }
}

Сравните текущий код с недавно организованным кодом, чтобы увидеть разницу. Вместо того, чтобы передавать $client в качестве параметра, мы использовали global $client во всех наших функциях и передавали его в качестве параметра только функциям API Google. Технически, даже функции API Google могли бы использовать переменную $client из глобальной области видимости, но я думаю, что лучше сохранять API как можно более независимым.

Некоторые функции явно предназначены только для печати объектов на экране, другие — для решения, что делать, а некоторые — и то, и другое. Когда это происходит, иногда лучше перенести эти функции специального назначения в их собственный файл. Мы начнем с функций, используемых исключительно для печати вещей на экране, они будут перемещены в файл functions_display.php . Смотрите их ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
function printHome() {
    print(‘Welcome to Google Calendar over NetTuts Example’);
}
 
function printMenu() {
    putLink(‘?home’, ‘Home’);
    putLink(‘?showCalendars’, ‘Show Calendars’);
    putLink(‘?logout’, ‘Log Out’);
    print(‘<br><br>’);
}
 
function putLink($href, $text) {
    print(sprintf(‘<a href=»%s» style=»font-size:12px;margin-left:10px;»>%s</a> | ‘, $href, $text));
}
 
function putTitle($text) {
    print(sprintf(‘<h3 style=»font-size:16px;color:green;»>%s</h3>’, $text));
}
 
function putBlock($text) {
    print(‘<div display=»block»>’.$text.'</div>’);
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
function printEventDetails() {
    global $client;
    foreach (retrieveEvents($_GET[‘calendarId’]) as $event)
        if ($event[‘id’] == $_GET[‘showThisEvent’]) {
            putTitle(‘Details for event: ‘. $event[‘summary’]);
            putBlock(‘This event has status ‘ . $event[‘status’]);
            putBlock(‘It was created at ‘ .
                    date(‘Ymd H:m’, strtotime($event[‘created’])) .
                    ‘ and last updated at ‘ .
                    date(‘Ymd H:m’, strtotime($event[‘updated’])) .
            putBlock(‘For this event you have to <strong>’ . $event[‘summary’] . ‘</strong>.’);
        }
}

Ясно, что мы видим, что все, что находится внутри оператора if — это просто код представления, а остальное — бизнес-логика. Вместо одной громоздкой функции, обрабатывающей все, мы разбиваем ее на несколько функций:

01
02
03
04
05
06
07
08
09
10
function printEventDetails() {
    global $client;
    foreach (retrieveEvents($_GET[‘calendarId’]) as $event)
        if (isCurrentEvent($event))
            putEvent($event);
}
 
function isCurrentEvent($event) {
    return $event[‘id’] == $_GET[‘showThisEvent’];
}

После разделения бизнес-логика стала очень простой. Мы даже извлекли небольшой метод, чтобы определить, является ли событие текущим. За весь код представления теперь отвечает функция с именем putEvent($event) которая находится в файле functions_display.php :

1
2
3
4
5
6
7
8
9
function putEvent($event) {
    putTitle(‘Details for event: ‘ . $event[‘summary’]);
    putBlock(‘This event has status ‘ . $event[‘status’]);
    putBlock(‘It was created at ‘ .
            date(‘Ymd H:m’, strtotime($event[‘created’])) .
            ‘ and last updated at ‘ .
            date(‘Ymd H:m’, strtotime($event[‘updated’])) .
    putBlock(‘For this event you have to <strong>’ . $event[‘summary’] . ‘</strong>.’);
}

Хотя этот метод отображает только информацию, мы должны помнить, что он зависит от глубоких знаний о структуре $event . Но пока это нормально. Что касается остальных методов, они были разделены аналогичным образом.

Последнее, что беспокоит меня в нашем текущем коде, это длинная инструкция if-else в нашей функции doUserAction() , которая используется для определения того, что делать для каждого действия. Теперь PHP довольно гибок, когда дело касается метапрограммирования (вызова функций по ссылке). Этот трюк позволяет нам коррелировать имена функций со значениями переменной $_GET . Таким образом, мы можем ввести один параметр action в переменную $_GET и использовать его значение в качестве имени функции.

1
2
3
4
5
function doUserAction() {
    putMenu();
    if (!isset($_GET[‘action’])) return;
        $_GET[‘action’]();
}

На основе этого подхода наше меню будет сгенерировано следующим образом:

1
2
3
4
5
6
function putMenu() {
    putLink(‘?action=putHome’, ‘Home’);
    putLink(‘?action=printCalendars’, ‘Show Calendars’);
    putLink(‘?logout’, ‘Log Out’);
    print(‘<br><br>’);
}

Как вы, вероятно, видите, эта реорганизация уже подтолкнула нас к объектно-ориентированному дизайну. Непонятно, какие у нас объекты и с каким поведением, но у нас есть некоторые подсказки здесь и там.

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

Еще один намек на объектно-ориентированный дизайн — это немного метапрограммирования, которое мы только что сделали. Мы называем метод, о котором ничего не знаем. Это может быть что угодно, и мы как будто имеем дело с низким уровнем полиморфизма.

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

organized_procedural_schema

Мы пометили синими линиями вызовы процедур. Как вы можете видеть, они текут в том же направлении, что и раньше. Кроме того, у нас есть зеленые линии, обозначающие косвенные вызовы. Все они проходят через doUserAction() . Эти два типа линий представляют поток управления, и вы можете заметить, что он в основном не изменился.

Красные линии, однако, представляют другую концепцию. Они представляют элементарную зависимость исходного кода. Я имею в виду элементарный, потому что это не так очевидно. Метод putMenu() включает в себя имена функций, которые должны быть вызваны для этой конкретной ссылки. Это зависимость, и то же правило применяется ко всем другим методам, создающим ссылки. Они зависят от поведения других функций.

Второй тип зависимости также можно увидеть здесь. Зависимость от данных. Ранее я упоминал $calendar и $event . Функции печати должны иметь глубокие знания о внутренней структуре этих массивов, чтобы выполнять свою работу.

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


Независимо от используемой парадигмы, не существует идеального решения проблемы. Итак, вот как я предлагаю организовать наш код объектно-ориентированным образом.

Мы уже начали разделять проблемы в бизнес-логике и презентации. Мы даже представили наш doUserAction() как отдельную сущность. Итак, мой первый инстинкт — создать три класса Presenter , Logic и Router . Скорее всего, они изменятся позже, но нам нужно место для начала, верно?

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

01
02
03
04
05
06
07
08
09
10
class Router {
 
    function doUserAction() {
        (new Presenter())->putMenu();
        if (!isset($_GET[‘action’]))
            return;
        (new Logic())->$_GET[‘action’]();
    }
 
}

Так что теперь мы должны явно вызвать наш putMenu() с использованием нового объекта Presenter а остальные действия будут вызваны с использованием объекта Logic . Однако это сразу вызывает проблему. У нас есть действие, которого нет в классе логики. putHome() находится в классе Presenter. Нам нужно ввести в Logic действие, которое делегирует методу putHome() в Presenter. Помните, что в настоящее время мы хотим только обернуть наш существующий код в три класса, которые мы определили в качестве возможных кандидатов для разработки ОО. Мы хотим делать только то, что абсолютно необходимо для работы дизайна. После того, как у нас будет рабочий код, мы изменим его дальше.

Как только мы putHome() метод putHome() в класс Logic, у нас возникает дилемма. Как вызвать методы из Presenter? Ну, мы могли бы создать и передать объект Presenter в Logic, чтобы он всегда имел ссылку на презентацию. Давайте сделаем это с нашего роутера.

01
02
03
04
05
06
07
08
09
10
class Router {
 
    function doUserAction() {
        (new Presenter())->putMenu();
        if (!isset($_GET[‘action’]))
            return;
        (new Logic(new Presenter))->$_GET[‘action’]();
    }
 
}

Теперь мы можем добавить конструктор в Logic и добавить делегирование в сторону putHome() в Presenter.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class Logic {
 
    private $presenter;
 
    function __construct(Presenter $presenter) {
        $this->presenter = $presenter;
    }
 
    function putHome() {
        $this->presenter->putHome();
    }
 
[…]
 
}

С небольшими изменениями в index.php и наличием Presenter, обертывающим старые методы отображения, Logic, обертывающим старые функции бизнес-логики, и Router, обертывающим старый селектор действий, мы можем фактически запустить наш код и заставить работать элемент меню «Home».

01
02
03
04
05
06
07
08
09
10
11
12
13
require_once ‘./google-api-php-client/src/Google_Client.php’;
require_once ‘./google-api-php-client/src/contrib/Google_CalendarService.php’;
require_once __DIR__ .
require_once ‘./functins_google_api.php’;
require_once ‘./Presenter.php’;
require_once ‘./Logic.php’;
require_once ‘./Router.php’;
session_start();
 
$client = createClient();
if(!authenticate($client)) return;
 
(new Router())->doUserAction();

И вот оно в действии.

oo_home_working

Далее в нашем классе логики нам нужно правильно изменить вызовы для отображения логики, чтобы работать с $this->presenter . Затем у нас есть два метода — isCurrentEvent() и retrieveEvents() — которые используются только внутри класса Logic. Мы сделаем их приватными и соответственно изменим звонки.

Затем мы применим тот же подход к классу Presenter. Мы изменим все вызовы методов, чтобы они указывали на $this->something и сделаем putTitle() , putLink() и putBlock() закрытыми, поскольку они используются только из Presenter. Проверьте код в каталоге GoogleCalObjectOrientedInitial в прилагаемом исходном коде, если вам трудно все эти изменения сделать самостоятельно.

На данный момент у нас есть рабочее приложение. В основном это процедурный код, заключенный в синтаксис OO, который по-прежнему использует глобальную переменную $client и имеет множество других анти-объектно-ориентированных запахов, но он работает.

Если мы нарисуем диаграмму классов с зависимостями для этого кода, она будет выглядеть так:>

oo_initial_class_diagram

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

Трудно сказать, что один принцип SOLID важнее другого, но я думаю, что принцип инверсии зависимостей оказывает самое непосредственное влияние на ваш дизайн. Этот принцип гласит:

A : Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций и B : абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

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

Сначала мы заставим наш Маршрутизатор использовать только Presenter и сломаем его зависимость от Logic.

01
02
03
04
05
06
07
08
09
10
class Router {
 
    function doUserAction() {
        (new Presenter())->putMenu();
        if (!isset($_GET[‘action’]))
            return;
        (new Presenter())->$_GET[‘action’]();
    }
 
}

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

Теперь функция putHome() , присутствующая в классах Logic и Presenter, исчезнет из Logic. Это хороший знак, так как мы удаляем дублирование. Конструктор и ссылка на Presenter также исчезают из логики. С другой стороны, конструктор, создающий объект логики, должен быть написан на Presenter.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
class Presenter {
 
    private $businessLogic;
 
    function __construct() {
        $this->businessLogic = new Logic();
    }
 
    function putHome() {
        print(‘Welcome to Google Calendar over NetTuts Example’);
    }
 
[…]
 
}

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

1
2
Fatal error: Call to undefined method Presenter::printCalendars()
in /[…]/GoogleCalObjectOrientedFinal/Router.php on line 9

Итак, наш маршрутизатор хочет вызвать метод, которого нет в Presenter, printCalendars() . Давайте создадим этот метод в Presenter и проверим, что он сделал в Logic. Он печатал заголовок, а затем перебирал некоторые календари и вызывал putCalendar() . В Presenter метод printCalendars() будет выглядеть так:

1
2
3
4
5
6
function printCalendars() {
    $this->putCalendarListTitle();
    foreach ($this->businessLogic->getCalendars() as $calendar) {
        $this->putCalendarListElement($calendar);
    }
}

С другой стороны, в логике метод становится довольно анемичным. Просто прямой звонок в библиотеку Google API.

1
2
3
4
function getCalendars() {
    global $client;
    return getCalendarList($client)[‘items’];
}

Это может заставить вас задать себе два вопроса: «Есть ли у нас необходимость в классе логики?» и «Есть ли у нашего приложения хоть какая-то логика?» Ну, мы еще не знаем. В настоящее время мы продолжим описанный выше процесс до тех пор, пока весь код не будет работать, и логика больше не будет зависеть от Presenter.

Итак, мы будем использовать метод printCalendarContents() в Presenter, как printCalendarContents() ниже:

1
2
3
4
5
6
function printCalendarContents() {
    $this->putCalendarTitle();
    foreach ($this->businessLogic->getEventsForCalendar() as $event) {
        $this->putEventListElement($event);
    }
}

Что, в свою очередь, позволит нам упростить getEventsForCalendar() в Logic, до чего-то вроде этого.

1
2
3
4
function getEventsForCalendar() {
    global $client;
    return getEventList($client, htmlspecialchars($_GET[‘showThisCalendar’]))[‘items’];
}

Сейчас это работает, но у меня есть беспокойство здесь. Переменная $_GET используется в классах Logic и Presenter. Разве только класс Presenter не должен использовать $_GET ? Я имею в виду, Presenter абсолютно необходимо знать о $_GET потому что он должен создавать ссылки, которые $_GET эту переменную $_GET . Так что это будет означать, что $_GET строго связан с HTTP. Теперь мы хотим, чтобы наш код работал с интерфейсом командной строки или графическим интерфейсом рабочего стола. Поэтому мы хотим сохранить эти знания только в Докладчике. Это делает два метода сверху, превращаются в два ниже.

1
2
3
4
function getEventsForCalendar($calendarId) {
    global $client;
    return getEventList($client, $calendarId)[‘items’];
}
1
2
3
4
5
6
7
function printCalendarContents() {
    $this->putCalendarTitle();
    $eventsForCalendar = $this->businessLogic->getEventsForCalendar(htmlspecialchars($_GET[‘showThisCalendar’]));
    foreach ($eventsForCalendar as $event) {
        $this->putEventListElement($event);
    }
}

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

1
2
3
4
5
function getEventById($eventId, $calendarId) {
    foreach ($this->getEventsForCalendar($calendarId) as $event)
        if ($event[‘id’] == $eventId)
            return $event;
}

И тогда соответствующий вызов на Presenter позаботится о его печати:

1
2
3
4
5
6
7
8
function printEventDetails() {
    $this->putEvent(
        $this->businessLogic->getEventById(
            $_GET[‘showThisEvent’],
            $_GET[‘calendarId’]
        )
    );
}

Вот и все. Мы здесь. Зависимость инвертирована!

oo_dip_class_diagram

Управление все еще течет от логики к предъявителю. Представленный контент полностью определяется Logic. Если, например, завтра мы хотим подключиться к другой службе календаря, мы можем создать другую логику, внедрить ее в Presenter, и Presenter даже не заметит никакой разницы. Кроме того, зависимость исходного кода была успешно инвертирована. Presenter — единственный, кто создает и напрямую зависит от логики. Эта зависимость имеет решающее значение в том, что позволяет Presenter изменять способ отображения данных без каких-либо последствий для логики. Кроме того, это позволяет нам переключать наш HTML Presenter с помощью CLI Presenter или любым другим способом отображения информации для пользователя.

Вероятно, последним серьезным недостатком дизайна является использование глобальной переменной для $client . Весь код в нашем приложении имеет к нему доступ. Чудесным образом единственный класс, которому действительно нужен $client — это наш класс логики. Очевидным шагом является создание переменной частного класса. Но для этого необходимо, чтобы мы передавали $client через маршрутизатор в Presenter, чтобы он мог создать объект Logic с переменной $client . Это решает мало наших проблем. Нам нужно строить наши классы в изолированном месте и правильно внедрять зависимости друг в друга.

Для любой более крупной структуры класса мы использовали бы Фабрики, но для нашего небольшого примера файл index.php будет отличным местом для хранения логики создания. И будучи точкой входа в наше приложение, иначе говоря, «основным» файлом в схеме архитектуры высокого уровня, он все еще находится за пределами нашей бизнес-логики.

HighLevelDesign

Поэтому мы изменим index.php в приведенном ниже коде, сохранив все include и команду session_start ():

1
2
3
4
5
6
$client = createClient();
if(!authenticate($client)) return;
 
$logic = new Logic($client);
$presenter = new Presenter($logic);
(new Router($presenter))->doUserAction();

И мы закончили. Конечно, есть и другие вещи, которые мы могли бы сделать, чтобы наш дизайн стал еще лучше. Если ничего другого, мы могли бы написать пару тестов для наших методов в классе Logic. Возможно, наш класс логики можно было бы переименовать во что-то более представительное, например, в GoogleCalendarGateway. Или мы могли бы создать классы Event и Calendar, чтобы еще лучше контролировать данные и поведение этих концепций и сломать зависимость Presenter от массива для этих типов данных. Еще одним улучшением и расширением будет создание классов полиморфных действий вместо простого вызова функций по ссылке из $_GET . Есть бесконечные маленькие вещи, которые мы могли бы сделать, чтобы улучшить даже этот простой дизайн. Я предоставлю вам эту прекрасную возможность для экспериментов, начиная с этой окончательной версии кода, которую вы можете найти в прикрепленном архиве в каталоге GoogleCalObjectOrientedFinal .

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

Спасибо за чтение.