Date post: | 16-Jun-2015 |
Category: |
Technology |
View: | 594 times |
Download: | 1 times |
PHP: прагматичный код
Interlabs
7 марта 2014
1 / 36
О чем речь
Какими правилами нужно руководствоваться, чтобы писатьпростой и удобный в сопровождении PHP-код:
• как структурировать код• как называть классы• как классифицировать и обрабатывать ошибки• как документировать• какие языковые конструкции использовать для того, чтобыупрощать, а не усложнять код
• как минимизировать ошибки в коде
2 / 36
Пространства имен• весь код в классах;• все классы — в пространствах имен;• код вне пространств имен — только непосредственно ввыполняемых сценариях.
• только автозагрузка классов, никаких явных include;• вложенные пространства имен могут содержать деталиреализации исходного пространства;
• никогда не определяем глобальные функции.
Главное средство структурирования кода
3 / 36
Простейшая автозагрузка
spl_autoload_register()
• spl_autoload_register() можно вызывать несколькораз, результат — набор загрузчиков
• выполняются все загрузчики, пока не будет загружен класс• простая, быстрая и удобная реализация• для каждой отдельной иерархии имен — свой autoload• однозначно удобнее composer на этапе разработки• не отменяет и не исключает composer и его загрузчик, ноудобно для экспериментов и в случае использованиясобственных библиотек
4 / 36
Автозагрузка: реализацияlib/ - пространство имен IMP
Common - пространство имен IMP\Common...autoload.php - загрузчик
script.php - выполняемый сценарий
// autoload.phpspl_autoload_register(function ($class) {
if (substr($class, 0, 4) == ’IMP\\’) {return include_once(
__DIR__ . ’/’ .str_replace(’\\’, DIRECTORY_SEPARATOR, substr($class, 4)) . ’.php’
);}
});
// script.phpinclude(’lib/autoload.php’);use IMP\Common\Container;...
5 / 36
Библиотечные классы• подгрузка функций пространства имен не реализована,функции определяются в классах
• библиотечный класс содержит только static-методы• методы группируются в класс по функциональности• нельзя создать экземпляр или наследовать
final class API // final запрещает наследование{
static public function apiMethod(){
...}
// Приватный конструктор запрещает создание экземпляра.private function __construct() {}
}
API::apiMethod();
6 / 36
Декларация зависимости ,Всегда явно прописываем зависимости класса через use, дажеесли язык может вывести их сам:
• сразу видны видны все зависимости без необходимостиразбираться в кодe
• облегчается рефакторинг, легче изменять имена классов ипереносить их между пространствами имен
• легко автоматизировать сбор информации о зависимостях• найти все классы, использующие тот или иной класс —просто grep по имени класса
Явные зависимости — вид документирования7 / 36
Зависимости: примерnamespace IMP\Data;
// DEPENDENCIES /////////////////////////////////////////////////
use IMP\Data\Schema;use IMP\Data\Collecton;
use IMP\Data\Relations\PrimaryRelation;use IMP\Common\Exceptions\InvalidOperationException;
use ArrayAccess;use Countable;use IteratorAggregate;use ArrayIterator;
// CLASS ////////////////////////////////////////////////////////
class Entity implements ArrayAccess, Countable, IteratorAggregate{
...}
8 / 36
There are only two hard things inComputer Science: cache invalidation andnaming things.
Phil Karlton
Именование классовклассы, абстрактные классы, интерфейсы
Типы
• не важно, что из себя представляет тип, важно, какуюфункциональность он описывает
• по мере развития проекта интерфейсы могут становитьсяклассами и наоборот
Не нужно вводить специальные правила дляимен интерфейсов и абстрактных классов.
10 / 36
Именование типов
• первый уровень пространства имен — классы,определяющие базовые понятия модуля, обычноиспользуемые другими модулями
• вспомогательные классы, необходимые для работы одногоиз классов первого уровня — вложенное пространствоимен, соответствующее имени основного класса вединственном числе
• альтернативные реализации класса — вложенноепространство имен, имя класса во множественном числе
• интерфейсы и абстрактные классы не отличаются поспособу именования от классов
11 / 36
Именование типовМодуль Data:
IMP\Data пространство имен модуляRelation базовый тип отношения, часто — интерфейсTable класс, использующий вспомогательные классы...
IMP\Data\Relations различные виды отношенийAbstractRelation абстрактный базовый классPrimaryRelation дальнейшая иерархия классовSecondaryRelation еще один абстрактный классOneToOneRelation реализация отношенияOneToManyRelation реализация отношения...
IMP\Data\Table вспомогательные классы для класса TableColumnColumnType
12 / 36
Помним о S.O.L.I.D.• особенно о Liskov Substitution Principle• один класс — одна ответственность• к статическим методам это тоже относится• производные классы наследуют весь функционал, в томчисле и статический
• лучше много мелких классов с разграниченнойфункциональностью
• если наследование от класса не предусмотрено —используем final.
Добавляя в класс очередной метод, думаем,будет ли он иметь смысл в производном классе.
13 / 36
Базовые интерфейсы
Проектируя очередной класс, прежде всего отвечаем навопрос, какие интерфейсы он должен поддерживать:
• $object->getProperty() — доступ к свойствам• $object->property — доступ к свойствам• $object[$key] — доступ к индексируемым элементам• foreach ($object) — итератор по содержимому• count($object) — размер содержимого• (string) $object — преобразование в строку
14 / 36
Базовая семантикаНекоторые рекомендации по базовым интерфейсам:
• работа с состоянием через методы доступа• прямой доступ к свойствам как дополнительнаянеобязательная возможность (иначе больше кода иневозможность контролировать интерфейсы)
• набор свойств всегда фиксированный• набор индексируемых элементов, как правило,произвольный
• итератор в отдельном классе обычно лучше, чем егонепосредственная реализация
• преобразование в строку — альтернативноепредставление объекта, а не сериализация
15 / 36
Работа с простыми типами
Иногда тип проще всего описать простым значением: строкой,числом, массивом. Первое желание сразу написать объектнуюобертку, но:
• в большинстве случаев не надо писать обертку;• достаточно библиотечного класса с процедурным API;• класс может содержать набор констант, описывающихдопустимые значения;
• класс может содержать процедурный API, крайнежелательно наличие функции, контролирующейкорректность значения.
16 / 36
Использование констант• гораздо меньше ошибок, например, из-за опечаток встроковых константах, можно использовать autocomplete
• нагляднее код• необходимо привязывать к какому-либо типу (autoloader)• технически можно использовать интерфейс илистатический класс
Используем статический класс:
• со временем могут добавиться дополнительные функции• нет наследования — нет последующих проблем с типамипри развитии проекта
17 / 36
Простые типы: примерfinal class Status{
const HTTP_CONTINUE = 100;...const HTTP_FOUND = 302;
// Проверяет допустимость значения статуса.static public function assert($status){
// проверка допустимости значения статуса}// Возвращает текстовое описание статуса.static public function text($status){
return self::$text[self::assert($status)];}// Проверяет успешность статуса.static public function isSuccessful($status){
return (int) $status >= 200 && (int) $status < 300;}
}18 / 36
Контроль типов
Чем лучше контролируем типы, тем меньше ошибок:
• всегда указываем типы аргументов в методах иконструкторах классов
• всегда проверяем типы скалярных аргументов илиприводим тип
• для строковых аргументов часто уместнее приведениетипов: можно передавать объекты, приводимые к строке.
19 / 36
Строковые аргументыПриведение к строке часто удобнее строгой типизации:
class File {public function __toString() {return $this->path;
}public function getStat() { ... }...
}class FS {static public function isWritable($path) {$path = (string) $path;...
}}
FS::isWritable(’data.json’); // метод работает со строкамиFS::isWritable(new File(’data.json’)); // и с объектами
20 / 36
Обработка ошибок• в базовом API часть функций возвращают код ошибок;• некоторые генерируют исключения;• некоторые позволяют выбирать (например, PDO).
Так жить нельзя:
• унифицируем обработку ошибок за счет использованиеисключений
• для встроенных функций пишем оболочки, еслинеобходимо
• не забываем обрабатывать исключения
Лучше безобразно, но единообразно21 / 36
Исключения: главноеНеобходимость и специфика типа исключения полностьюопределяется спецификой его обработки:
• прежде чем ввести очередной тип исключений,представьте, кто и как его будет обрабатывать
• нет необходимости описывать каждую возможную ошибкув виде отдельного типа
• диспетчеризация по типу исключения — тип инаследование очень важны
• соответствие уровней абстракции исключения иобработчика тоже важно
• стандартная схема именования с суффиксом Exceptionвполне оправдана
22 / 36
Встроенные исключенияException
ошибка
LogicExceptionошибка разработчика
• BadFunctionCallException• BadMethodCallException
• DomainException• InvalidArgumentException• LengthException• OutOfRangeException
RuntimeExceptionошибка выполнения
• OutOfBoundsException• OverflowException• RangeException• UnderflowException• UnexpectedValueException
23 / 36
Logic vs RuntimeLogicExceptionошибка разработчика
• вообще говоря, не должнавозникать, если всенаписано правильно
• стандартный наборисключений для всехмодулей, обработкаоднотипна
• контекст обработки не такуж важен
RuntimeExceptionошибка выполнения
• зависит от внешнихданных, ошибкинеизбежны
• индивидуальныеисключения для каждогомодуля, обработкаиндивидуальна
• локальная обработкавнутри модуля
24 / 36
Exception__construct($message, $code, Exception $previous)
$message сообщение об ошибке$code код ошибки, используем, если что-то вернуло нам
этот код (PDO, XML, etc)$previous предыдущее исключение: смена уровня
абстракции при обработке исключения
Смена уровня абстракции: если модуль не может обработатьRuntime-исключение, сгенерированное внутри модуля, онгенерирует собственное Runtime-исключение, содержащеессылку previous на исходное.
Чем раньше обрабатывается исключение, тем лучше.25 / 36
Обработка исключенийtry {
...$this->statement->execute();...
} catch (PDOException $e) {throw new DBIException(
$e->getmessage(),(int) $e->getCode(),$e
);}
$error = null;try {
$xml = smnple_xml_load_file($file, $class, $opts, $ns, $prefix
);} catch (Exception $e) {
$error = $e;}
if ($xml === false || $error !== null) {$xmlErrors = libxml_get_errors();libxml_clear_errors();throw new XMLException(array(
’Error loading XML file %s’, $file),$xmlErrors,$error
);}
26 / 36
IMP\Common\ExceptionsLogicException
• InvalidKeyException
• InvalidOperationException
• InvalidPropertyException
• InvalidTypeException
• NotImplementedException
RuntimeException
• базовый класс для runtime-исключенийотдельных модулей
• каждый модуль определяет свой классruntime-исключения
• при необходимости — производныеисключения
Базовые классы: форматирование сообщений ипреобразование в строку:
throw new DataException(array(’Unsupported relation type %s:%s’, $selection, $method
));27 / 36
ЗамыканияКрайне полезны:
• удобный способ коротко описать какое-либо действие• часто позволяют избежать определения классов• позволяют реализовывать простую и логичную архитектуру
Проблемы:
• объект типа Closure, но работать как с объектом нельзя• нельзя (пока) создать с помощью new• нельзя (пока) изменить свойства и добавить свои• привязка this доступна с PHP 5.4
28 / 36
HTTP-приложение$this->path(’news’, function (Request $r){
$this->path(’daily’, function (Request $r){
$this->match(Pattern::DATE, function (Request $r, $date){
$news = $this[’db’]->select(...);
$this->GET(function (Request $r) use ($news) {
$this->format(’html’, function (Request $r) use ($news) {return new View(’page.news.daily’, array(’news’ => $news));
});
$this->format(’json’, function (Request $r) use ($news) {return $news->asJSON();
});});
});});
});
29 / 36
Замыкания: шаблоныЗамыкание может содержать не только код, но и разметку:
<?php $this->is(’html’, function ($data, $yield) { ?><div class="page-head"><img src="logo.png" class="page-head-logo">...
</div><ul class="page-menu"><?php foreach ($data[’menu’] as $item) { ?>
...<?php } ?>
</ul><div class="page-content"><?php $yield($data); ?>
</div><?php }) ?>
30 / 36
Самый главный интерфейс__invoke()
• позволяет сделать любой объект выполняемым• выполняемый объект взаимозаменяем с замыканием• в отличие от замыкания, объект можно долгоконструировать, добавлять дополнительные свойства и т.д.
Если объект конструируется для выполнения одногодействия, есть смысл сделать его выполняемым
Например: текстовый шаблон, правило роутинга и т.д.31 / 36
HTTP: запросыИногда выполняемый объект и замыкание должны бытьвзаимозаменяемы:
$this->path(’/’, new FrontPage($this))->path(’news’, new NewsController($this))->path(’special’, function(Request $r) {
...});
class FrontPage {
public function __construct(Application $app) {$this->app = $app;...
}public function __invoke(Request $r) {
return new View(’page.front’, ...);}
}
32 / 36
ШаблонизацияИногда проще всегда использовать объект определенного типа:
class Template {
public function __construct($name) {$this->name = $name;$this->load($name);
}
public function __invoke($data, $yield = null) {...
}}
// Однократная загрузка шаблона ...$t = new Template(’catalog.item’);
foreach ($items as $item) {$t($item); // ... и многократное применение
}
33 / 36
ДокументированиеПрактически все готовые системы документированияиспользуют формат JavaDoc:
• очень нудно и многословно• много усилий на соблюдение формальностей
Самый простой вариант:
• для каждого метода и класса обязательно однострочноеописание в виде обычного комментария
• по возможности — дополнительное описание в asciidoc.
Качество документации — в полноте описания, а не в формате.
34 / 36
Документирование: пример// Краткое описание класса//// Подробное описание класса в произвольной форме.// Можно ссылаться на другие классы, например// [class:IMPNamespaceOtherclass]class Test extends BaseClass{
protected $property; // - краткое описание свойства
// Краткое описание метода.//// Подробное описание метода. Можно использовать// *простую разметку*. Можно ссылаться на аргументы,// например, [arg:arg1] и [arg:arg2].public function __construct($arg1, $arg2){
...}
}
35 / 36
ИтогоСтараемся не делать вещи сложнее, чем они должны быть:
• код — небольшие классы в иерархии пространств имен• явная декларация зависимостей — упрощает рефакторинг• стандарты именования — для удобства, а не для эстетики• типизация, приведение типов если необходимо• исключения для обработки ошибок• новые классы исключений если есть повод ихобрабатывать отдельно
• семантика выполнения — важный элемент архитектуры• замыкания и объекты могут быть взаимозаменяемы• проще формат документирования — больше документации
36 / 36