Однією з проблем PHP як мови програмування є той факт, що Ви можете реалізувати лише одне спадкування. Це означає що кожен клас може бути потомком лише одного класу.
Однак, в багатьох випадках, було б зручно мати спадкування від багатьох класів. Наприклад, було б непогано успадковувати методи кількох різних класів задля запобігання дублювання коду.
Ця проблема може призвести до того, що клас матиме довгу історії спадкування, а це, часом, не має ніякого сенсу.
Новою особливістю PHP 5.4 стало введення так званих трейтів (traits). Трейти чимось схожі на міксини, тим, що дають змогу змішувати класи трейтів з існуючими класами. Це означає, що Ви можете знизити дублювання коду і отримати певні переваги, уникаючи проблеми багаторазового спадкування.
В цій статті я досліджуватиму трейти та покажу як Ви можете використовувати їх в Ваших проектах.
Що таке трейти в PHP?
Трейт це проста група методів які Ви хочете включити в інший клас. Трейт, так само як і абстрактний клас, не може слугувати для створення екземпляру.
Наприклад, трейт може бути таким:
trait Sharable {
public function share($item)
{
return 'share this item';
}
}
Ви можете включити трейт в інші класи наступним чином:
class Post {
use Sharable;
}
class Comment {
use Sharable;
}
Тепер, якщо Ви створите нові об'єкти цих класів, то помітите, що в обох з них
доступний метод share()
:
$post = new Post;
echo $post->share(''); // 'share this item'
$comment = new Comment;
echo $comment->share(''); // 'share this item'
Як працюють трейти?
Як видно з останнього прикладу, в обох об'єктах - Post
та
Comment
- доступний метод share()
, незважаючи на відсутність
його оголошення.
Трейти, в основному, є лише способом копіпасту коду під час виконання.
Це означає, що трейт вноситься в класи Post
та Comment
коли Ви
створюєте новий екземпляр, і стає доступним код метода share()
.
Яка різниця між трейтами та абстрактними класами?
Трейт відрізняються від абстрактного класу тим, що він, насправді, не служить для спадкування.
Уявіть, що класи Post
та Comment
успадковуються від класу
AbstractSocial
. Ми, швидше за все, хочемо більшого аніж просто
поділитися записами та коментарями на соціальних вебсайтах, тому ми, ймовірно,
врешті-решт, прийдемо до складного дерева спадкування на зразок:
class AbstractValidate extends AbstractCache {}
class AbstractSocial extends AbstractValidate {}
class Post extends AbstractSocial {}
Це складне спадкування досить погане, але складність також додається, коли
такий простий об'єкт його не матиме. Наприклад, якщо ми маємо об'єкт
Message
, що не буде реалізовувати поширення, тоді цей об'єкт
вимагатиме дещо іншої структури спадкування.
Яка різниця між трейтами та інтерфейсами?
Трейти дуже схожі на інтерфейси. Обоє, є, зазвичай, простими і короткими, та не можуть використовуватись без справжнього реалізованого класу. Однак різниця між ними є важливою.
Інтерфейс -- це контракт, що каже: "цей об'єкт може зробити цю річ", в той час як трейт дає змогу об'єкту її зробити.
Наприклад:
// Interface
interface Sociable {
public function like();
public function share();
}
// Trait
trait Sharable {
public function share($item)
{
// share this item
}
}
// Class
class Post implements Socialable {
use Sharable;
public function like()
{
//
}
}
В цьому прикладі ми маємо інтерфейс Sociable
, який вказує, що об'єкту
Post
доступні методи like()
та share()
.
Трейт Sharable
, реалізовує методи share()
та_ like()
_,
які згодом реалізуються в класі Post
.
Ви можете відмітити, що ми можемо отримати об'єкт Post
та визначити що
він реалізує інтерфейс Sociable
, в той час як трейт описує метод, який
можна додати до інших подібних класів.
$post = new Post;
if($post instanceOf Sociable)
{
$post->share('hello world');
}
Чим корисні трейти?
Користь використання трейтів полягає в запобіганні дублювання коду та складного наслідування класів, яке може бути недоречним в контексті вашої програми.
Це дозволяє Вам визначати прості, короткі та зрозумілі трейти, і потім реалізовувати цю функціональність в разі необхідності.
Які недоліки трейтів?
Також, варто зазначити, що при використанні трейтів також можливі недоліки.
З їх використанням дуже легко писати роздуті класи, які несуть занадто багато функціональності. Трейти є, по суті, способом копіпасту коду між класами. Володіючи методом простого додавання ще однієї групи методів до класу, дуже легко відходити від принципу єдиного обов'язку.
Одним з недоліків є неможливість побачити всі методи класу при перегляді вихідного коду, так само, як і конфлікти методів та дублювання коду.
Я вважаю, що трейти, при коректному використанні, є прекрасним інструментом який є в нашому розпорядженні. Однак трейти можуть бути милицею при, так званому, "ледачому програмуванні". Це дуже просто - додати трейт для вирішення поточної проблеми, але, зазвичай, краще віддати перевагу композиції, аніж наслідуванню чи трейтам.
Які типові ситуації використання трейтів?
Отож, які є типові ситуації, коли використання трейтів - хороша ідея?
Я вважаю, що трейти це чудовий місце для коду що використовується повторно між
кількома схожими класами, які не успадковуються від одного абстрактного класу.
Використаємо соціальний додаток розглянутий вище. Уявімо, ми маємо об'єкти для
Post
, _**Photo**_
, _**Note**_
, _**Message**_
та _**Link**_
.
Здебільшого, ці об'єкти є досить схожими в нашій системі і вони створюються
для взаємодії між користувачами.
Однак, об'єкти _**Post**_
, _**Photo**_
, _**Note**_
та _**Link**_
успадковують інтерфейс _**Shareable**_
:
interface Shareable {
public function share();
}
Чи доречно дублювати методу_ share()
_ в кожному класі, що реалізовує
інтерфейс Shareable
?
Ні.
Чи доречно мати клас AbstractShare
для об'єктів що розширюють
інтерфейс _**Shareable**_
?
Ні.
Чи доречно мати метод share()
реалізований в класі
_**AbstractEntity**_
, але потім заблокувати його для об'єкта
_**Message**_
?
Ні.
Чи доречно реалізувати _**ShareableTrait**_
, внаслідок чого, буде простіше
додати його до кожного об'єкту, де це є необхідним аби виконати умови
інтерфейсу?
Так!
Який приклад використання трейтів в реальному житті?
Коли Ви вперше зіткнетесь з трейтом як з інструментом, що є в Вашому розпорядженні, можливо, Вам буде важко визначити -- справді певна ситуація потребує саме його використання, чи слід використати один з багатьох інших способів вирішення такої проблеми.
Я вважаю, коли перед Вами постане це питання, справді хороша ідея - зазирнути в світ вільного програмного забезпечення та подивитися як інші використовують цю специфічну техніку.
На мою думку, проектом з відкритим кодом, що дозволяє ефективно використовувати трейти є пакунок для Laravel Cashier.
Пакунок Cashier додає функціональності Вашим моделям Eloquent, що робить керування підписками в Вашому SaaS додатку дійсно простим.
При спробі додати функціональність в існуючі моделі, Ви потрапляєте в складне становище.
Перший варіант, коли Ви просто реалізувати методи в себе за допомогою копіпасту прикладів методів з документації. Це не є хорошим рішенням, тому що ви можете створити баг при некоректному копіюванні, і коли пакунок Cashier оновиться Ви будете змушені обійти всі Ваші моделі та оновити код що повторюється.
Другим варіантом є розширення об'єкта спадкуванням від об'єкта Cashier з необхідними Вам методами. Це також погане рішення - що трапиться, якщо Ви захочете додати свій метод який реалізовує валідацію ваших моделей? Ви зіштовхнетеся зі складною лінією спадкування, що зробить Ваш код роздутим та заважатиме коли Ви, Ваші колеги чи наступники повернуться до нього в майбутньому.
Замість цього, пакунок Cashier надає трейти, які дозволяють Вам додати функціональності будь-якій моделі без дублювання коду чи отримання неприємної історії спадкування.
use Laravel\\Cashier\\BillableTrait;
use Laravel\\Cashier\\BillableInterface;
class User extends Eloquent implements BillableInterface {
use BillableTrait;
protected $dates = ['trial_ends_at', 'subscription_ends_at'];
}
Схоже чудове використання трейтів присутнє в пакунку Validating, який також створений для моделей Eloquent в Laravel. Цей пакунок дозволяє Вам створювати моделі з автовалідацією за допомогою підключення трейту.
Як Ви можете помітити, замість того щоб розширити об'єкт Cashier додаванням метода підписки, а, потім, знову розширювати його чимось на зразок Magniloquent для додавання валідації, Ви можете просто додати два трейти до Вашої моделі. Це запобігає утворенню божевільного дерева успадкування.
Висновки
Отож, найбільше питання - чи слід використовувати трейти? Я вважаю, Ви обов'язково маєте розглянути питання щодо використання трейтів в Вашому проекті. Трейти пропонують гарне вирішення проблеми неприємного безладу спадкування, що виникає в мовах програмування з одиничним спадкуванням, таких як PHP.
Трейти надають можливість додати Вашим класам функціональності горизонтально без серйозних складнощів та дублювання коду.
Однак, дуже просто використовувати трейти як милицю - вони не є вирішенням всіх Ваших проблем. Використання трейтів в невірних ситуаціях є поганим рішенням. Якщо Ви намагаєтесь вбити ломом функціональність в клас за допомогою трейтів, то це означає, що Ви робите щось неправильно.
Як і всюди в комп'ютерному програмуванні, є найбільш очевидні правильні та помилкові ситуації для використання певних компонентів та шаблонів. Трейти можуть вирішити Ваші поточні проблеми, але відповідь на Ваші труднощі має полягати в використанні композиції. Не дивіться на ситуацію з точки зору використання трейтів, завжди намагайтеся вибрати правильний інструмент з усіх Вам відомих, коли зіштовхуєтесь з проблемою. Розуміння того, де та коли слід використовувати трейти, додасть в ваш арсенал ще одну зброю.
Ще немає коментарів