Нужно ли TDD?
Когда его стоит внедрять?
Замедляет ли работу написание тестов?
Какие подводные камни вы можете встретить?
Нужно ли TDD.
Очень трудно ответить на этот вопрос, для начала нужно внести ясность.
Вокруг веб разработки
docker run -d memcached:1.482a90b065744f43fa2efd8f00fece0faeacc53c7d306cc54b137482728d8c9bb
docker pull memcached:1.4
docker pull memcached
docker pull memcached:latestТ.к. latest это версия используемая по умолчанию.
docker psCONTAINER ID IMAGE COMMAND ....82a90b065744 memcached:1.4 "memcached" ...
docker inspect 82a90b065744 | less
docker inspect --format '{{ .NetworkSettings.IPAddress }}' 82a90b065744
cd:$ telnet 172.17.0.89 11211Trying 172.17.0.89...Connected to 172.17.0.89.Escape character is '^]'.set key 0 900 4dataSTOREDget keyVALUE key 0 4dataENDquitConnection closed by foreign host.
cd:$ docker stop 82a90b06574482a90b065744
cd:$ docker start 82a90b06574482a90b065744
cd:$ docker exec -it 82a90b065744 /bin/bashmemcache@82a90b065744:/$ lsbin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
cd:$ docker rm 82a90b06574482a90b065744
mysqldump mydatabasename -u myuser -pmypassword > projectname.dump.sql
docker run -e MYSQL_ROOT_PASSWORD=root mysql:5.4
docker ps
docker inspect --format '{{ .NetworkSettings.IPAddress }}' IDOFMYSQLCONTAINERВнутри контейнера создаем базу данных, заливаем дамп, меняем настройки доступа - исправляем ошибку.
Дана последовательность четных и нечетных чисел, в которой только одно четное число или только одно нечетное, нужно вернуть позицию того единственного числа. Нумерация начинается с 1.
iq_test("2 4 7 8 10") => 3 // Third number is odd, while the rest of the numbers are even
iq_test("1 2 1 1") => 2 // Second number is even, while the rest of the numbers are odd
def iq_test(numbers):
e = [int(i) % 2 == 0 for i in numbers.split()]
return e.index(True) + 1 if e.count(True) == 1 else e.index(False) + 1
Мое решение:
def iq_test(numbers):
seq = map(lambda x: int(x) % 2, numbers.split())
return seq.index(sum(seq) < 2) + 1
А вот просто одно из последних
def iq_test(numbers):
list = numbers.split()
evenNum = 0
oddNum = 0
count = 0
flag = 0
for i in list:
count+=1
if int(i) % 2 :
if oddNum == 0:
oddNum = count
else:
flag = 1
else:
if evenNum == 0:
evenNum = count
else:
flag = 2
if flag == 1:
return evenNum
else:
return oddNum
Не менее фееричное из последних решений на javascript
function iqTest(numbers) {
var array = numbers.split(' ');
if (array.length == 0) {
return 0;
}
if (array.length == 1) {
return 1;
}
var type1 = array[0]%2;
var type2 = array[1]%2;
if (type1 == type2) {
for (var i = 2; i < array.length; i++) {
if (array[i]%2 != type1) {
return i + 1;
}
}
} else {
var type3 = array[2]%2;
if (type3 == type1) {
return 2;
} else {
return 1;
}
}
}
<?php
foreach ($resultSet as $row) {
$entry = new Application_Model_Guestbook();
$entry->setId($row->id)
->setEmail($row->email)
->setComment($row->comment)
->setCreated($row->created);
$entries[] = $entry;
class I_Love_Zend_Naming_Huh_Model
{
protected $_properties = array();
protected $_data = array();
public function __construct(array $options = null )
{
if ($options !== null )
$this->fromArray($options);
}
public function __set($name, $value)
{
$this->__checkProperty($name);//check if property exists
$setterMethod = 'set'.$name;//buid setterName
if (method_exists($this, $setterMethod))// if setter exists
return $this->$setterMethod($value);//call it
$this->_data[$name] = $value;//no setter, remember data
return $this;
}
public function __get($name)
{
$getterMethod = 'get'.$name;
if (method_exists($this, $getterMethod))
return $this->$getterMethod();
if (key_exists($name, $this->_data))
return $this->_data[$name];
return null;
}
public function toArray()
{
return $this->_data;
}
public function fromArray(array $options)
{
foreach ($options as $key => $value)
$this->$key = $value;
return $this;
}
private function __checkProperty($name)
{
if (!in_array($name, $this->_properties))
throw new Exception('Undefined property ' . $name );
}
}
class Super_Puper_Model extends I_Love_Zend_Naming_Huh_Model
{
protected $_properties = array('id', 'first_name', 'last_name');
}
class Super_Puper_Model extends I_Love_Zend_Naming_Huh_Model
{
protected $_properties = array('id', 'first_name', 'last_name');
public function getId()
{
return $this->first_name . $this->last_name;
}
}
Я давно работаю с yii framework. И пока это единственный framework в мире php. Из минусов для меня то, что он слишком низкоуровневый, отсутствует много плюшек, которые я с успехом возмещаю разумным использованием Zend компонент. Как набор компонент Zend хорош, чего не скажешь о его framework'е.
Ну так вот, возвращаясь к низкоуровневому yii. Его CRUD хорош, хорош для прототипирования, но плодит кучу файлов. Многие из которых со временем вырезаются и сильно модифицируются. Моим кошмаром была работа на небольшим по объему backend проектом. Где вся работа сводилась к администрированию кучи таблиц с текстовыми и буленовскими полями. При помощи CRUD все было сделано быстро, но большую часть времени я потратил на украшение админки, checkboxes вместо input, фильтры вида true|false вместо 0,1 и тому подобной ерунды. Таблиц было много, view еще больше. Я с нежностью вспоминал django и жалел что проект на PHP.
Прошло время, и вот во время работы над очередным проектом выдалась несколько часов, в результате которых было рождено небольшое расширение над CRUD.
Краткая суть. CRUD очень похожи, и их можно объединить. Второй задачей является способ указания системе типов данных в моделях, для того чтобы CRUD мог на основе этих типов создавать фильтры, приводить к строковому виду автоматически.
Что получилось? Получился модуль который позволяет не запуская CRUD получить CRUD странички над любой моделью при условии что она отнаследована от особого ActiveRecord + в ней определен один единственный метод. В качестве небольшого бонуса есть простая возможность слегка менять список полей и их порядок на полученных страницах.
Ниже небольшой туториал как заставить это работать. Текста много, но он расчитан на новичка в yii. Если вы накоротке с yii можете сразу читать с установки модуля.
Переходим в директорию расширений. (Если пропустили первый шаг, замените /var/www/AdminTest/ на путь до папки вашего проекта, содержащей protected)
claymore:$ cd /var/www/AdminTest/protected/extensions/
Скачиваем расширение.
claymore:$ hg clone -q https://yii-rextensions.googlecode.com/hg/ YiisyCrudAdmin -r YiisyCrudAdmin
[Опционально] удаляем информацию о репозитории, особенно ваш проект уже под системой контроля версий.
claymore:$ rm -rf YiisyCrudAdmin/.hg
Среда 2011-03-30 11:03 box:/var/www/AdminTest/protected/extensions
Модуль установлен.
Для примера нам понадобится любая модель.
Создаем таблицу в созданной базе данных.
mysql> create table example(id int primary key auto_increment, name char(20), is_good_record tinyint(1) default 0 , long_description text);
Query OK, 0 rows affected (0.08 sec)
Переходим в папку protected
claymore:$ cd /var/www/AdminTest/protected
Создаем модель.
claymore:$ ./yiic shell config/main.php
Yii Interactive Tool v1.1 (based on Yii v1.1.7)
Please type 'help' for help. Type 'exit' to quit.
>> model Example example
generate models/Example.php
generate fixtures/example.php
generate unit/ExampleTest.php
The following model classes are successfully generated:
Example
If you have a 'db' database connection, you can test these models now with:
$model=Example::model()->find();
print_r($model);
>>
Модифицируем модель.
Цель. Отнаследоваться от расширения CActiveRecord объявленному в YiisyCrudAdmin.RActiveRecord. И реализовать абстрактный метод getFieldsDescription
Открываем /var/www/AdminTest/protected/models/Example.php в любимом редакторе
Yii::import("ext.YiisyCrudAdmin.RActiveRecord"); //Добавляем эту строчку импорта в верх файла.
В модель Example добавляем новый метод
Yii::import("ext.YiisyCrudAdmin.RActiveRecord");
class Example extends RActiveRecord
{
//...
// в последней версии данный метод не требуется в модели, может создаваться по желанию в классе ExampleAdmin
public function getFieldsDescription()
{
return array(
'is_good_record' => 'RDbBoolean',
'long_description' => 'RDbText',
);
}
//...
}
Этот метод предоствляет расширенную информацию о полях модели. На данный момент реализованы пока Bool и Text.
Т.к. метод абстрактный ( чтобы про него не забывали), реализация обязательна, метод может возвращать пустой массив если нет желания заморачиваться.
Создаем в вашем любимом редакторе controllers/ExampleController.php
Yii::import('ext.YiisyCrudAdmin.RController');
class ExampleController extends RController
{
protected $_modelName = 'Example';
}
Тестируем
Открываем в браузере http://127.0.0.1/AdminTest/index.php?r=example/admin
Получаем типичный yii crud с небольшими улучшениями.
Из плюсов такого подхода, все сделано стандартными средствами yii. Т.е. после этапа прототипирования, вы уже можете использовать существующую базу кода, достаточно заглянуть в RController
Гибкость, например в методе getFieldsDescription модели, элеметом массива может быть массив, формирующий CGridColumn, в этом случае все обрабатывается в обычном для yii ключе.
Как поменять порядок полей в actionAdmin ?
Прямо в файле модели создаем класс
Yii::import('ext.YiisyCrudAdmin.RModelAdmin');
class ExampleAdmin extends RModelAdmin
{
public function getAdminFields()
{
return array(
'name', 'long_description', 'is_good_record', 'id'
);
}
}
После этого в actionAdmin наблюдаем поля в указанном порядке
.
Как исключить поле из отображения в actionAdmin ?
Yii::import('ext.YiisyCrudAdmin.RModelAdmin');
class ExampleAdmin extends RModelAdmin
{
public function getAdminExcludedFields()
{
return array(
'id',
);
}
}
И это все. Оба метода можно комбинировать.
Ну и вот интерфейс RModelAdmin, отвечающий за порядок и отображаемые поля
public function getViewExcludedFields();// список полей для исключения в actionView
public function getAdminExcludedFields();//список полей для исключения в actionAdmin
public function getViewFields();// список полей для отображения в actionView
public function getSearchFields();//список полей для отображения в расширенной форме поиска actionAdmin
public function getAdminFields();// список полей для отображения в actionAdmin
public function getFormFields();// список полей для создания/редактирования.
Почему методы а не массивы? Например для динамической генерации в зависимости от условий.
В последней версии метод getFieldsDescription перенесен из модели в ModelAdmin класс.
Большую часть работы я трачу на поддержку и развитие уже написанных сайтов. Немалую долю времени при поддержке приходится тратить на оптимизацию. Именное ей и будет посвящен набор статей виде отдельных историй об одном абстрактном программисте.
Начинающий веб программист получает заказ на небольшой сайт. На удивление сайт завершен за 2 недели до сроков, даже с учетом затребованных изменений. Программист понимает что код его далек от идеала, многие вещи реализованы не оптимально. Принято решение заняться оптимизацией сайта. Сказал - сделал. Время генерации сайта до оптимизации 0.02 секунды, время генерации сайта после оптимизации 0.015 секунды. Т.е. страница генерируется на 25% быстрее. Итак по мнению программиста сайт обслужит на 25% больше пользователей.
Пусть следующий скрипт эмулирует сайт до оптимизации.
$echo “<?php usleep(20000);” > /var/www/site_before.php
А следующий после:
$echo “<?php usleep(15000);?>” > /var/www/site_after.php
Запускаем проверку.
$ ab -q -n 1000 -c 150 http://127.0.0.1/site_before.php | grep "Requests per second"
Requests per second: 64, 3[#/sec] (mean) (усреднено по трем замерам)
$ ab -q -n 1000 -c 150 http://127.0.0.1/site_after.php | grep "Requests per second"
Requests per second: 64 [#/sec] (mean) (усреднено по трем замерам)
Прироста в 25% нет. Есть падение производительности в рамках погрешности.
При фиксированных параметрах веб серверва и физических ресурсах сервера есть некое пороговое значение времени выполнения процесса ниже которого оптимизация теряет смысл.
$echo “<?php ” > /var/www/site_null.php
$ ab -q -n 1000 -c 150 http://127.0.0.1/site_null.php | grep "Requests per second"
Requests per second: 67 [#/sec] (mean) (усреднено по трем замерам)
Т.е. даже если убрать весь код из нашего проекта мы получим лишь 4% прирост производительности вебсервера.
Пороговая производительность веб сервера при выполнении какой либо задачи напрямую связана с типом задачи. В нашем примере скорее всего значительную часть времени было потрачено на запуск интерпретатора, что составляло настолько большую долю в процессе отдачи страницы, что временем выполнения скрипта вообще можно было пренебречь.
При выполнении оптимизации вы должны быть четко уверены в том что она нужна.
Перед оптимизацией кода будьте уверены что проблема именно в нем. Есть много способов оптимизации сайта, которые не затрагивают исходные коды.
Вообще вывод тривиален, да, согласен. Но я склонен считать что этой ошибке подвержены все начинающие веб программисты. В целом нет ничего плохого в том, что делал наш абстрактный программист, он изучал возможности языка и это очень полезное качество. Вопрос тут в другом. Нужно правильно расставлять акценты. Не нужно подменять понятия. В своей жизни я видел задержки проектов по срокам, из-за подобной деятельности, при этом человек занявшись "оптимизацией" (на самом деле просто игрался), начинал сам верить что оптимизирует сайт и даже обижался когда ему указывали на срыв сроков. И начинал заговариваться утверждая, что его действия необходимы для поддержки и развития проекта. Начинающий программист должен понимать, что если он занимается оптимизацией, а никаких оснований для этого нет, то он просто изучает язык, приемы, но никак не улучшает конечный продукт.
В статье обсуждаются предлагаемые в будущих реализациях механизмы проверки типов, в комментариях задевается текущая. А я хочу рассказать о том как я боролся с проверкой типов, в том числе и примитивных, в php средствами php.
Идеология. Проверка типов должна быть легко отключаема, или поведение на несоотвестие типов должно легко подменяться.
Набросок использования предполагаемого решения для проверки типов данных на примере Yii framework. (В принципе от yii тут только component, что например в Zend может быть сделано на Registry, если там ничего не появилось нового, или на Toolkit в limb, ну или уж в крайнем случае singleton, глобальные переменные)
class AnyClass
{
/**
* anyFunction description
*
* @param integer $arg1
* @param string $arg2
*/
public function anyFunction($arg1, $arg2)
{
Yii::app()->typeHintingComponent->assertFunctionArgs($this,'anyFunction' get_funciton_args());
//orYii::app()->typeHintingComponent->assertFunctionArgs(__CLASS__,__METHOD__, get_funciton_args());
}
Ожидаемое поведение : в случае если тип $arg1 не integer или тип $arg2 не string или полученное количество аргументов не совпадает, мы получаем например RuntimeException.
Реализация :
В моем примере поведение меняется заданием component в конфиге.
В результате получаем отключаемую проверку типов, и можем управлять поведением.
Основным недостатком мне видится необходимость каждый раз явно указывать объект, метод, передавать полученные аргументы
Мое решение это написать функцию обертку что берет эти данные из debug_backtrace
//docstring
public function func($arg1, $arg2)
{
Yii::app()->typeHintingComponent->assertFunctionArgs();
}
//...
public function assertFunctionArgs()
{
$backTrace = debug_backtrace();
if ( ! isset($backTrace[1]['object'], $backTrace[1]['function'], $backtrace[1]['args']))
throw new RCAssertException(/* Details */);
$obj = $backTrace[1]['object'];
$function = $backTrace[1]['function'];
$args = $backTrace[1]['args'];
//далее по предыдущему алгоритму
}
Еще один недостаток, на этапе разработки можно получить несоотвествие документации и ождаемых параметров, лично я это к недостаткам не отношу, т.к. позволит поддерживать актуальность документации как минимум.
Ну а самый большой недостаток такой реализации, это все же необходимость в методе постоянно вызывать ....->assertFunctionArgs(), если этого не делать, то и схема получится не работающая, это уже нужно насаждать на уровне review кода.
Зато самое большое преимущество это возможность проверять даже не существующие типы
/**
* @param mixed $var ну и нафига проверка типов если используются такие переменные? :)
* @param string|integer $var2 тоже смешно, но на промежуточном этапе рефакторинга может пригодится
*/
На практике у меня это не прижилось т.к. пытался насадить это вместе с кучей других требований и это не было приоритетным, возможно теперь перейдя на yii framework и реализовав это в виде компоненты у меня получится это сделать со второй попытки. Но тем не менее мне пришлось при рефакторинге одного запутанного кода использовать эту наработку активно, просто в том проекте одни и те же функции/методы использовались для разных целей, и соотвественно передаваемые типы данных были непредсказуемыми.
<?php
class BaseObject extends stdClass
{
public function __call($method, $args = array())
{
return call_user_func_array($this->$method, $args); //call closure
}
}
/**
* Create className instance add getIncrementedAge, getName behaviours
*
* @param string $className
* @param integer $age
* @param string $name
* @return object same type as $className
*/
function Type()
{
$props = func_get_args();
$className = array_shift($props);//classname should be first
$refFunction = new ReflectionFunction('Type');
$props = array_combine(// make dictionary from function arg names (from phpdoc) and function arguments
array_map(//extract variable names from phpdoc @param line
function ($docString) { //search variable name and return it
preg_match('/\$(\w+)/',$docString, $data);
return $data[1];
},
array_filter( //make lines array from phpdoc, filtering all except strings with @param and skip string with $className
explode("\n", $refFunction->getDocComment()),
function ($line)
{
return strpos($line, 'param') > 0 && strpos($line, '$className' ) < 1 ;
}
)
),
$props
);
$ob = new $className();
foreach ( $props as $name => $value)
$ob->$name = $value;
$ob->getName = function () use($ob)
{
return $ob->name;
};
$ob->getIncrementedAge = function ($inc =1) use($ob)
{
return $ob->age + $inc;
};
return $ob;
}
$cd = Type('BaseObject', 10,'cd');
$stas = Type('BaseObject', 20, 'stas');
var_dump($cd->getName());
var_dump($cd->getIncrementedAge(2));
var_dump($stas->getName());
var_dump($stas->getIncrementedAge(3));
php test.php
string(2) "cd"
int(12)
string(4) "stas"
int(23)
/**
* Returns the data model based on the primary key given in the GET variable.
* If the data model is not found, an HTTP exception will be raised.
* @param integer the primary key value. Defaults to null, meaning using the 'id' GET variable
*/
public function loadUser()
{
if($this->_model===null)
{
if(Yii::app()->user->id)
$this->_model=User::model()->findbyPk(Yii::app()->user->id);
if($this->_model===null)
$this->redirect(Yii::app()->controller->module->loginUrl);
}
return $this->_model;
}
Yii::app()->controller->module->loginUrl
Gtk-Message: Failed to load module "globalmenu-gnome": libglobalmenu-gnome.so: cannot open shared object file: No such file or directory
и
class DataSource // в моем случае это был итератор по таблице базы данных
{
protected $_counter = 1;
public function next()
{
return array('xxxxxxxxxxxxxxxxxxxxxxxx' => 'yyyyyyyyyyyyyyyyyyy');
}
}
class UserHelper // род классов, для получения данных связаных с пользователями, но выходящих за рамки этого объекта
{
protected $_u = null;
public function __construct(User $u)
{
$this->_user = $u;
}
public function getSomeData()
{
return array('x' => 10, 'y' => 20);
}
public function __desctruct()
{
$this->_u = null;
}
}
class User
{
protected $_dbRow = array();
protected $_helpers = array();
public function __construct(array $data )
{
$this->_dbRow = $data;
}
public function getHelper()// получения хелпера, в данном случае ситуация урезана до одного
{
if ( isset($this->_helpers['x'] ) )
return $this->_helpers['x'];
return $this->_helpers['x'] = new UserHelper($this); // автор посчитал это полезным
}
public function __destruct()
{
unset($this->_dbRow);
$this->_helpers['x'] = null;
}
}
// кусок кода вызвавший проблему.
$ds = new DataSource();
for($i= 1; $i++; $i< 1000000)
{
$u = new User($ds->next()); // загружаем пользвоателя напрямую по данным
$helper = $u->getHelper();
$helperData = $helper->getSomeData();// получаем данные хелпера
//делаем то что нужно с данными
unset($helperData);
unset($helper);
unset($u); // думаем что убили пользователя
}
$ds = new DataSource(); // получаем итератор
for($i= 1; $i++; $i< 1000000)
{
$u = new User($ds->next()); // загружаем пользвоателя напрямую по данным
//$helper = $u->getHelper(); вот тут и была проблема.
$helper = new UserHelper($u);
$helperData = $helper->getSomeData();// получаем данные хелпера
//делаем то что нужно с данными
unset($helper);
unset($u); // думаем что убили пользователя.
}
Для массивов $a и $b
$a == $b Равно TRUE в случае, если $a и $b содержат одни и те же элементы
php -a
php > $a = array(1,2);
php > $b = array(2,1);
php > var_dump($a == $b);
bool(false)
php > class X {};
php > $x1 = new X();
php > $x2 = new X();
php > $a = array($x1, $x2);
php > $b = array($x2, $x1);
php > var_dump($a == $b);
bool(true)
php > $x = $object->getResult();
php > $x->value = 'smth';
php > $x->callSmth();
php > $a = null
php > ;
php > $a->x = 10;
php > var_dump($a);
object(stdClass)#1 (1) {
["x"]=>
int(10)
}
php > $b = 'sdf';
php > $b->x = 10;
Warning: Attempt to assign property of non-object in php shell code on line 1
php > class null{};#я думал так нельзя
php > class false extends null{};# можно и так
php > class true extends false { function __toString() { return 'false'; } } ; #это уже понятно
php > $false = 'false';
php > $true = new True();
php > var_dump($$true);#ну это уже стеб.
string(5) "false"
Создание директории проекта.
Я буду использовать локальный вебсервер, с дефолтными настройками. Проект создам в папке AdminTest внутри моего document root /var/www/
Папка создана, скачиваем yii-framework
Распаковываем
Переименовываем, или делаем символическую ссылку( кому как удобнее), я переименовываю, т.к. работаю в рамках демонстрации
Удаляю скаченный архив
Проверяем что у нас есть распаковааная папка с yii framework
Переходим в папку со скриптами yii framework
Запускаем скрипт создания проекта
Проверяем
Создаем базу данных admintest. При помощи вашего любимого средства.
Прописываем настройки базы данных в protected/config/main.php
Теперь у нас есть болванка для веб сайта на основе шаблона yii.