23 posts tagged

web

Later Ctrl + ↑

Абстрактная фабрика и примеси в PHP (Часть 2)

Первая часть

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

<?php

abstract class Infantry {
    //общие для всей пехоты методы
    abstract public function Attack();
    abstract public function Defend();
}
abstract class Archer {
    abstract public function Shoot();
}
abstract class SiegeWeapons {
    abstract public function Fire();
}

class GoodInfantry extends Infantry {
    //Спецэфическая реализация для доброго пехотинца
    public function Attack();
    public function Defend();
    
    public function Treat();
}
class GoodArcher extends Archer{
    public function Shoot();
    
    public function Treat();
}
class GoodSiegeWeapons extends SiegeWeapons{
    public function Fire(); 
    
    public function Treat();
}

class EvilInfantry extends Infantry{
    //Спецэфическая реализация для злого пехотинца
    public function Attack();
    public function Defend();
    
    public function Invisible();
}
class EvilArcher extends Archer{
    public function Shoot();
    
    public function Invisible();
}
class EvilSiegeWeapons extends SiegeWeapons{
    public function Fire();
    
    public function Invisible();
}

abstract class AbstractFactory
{
    abstract public function createInfantry();
    abstract public function createArcher();
    abstract public function createSiegeWeapons();
}

class GoodFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new GoodInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new GoodArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new GoodSiegeWeapons();
    }
}

class EvilFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new EvilInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new EvilArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new EvilSiegeWeapons();
    }
}

Код стал не таким красивым по тому, что появились методы, которые приходится копировать. В php нет множественного наследования и приходится выбирать наследоваться белому лучнику от белых или от лучников. Мы априори выбрали второй вариант и теперь думаем, как сделать наш код красивее, а его поддержку удобнее. НР предлагает использовать примеси (trait), которые появились в php 5.4. Вынесем обшие методы в примесь.

<?php

abstract class Infantry {
    //общие для всей пехоты методы
    abstract public function Attack();
    abstract public function Defend();
}
abstract class Archer {
    abstract public function Shoot();
}
abstract class SiegeWeapons {
    abstract public function Fire();
}

trait Good {
    public function Treat();
}

trait Evil {
    public function Invisible();
}

class GoodInfantry extends Infantry {
    use Good;
    
    //Спецэфическая реализация для доброго пехотинца
    public function Attack();
    public function Defend();
}
class GoodArcher extends Archer{
    use Good;
    public function Shoot();
}
class GoodSiegeWeapons extends SiegeWeapons{
    use Good;
    public function Fire();
}

class EvilInfantry extends Infantry{
    use Evil;
    
    //Спецэфическая реализация для злого пехотинца
    public function Attack();
    public function Defend();
}
class EvilArcher extends Archer{
    use Evil;
    public function Shoot();
}
class EvilSiegeWeapons extends SiegeWeapons{
    use Evil;
    public function Fire();
}

abstract class AbstractFactory
{
    abstract public function createInfantry();
    abstract public function createArcher();
    abstract public function createSiegeWeapons();
}

class GoodFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new GoodInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new GoodArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new GoodSiegeWeapons();
    }
}

class EvilFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new EvilInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new EvilArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new EvilSiegeWeapons();
    }
}

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

P.S. Мне очень хотелось бы услышать критику моего изложения общеизвестных вещей и моего варианта использования примесей от коллег. Я открыл для себя паттерны проектирования пару лет назад, но использовать их начал только сейчас.
И если будут у кого-то вопросы, то с радостью попробую ответить.

Абстрактная фабрика и примеси в PHP (Часть 1)

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

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

#Джуниор-разработчик и игра
Допустим, мы хотим написать браузерную игру, в которой «добро» борется со «злом». Пусть у обеих сторон есть пехота, лучники и осадные орудия. Для начала посадим за IDE джуниора. Через некоторое время мы, скорее всего, увидим следуюшие классы:

class GoodInfantry{}
class GoodArcher{}
class GoodSiegeWeapons{}

class EvilInfantry{}
class EvilArcher{}
class EvilSiegeWeapons{}

#Опытный разработчик и абстрактная фабрика

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

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

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

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

Вспомним так же, что все пехотинцы будут наследовать класс Infantry. И подумаем о том, что фабрики, вероятно, будут иметь одинаковый набор методов, а значит хорошо бы им унаследовать единый абстрактный класс. Такой подход гарантирует нам, что ОР, создав абстрактные классы может идти курить и доверить написание реализации джуниору, ведь любая ошибка в реализации будет сразу видна. То, что у нас получилось, фактически, и есть паттерн Абстрактная фабрика. стрелки без подписи «создает» обозначают наследование.

Такая структура — пример хорошего кода для общего случая создания групп связанных обьектов. То есть всегда, когда у нас есть необходимость создавать экземпляры классов не в ручном режиме и когда у нас есть группы связанных объектов (в нашем случае все светлые и все темные связаны) паттерн подходит. Удобно то, что он раширяется, как горизонтально (можно добавлять еще стороны, кроме света и тьмы), так и вертикально без переписывания кода многократно.

<?php

abstract class Infantry {
    //общие для всей пехоты методы
    abstract public function Attack();
    abstract public function Defend();
}
abstract class Archer {
    abstract public function Shoot();
}
abstract class SiegeWeapons {
    abstract public function Fire();
}

class GoodInfantry extends Infantry {
    //Спецэфическая реализация для доброго пехотинца
    public function Attack();
    public function Defend();
}
class GoodArcher extends Archer{
    public function Shoot();
}
class GoodSiegeWeapons extends SiegeWeapons{
    public function Fire();
}

class EvilInfantry extends Infantry{
    //Спецэфическая реализация для злого пехотинца
    public function Attack();
    public function Defend();
}
class EvilArcher extends Archer{
    public function Shoot();
}
class EvilSiegeWeapons extends SiegeWeapons{
    public function Fire();
}

abstract class AbstractFactory
{
    abstract public function createInfantry();
    abstract public function createArcher();
    abstract public function createSiegeWeapons();
}

class GoodFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new GoodInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new GoodArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new GoodSiegeWeapons();
    }
}

class EvilFactory extends AbstractFactory
{
    /**
     * @return Infantry
     */
    public function createInfantry()
    {
        return new EvilInfantry();
    }

    /**
     * @return Archer
     */
    public function createArcher()
    {
        return new EvilArcher();
    }

    /**
     * @return SiegeWeapons
     */
    public function createSiegeWeapons()
    {
        return new EvilSiegeWeapons();
    }
}

Причем же тут примеси? Продолжение следует.

Как я открыл для себя миграции

Как правило, перенос сайта с одного сервера на другой состоит из двух этапов: перенос файлов и базы данных. Когда мы говорим о разработке и поддержке серьезного проекта, обычно, мы имеем несколько серверов с разными версиями. Например сервер разработки (DEV), сервер тестирования (TEST) и «боевой» (PROD). А иногда, по копии сайта для каждого разработчика.

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

Решение, как все гениальное, кажется очевидным, когда ты его знаешь. Как избавится от двух этапов переноса? Сделать так, что бы один этап автоматически «подтягивал» второй. Например, хранить все изменения для БД в файловой системе. Благо, SQL нам в этом помогает очень сильно. Такие изменения назвали миграциями.

Я впервые познакомился с механизмом миграций в CMS yupe, на базе фраемворка Yii. По этому все, что напишу далее относится к этой конкретной реализации, хотя идея, в целом, остается неизменной везде.

Итак, если мы хотим внести изменения в базу, то вместо того, что бы лезть туда руками, мы создаем скрипт, который знает что нужно делать для применения изменений и для их отмены. Например, для этого создается класс с двумя методами up(), который создает новую таблицу, и down(), который её удаляет. Все остальное, что ему нужно он наследует от системного класса.

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

  1. Всегда уверены в актуальности бд. Всегда точно знаем, что к ней уже применилось, а что еще нет.
  2. Храним всю историю в системе контроля версий вместе со всей остальной историей разработки.
  3. Можем в любой момент откатить не только состояние кода, но и базы данных к любому этапу.
  4. Легко сливаем изменения от нескольких разработчиков. Теоретически, могут возникнуть конфликты, но на практике ни разу у меня не возникали.

Для того, что бы этот механизм принес максимум пользы, я выделил для себя ряд правил «гигиены».

  1. Создавать всю базу с самого начала миграциями. Казалось бы, в начале разработки, до того, как сайт перекочует дальше с первого сервера разработки проблем, о которых я писал в начале, не должно быть. Но, если уж начал делать миграции становится сложно остановиться. А серьезно, такое правило позволяет с самого начала делать все по одной схеме и не будет такого, что вот до этого момента мы можем посмотреть все изменения базы, а ранее она монолитна.
  2. Стараться делать миграции с привязкой к коммитам. Идеально: одна задача — один коммит — одна миграция. Последней конечно может и не быть. Сами задачки при этом должны быть небольшими 1-4 часа.
  3. При соблюдении предыдущего правила в название миграции хорошо бы включать id задачи. Но не ограничиваться только им, а написать еще и на человекопонятном языке.

Гиковский сайт

В блоге «Записки программиста» нашел ссылку на суперминималистичный сайт Андрея Прокопюка. Решил пофантазировать на тему, как можно сделать сайт еще более гиковским?

Представляю вашему вниманию мою первую попытку. Буду рад отзывам, замечаниям и предложениям.

Как мы вводили git

Git — это одна из популярных систем контроля версий. Код — это, прежде всего, текст. Такой текст, который правят многократно и по чуть-чуть в разных местах. Хранить только последнюю версию кода не дальновидно, по тому, что часто какие-то правки приходится отменять, а в каких-то случаях надо посмотреть «как оно работало раньше». Хранить каждую новую версию кода, то есть копировать всю директорию, допустим раз в день, не удобно по двум причинам: во-первых, занимает много места, а во-вторых, не дает никакого понимания о том, что именно изменилось.
Умные программисты придумали решение этих проблем. Оно заключается в том, что бы хранить какую-то базовую версию, а после сохранять только маску изменений. Например, мы храним 1000 строк кода и после изменений запоминаем, что поменялись 387 строка и 425.
Такое решение дало еще один неожиданный плюс. Оно позволило работать над одним и тем же кодом разным людям одновременно. Первый программист поменял вторую, третью и четвертую строку, а второй — девятую и десятую. Система сама поняла, что может применить к начальному коду и те изменения и эти. И только, если возникнет конфликт, придется разруливать людям.

Америка

Впрочем, если вы так или иначе связаны с разработкой программного обеспечения, скорее всего, Америку я вам не открыл. Во всем мире различные системы контроля версий — неотъемлемый атрибут любого программиста. Читая профессиональные форумы, блоги известных web-разработчиков и доклады с различных конференций сложно себе представить, что кто-то может жить без git-а или svn-а. Однако, в наших реалиях куча небольших веб студий и одиночек-фрилансеров работают «по-старинке». Так получилось, что и я на первых своих местах работы с системами контроля версий познакомился только заочно. До сих пор стыдно признаться, что несколько лет я писал код без таких систем.
Впрочем, я был такой не один. И так получилось, что у нас собралась команда из четырех человек, где никто полноценно с контролем версий не работал. У меня был до этого опыт, где все настроили до меня и оставалось только коммитить, то есть записывать изменения которые я внес. Еще у двоих членов команды был подобный опыт. Однако никто из нас не представлял, как полностью должна работать система от компьютера программиста до «боевого сервера».

Как ни странно, гугление не могло ответить на те вопросы, которые возникали у меня в первую очередь. Например гугл на вопрос «Как смержить ветки git» дает около 700 результатов и вся первая страница по делу. А на запрос «git локально или на сервере» дает 275 тыс ответов и я не смог найти что-то в тему.

Наши правила

Многое мне было непонятно. Но в итоге чтения кучи статей и советов я принял ряд решений.

  1. Мы отказались от гита локально на каждом компьютере разработчика. Вместо этого, мы используем IDE и копию проекта на нашем сервере. У каждого разработчика есть своя локальная копия и своя удаленная копия. Уже на сервере стоит гит и туда надо зайти что бы провести с ним какие-то операции. Такое решение я принял по целому ряду причин. Основная причина в том, что мы взяли курс на проекты выше среднего по сложности и настраивать у каждого разработчика веб-сервер с нужными пакетами в условиях разных ОС и различной удаленности друг от друга сложная задача. А внедрить все это надо было без ущерба для производства.
  2. Никаких встроенных в IDE систем поддержки git и других «улучшителей вкуса» мы решили не использовать. Во-первых, в нативных командах из консоли нет ничего, чему нельзя научить за пару дней. А во-вторых, меня однажды такая система поддержки сильно подвела — вывела совсем не то, что произошло на самом деле.
  3. Мы ввели правило одна задача — одна ветка. Коммитов в нее может быть много, но чаще 1-2. Сливаем изменения только после завершения задачи.
  4. Все, что не касается напрямую нашего кода мы стараемся исключить из системы контроля версий. С этой задачей до сих пор справляемся хуже всего.
  5. Ввели правило «сервера для тестирования». Каждый раз после сливания изменений от разных разработчиков сайт поступает на тестирование и только потом выходит в продакшн, как бы не торопил заказчик.
  6. Мы стали использовать git в связке с Bitbucket. Система контроля версий без удаленного репозитория и в половину не так хороша. Но это, наверное, тема для отдельной заметки.

И все пошло, как по маслу?

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

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

А какую систему контроля версий используйте вы? И какие сложности были с её внедрением?

Earlier Ctrl + ↓