Статичні члени класу

«Клас-орієнтування Програмування» - це коли використовуються класи, що складаються тільки з статичних методів і властивостей, а екземпляр класу ніколи не створюється. У цій статті я буду говорити про те, що:

  • це не дає ніяких переваг в порівнянні з процедурним програмуванням
  • не варто відмовлятися від об'єктів
  • наявність статичних членів класу! = смерть тестів

залежності

Зазвичай, код залежить від іншого коду. наприклад:

Цей код залежить від змінної $ bar і функції substr. $ Bar - це просто локальна змінна, певна трохи вище в цьому ж файлі і в тій же області видимості. substr - це функція ядра PHP. Тут все просто.

Тепер, такий приклад:

normalizer_normalize - це функція пакету Intl, який інтегрований в PHP починаючи з версії 5.3 і може бути встановлений окремо для старіших версій. Тут вже трохи складніше - працездатність коду залежить від наявності конкретного пакета.

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

Це типовий приклад клас-орієнтованого програмування. Foo жорстко зав'язаний на Database. І ще ми припускаємо, що клас Database був уже инициализирован і з'єднання з базою даних (БД) вже встановлено. Імовірно, використання цього коду буде таким:

Foo :: bar неявно залежить від доступності Database і його внутрішнього стану. Ви не можете використовувати Foo без Database. а Database. імовірно, вимагає з'єднання з БД. Як можна бути впевненим, що з'єднання з БД вже встановлено, коли відбувається виклик Database :: fetchAll. Один із способів виглядає так:

При виклику Database :: fetchAll. перевіряємо існування сполуки, викликаючи метод connect. який, при необхідності, отримує параметри з'єднання з конфіга. Це означає, що Database залежить від файлу config / database.php. Якщо цього файлу немає - він не може функціонувати. Їдемо далі. Клас Database прив'язаний до однієї бази даних. Якщо Вам знадобиться передати інші параметри з'єднання, то це буде, як мінімум, нелегко. Ком наростає. Foo не тільки залежить від наявності Database. але також залежить від його стану. Database залежить від конкретного файлу, в конкретній папці. Тобто неявно клас Foo залежить від файлу в папці, хоча по його коду цього не видно. Більш того, тут купа залежностей від глобального стану. Кожен шматок залежить від іншого шматка, який повинен бути в потрібному стані і ніде це явно не позначено.

Щось знайоме.

Чи не так, схоже на процедурний підхід? Давайте спробуємо переписати цей приклад в процедурному стилі:

Знайдіть 10 відмінностей ...
Підказка: єдина відмінність - це видимість Database :: $ connection і $ database_connection.

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

Клас-орієнтоване програмування - це як покупка машини, для того щоб сидіти в ній, періодично відкривати і закривати двері, стрибати на сидіннях, випадково змушуючи спрацьовувати подушки безпеки, але ніколи не повертати ключ запалювання і не зрушити з місця. Це повне нерозуміння суті.

Повертаємо ключ запалювання

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

Тепер Foo не залежить від конкретного Database. При створенні екземпляра Foo. потрібно передати певний об'єкт, що володіє характеристиками Database. Це може бути як екземпляр Database. так і його нащадок. Значить ми можемо використовувати іншу реалізацію Database. яка може отримувати дані звідки-небудь з іншого місця. Або має кешируєтся шар. Або є заглушкою для тестів, а не справжнім з'єднанням з БД. Тепер потрібно створювати екземпляр Database. це означає, що ми можемо використовувати кілька різних підключень до різних БД, з різними параметрами. Давайте реалізуємо Database:

Зверніть увагу, наскільки простіше стала реалізація. В 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. Процедурний приклад:

Написати цю строчку можна в будь-якому місці і вона виконається. Її поведінка залежить від глобального стану підключення до БД. Хоча з коду це не очевидно. Додамо обробку помилок:

Через неявних залежностей foo_bar. в разі помилки буде важко зрозуміти, що саме зламалося.

Для порівняння, ось клас-орієнтована реалізація:

Ніякої різниці. Обробка помилок - ідентична, тобто все також складно знайти джерело проблем. Це все тому, що виклик статичного методу - це просто виклик функції, який нічим не відрізняється від будь-якого іншого виклику функції.

PHP впаде з фатальною помилкою, коли дійде до new Foo. Ми вказали що Foo необхідний екземпляр Database. але не передали його.

PHP знову впаде, тому що ми не передали параметри підключення до БД, які ми вказали в Database :: __ construct.

Тепер ми задовольнили всі залежності, які обіцяли, все готово до запуску.

Але давайте уявимо, що параметри підключення до БД невірні або у нас якісь проблеми з БД і з'єднання не може бути встановлено. У цьому випадку буде кинуто виключення при виконанні 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:

Статичні члени класу

Клас-орієнтований підхід виглядає оманливо просто, але намертво прибиває код цвяхами залежностей. Об'єктно-орієнтований підхід залишає все гнучким і ізольованим до моменту використання, що може виглядати більш складним, але це більш керовано.

статичні члени

Навіщо ж потрібні статичні властивості і методи? Вони корисні для статичних даних. Наприклад, дані від яких залежить екземпляр, але які ніколи не змінюються. Повністю гіпотетичний приклад:

Уявімо, що цей клас повинен пов'язувати типи даних з БД з внутрішніми типами. Для цього потрібна карта типів. Ця карта завжди однакова для всіх примірників Database і використовується в декількох методах Database. Чому б не зробити карту статичним властивістю? Дані ніколи не змінюються, а тільки зчитуються. І це дозволить заощадити трохи пам'яті, тому що дані загальні для всіх примірників Database. Оскільки доступ до даних відбувається тільки всередині класу, це не створить ніяких зовнішніх залежностей. Статичні властивості ніколи не повинні бути доступні зовні, тому що це просто глобальні змінні. І ми вже бачили до чого це призводить ...

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

Проблема статичних методів в тому, що вони створюють жорстку залежність. Коли Ви викликаєте Foo :: bar (). цей рядок коду стає пов'язана з конкретним класом Foo. Це може привести до проблем.

Використання статичних методів допустимо при наступних обставинах:

  1. Залежність гарантовано існує. У разі якщо виклик внутрішній або залежність є частиною оточення. наприклад:

Тут Database залежить від конкретного класу - PDO. Але PDO - це частина платформи, це клас для роботи з БД, що надається PHP. У будь-якому випадку, для роботи з БД доведеться використовувати якийсь API.

  • Метод для внутрішнього використання. Приклад з реалізації фільтра Блума.

    Ця маленька допоміжна функція просто надає обгортку для конкретного алгоритму, який допомагає розрахувати гарне число для аргуметом $ k. використовуваного в конструкторі. Оскільки вона повинна бути викликана до створення екземпляра класу, вона повинна бути статичною. Цей алгоритм не має зовнішніх залежностей і навряд чи буде замінений. Він використовується так:

    Це не створює ніяких додаткових залежностей. Клас залежить сам від себе.

  • Альтернативний конструктор. Хорошим прикладом є клас DateTime. вбудований в PHP. Його екземпляр можна створити двома різними способами:

    В обох випадках результатом буде екземпляр 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 (.). немає ніяких гарантій, що з'єднання з БД вже встановлено або буде встановлено.

    Якщо код всередині цієї функції буде виконаний - це значить, що екземпляр Database був успішно переданий, що означає, що екземпляр об'єкта Database був успішно створений. Якщо клас Database спроектований вірно, то можна бути впевненим, що може бути екземпляр цього класу означає можливість виконувати запити до БД. Якщо примірника класу не буде, то тіло функції не буде виконано. Це означає, що функція не повинна піклуватися про стан БД, клас Database це зробить сам. Такий підхід дозволяє забути про залежності і сконцентруватися на вирішенні завдань.

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

    Клас-орієнтоване програмування - дурість. Вчіться використовувати ООП.

    Схожі статті