Wprowadzenie do interfejsów w programowaniu

Wprowadzenie do interfejsów, które, poprawiają przejrzystość i elastyczność kodu. W tym artykule pokażę przykłady i sposoby ich wykorzystania

Czym jest interfejs?

A czym nie jest? 😀

Interfejsy są to takie twory, dzięki którym możemy określić jakie publiczne metody chcemy, aby posiadały nasze klasy. Są one banalne do utworzenia i implementacji, a idące za nimi korzyści są większe niż przy wykorzystaniu zwykłych klas.

Przykład

Aby to wyjaśnić to pomyślmy: przyjmijmy, że tworzymy grę, w której gracze mogą atakować innych graczy, ale i też potworki mogą atakować graczy ale i też inne moby -> takie wszystko vs wszystko. Fajnie by było, gdyby to jakoś ustandaryzować i graczy i potworki potraktować jako moby – wtedy mamy prościej bo mamy moby vs moby i cała logika ubijania się jest uproszczona.

Idźmy teraz z tym dalej: moby muszą mieć jakieś HP, muszą się poruszać i oczywiście jakoś się nazywać. Pod to wszystko oczywiście utworzymy pewne standardy i będziemy mieli gotową klas….. – no właśnie nie klasę, a interfejs 😉. Dlaczego? – wspomniałem, że gracz i potwór to są moby oczywiście pasuje ich w kodzie rozróżnić, ale w 99% przypadkach robią to samo, więc te 99% określimy jako wspólny interfejs IMob (duża literka i na początku nazwy interfejsu będzie mówić nam, że to interfejs, a nie klasa).

Jako też interfejsy zrobimy IHp, IVector (do poruszania się) IName (do nazw) oraz kilka innych do przećwiczenia temat. Do implementacji interfejsu IMob wykorzystam Trait (o którym pisałem tutaj), aby część kodu nie pisać dwa razy.

Zatem nasze interfejsy wraz z traitem będą wyglądały tak:

<?php

interface IVector {
    // jakiś kod; dla tego przykładu to nie jest istotne;
}

// Do itemów
interface IItem
{
    public function name() : IName;
}

interface ILoot {
    public function item() : IItem;
    public function count() : int;
}

interface IName {
    public function value() : string;
}

interface IHp
{
    public function value() : int;
    public function lose(int $value) : void;
    public function heal(int $value) : void;
}

interface IMob
{
    public function attack(IMob $victim, int $power) : void;
    public function damage(int $value) : void;
    public function move(IVector $vel) : void;
    public function name() : IName;
    /** @return Ilots[] */
    public function loots() : array;
    public function hp() : IHp;
}

trait MobTrait
{
    public function __construct(
        private IHp $hp,
        private IName $name,
        private array $loots
    ) { }

    public function attack(IMob $victim, int $power) : void
    {
        $victim->damage($power);
    }

    public function damage(int $value) : void
    {
        $this->hp->lose($value);
    }

    public function move(IVector $vel) : void
    {
        // jakiś kod; dla tego przykładu to nie jest istotne;
    }

    public function name() : IName
    {
        return $this->name;
    }

    /** @return ILoot[] */
    public function loots() : array
    {
        return $this->loots;
    }

    public function hp() : IHp
    {
        return $this->hp;
    }
}

Zauważ, że kod interfejsów mówi nam jakie czynności mogą zostać wykonane w ramach klas, które będą je implementować. Mamy jasno określone typy argumentów i typy zwracane. Jeśli chodzi o końcowy MobTrait to dzięki niemu będzie nam łatwiej zaimplementować metody interfejsu IMob w klasach Player i Monster, bo tylko go użyjemy

Implementacja

Ok czas utworzyć kilka klas. Oczywiście kod będzie czysto przykładowy, więc zaimplementowane będzie tylko kilka interfejsów:

<?php

class Coin implements IItem
{
    private IName $name;

    public function __construct() {
        $this->name = new Name("Moneta");
    }

    public function name() : IName
    {
        return $this->name;
    }
}

class Crystal implements IItem
{
    private IName $name;

    public function __construct() {
        $this->name = new Name("Kryształ");
    }

    public function name() : IName
    {
        return $this->name;
    }
}

class Loot implements ILoot
{
    public function __construct(
        private IItem $item,
        private int $count
    ) { }

    public function item() : IItem
    {
        return $this->item;
    }

    public function count() : int
    {
        return $this->count;
    }
}

class Name implements IName
{
    public function __construct(
        private string $value
    ) { }

    public function value() : string
    {
        return $this->value;
    }
}

class Player implements IMob
{    
    use MobTrait;

    public function __construct(
        private IHp $hp,
        private IName $name,
        private array $loots = []
    ) { }
}

class Moster implements IMob
{
    use MobTrait;

    public function __construct(
        private IHp $hp,
        private IName $name
    ) {
        $this->loots = [
            new Loot(new Coin, 30),
            new Loot(new Crystal, 2)
        ];
    }
}

class Hp implements IHp
{
    private int $current;
    public function __construct(
        private int $max,
    ) { 
        $this->current = $max;
    }
    public function value() : int
    {
        return $this->current;
    }
    public function lose(int $value) : void
    {
        $this->current -= $value;
        if($this->current < 0 ) {
            $this->current = 0;
        }
    }
    public function heal(int $value) : void
    {
        $this->current += $value;
        if($this->current > $this->max ) {
            $this->current = $this->max;
        }
    }
}

Zatem mamy:

  • implementujące IItems klasy: Coin oraz Crystal
  • ILoot i implementująca klasa Loot
  • dla IName mamy Name
  • klasę Hp implementującą IHp
  • i oczywiście nasze IMob czyli klasy Player oraz Monster

Jak łatwo po kodzie zauważyć gracz od potworka różni się tylko tym jakie łupy można zdobyć. Od gracza w zasadzie tych łupów jest całe zero, a dla potwora są dwa łupy.

W praktyce

W praktyce jeśli będziemy zrobić jakiś kod bitwy pomiędzy mobami to niezależnie od tego czy będziemy chcieli zadać je gracz vs gracz czy gracz vs potwór czy potwór vs potwór czy coś prostszego to będziemy mogli to zrobić w łatwy sposób. Ja zademonstruję prosty kod:

<?php

function battle(IMob $firstMob, IMob $secondMob) : string
{
    while($secondMob->hp()->value() > 0 && $firstMob->hp()->value() > 0) {
        $firstMob->attack($secondMob, 1);
        $secondMob->attack($firstMob, 1);
    }
    
    if($firstMob->hp()->value() > 0) {
        $winner = $firstMob;
    } else {
        $winner = $secondMob;
    }
    return "Zwyciężył {$winner->name()->value()}";
}

$player1 = new Player(
    new Hp(20),
    new Name("Bezimienny Wybraniec")
);

$player2 = new Player(
    new Hp(22),
    new Name("Imienny Wybraniec")
);

$monster1 = new Moster(
    new Hp(5),
    new Name("Szlam")
);

$monster2 = new Moster(
    new Hp(8),
    new Name("Większy Szlam")
);

echo "1. " . battle($player1, $monster1) . "\n";
echo "2. " . battle($player1, $player2) . "\n";
echo "3. " . battle($monster1, $monster2) . "\n";

Po wywołaniu tego prostego kodu naszym oczom ukaże się następują tablica wyników:

  1. Zwyciężył Bezimienny Wybraniec
  2. Zwyciężył Imienny Wybraniec
  3. Zwyciężył Większy Szlam

Oczywiście można pokusić się o bardziej skomplikowane systemy, ale już nie będę obierać wam tej przyjemności! 😃

W ramach przećwiczenia proponuję rozwinąć ww. kod, wymyślić kolejne mechanizmy. Dajcie też znać w komentarzu jak poszło 😉

Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments
Włączyć powiadomienia? Bardzo pragnę Dam sobie radę