Рубрики
Без рубрики

Функциональные перечисления в Java

Рассмотрим использование составных, вызываемых перечислений для решения старой проблемы в удобочитаемом и кратком виде на Java. Помеченный как java, функциональный, эмуляция, tdd.

Пересказ моего GitHub.io пост._

Вступление

Самый крупный личный проект, которым я сейчас занимаюсь, – это эмулятор ( Emu Roxy ). Главная цель состояла в том, чтобы создать более крупный продукт, который я полностью контролирую и могу экспериментировать с инструментами и методами, чтобы стать лучшим программистом. Подцели включают в себя изучение чего-то о машинах, на которых я вырос, и, возможно, возможно, дойду до того, что я написал свой собственный ретро-эмулятор NES. В долгосрочной перспективе цель pie in the sky состояла в том, чтобы создать хорошо продуманный, хорошо протестированный, доступный, подключаемый мульти-эмулятор с открытым исходным кодом.

Однако в первую очередь я хотел сосредоточиться на том, что, по моему мнению, важно в хорошей Java-программе:

  • Тщательно проверенный
  • Читаемый
  • Эффективный
  • Ремонтопригодный/Расширяемый

Удовлетворяя всему вышесказанному, ИМХО, я бы сказал, что в целом он был хорошо разработан.

Итак, я начал с наиболее документированной части NES. Процессор MOS 6502 . О чем я хочу поговорить сегодня, так это о том, как я эмулировал коды операций и как мой подход дал мне хороший путь к обновлению, когда я понял, что моя реализация не так хороша, как могла бы быть. В качестве примечания я расскажу, почему TDD – если вы правильно его понимаете и используете – очень полезный метод в разработке Java.

Подход № 1: Результат разработки, основанной на тестировании (TDD)

Один из подходов, с которым я играл, – это TDD. Где я пишу неудачный тест, затем пишу код, чтобы заставить его работать, и итеративно создаю полностью протестированный модульный продукт. Это привело меня к написанию минимального объема кода, чтобы тесты работали. Результатом был отличный процесс и… это:

    public void step() {
        log.debug("STEP >>>");

        final OpCode opCode = OpCode.from(nextProgramByte().getRawValue());

        //Execute the opcode
        log.debug("Instruction: {}...", opCode.getOpCodeName());
        switch (opCode){
            case ASL_A:
                withRegister(Register.ACCUMULATOR, this::performASL);
            break;

            case ASL_Z:
                withByteAt(RoxWord.from(nextProgramByte()), this::performASL);
            break;

            case ASL_Z_IX:
                withByteXIndexedAt(RoxWord.from(nextProgramByte()), this::performASL);
            break;

            case ASL_ABS_IX:
                withByteXIndexedAt(nextProgramWord(), this::performASL);
            break;

            case ASL_ABS:
                withByteAt(nextProgramWord(), this::performASL);
            break;

            case LSR_A:
                withRegister(Register.ACCUMULATOR, this::performLSR);
            break;

            case LSR_Z:
                withByteAt(RoxWord.from(nextProgramByte()), this::performLSR);
            break;

            case LSR_Z_IX:
                withByteXIndexedAt(RoxWord.from(nextProgramByte()), this::performLSR);
            break;

            case LSR_ABS:
                withByteAt(nextProgramWord(), this::performLSR);
            break;

            case LSR_ABS_IX:
                withByteXIndexedAt(nextProgramWord(), this::performLSR);
            break;

            ...

Это, конечно, усечено, так как на 6502 гораздо больше кодов операций. Соответствует ли это моим критериям для хорошо разработанной Java-программы?

Хорошо протестировано : Покрытие линии составляет около 98-99%. Статический анализ. Мутация проверена на 98%

Читаемый : Частично. У каждого кода операции есть путь. Инструкции по его выполнению даны на английском языке. Немного раздражает, что вам нужно так много прокручивать, а сложные инструкции в конечном итоге оказываются запутанными для вложенных вызовов методов, чтобы они читались плавно.

Эффективный : Для большого числа случаев операторы переключения довольно эффективны.

Ремонтопригодный : Нет. Существует множество методов, позволяющих уменьшить дублирование, которые в конечном итоге сами становятся дублированием. Части логики не разделимы, как адресация и операции.

Расширяемый : Давайте посмотрим…

Поэтому я хотел посмотреть, смогу ли я добиться большего.

Наследие и красота TDD

Это привело к довольно сложному в обслуживании коду, но правильному; Благодаря нашим тестам, быстро и точно проверяемому правильному. Это означает, что мы можем возиться с другими реализациями, и огромное пространство для тестирования теоретически позволит выявить любые ошибки за очень короткий цикл разработки.

Подход № 2: Функциональный Java-код

Я хочу свести к минимуму дублирование, написав код один раз для каждой операции и один раз для каждого режима адресации. Поскольку мы уже ссылаемся на коды операций по перечислению, я подумал, можно ли проанализировать значение кода операции ( 0x0A ) в его перечисление ( ASL_A ), а затем просто вызвать выполнить () метод в этом экземпляре перечисления, который запускает подключенный лямбда-код, предоставляющий среду (память, регистры и alu). Поскольку каждое перечисление кода операции знает свой собственный режим адресации, оно может вызывать address() в этом режиме адресации (который также вызывает подключенную лямбду), предоставляя нам все необходимое для создания составных инструкций.

Обзор

Таким образом, myenum становится определением, содержащим байтовое значение, его режим адресации и выполняемую операцию. Таким образом, код операции является пересечением режима адресации и операции

ASL_A(0x0A, AddressingMode.ACCUMULATOR, Operation.ASL);  

Аргумент операции – это операция над средой (a: накопитель, r: регистры, m: память), заданное адресное значение (v):

    public enum Operation implements AddressedValueInstruction {
        /** Shift all bits in byte left by one place, setting flags based on the result */
        ASL((a,r,m,v) -> {
            final RoxByte newValue = a.asl(v);
            r.setFlagsBasedOn(newValue);
            return newValue;
        }),

        ...

        @Override
        public RoxByte perform(Mos6502Alu alu, Registers registers, Memory memory, RoxByte value) {
            return instruction.perform(alu, registers, memory, value);
        }

Режим адресации использует среду (r: регистры, m: память, a: накопитель) для адресации значения, выполняет над ним заданную операцию (i) и помещает возвращаемое значение обратно в адресное местоположение:

public enum AddressingMode implements Addressable {
    /** Expects no argument, operation will be performed using the Accumulator Register*/
    ACCUMULATOR("Accumulator", 1, (r, m, a, i) -> {
        final RoxByte value = r.getRegister(Registers.Register.ACCUMULATOR);
        r.setRegister(Registers.Register.ACCUMULATOR, i.perform(a, r, m, value));
    }),

    ...

    @Override
    public void address(Registers r, Memory m, Mos6502Alu alu, AddressedValueInstruction instruction) {
        address.address(r, m, alu, instruction);
    }

Затем операционный код объединяет все это вместе, комбинируя режим адресации и операции:

public enum OpCode implements Instruction {

    ...

    @Override
    public void perform(Mos6502Alu alu, Registers registers, Memory memory) {
        addressingMode.address(registers, memory, alu, operation::perform);
    }

Это означает, что огромная инструкция switch становится одной строкой кода

opCode.perform(alu, registers, memory);

Проблемы

Есть некоторые инструкции, которые не очень хорошо соответствуют этому шаблону:

Режим адресации. подразумеваемый : Режим адресации подразумевается последними двумя битами байта инструкции Подразумеваемые инструкции выполняют несколько иную адресацию в зависимости от операции. Мне нужно будет разобраться с ними как с отдельными случаями, когда операции выполняют свою собственную адресацию.

Операция. JMP : Он – в отличие от всех других инструкций – передает слово (не байт) из режима адресации, который может быть режимом адресации. КОСВЕННЫЙ Они делают две вещи, из-за которых с ними трудно иметь дело. Во-первых, их адресация является АБСОЛЮТНОЙ; двухбайтовый (словесный) адрес, который Операция должна использовать для загрузки в Программный счетчик. Мы могли бы справиться с этим так же, как и с ПОДРАЗУМЕВАЕМЫМ (в том смысле, что операция затем выполняет некоторую собственную адресацию), если бы не случай, когда JMP использует КОСВЕННУЮ АБСОЛЮТНУЮ адресацию, которая будет принимать двухбайтовый аргумент, а затем с адреса, указанного этим словом, загружает двухбайтовый адрес в программный счетчик. В этом случае это невозможно сделать во время операции, потому что она понятия не имеет, что такое режим адресации.

Решение

Так что в итоге у нас получается что-то вроде гибрида:

        final Mos6502OpCode opCode = Mos6502OpCode.from(nextProgramByte().getRawValue());

        //Execute the opcode
        log.debug("Instruction: {}...", opCode.getOpCodeName());
        switch (opCode){
            case JMP_ABS:
                registers.setPC(nextProgramWord());
            break;

            case JMP_IND:
                registers.setPC(getWordOfMemoryAt(nextProgramWord()));
            break;

            default:
                opCode.perform(alu, registers, memory);
                break;
        }

Крайние случаи убеждают, что это не самое красивое, но намного приятнее, чем было, и когда я думаю о следующей итерации, чтобы упростить это, я знаю, что мои тесты специфичны, но достаточно универсальны, чтобы поддерживать рефакторинг и редизайн.

Оригинал: “https://dev.to/rossdrew/functional-enums-in-java-34o1”