Jak otworzyłem fabrykę.

przez | 13 grudnia 2019
By Canaletto – Web Gallery of Art: Image Info about artwork Transferred from en.wikipedia to Commons. ([1]) —Dacxjo 17:47, 1 March 2006 (UTC), Public Domain, https://commons.wikimedia.org/w/index.php?curid=606582

Dzisiaj przykład jak w praktyce zastosowałem wzorzec projektowy „Fabryka” w moim hobbystycznym projekcie. Od jakiegoś czasu dłubię sobie w czasie wolnym przy swojej „grze” (https://koltys.info/projectx/). Właściwie ciężko to nazwać grą (przynajmniej póki co), przy okazji napotykam wiele ciekawych problemów (i rozwiązań np. trochę wiedzy na temat map złożonych z heksów: https://www.redblobgames.com/grids/hexagons/) a przy okazji mogę poćwiczyć zastosowanie wiedzy teoretycznej w praktyce.

Dziś chce napisać o tym jak teoria została przekuta w praktyczne rozwiązanie. W grze docelowo będą występowały różne klasy przeciwników w związku z tym powstała klasa bazowa EnemyObject i pierwszy typ przeciwnika EnemyFranca. Powstała też klasa nadrzędna która zarządzała wszystkim związanym z przeciwnikami (rysowanie, przesuwanie, itd. ). Zapewne niektórym osobom już się zapaliły czerwone lampki – klasa która zajmuje się „wszystkim”? Przecież to znaczy, że klasa jest źle zaprojektowana i stanowczo za duża, da się to w ogóle przetestować? No właśnie. Ta klasa była napisana tak źle jak to tylko możliwe (ba! nadal jest, ale teraz już jestem tego świadomy i to naprawiam).

Pierwszy błąd był taki, że obiekty reprezentujące poszczególnych przeciwników na mapie były przeze mnie potraktowane jak zwykłe struktury danych, nie robiły nic poza przechowywaniem informacji (nazwa, aktualny poziom zdrowia, dostępne ataki, skrypt SI, itd.). To klasa nadrzędna zajmowała się tym jak narysować daną postać, jak ma się ona poruszać i pilnować jej poziomu zdrowia. To jeszcze dało się przełknąć, ale w pewnym momencie postanowiłem dodać możliwość wyświetlenia menu po kliknięciu prawym przyciskiem myszy na przeciwniku w celu wyświetlenia informacji na jego temat. Doszło kilka dodatkowych pól klasie nadrzędnej. Jakoś się udało.

Po jakimś czasie wpadłem na pomysł, że pokonany przeciwnik powinien zostawiać po sobie łupy. Czyli potrzebowałem dodatkowego pola w klasie nadrzędnej, żeby mieć uchwyt do okna wyświetlającego łup. Zrobiło się tego stanowczo za dużo. Klasa nadrzędna puchła w oczach a ja po tygodniu pracy nad inną klasą musiałem poświęcić dłuższa chwilę, żeby się połapać co się właściwie w tej klasie dzieje i jak mogę dodać do niej nowe funkcjonalności.

Pierwszy krok to przeniesienie logiki związanej z obiektami reprezentującymi przeciwników do klas które te obiekty opisywały, tak żeby nie były to tylko głupie struktury danych. To znacznie odchudziło klasę nadrzędną i uporządkowało kod.

Kolejnym krokiem było pozbycie się metody która w zależności od typu przeciwnika inicjalizowała odpowiednio obiekty. Funkcja ta przyjmowała 6 parametrów (a w przyszłości mogło się okazać, że nawet więcej). I tu dochodzimy do tytułowej fabryki. Została stworzona „fabryka” przeciwników, która przyjmuje tylko trzy argumenty: 1. typ przeciwnika jaki ma zostać stworzony, 2. pozycja w osi X i 3. pozycja w osi Y, resztą zajmuje się konstruktor wybranej klasy przeciwnika – proste i przejrzyste.

    export enum EnemyType {
        NONE = 0,
        FRANZA = 1,
    }

    export class EnemyObject {
        name: string = "";
        portrait: string = "";
        scale: number = 1.0;
        dx: number = 0;
        dy: number = 0;
        x: number = -1;
        y: number = -1;
        maxHP: number = -1;
        HP: number = -1;
        maxAP: number = -1;
        AP: number = -1;
        type: EnemyType = EnemyType.NONE;
        sprite: Phaser.Sprite | undefined = undefined;
        attacks?: Array<EnemyAttack> = undefined;
        ai: any = undefined;
        loot: any = undefined;
        useCallback: any = undefined;
        initialised: boolean = false;

        constructor(posX: number, posY: number) {
            this.x = posX;
            this.y = posY;
            this.initialised = true;
        }

        getAttackList(): Array<EnemyAttack> | undefined {
            return this.attacks;
        }
    }

    class EnemyFactory {
        mGame: ProjectX.ProjectX;
        mMap: Mapx.GameTiles;
        mPlayer: Player.Player;
        mEnemies: Enemy.Enemy;
        mLogger: Common.Logger;

        constructor(game: ProjectX.ProjectX, map: Mapx.GameTiles, player: Player.Player, enemies: Enemy.Enemy, logger: Common.Logger) {
            this.mGame = game;
            this.mMap = map;
            this.mPlayer = player;
            this.mEnemies = enemies;
            this.mLogger = logger;
        }

        getEnemyObject(enemyType: EnemyType, posX: number, posY: number) : EnemyObject {
            var result: EnemyObject;
            switch (enemyType) {
                case EnemyType.FRANZA:
                    result = new EnemyFranza.EnemyFranza(posX, posY, this.mGame, this.mMap, this.mPlayer, this.mEnemies, enemyType);
                    break;
                default:
                    result = new EnemyObject(posX, posY);
                    break;
            }
            return result;
        }
    }

Powyższy kod jest napisany w TypeScripcie (https://www.typescriptlang.org/).

Prosta zmiana, ale jak człowiek sam w pewnym momencie dochodzi do tego, że warto zastosować jakiś wzorzec projektowy i dlaczego powinien to zrobić to od razu łatwiej mu go zrozumieć i zapamiętać.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *