Статьи

Практическая ООП: создание приложения для викторины — начальная загрузка

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

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

Почему MVC не достаточно

MVC, что означает Model-View-Controller, представляет собой мощный шаблон проектирования для веб-приложений. К сожалению, с его повышением статуса модного слова это было вырвано из контекста и использовалось как чудодейственное лекарство. Становится стандартной практикой использование инфраструктур MVC, и многим разработчикам удалось использовать их для разделения логики отображения и логики домена. Проблема в том, что разработчики на этом останавливаются, в лучшем случае создавая квазиобъектно-ориентированные системы, а в худшем — процедурный код, заключенный в классы, часто в контроллеры.

При создании нашего приложения для викторины мы будем использовать шаблон модели предметной области, описанный в статье «Шаблоны архитектуры корпоративных приложений» Мартина Фаулера . Модель предметной области — это просто причудливый способ сказать, что мы будем использовать объектно-ориентированный подход к проектированию системы: сеть объектов с различными обязанностями, которые в целом будут составлять наше приложение.

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

Даже при использовании шаблона модели предметной области все еще существует проблема выполнения операций, требующих совместной работы нескольких классов. Мы будем решать это с помощью шаблона Service Layer.

Шаблон сервисного уровня:

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

Шаблон Service Layer решает эту проблему. Мы группируем все операции нашей системы (регистрация, покупка продукта, решение викторины) по классам обслуживания, один сервис на операцию или группу тесно связанных операций. Мы отделяем эти сервисные классы от классов, которым они делегируются. Это позволяет нам повторно использовать службы между различными вариантами использования, например, веб-интерфейсом и интерфейсом CLI, интерфейсами переднего и заднего плана и т. Д.

Начиная:

Мы будем использовать Slim в качестве нашей инфраструктуры MVC. Slim — это легкий фреймворк, который легко освоить и который идеально подходит для нашего простого приложения. Как вы увидите в следующей статье, когда мы будем писать код контроллера, вам будет легко заменить Slim на любой фреймворк, который вы предпочитаете. Мы установим Slim с Composer. Создайте каталог для проекта со следующим файлом composer.json :

 { "require": { "slim/slim": "2.*" } "autoload": { "psr-O": {"QuizApp\\": "./lib/"} } } 

Кодирование класса обслуживания:

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

Давайте определим интерфейс для сервиса. Создайте файл lib/QuizApp/service/QuizInterface.php со следующим содержимым:

 <?php namespace QuizApp\Service; interface QuizInterface { /** @return Quiz[] */ public function showAllQuizes(); public function startQuiz($quizOrId); /** @return Question */ public function getQuestion(); /** @return bool */ public function checkSolution($id); /** @return bool */ public function isOver(); /** @return Result */ public function getResult(); } 

Большинство операций должны говорить сами за себя, но getQuestion() и getResult() могут быть не совсем понятны. getQuestion() возвращает следующий вопрос, на который должен ответить пользователь. getResult() возвращает объект с информацией о количестве правильных и неправильных ответов и о том, прошел ли пользователь тест.

Прежде чем мы реализуем этот сервис, мы должны определить интерфейс mapper, так как сервис должен будет использовать его. Службе нужны две операции: find() которая возвращает один тест по идентификатору, и findAll() .

 <?php namespace QuizApp\Mapper; interface QuizInterface { /** @return \QuizApp\Entity\Quiz[] */ public function findAll(); /** * @param int $i * @return \QuizApp\Entity\Quiz */ public function find($i); } 

Эти операции возвращают объекты класса \QuizApp\Entity\Quiz , который представляет один тест. Класс Quiz , в свою очередь, содержит \QuizApp\Entity\Question , которые представляют вопросы теста. Давайте реализуем это, прежде чем вернуться в сервис.

 <?php namespace QuizApp\Entity; class Question { private $id; private $questions; private $solutions; private $correctIndex; /** * @param string $question * @param string[] $solutions * @param int $correctSolutionIndex */ public function __construct ($question, array $solutions, $correctSolutionIndex) { $this->question = $question; $this->solutions = $solutions; $this->correctIndex = $correctSolutionIndex; if (!isset($this->solutions[$this->correctIndex])) { throw new \InvalidArgumentException('Invalid index'); } } public function setId($id) { $this->id = $id; } public function getId() { return $this->id; } public function getQuestion() { return $this->question; } public function getSolutions() { return $this->solutions; } public function getCorrectSolution() { return $this->solutions[$this->correctIndex]; } public function isCorrect($solutionId) { return $this->correctIndex == $solutionId; } } 

Обратите внимание, что в дополнение к \QuizApp\Entity\Question получения и установки в \QuizApp\Entity\Question есть метод isCorrect() для проверки правильности определенного ответа на вопрос.

И \QuizApp\Entity\Quiz :

 <?php namespace QuizApp\Entity; class Quiz { private $id; private $title; private $questions; /** * @param string $title * @param Question[] $questions */ public function __construct($title, array $questions) { $this->title = $title; $this->questions = $questions; } public function setId($id) { $this->id = $id; } public function getId() { return $this->id; } public function getTitle() { return $this->title; } public function getQuestions() { return $this->questions; } } 

И \QuizApp\Service\Quiz\Result :

 <?php namespace QuizApp\Service\Quiz; class Result { private $correct; private $incorrect; private $passScore; public function __construct($correct, $incorrect, $passScore) { $this->correct = $correct; $this->incorrect = $incorrect; $this->passScore = $passScore; } public function getCorrect() { return $this->correct; } public function getIncorrect() { return $this->incorrect; } public function getTotal() { return $this->correct + $this->incorrect; } public function getPassScore() { return $this->passScore; } public function hasPassed() { return $this->correct >= $this->passScore; } } 

Написание заполнителя Mapper:

Нам нужен конкретный \QuizApp\MapperMapper\MapperInterface для использования сервиса. Давайте пока определим фиктивную реализацию, чтобы мы могли протестировать код, прежде чем писать настоящий маппер, который получит доступ к базе данных MongoDB. Манекен использует жестко закодированные объекты ‘\ QuizApp \ Entity \ Question’ для возврата из методов find($id) и findAll() .

 <?php namespace QuizApp\Mapper; class Hardcoded implements QuizInterface { private static $MAP = array(); /** @return \QuizApp\Entity\Quiz[] */ public function findAll() { return [ $this->find(0), $this->find(1) ]; } /** * @param int $id * @return \QuizApp\Entity\Quiz */ public function find($id) { if (isset (self::$MAP[$id])) { return self::$MAP[$id]; } $result = new \QuizApp\Entity\Quiz( 'Quiz' . $id, [ new \QuizApp\Entity\Question( 'What colour was George Washington\'s white horse?', [ 'White', 'Gray', 'Yellow', 'All of the above' ], 0 ), new \QuizApp\Entity\Question( 'Who\'s buried in Grant\'s tomb?', [ 'Grant', 'George Washington', 'George Washingtion\'s horse', 'All of the above' ], 0 ), ] ); $result->setId($id); self::$MAP[$id] = $result; return $result; } } 

Класс реализует интерфейс, возвращая несколько жестко закодированных объектов Quiz. Он использует статическое свойство $MAP в качестве Identity Map, чтобы гарантировать, что класс возвращает одни и те же объекты при каждом вызове.

Вывод:

В этой первой части нашей серии Pratical OOP мы начали разработку нашего приложения для викторины. Мы обсуждали MVC и почему это не серебряная пуля; мы рассмотрели шаблоны проектирования доменной модели и сервисного уровня; мы набросали интерфейс для нашего сервиса викторины, который будет содержать логику, стоящую за пользователем, решающим викторину; мы смоделировали тесты и вопросы как объекты; и мы создали фиктивный маппер для поиска тестов из «базы данных», которая пригодится во второй части.

Будьте на связи! В следующий раз мы будем дорабатывать наше приложение, писать класс обслуживания и настоящий картограф базы данных, который будет подключаться к MongoDB. Как только мы это установим, вы увидите, как легко написать наши контроллеры и представления, и как наш элегантный дизайн разделяет буквы «M», «V» и «C», обслуживаемые и расширяемые. Вы можете найти полный исходный код для этой части здесь .