Пользователь
0,0
рейтинг
28 мая 2013 в 11:08

Разработка → Статические члены класса. Не дай им загубить твой код перевод

ООП*, PHP*
Давно хотел написать на эту тему. Первым толчком послужила статья Miško Hevery "Static Methods are Death to Testability". Я написал ответную статью, но так и не опубликовал ее. А вот недавно увидел нечто, что можно назвать «Классо-Ориентированное Программирование». Это освежило мой интерес к теме и вот результат.

«Классо-Ориентированое Программирование» — это когда используются классы, состоящие только из статических методов и свойств, а экземпляр класса никогда не создается. В этой статье я буду говорить о том, что:
  • это не дает никаких преимуществ по сравнению с процедурным программированием
  • не стоит отказываться от объектов
  • наличие статических членов класса != смерть тестам

Хотя эта статья про PHP, концепции применимы и к другим языкам.


Зависимости


Обычно, код зависит от другого кода. Например:

$foo = substr($bar, 42);

Этот код зависит от переменной $bar и функции substr. $bar — это просто локальная переменная, определенная немного выше в этом же файле и в той же области видимости. substr — это функция ядра PHP. Здесь все просто.

Теперь, такой пример:

$foo = normalizer_normalize($bar);

normalizer_normalize — это функция пакета Intl, который интегрирован в PHP начиная с версии 5.3 и может быть установлен отдельно для более старых версий. Здесь уже немного сложнее — работоспособность кода зависит от наличия конкретного пакета.

Теперь, такой вариант:

class Foo {

   public static function bar() {
       return Database::fetchAll("SELECT * FROM `foo` WHERE `bar` = 'baz'");
   }

}

Это типичный пример классо-ориентированного программирования. Foo жестко завязан на Database. И еще мы предполагаем, что класс Database был уже инициализирован и соединение с базой данных (БД) уже установлено. Предположительно, использование этого кода будет таким:

Database::connect('localhost', 'user', 'password');
$bar = Foo::bar();

Foo::bar неявно зависит от доступности Database и его внутреннего состояния. Вы не можете использовать Foo без Database, а Database, предположительно, требует соединения с БД. Как можно быть уверенным, что соединение с БД уже установлено, когда происходит вызов Database::fetchAll? Один из способов выглядит так:

class Database {

   protected static $connection;

   public static function connect() {
       if (!self::$connection) {
           $credentials = include 'config/database.php';
           self::$connection = some_database_adapter($credentials['host'], $credentials['user'], $credentials['password']);
       }
   }

   public static function fetchAll($query) {
       self::connect();

       // используем self::$connection...
       // here be dragons...

       return $data;
   }

}

При вызове Database::fetchAll, проверяем существование соединения, вызывая метод connect, который, при необходимости, получает параметры соединения из конфига. Это означает, что Database зависит от файла config/database.php. Если этого файла нет — он не может функционировать. Едем дальше. Класс Database привязан к одной базе данных. Если Вам понадобится передать другие параметры соединения, то это будет, как минимум, нелегко. Ком нарастает. Foo не только зависит от наличия Database, но также зависит от его состояния. Database зависит от конкретного файла, в конкретной папке. Т.е. неявно класс Foo зависит от файла в папке, хотя по его коду этого не видно. Более того, здесь куча зависимостей от глобального состояния. Каждый кусок зависит от другого куска, который должен быть в нужном состоянии и нигде это явно не обозначено.

Что-то знакомое...


Неправда ли, похоже на процедурный подход? Давайте попробуем переписать этот пример в процедурном стиле:

function database_connect() {
   global $database_connection;
   if (!$database_connection) {
       $credentials = include 'config/database.php';
       $database_connection = some_database_adapter($credentials['host'], $credentials['user'], $credentials['password']);
   }
}

function database_fetch_all($query) {
   global $database_connection;
   database_connect();

   // используем $database_connection...
   // ...

   return $data;
}

function foo_bar() {
   return database_fetch_all("SELECT * FROM `foo` WHERE `bar` = 'baz'");
}

Найдите 10 отличий…
Подсказка: единственное отличие — это видимость Database::$connection и $database_connection.

В классо-ориентированном примере, соединение доступно только для самого класса Database, а в процедурном коде эта переменная глобальна. Код имеет те же зависимости, связи, проблемы и работает так же. Между $database_connection и Database::$connection практически нет разницы — это просто разный синтаксис для одного и того же, обе переменные имеют глобальное состояние. Легкий налет пространства имен, благодаря использованию классов — это конечно лучше, чем ничего, но ничего серьезно не меняет.

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

Поворачиваем ключ зажигания


Теперь, давайте попробуем ООП. Начнем с реализации Foo:

class Foo {

   protected $database;

   public function __construct(Database $database) {
       $this->database = $database;
   }

   public function bar() {
       return $this->database->fetchAll("SELECT * FROM `foo` WHERE `bar` = 'baz'");
   }

}

Теперь Foo не зависит от конкретного Database. При создании экземпляра Foo, нужно передать некоторый объект, обладающий характеристиками Database. Это может быть как экземпляр Database, так и его потомок. Значит мы можем использовать другую реализацию Database, которая может получать данные откуда-нибудь из другого места. Или имеет кеширующий слой. Или является заглушкой для тестов, а не настоящим соединением с БД. Теперь нужно создавать экземпляр Database, это означает, что мы можем использовать несколько разных подключений к разным БД, с разными параметрами. Давайте реализуем Database:

class Database {

   protected $connection;

   public function __construct($host, $user, $password) {
       $this->connection = some_database_adapter($host, $user, $password);
       if (!$this->connection) {
           throw new Exception("Couldn't connect to database");
       }
   }

   public function fetchAll($query) {
       // используем $this->connection ...
       // ...
       return $data;
   }

}

Обратите внимание, насколько проще стала реализация. В Database::fetchAll не нужно проверять состояние подключения. Чтобы вызвать Database::fetchAll, нужно создать экземпляр класса. Чтобы создать экземпляр класса, нужно передать параметры подключения в конструктор. Если параметры подключения не валидны или подключение не может быть установлено по другим причинам, будет брошено исключение и объект не будет создан. Это все означает, что когда Вы вызываете Database::fetchAll, у Вас гарантировано есть соединение с БД. Это значит, что Foo нужно только указать в конструкторе, что ему необходим Database $database и у него будет соединение с БД.

Без экземпляра Foo, Вы не можете вызвать Foo::bar. Без экземпляра Database, Вы не можете создать экземпляр Foo. Без валидных параметров подключения, Вам не создать экземпляр Database.

Вы попросту не сможете использовать код, если хоть одно условие не удовлетворено.

Сравним это с классо-ориентированным кодом: вызвать Foo::bar можно в любое время, но возникнет ошибка, если класс Database не готов. Вызвать Database::fetchAll можно в любое время, но возникнет ошибка, если будут проблемы с файлом config/database.php. Database::connect устанавливает глобальное состояние, от которого зависят все остальные операции, но эта зависимость ничем не гарантируется.

Инъекция


Посмотрим на это со стороны кода, который использует Foo. Процедурный пример:

$bar = foo_bar();

Написать эту строчку можно в любом месте и она выполнится. Ее поведение зависит от глобального состояния подключения к БД. Хотя из кода это не очевидно. Добавим обработку ошибок:

$bar = foo_bar();
if (!$bar) {
   // что-то не так с $bar, завершаем работу!
} else {
   // все хорошо, идем дальше
}

Из-за неявных зависимостей foo_bar, в случае ошибки будет тяжело понять, что именно сломалось.

Для сравнения, вот классо-ориентированная реализация:

$bar = Foo::bar();
if (!$bar) {
   // что-то не так с $bar, завершаем работу!
} else {
   // все хорошо, идем дальше
}

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

Теперь ООП:

$foo = new Foo;
$bar = $foo->bar();

PHP упадет с фатальной ошибкой, когда дойдет до new Foo. Мы указали что Foo необходим экземпляр Database, но не передали его.

$db  = new Database;
$foo = new Foo($db);
$bar = $foo->bar();

PHP опять упадет, т.к. мы не передали параметры подключения к БД, которые мы указали в Database::__construct.

$db  = new Database('localhost', 'user', 'password');
$foo = new Foo($db);
$bar = $foo->bar();

Теперь мы удовлетворили все зависимости, которые обещали, все готово к запуску.

Но давайте представим, что параметры подключения к БД неверные или у нас какие-то проблемы с БД и соединение не может быть установлено. В этом случае будет брошено исключение при выполнении new Database(...). Следующие строки просто не выполнятся. Значит у нас нет необходимости проверять ошибку после вызова $foo->bar() (конечно, Вы можете проверить что Вам вернулось). Если что-то пойдет не так с любой из зависимостей, код не будет выполнен. А брошенное исключение будет содержать полезную для отладки информацию.

Объектно-ориентированный подход может показаться более сложным. В нашем примере процедурного или классо-ориентированного кода всего лишь одна строчка, которая вызывает foo_bar или Foo::bar, в то время как объектно-ориентированный подход занимает три строки. Здесь важно уловить суть. Мы не инициализировали БД в процедурном коде, хотя нам нужно это сделать в любом случае. Процедурный подход требует обработку ошибок постфактум и в каждой точке процесса. Обработка ошибок очень запутана, т.к. сложно отследить какая из неявных зависимостей вызвала ошибку. Хардкод скрывает зависимости. Не очевидны источники ошибок. Не очевидно от чего зависит ваш код для нормального его функционирования.

Объектно-ориентированный подход делает все зависимости явными и очевидными. Для Foo нужен экземпляр Database, а экземпляру Database нужны параметры подключения.

В процедурном подходе ответственность ложится на функции. Вызываем метод Foo::bar — теперь он должен вернуть нам результат. Этот метод, в свою очередь, делегирует задачу Database::fetchAll. Теперь уже на нем вся ответственность и он пытается соединиться к БД и вернуть какие-то данные. И если что-то пойдет не так в любой точке… кто знает что Вам вернется и откуда.

Объектно-ориентированный подход перекладывает часть ответственности на вызывающий код и в этом его сила. Хотите вызвать Foo::bar? Хорошо, тогда дайте ему соединение с БД. Какое соединение? Неважно, лишь бы это был экземпляр Database. Это сила внедрения зависимостей. Она делает необходимые зависимости явными.

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



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



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

Статические члены


Зачем же нужны статические свойства и методы? Они полезны для статических данных. Например, данные от которых зависит экземпляр, но которые никогда не меняются. Полностью гипотетический пример:

class Database {

   protected static $types = array(
       'int'    => array('internalType' => 'Integer', 'precision' => 0,      ...),
       'string' => array('internalType' => 'String',  'encoding'  => 'utf-8', ...),
       ...
   )

}

Представим, что этот класс должен связывать типы данных из БД с внутренними типами. Для этого нужна карта типов. Эта карта всегда одинакова для всех экземпляров Database и используется в нескольких методах Database. Почему бы не сделать карту статическим свойством? Данные никогда не изменяются, а только считываются. И это позволит сэкономить немного памяти, т.к. данные общие для всех экземпляров Database. Т.к. доступ к данным происходит только внутри класса, это не создаст никаких внешних зависимостей. Статические свойства никогда не должны быть доступны снаружи, т.к. это просто глобальные переменные. И мы уже видели к чему это приводит…

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

Проблема статических методов в том, что они создают жесткую зависимость. Когда Вы вызываете Foo::bar(), эта строка кода становится связана с конкретным классом Foo. Это может привести к проблемам.

Использование статических методов допустимо при следующих обстоятельствах:

  1. Зависимость гарантированно существует. В случае если вызов внутренний или зависимость является частью окружения. Например:

    class Database {
    
       ...
    
       public function __construct($host, $user, $password) {
           $this->connection = new PDO(...);
       }
    
       ...
    
    }
    

    Здесь Database зависит от конкретного класса — PDO. Но PDO — это часть платформы, это класс для работы с БД, предоставляемый PHP. В любом случае, для работы с БД придется использовать какое-то API.

  2. Метод для внутреннего использования. Пример из реализации фильтра Блума:

    class BloomFilter {
    
       ...
    
       public function __construct($m, $k) {
           ...
       }
    
       public static function getK($m, $n) {
           return ceil(($m / $n) * log(2));
       }
    
       ...
    
    }
    

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

    $m = 10000;
    $n = 2000;
    $b = new BloomFilter($m, BloomFilter::getK($m, $n));
    

    Это не создает никаких дополнительных зависимостей. Класс зависит сам от себя.

  3. Альтернативный конструктор. Хорошим примером является класс DateTime, встроенный в PHP. Его экземпляр можно создать двумя разными способами:

    $date = new DateTime('2012-11-04');
    $date = DateTime::createFromFormat('d-m-Y', '04-11-2012');
    

    В обоих случая результатом будет экземпляр DateTime и в обоих случаях код привязан к классу DateTime так или иначе. Статический метод DateTime::createFromFormat — это альтернативный коструктор объекта, возвращающий тоже самое что и new DateTime, но используя дополнительную функциональность. Там, где можно написать new Class, можно написать и Class::method(). Никаких новых зависимостей при этом не возникает.

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

Слово об абстракции


Зачем вся эта возня с зависимостями? Возможность абстрагировать! С ростом Вашего продукта, растет его сложность. И абстракция — ключ к управлению сложностью.

Для примера, у Вас есть класс Application, который представляет Ваше приложение. Он общается с классом User, который является предствлением пользователя. Который получает данные от Database. Классу Database нужен DatabaseDriver. DatabaseDriver нужны параметры подключения. И так далее. Если просто вызвать Application::start() статически, который вызовет User::getData() статически, который вызовет БД статически и так далее, в надежде, что каждый слой разберется со своими зависимостями, можно получить ужасный бардак, если что-то пойдет не так. Невозможно угадать, будет ли работать вызов Application::start(), потому что совсем не очевидно, как себя поведут внутренние зависимости. Еще хуже то, что единственный способ влиять на поведение Application::start() — это изменять исходный код этого класса и код классов которые он вызызвает и код классов, которые вызызвают те классы… в доме который построил Джек.

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

function (Database $database) {
   ...
}

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

Без возможности не думать о зависимостях и зависимостях этих зависимостей, практически невозможно написать хоть сколь-нибудь сложное приложение. Database может быть маленьким классом-оберткой или гигантским многослойным монстром с кучей зависимостей, он может начаться как маленькая обертка и мутировать в гигантского монстра со временем, Вы можете унаследовать класс Database и передать в функцию потомок, это все не важно для Вашей function (Database $database), до тех пор пока, публичный интерфейс Database не изменяется. Если Ваши классы правильно отделены от остальных частей приложения с помощью внедрения зависимостей, Вы можете тестировать каждый из них, используя заглушки вместо их зависимостей. Когда Вы протестировали класс достаточно, чтобы убедиться, что он работает как надо, Вы можете выкинуть лишнее из головы, просто зная, что для работы с БД нужно использовать экземпляр Database.

Классо-ориентированное программирование — глупость. Учитесь использовать ООП.
Перевод: David C. Zentgraf
@truezemez
карма
90,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (48)

  • +7
    Отличная статья и отличный перевод. Заплюсовал.
    Однако, некоторые утверждения кажутся мне странными.

    это не дает никаких преимуществ по сравнению с процедурным программированием

    Если несколько функций можно как-то сгруппировать по тематике, то я их группирую в один статический класс:
    <?php
    class My_Math {
    	public static function fibonachi($n) {
    		return ...;
    	}
    	public static function getK($m, $n) {
    		return ceil(($m / $n) * log(2));
    	}
    }
    

    Преимущества от этого следующие:
    • Вместо одного огромного файла functions.php, функции раскиданы по нескольким файлам и их легко найти
    • Работает autoload. Класс Math загрузится только тогда когда мне понадобится


    Статические свойства никогда не должны быть доступны снаружи

    Почему бы и нет?
    <?php
    class My_Date {
    	// Пусть в моей системе будет только один справочник со списком месяцев
    	public static $months = array('Январь', 'Февраль', ...);
    	// Сделал бы его константой класса, но константы не могут быть массивами :(
    }
    


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

    Как насчёт этого?
    <?php
    class My_Pager {
    	// Каждый экземпляр должен знать свой порядковый номер, что бы генерить уникальные ссылки
    	protected $_num = 0;
    
    	// Но как же его посчитать автоматически?
    	// А вот как:
    	protected static $_cntPagers = 0;
    
    	public function __construct()
    	{
    		$this->_num = self::$_cntPagers++;
    	}
    }
    


    Итог: статические члены классов, это неотъемлемый инструмент, которым надо уметь правильно пользоваться, не создавая лишних зависимостей.
    • 0
      Если несколько функций можно как-то сгруппировать по тематике, то я их группирую в один статический класс

      Конечно, здесь есть смысл. В примере из статьи никаких преимуществ нет.

      Статические свойства никогда не должны быть доступны снаружи

      Почему бы и нет?

      Вам придется жестко привязаться к My_Date::$months. Это ни хорошо, ни плохо. Важно понимать что это за собой влечет.

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

      Как насчёт этого?


      Неплохой трюк. Но Вы же и философию из этого не делаете, верно?

      Автор призывает использовать инструменты с пониманием и избегать крайностей.
      • +2
        Я только хотел сказать, что в статье показана одна сторона медали и практически не освещена другая. Я привёл конкретные примеры противоречащие утверждениям автора:
        это не дает никаких преимуществ

        даёт.

        Вам придется жестко привязаться к

        Я хочу, чтобы в моей системе был один справочник в одном месте => мне приходится привязываться к этому месту. Вы можете изобретать сложные механизмы, например, хранить справочник в отдельном файле или в БД или просто в атрибуте объекта, но зачем?

        Но Вы же и философию из этого не делаете, верно?

        Делаю. В некоторых случаях нельзя делать статические члены класса; в некоторых случаях это необходимо. Автор статьи плохо описал случаи в которых это необходимо. Я попытался дополнить.

        P.S.> В качестве примера можете посмотреть в Zend Framework. Они не брезгуют использовать статические члены класса. Просто они грамотно их используют и это не создаёт лишних зависимостей и не мешает тестированию.
      • 0
        >Конечно, здесь есть смысл. В примере из статьи никаких преимуществ нет.

        Вот только автор статьи на этом примере пытается доказать более общее утверждение, которое и было опровергнуто контрпримером.

        В итоге если так дальше рассуждать, то статья сводится к «вот в таких-то конкретных ситуациях нет никакого смысла так делать». Так и хочется ответить: «Спасибо, Кэп! А что скакзать-то хотел?»
    • 0
      Если несколько функций можно как-то сгруппировать по тематике, то я их группирую в один статический класс:

      Мне кажется, что это костыль времен PHP 5.2. Для таких вещей есть неймспейсы.
      • +3
        Но autoload работать не будет.
        • 0
          Единственный серьезный аргумент в пользу статических методов в большом числе случаев.
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Если строго следовать этой логике, то вообще все переменные должны быть закрытыми + куча геттеров.

        И это правильно.

        Но иногда, встречаются исключения из правил. Я показал такое исключение, которое ко всему прочему, оказалось статическим членом.

        вы бы легко впилили туда перевод

        Этого не надо делать ни в коем случае! В справочнике могут быть ключевые слова. Например, они могут фигурировать в БД в ENUM полях. Да и перепиливать базовые классы под проект — это не хорошо.

        Если вдруг моё приложение станет мультиязычным, то переводом я буду заниматься на совсем другом уровне. Там где происходит вывод:

        echo '<select>';
        foreach (My_Date::$months as $month) {
           echo '<option>'. translate($month) .'</option>';
        }
        echo '</select>';
        

        Это немного сложнее, но правильнее. Это MVC-принципы. Перевод относится именно к «V».
        Например, потом вам понадобится сделать API к вашему сайту. А у вас все справочники локализованы и API будет постоянно выдавать месяца на разных языках. Как клиентское ПО будет с этим работать?
        • 0
          Как клиентское ПО будет с этим работать?

          Серверное будет ему указывать локаль. :)
          • 0
            И, за одно, передавать все свои словари :)
    • 0
      Почему бы и нет?

      <?php
      class My_Date {
          // Пусть в моей системе будет только один справочник со списком месяцев
          public static $months = array('Январь', 'Февраль', ...);
          // Сделал бы его константой класса, но константы не могут быть массивами :(
      }
      




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

      Пример из документации по ссылке как раз класс с месяцами.

      • 0
        // Из доков:
        echo new Month(Month::June); // выведет: 6
        // Тот же самый результат:
        echo Month::June;
        

        Получается, смысл только в валидации входных значений:
        // Но мне проще написать:
        if ( ! array_key_exists(13, My_Date::$months)) {
            echo 'Error!';
        }
        // чем писать это:
        try {
            new Month(13);
        } catch (UnexpectedValueException $uve) {
            echo $uve->getMessage();
        }
        

        Чем же SplEnum лучше? Мне кажется, чем проще, тем лучше. Простые нативные массивы лучше чем класс, который никак не упрощает работу.
        • 0
          Ну, запись типа if ($month == Month::June) является и более понятной, и более гибкой для модификации, и более защищенной от ошибок чем if ($month == 6) или if ($month == arrray_search("Июнь", Month::$months)) (кстати, эти два фрагмента не эквивалентны с вашим определением Month::$month, у вас нумерация месяцев начинается с нуля )
  • 0
    Отличный материал! Наокнец-то кто-то про это написал!
  • 0
    Да, принцип инверсии контроля очень полезен и, кроме того, справедлив в любых объектно-ориентированных языках. Очень жаль, что очень много программистов про него забывают :( после них приходится такое разгребать, что удивляешься, как это вообще можно было написать, и как готовая программа в принципе работает.
  • +1
    Так и хочется сказать: «Садись, пять!»
    Спасибо за отличный перевод интересной статьи!
  • –7
    Новый оксюморон. «Загубить код на PHP».
  • 0
    Часто использую статику (в разумных пределах) в своих проектах, однако в своей статье, за пример её использования получил критику.

    Но, критикующий так и не ответил на вопрос, чем плох код:
        static private  $userData = NULL;
     
        public function getById($uid) {
                   if (self::$userData) return self::$userData;
                   self::$userData = $this->locGetById($uid); 
                   return self::$userData;
                }
     
        private function locGetById($uid) {
                  // some code $retData = ...               
                   return $retData;
                }


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

    А может я в чем-то не прав?
    • +1
      Нельзя говорить, что код «плохой», не зная контекста. В статье делается акцент на том, что при вызове метода, нельзя быть уверенным в наличии соединения к БД. В Вашем примере я вижу простой кеш. У Вас разные экземпляры класса будут возвращать одинаковый результат, если это «ок», тогда — «ок».
      • 0
        это Ok, так как мы один раз запрашиваем данные из БД, а используем их 10 раз.
        Зачем же нам множить 10 раз один и тот же запрос, если кеш можно сделать в коде.
    • +4
      У данного кода очевидная проблема: метод getById($uid) отработает корректно только один раз. Далее с каким $uid не вызывай, он будет возвращать значение статического свойства $userData.
      Очевидно, чтобы он работал корректно, $userData нужно сделать массивом и класть данные конкретного юзера используя в качестве ключа его $uid. Ну и проверять наличие данных в массиве по ключу соответственно.

      Всё вышеперечисленное разумеется никак не касается использования статических свойств, просто ошибка в алгоритме.
      • 0
        принципиально ты прав, и спасибо за замечание.
        В данном проекте мы используем данные только одного Пользователя (это однопользовательская онлайн игра).
        Если бы Проекту нужно было несколько разных пользователей… например «лента друзей», то несомненно сделал бы $userData массивом.
        • +1
          Описание кода отличается от реализации (да и имя не подходит). Лучше бы вынести передачу $uid в конструктор/еще_куда из getById (переименовав его в getByUid). Тогда заголовок метода получится соответствующим реализации.
  • –6
    Спасибо за перевод, но статья ужасная, а её автор посмотреть бы на фреймверки, и почитать про исключения.
    Я уже представляю как в каждом контролере надо будет писать $db = new Database('localhost', 'user', 'password');
    • 0
      Спасибо за перевод, но статья ужасная, а её автор посмотреть бы на фреймверки, и почитать про исключения.

      Чем Вам не понравилась статья? Что не так с исключениями? Причем тут фреймворки?

      Я уже представляю как в каждом контролере надо будет писать $db = new Database('localhost', 'user', 'password');

      С чего бы это?
      • –6
        Автор акцентирует внимания что при вызове $bar = Foo::bar(); он не может узнать где произошла ошибка, в этом ему могут помочь исключения и трейс.

        Ну давайте представим что у нас есть контроллер с 2 методами
        class Controller {
            
            public function foo(){
                return My_Class::foo();
            }
            
            public function bar(){
                return My_Class::bar();
            }
        }

        Судя по замыслу автора это должно быть как то так,
        
        
        class Controller {
            
            public function foo(){
                $db_confog = include 'config/database.php';
                $db  = new Database($db_confog['host'], $db_confog['user'], $db_confog['password']);
                $my_class = new My_Class($db);
                return $my_class->foo();
            }
            
            public function bar(){
                $db_confog = include 'config/database.php';
                $db  = new Database($db_confog['host'], $db_confog['user'], $db_confog['password']);
                $my_class = new My_Class($db);
                return $my_class->bar();
            }
        }
        

        А теперь давайте подумаем какой код лучше.
        • +2
          Судя по замыслу автора это должно быть как то так

          Мне кажется, что Вы совсем не поняли замысла автора.
          • 0
            Я вам буду очень признателен если вы на примере моего кода покажете что имел ввиду автор.
            • +1
              Я думаю автор имел ввиду это:
              class Controller {
              
                  private $my_class;
              
                  public __construct(My_Class $myClass) {
                      $this->my_class = $myClass;
                  }
                  
                  public function foo(){
                      return $this->my_class->foo();
                  }
                  
                  public function bar(){;
                      return $this->my_class->bar();
                  }
              }
              

              Никто не говорит что инициализацию надо проводить каждый раз. А вам полезно было бы посмотреть на фреймворки а именно паттерн Dependancy Injection и его использование.
              • 0
                И в итоге ваш контролер будет работать только с 1 классом?
                И чем Dependancy Injection поможет в данном случае?
                Пример контролера в Laravel 4
                class UserController extends BaseController {
                
                    /**
                     * Show the profile for the given user.
                     */
                    public function showProfile($id)
                    {
                        $user = User::find($id);
                
                        return View::make('user.profile', array('user' => $user));
                    }
                
                }
                </spoiler>
                
                • +1
                  И в итоге ваш контролер будет работать только с 1 классом?

                  Как раз код со статическими методами сможет работать с одним классом, а обьект параметр может принадлежать любому классу наследнику My_Class.

                  И чем Dependency Injection поможет в данном случае?

                  Dependency Injection поможет избежать дублирования логики инциализации.

                  User::find($id);

                  Ваш пример не совсем корректен т.к. не достаточно контекста чтобы определить насколько данный код хорош или плох.
                  Могу высказать свое ИМХО по этому поводу.
                  class View — скорее всего это класс из ядра фреймворка и его изменение не планируется. соответственно статические метода для него вцелом допустимы.
                  class User — удобный и логичный метод по созданию обьектов. Однако скорее всего рано или поздно прийдется изменять логику обработки пользователей (например создать отдельный класс для анонимных пользователей или пользователей с особыми правами) в таком случае статический метод может помешать таким изменениям так что в данном случае он скорее не оправдан. Кроме того такой метод затруднит тестирование кода если мы захотим подложить в этот контроллер тестовых пользователей.
  • 0
    Typofix:
    Это может привести {в => к} проблемам.
    • 0
      Спасибо.
  • +1
    Очень предвзятая статья. Никаких преимуществ, кроме личного мнения не продемонстрировано.

    1. Как раз в примере с ООП зависимостей становится гораздо больше: класс Foo должен знать о Database с самого начала, хотя там может быть всего один метод использующий Database. Получается, что ВСЕ методы класса Foo должны знать о Database.

    2. Мы обязаны производить подключение к БД, хотя оно может нам вовсе не понадобиться.

    3. Любой нормальный код (со статическими методами или без) умеет генерировать исключения при обнаружении ошибок, например, отсутствии подключения к БД. Причем гораздо логичнее бросать их при использовании БД, а не в начале программы (получается вся программа должна знать о БД).

    4. Способов реализации примера множество, например, статический конструктор при помощи autoload.

    Вообще, в данном случае статический класс служит контекстом создаваемым абстракцией более высокого уровня для Foo.
    Так как встроенных средств разделения слоев абстракции в PHP нет, то статические классы и глобальные переменные практически единственные НОРМАЛЬНЫЕ способы их реализации. И их использование это не ошибка программистов — это недостаток языка, который приходится покрывать костылями.

    А пример в статье про плюсы подхода для абстракции легко решается созданием нескольких статических классов БД.

    ИмяПриложения\БД1
    ИмяПриложения\БД2
    • 0
      1. Зависимостей ровно столько же, только они не статические, а динамические и передаются явно.
      Если всего один метод хочет знать Database, можно передать его туда явно. Не обязательно в конструктор же:
      public function getSomething(Database $db);

      2. Не обязаны. Можно коннектиться лениво — это зависит от реализации Database
      • 0
        Вы просто видите лишь верхушку айсберга, а вся ситуация такова:

        Функции и методы используют множество слов, которые не были переданы им явно — операторы, функции и т.д. Все эти литеры являются глобальными, а для функции они являются кубиками из которых она строится.
        Например, PHP — это не просто язык с операторами, это множество кубиков: сессии, расширения, глобальные супермассивы, модули к http-серверам и т.д. А глобальные переменные и статические методы всего лишь способы добавить новые кубики для построения функция средствами самого языка.
        Просто нужно понять, что в момент запуска скрипта PHP контекст УЖЕ создан ядром PHP и можно либо поверх него создавать собственную систему, абстрагируясь от встроенных возможностей (только зачем тогда вообще использовать PHP, как основную систему?????), либо спокойно относится к хранению состояния в статическом классе (ведь он и будет объектом с состоянием в данном случае, его время жизни ограниченно запросом).

        Т.е. вопрос не в правильном использовании ООП-подхода, а грамотной проекции их в PHP.
        • 0
          Просто нужно понять, что в момент запуска скрипта PHP контекст УЖЕ создан ядром PHP и можно либо поверх него создавать собственную систему, абстрагируясь от встроенных возможностей (только зачем тогда вообще использовать PHP, как основную систему?????)

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

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

          Т.е. вопрос не в правильном использовании ООП-подхода, а грамотной проекции их в PHP.

          Не понимаю, чем в этом случае состоит грамотная проекция ООП на PHP и кто эту «грамотность» оценивает?
          В 5.4 уже есть встроенные возможности по агрегации функционала в класс (те самые кубики). Если грамотно и в меру это использовать, статики действительно будет по минимуму.
          • 0
            Постойте, никто не предлагал абстрагироваться от возможностей самого языка и городить абстракции на встроенные возможности.

            А это часто бывает полезно :)
        • +1
          Например, PHP — это не просто язык с операторами, это множество кубиков: сессии, расширения, глобальные супермассивы, модули к http-серверам и т.д.

          Когда-то я тоже подобным образом рассуждал, но потом понял, что сессии и прочие суперглобалы нельзя (не в смысле «запрещено», а в смысле «не должно») использовать где-то кроме заведомо глобального кода (типа index.php). С тех пор вместо, утрируя,
          if ($_SERVER["method"] == "POST")
            do_smth_with_post_superglobal();
          

          я пишу
          if ($_SERVER["method"] == "POST")
            do_smth_with_array($_POST);
          
          , а то и
          do_smth_with_two_arrays($_SERVER, $_POST);
          • 0
            Вам просто стал не нужен PHP, а вы этого не поняли.
            • 0
              Что значит «не нужен»? Он справляется со своими задачами (прошли те времена, когда я писал на нем почти всё, включая «шелл-скрипты», лишь редко используя Basic для GUI или C/C++ для экономии ресурсов) не хуже известных мне конкурентов. А если учесть, что для получения $_SERVER и $_POST мне не нужно делать вообще ничего (например не подключать модуль cgi), то и лучше. Ну и другие причины есть не расставаться с PHP — как минимум с ним проще зарабатывать на жизнь.
  • +1
    > Классо-ориентированное программирование — глупость. Учитесь использовать ООП.

    ООП — глупость. Учитесь использовать функциональное программирование.
    • +2
      > ООП — глупость. Учитесь использовать функциональное программирование.

      Функциональное программирование — глупость. Учитесь декларативному программированию.
      • +2
        пацаны дух дениса ритчи жив только в си, где руками управляют памятью, где пацаны живут с машинно-зависимыми интеджерами и используют небезопасные указатели. только си, только хардкор!
    • +2
      ООП и ФП прекрасно дополняют друг друга.
  • 0
    Действительно, классо-ориентированное программирование практически идентично процедурному. Но конкретно в PHP это оправдано наличием автозагрузки классов (spl_autoload_register, __autoload).

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

    А вот, например, в Пайтоне такой подход не имеет смысла, там лучше воспользоваться честным процедурно-ориентированным программированием и поместить функции в файл/модуль arr.py.
  • 0
    научите писать грамотный код на php!!!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.