Вопрос по интерфейсам и фабрике обьектов.

hgjtfrdredreder

Путник
Участник
Сообщения
93
Розыгрыши
0
Репутация
-16
Реакции
33
Баллы
46
Хроники
  1. Chaotic Throne: High Five
Исходники
Присутствуют
Сборка
L2J
Возможно это не совсем адекватный вопрос)

Копаюсь в лыже. Хочу максимально уйти от импорта имплементаций и заменить всё что можно интерфейсами. Встает вопрос об инстанцировании обьектов. Есть мысль сделать синглтон-фабрики со статик методами, которые будут создавать обьекты. То есть вся имплементация по сути будет обьявлена лишь в этих фабриках, а весь остальной код будет оперировать только интерфейсами и обращаться за реализацией к ним. Насколько адекватен такой подход? Какие проблемы могут быть проблемы?

И еще глупый вопрос - насколько ресурсоемок каст интерфейса к имплементации? Еслть смысл на это обращать внимание или накладные расходы ничтожны. Чет не смог нагуглить.
 
Посмотреть вложение 78410

Есть вот такое дерево классов. Оно не полное - там ниже еще инстансы и прочее. Что мы имеем:
1. Корабли наследуются от Character. Почему? Потому что логика движения реализована там, а не в корневом обьекте.
2. Сам Character 10к строк. При том, что часть его методов можно пихнуть в родительский обьект (и отнаследовать корабль от него), а часть вроде как вообще в потомков перенести.
3. Типичное использование в коде примерно такое:
Java:
private PCInstance player;

public void foo(PCInstance p, AVehicle v, Location loc) {
    p.moveTo(loc);
    v.moveTo(loc);
    player = p;
}

public PCInstance get() {
    return player;
}

Зачем тут имплементация, если код moveTo реализован будет в AObject вообще?
Почему не сделать так? Количество сущностей мгновенно сократилось, абстракция повысилась. Оккама одобряе.

Java:
private IObject player;

public void foo(IObject p, IObject v, ILocation loc) {
    p.moveTo(loc);
    v.moveTo(loc);
    this.player = p;
}


public IObject get() {
    return player;
}

// допустим это некий метод обработки игрока
// и мы знаем, что тут прилетает PCInstance
private void doWithPlayer() {
    IObject p = this.get();
    ((IPCInstance) p).doSmth()
}

Мне кажется такой подход поможет привести классы в порядок в процессе приведения их к минимальному количеству интерфейсов.

4. Ещё там есть класс L2Item. Финализированый. То есть он описывает вообще любой айтем. В нём полей 50 наверно. Этому конкретному предмету нужно только 2 поля из 50? [А по щам?] там - он будет хранить все 50.

Мне кажется, что повысив уровень абстракции в процессе пиления интерфейсов мне станет лучше понятно как распределить методы по родителям/наследникам/сиблингам, где может добавить сущностей, где наоборот убрать. А сейчас это дом из говна и палок - одну убираешь и за ней падает к хуям всё вообще по цепочке.
Чтобы делать как ты хочешь - тебе придется знать в каждом конкретном методе какой у тебя будет объект и все это придется как-то менеджить. Поэтому все обычно и пихают в базовый класс который отвечает за функционал ( аля Creature / Character ), чтобы не ебаться с резолвингом кастов, и где какой объект должен был вернуться.
Но если тебя раздражает 10к строк - можешь вынести функциональный код в сервисы и компоненты. И тогда мувинг у тебя будет не в Creautre, а в MoveController (можно сделать его интерфейсом или абстрактным классом, если базовый мув контроллер один). И вот тут уже фабрика тебе поможет все дженерализировать (через дженерики или вайлдкард):
Описываешь интерфейс фабрики со всеми функциями-генераторами и фабрикой классов, которые будут наследовать / переопределять эти генераторы:
Java:
public interface ObjectFactory<T extends Creature> {
    static ObjectFactory<Creature> playerFactory() {
        return (ObjectFactory<Creature>) Factories.PLAYER_FACTORY;
    }

    static ObjectFactory<Creature> boatFactory() {
        return (ObjectFactory<Creature>) Factories.BOAT_FACTORY;
    }

    MoveController newMoveController(T creature);
}
Имплементируешь генераторы под нужные объекты(родителей-держателей этих сервисов), которые будут использовать разный функциональный код.
Java:
class PlayerFactory implements ObjectFactory<Player> {
    static final ObjectFactory<? extends Creature> FACTORY = new PlayerFactory();

    private PlayerFactory() {}
   
    @Override
    public MoveController newMoveController(Player creature) {
        return new PlayerMoveController(owner);
    }
}
Где то у тебя в базовом объекте все это инициализируется через фабрику один раз. Там же создается геттер под нужный сервис.
Java:
public abstract class Creature extends GameObject {
    private final MoveController moveController;
   
    protected Creature(int objectId) {
        moveController = getFactory().newMoveController(this);
    }
   
    protected abstract ObjectFactory<Creature> getFactory();

    public final MoveController getMove() {
        return moveController;
    }

}

В объекте наследнике фабрика вернет соответствующий этому объекту генератор. И потом когда к обращаешься за нужным сервисом, переопределяешь возвращаемый объект и получаешь нужный класс:
Java:
public class Player extends Creature {

    @Override
    protected ObjectFactory<Creature> getFactory() {
        return ObjectFactory.playerFactory();
    }

    @Override
    public final PlayerMoveController getMove() {
        return (PlayerMoveController) super.getMove();
    }
}

Что это все дает:
Можно разделить функционально код, очень гранулярно разделить его на дерево сервисов и контроллеров, которые будут наследовать и переопределять код друг друга (например, CreatureService с общей логикой, наследованные от него PlayableService и NpcService с собственной) и уже эти компоненты определить нужному родителю (PlayableService - для Player и всех его наследников типа Summon, NpcService - для Npc)
Ну а дальше просто исходя из уровня фантазии кусками любого размера код можно выносить в компоненты.
Насчет производительности - естественно тут будут накладные расходы, тк компилятор не сможет сам определять когда какой объект будет возвращен и все это будет происходить в рантайме. Поэтому все пихать как угарелый в отдельные компоненты не надо, стоит быть рациональным и делать компоненту, которая будет часто переиспользоваться. Тогда вместо постоянного обращения к переменным и функциям через Creature, вся работа будет внутри компоненты PlayerMoveController, который мы получим всего один раз за действие. Это должно быть недорого относительно, остальной нагрузки.
 

Чтобы делать как ты хочешь - тебе придется знать в каждом конкретном методе какой у тебя будет объект и все это придется как-то менеджить. Поэтому все обычно и пихают в базовый класс который отвечает за функционал ( аля Creature / Character ), чтобы не ебаться с резолвингом кастов, и где какой объект должен был вернуться.
Но если тебя раздражает 10к строк - можешь вынести функциональный код в сервисы и компоненты. И тогда мувинг у тебя будет не в Creautre, а в MoveController (можно сделать его интерфейсом или абстрактным классом, если базовый мув контроллер один). И вот тут уже фабрика тебе поможет все дженерализировать (через дженерики или вайлдкард):
Описываешь интерфейс фабрики со всеми функциями-генераторами и фабрикой классов, которые будут наследовать / переопределять эти генераторы:
Java:
public interface ObjectFactory<T extends Creature> {
    static ObjectFactory<Creature> playerFactory() {
        return (ObjectFactory<Creature>) Factories.PLAYER_FACTORY;
    }

    static ObjectFactory<Creature> boatFactory() {
        return (ObjectFactory<Creature>) Factories.BOAT_FACTORY;
    }

    MoveController newMoveController(T creature);
}
Имплементируешь генераторы под нужные объекты(родителей-держателей этих сервисов), которые будут использовать разный функциональный код.
Java:
class PlayerFactory implements ObjectFactory<Player> {
    static final ObjectFactory<? extends Creature> FACTORY = new PlayerFactory();

    private PlayerFactory() {}
  
    @Override
    public MoveController newMoveController(Player creature) {
        return new PlayerMoveController(owner);
    }
}
Где то у тебя в базовом объекте все это инициализируется через фабрику один раз. Там же создается геттер под нужный сервис.
Java:
public abstract class Creature extends GameObject {
    private final MoveController moveController;
  
    protected Creature(int objectId) {
        moveController = getFactory().newMoveController(this);
    }
  
    protected abstract ObjectFactory<Creature> getFactory();

    public final MoveController getMove() {
        return moveController;
    }

}

В объекте наследнике фабрика вернет соответствующий этому объекту генератор. И потом когда к обращаешься за нужным сервисом, переопределяешь возвращаемый объект и получаешь нужный класс:
Java:
public class Player extends Creature {

    @Override
    protected ObjectFactory<Creature> getFactory() {
        return ObjectFactory.playerFactory();
    }

    @Override
    public final PlayerMoveController getMove() {
        return (PlayerMoveController) super.getMove();
    }
}

Что это все дает:
Можно разделить функционально код, очень гранулярно разделить его на дерево сервисов и контроллеров, которые будут наследовать и переопределять код друг друга (например, CreatureService с общей логикой, наследованные от него PlayableService и NpcService с собственной) и уже эти компоненты определить нужному родителю (PlayableService - для Player и всех его наследников типа Summon, NpcService - для Npc)
Ну а дальше просто исходя из уровня фантазии кусками любого размера код можно выносить в компоненты.
Насчет производительности - естественно тут будут накладные расходы, тк компилятор не сможет сам определять когда какой объект будет возвращен и все это будет происходить в рантайме. Поэтому все пихать как угарелый в отдельные компоненты не надо, стоит быть рациональным и делать компоненту, которая будет часто переиспользоваться. Тогда вместо постоянного обращения к переменным и функциям через Creature, вся работа будет внутри компоненты PlayerMoveController, который мы получим всего один раз за действие. Это должно быть недорого относительно, остальной нагрузки.
Круто, спасибо за развёрнутое пояснение.
Меня не столько даже раздражает код в 10к строк, хотя подобное и усложняет работу с классами, сколько я вижу в этом источник проблем, которые постепенно ведут к говну и палкам - это неизбежно в конечном итоге, когда у тебя в коде срач и не пойми что где валяется и как скомпоновано.
Я ушёл от мысли пихать всё в интерфейсы - слишком много лишних сущностей и гемора с ними. Пришёл примерно к той же мысли, что описана тобой, но гораздо примитивнее пока что - без генераторов) Я подумаю над прочитаным - мне нравится. Благодарю.
 
Назад
Сверху Снизу