Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

пятница, 8 марта 2013 г.

Бомбермен: немодифицируемый но синхронный список бомб

Самый главный объект в Bomberman - это доска, на которой происходит вся игра. Доска имеет ряд public методов, один из которых - дай мне все бомбы на поле. В первой версии он был простой как двери и потому небезопсный.
    public List<Bomb> getBombs() {
        return bombs;
    }
Вернуть копию списка - не вариант, поскольку, получив список на руки, я как клиент смогу воздействовать на бомбы непосредственно, меняя ход истории.
    public List<Bomb> getBombs() {
        return ListUtils.unmodifiableList(bombs); // из apache commons collections
    }
Тогда стоит итерироваться по всему списку и пересоздавать все бомбочки
    public List<Bomb> getBombs() {
        List<Bomb> result = new LinkedList<Bomb>();
        for (Bomb bomb : super.getBombs()) {
            result.add(new Bomb(bomb));
        }
        return result;
    }
Но тут другая задача. Те бомбы, что я скопирую рассинхронизированы с игрой - у них таймер замер сразу после копирования.
    public Bomb(Bomb bomb) {
        this.power = bomb.power;
        this.x = bomb.x;
        this.y = bomb.y;
        this.timer = bomb.timer;
        this.affect = null; // бомба - муляж
    }
Копирующий конструктор мне в помощь! Он кроме того, что копирует необходимые свойства, еще и подписывает копии бомб на события, которые получает оригинальная бомба. Для этого был создан наследник, чтобы не влазить в работающий Bomb.
public class BombCopier extends Bomb {

    private List<Tickable> copies;

    public BombCopier(int x, int y, int power) {
        super(x, y, power);
        copies= new LinkedList<Tickable>();
    }

    public BombCopier(Bomb bomb) {
        this(bomb.x, bomb.y, bomb.power);
        this.affect = bomb.affect;
        this.timer = bomb.timer;

        if (bomb instanceof BombCopier) {
            BombCopier copier = (BombCopier)bomb;
            copier.copies.add(this);
            this.affect = null; // бомба - муляж
        }
    }

    public void tick() {
        for (Tickable bomb : copies) {
            bomb.tick();
        }
        super.tick();
    }
}
Теперь еще усложним себе жизнь!

Допустим, синхронными должны быть не только таймеры в бомбах, но и сам список бомб, который вернули из board. Дело в том, что спустя несколько тактов могу на руках иметь список, якобы содержащий бомбы, но на карте уже будет совсем другая история.

Любопытно, как это у меня получится!

Да, совсем забыл, я тут усложняю специально. На самом деле никому из клиентов игры (а это я сам) не нужен немодифицируемый список, и все то лишнее что я тут пишу не привносит пользы, кроме как одной - я получаю опыт, мне интересно и я экспериментирую со сложностями. 

Итак, мне надо вернуть List но необычный, а синхронный с оригинальным. Хехех :) Но в начале тест, которй скажет что все ок, когда я это сделаю...
    @Test
    public void shouldReturnShouldSynchronizedBombsList_whenUseBoardApi() {
        bomberman.bomb();
        bomberman.right();
        board.tact();

        List<Bomb> bombs1 = board.getBombs();
        assertEquals(1, bombs1.size());

        board.tact();
        board.tact();
        board.tact();
        board.tact();

        List<Bomb> bombs2 = board.getBombs();
        assertEquals(0, bombs2.size());

        assertEquals(0, bombs1.size());
    }
А вот и реализация
    public List<Bomb> getBombs() {
        return ListUtils.getUnmodifiableList(new ListUtils.ListFactory() {
            @Override
            public List create() {
                List<Bomb> result = new LinkedList<Bomb>();
                for (Bomb bomb : getSuperBombs()) {
                    result.add(new BombCopier(bomb));
                }
                return result;
            }
        });
    }
При этом пришлось откопать свои старые залежи и немного порефлексировать с ними, расширив ProxyFactory, который я давным давно писал. В результате теперь я имею возможность делать так
public class ListUtils {

    public interface ListFactory {
        Object create();
    }

    public static List<Bomb> getUnmodifiableList(final ListFactory factory) {
        InvocationHandler handler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Object result = factory.create();
                Object invoke = method(method.getName()).withParameterTypes(method.getParameterTypes()).in(result).invoke(args);
                return ProxyFactory.resultBuilder().dontCallRealMethod().returns(invoke).get();
            }
        };
        return ProxyFactory.object(factory.create()).spy(handler).getAs(List.class);
    }
}
Короче поигрался в свое удовольствие. Старая версия описана тут, а новую версию ProxyFactory можно скачать отсюда (а вот и репозиторий).

Суть нового функционала в том, что я могу взять любой объект и встроить в него шпиона, который будет делать полезные для меня действия. AOP только без спринга. Напомню критикам, что я всего лишь играюсь в код... Не воспринимайте это серьензо.
    interface Some {
        String method(String input);
    }

    class SomeImpl implements Some {
        public String method(String input) {
            return "SomeImpl say: " + input;
        }

        public String method2(String input) {
            return "SomeImpl say from another method: " + input;
        }
    }

    @Test
    public void shouldCanUseAfterRealMethod() throws Throwable {
        final ProxyFactory.After after = new ProxyFactory.After() {
            public Object doit(Object result) {
                return result + "_zxc";
            }
        };

        InvocationHandler before = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return ProxyFactory.resultBuilder().with("asd_" + args[0]).doAfter(after).get();
            }
        };

        Some impl2 = ProxyFactory.object(impl).spy(before).getAs(Some.class);

        assertEquals("SomeImpl say: asd_qwe_zxc", impl2.method("qwe"));
    }
Тут ProxyFactory.resultBuilder() можно использоваться несколькими способами.
ProxyFactory.resultBuilder().with("new args").returns("new answer").get()
сразу после handler будет вызван исходный метод объекта но с новым аргументом "new args", а после его выполнения результат подменится на "new answer".
ProxyFactory.resultBuilder().get()
Ничего не изменился - сразу после отработки handler будет вызван исходный метод с исходными артументами, а его результат вернется клиенту.
ProxyFactory.resultBuilder().call("method2").with("new args").get()
Сразу после handler будет вызван другой (method2) метод объекта, за которыми шпионим, с другими аргмуентами "new args", а результат выполнения уйдет клиенту.
ProxyFactory.resultBuilder().call("method2").get()
Сразу после handler будет вызван другой (method2) метод объекта, за которыми шпионим, с другими исходными аргументами, а результат выполнения уйдет клиенту.
ProxyFactory.resultBuilder().returns("new answer").get()
Ничего не трогаем, переопределяем только результат "new answer"
ProxyFactory.resultBuilder().dontCallRealMethod().returns("new answer").get()
Говорим, чтобы выполнялся только handler и клиенту подсунем новый результат "new answer"
ProxyFactory.resultBuilder().after(new ProxyFactory.After() {
        public Object doit(Object result) {
            return result; // do smth
        }
    }).get() 
Говорим, что после выполнения исходного метода надо вызвать наш Answer.doit()
Как-то так.

Может кому пригодится...

Комментариев нет:

Отправить комментарий