Пример 4.1. Определение конечной точки сервиса с помощью Java RMI
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface CustomerRemote extends Remote {
public Customer findCustomer(String id) throws RemoteException;
public Customer createCustomer(String firstname, String surname, String emailAddress)
throws RemoteException;
}
В данном интерфейсе findCustomer получает имя, фамилию и адрес электронной почты. А что произойдет, если будет принято решение разрешить объекту Customer также быть созданным лишь с адресом электронной почты? В данном случае мы без особого труда можем добавить следующий новый метод:
…
public Customer createCustomer(String emailAddress) throws RemoteException;
…
Проблема в том, что теперь необходимо заново создать также клиентские заглушки. Клиенты, желающие использовать новый метод, нуждаются в новых заглушках, и в зависимости от характера изменений в спецификации тем потребителям, которые не нуждаются в новом методе, также может потребоваться обновление их заглушек. Конечно, с этим можно справиться, но до определенного момента. Дело в том, что подобные изменения встречаются довольно часто. На поверку конечные точки RPC зачастую имеют множество методов для различных способов создания объектов или взаимодействия с ними. Отчасти это связано с тем, что мы до сих пор думаем об этих удаленных вызовах как о локальных.
Но есть еще одна разновидность хрупкости. Посмотрим, на что похож наш объект Customer:
public class Customer implements Serializable {
private String firstName;
private String surname;
private String emailAddress;
private String age;
}
Что, если теперь выяснится, что, несмотря на выставление на всеобщее обозрение в наших объектах Customer поля возраста age, никто из потребителей им не пользуется? Мы решим, что поле нужно удалить. Но если серверная реализация удаляет поле age из своего определения данного типа, а мы не сделаем того же самого для всех потребителей, даже если они никогда не воспользуются этим полем, код, связанный с десериализацией объекта Customer на стороне потребителя, будет поврежден. Чтобы отменить это изменение, я буду вынужден одновременно развернуть как новый сервер, так и новых клиентов. В этом и состоит основная проблема с использованием любого RPC-механизма, продвигающего создание двоичной заглушки: вы не получаете возможности отдельных развертываний клиента и сервера. При использовании данной технологии в будущем вас ожидают такие вот одновременные выпуски с блокировкой всей работы.
Такая же проблема возникнет при желании реструктурировать объект Customer, даже если я не стану удалять какие-либо поля, например, если мне захочется для упрощения управления заключить firstName и surname в новый поименованный тип. Разумеется, я могу справиться с этим путем повсеместной передачи словарных типов в качестве параметров своего вызова, но тогда мне придется расстаться со многими преимуществами создания заглушек, поскольку мне все равно придется вручную искать соответствия и извлекать нужные поля.
На практике объекты, используемые как часть двоичной сериализации отправляемых по сети данных, могут рассматриваться как типы, предназначенные только для раскрытия. Такая хрупкость приводит к типам, раскрываемым для всех, кто находится в сети, и превращаемым во множество полей, часть из которых больше не используются, но не могут быть безопасно удалены.
Несмотря на все недостатки RPC, я не стану сгущать краски и называть его страшным. Некоторые из наиболее распространенных реализаций, с которыми мне приходилось сталкиваться, могли приводить к возникновению тех проблем, которые здесь уже были очерчены. Из-за сложностей использования RMI я, конечно же, постарался бы обойтись без этой технологии. В модель на основе RPC неплохо вписываются многие операции, а более современные механизмы, такие как Protocol Buffers или Thrift, сглаживают некоторые из прошлых грехов, исключая необходимость одновременных выпусков кода клиента и сервера с блокировкой всей работы.
Собираясь остановить свой выбор на этой модели, вы должны быть в курсе всех потенциально возможных подводных камней, связанных с использованием RPC. Не нужно доводить свои удаленные вызовы до такого состояния, при котором использование сети полностью скрыто и следует обеспечить возможность такого развития серверного интерфейса, которое исключало бы необходимость настоятельного требования обновления кода клиентов в режиме блокировки всей работы. К примеру, очень важно выдерживать правильный баланс клиентского кода. Нужно гарантировать, что клиенты не будут обращать никакого внимания на тот факт, что будет производиться вызов по сети. В контексте RPC часто используются клиентские библиотеки, и при неправильной структуризации они могут вызвать ряд проблем. Более подробно данный вопрос будет рассмотрен чуть позже.