Java: ограничение мутации объекта определенным методом - PullRequest
0 голосов
/ 06 сентября 2018

В настоящее время я пытаюсь создать библиотеку для передачи сообщений, и один из принципов передачи сообщений заключается в том, что изменяемое состояние изменяется только через сообщения. «Сообщения», которые будут переданы, являются функциональными объектами, которые принимают «отправителя» (который создал сообщение) и «получателя» (рабочий / субъект / что вы обрабатываете сообщение из его очереди.)

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

public interface Worker<T extends Worker<T>> {
    T getWorker(); //convert a Worker<T> to it's <T> since T extends Worker<T>
    <S extends Worker<S>> void message(Message<S,T> msg);
}

где <S> - тип работника, который отправил сообщение.

Сообщения определяются следующим образом:

public interface Message<S extends Worker<S>,R extends Worker<R>> {
    void process(S sender, R thisWorker);
}

где <S> - это тип класса отправителя, а <R> - это тип класса работника обработки. Как и ожидается интерфейсом и механизмом передачи сообщений, функция сможет изменять состояние thisWorker. Тем не менее, если бы сообщение было непосредственно изменено sender, возникло бы состояние гонки, потому что синхронизация между рабочими / потоками отсутствует (в этом и заключается весь смысл использования сообщений для начала!)

Хотя я мог бы объявить <S> родовым Worker<?>, это лишило бы способность сообщения «отвечать» содержательному ответу отправителю, поскольку оно больше не могло ссылаться на свои конкретные поля.

Следовательно, как я могу гарантировать, что sender не будет изменен, кроме как в контексте сообщения, адресованного специально ему?

T extends Worker<T> действительно используется для предотвращения возврата классом неработающего типа. Я мог бы просто сойти с рук Worker<T> и доверять пользователю.

Чего я конкретно пытаюсь добиться, так это системы передачи сообщений, в которой сообщения являются кодом, что позволило бы избежать необходимости какой-либо специальной обработки различных типов сообщений у каждого работника. Тем не менее, я, вероятно, понимаю, что не существует элегантного способа сделать это без соблюдения какого-либо протокола сообщений, если бы я хотел применить его, например, при выполнении на Swing EDT.

1 Ответ

0 голосов
/ 06 сентября 2018

Я не совсем понимаю ваш дизайн. Какова цель рекурсивных обобщений в определениях интерфейса, таких как interface Worker<T extends Worker<T>>? Разве не достаточно interface Worker<T>, все еще позволяя реализации T getWorker() получить конкретный тип класса, который реализует интерфейс Worker? Это попытка ограничить реализацию интерфейса от возврата чего-то, что не является Worker из getWorker()? В этом случае, я думаю, interface Worker<T extends Worker<?>> создаст меньше путаницы.

Следовательно, как я могу гарантировать, что sender не будет изменен, кроме как в контекст сообщения, адресованного специально ему?

Насколько мне известно, единственный способ предотвратить вызов методов объекта вне желаемого контекста - ограничить выполнение методов объекта одним потоком, который недоступен клиентскому коду. Для этого требуется, чтобы объект проверял текущий поток как самое первое в каждом отдельном методе. Ниже приведен простой пример. По сути, это стандартная реализация производителя / потребителя, в которой изменяемый объект ( не * Mailbox / потребительская реализация!) Предотвращает изменение себя в потоках, отличных от назначенный поток (тот, который потребляет входящие сообщения).

class Mailbox {

    private final BlockingQueue<Runnable> mQueue = new LinkedBlockingQueue<>();

    private final Thread mReceiverThread = new Thread(() -> {
        while (true) {
            try {
                Runnable job = mQueue.take();
                job.run();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

    public Mailbox() {
        mReceiverThread.start();
    }

    public void submitMsg(Runnable msg) {
        try {
            mQueue.put(msg);
        } catch (InterruptedException e) {
            // TODO exception handling; rethrow wrapped in unchecked exception here in order to allow for use of lambdas.
            throw new RuntimeException(e);
        }
    }

    public void ensureMailboxThread(Object target) {
        if (Thread.currentThread() != mReceiverThread) {
            throw new RuntimeException("operations on " + target + " are confined to thread " + mReceiverThread);
        }
    }
}

class A {

    // Example use
    public static void main(String[] args) {
        Mailbox a1Mailbox = new Mailbox();
        Mailbox a2Mailbox = new Mailbox();
        A a1 = new A(a1Mailbox);
        A a2 = new A(a2Mailbox);
        // Let's send something to a1 and have it send something to a2 if a certain condition is met.
        // Notice that there is no explicit sender:
        // If you wish to reply, you "hardcode" the reply to what you consider the sender in the Runnable's run().
        a1Mailbox.submitMsg(() -> {
            if (a1.calculateSomething() > 3.0) {
                a2Mailbox.submitMsg(() -> a2.doSomething());
            } else {
                a1.doSomething();
            }
        });
    }

    private final Mailbox mAssociatedMailbox;

    public A(Mailbox mailbox) {
        mAssociatedMailbox = mailbox;
    }

    public double calculateSomething() {
        mAssociatedMailbox.ensureMailboxThread(this);
        return 3 + .14;
    }

    public void doSomething() {
        mAssociatedMailbox.ensureMailboxThread(this);
        System.out.println("hello");
    }

}

Позвольте мне повторить выделенную часть: каждый отдельный класс, участвующий в передаче сообщений, должен проверять текущий поток в начале каждого отдельного метода. Нет способа извлечь эту проверку в класс / интерфейс общего назначения, так как метод объекта может быть вызван из любого потока. Например, Runnable, отправленный с использованием submitMsg в приведенном выше примере , может выбрать порождение нового потока и попытаться изменить объект в этом потоке:

a1Mailbox.submitMsg(() -> {
    Thread t  = new Thread(() -> a1.doSomething());
    t.start();
});

Однако это можно предотвратить, но только потому, что проверка является частью самого A.doSomething().

...