| Попытка не пытка: проект Охламон |
[Окт. 13, 2009|03:11 am] |
Смотрите: попробуем добавить алгебраические типы данных (или union'ы) в Java'у.
Есть готовый компилятор.
Я не знаю зачем и кому это нужно
Данные, с которыми мы
работаем — они многообразны. Допустим integer мы знаем как облупленного, а иной абстрактный интерфейс можем использовать
без малейшей мысли, какие у него могут быть реализации, что они на самом деле
делают и сколько их. А бывает так: было нам до поры не важно, что это за тип
данных, а потом уточнить понадобилось. Вот, например, шахматная фигура: позицию
ее узнали, цвет определили, ходить пора — осталось выяснить, пешка это (будем
политкорректны) или ладья? Ну т.е. мы заранее знаем все возможные варианты, но на run-time'е надо как-то определить, какой именно вариант и взять его (т.е. убедить компилятор, что
там правда была ладья, он же не соглашается).
Что нам тут может
предложить Java? Два основных решения:
- при детях (т.е. на лекциях) брать такие примеры,
где подходит виртуальный метод (а пусть фигура сегодня сама походит?) и один раз
рассказать про паттерн Visitor (а давайте сегодня просто так удлиним программу?);
- не при детях — cast'ы, cast'ы, cast'ы.
Ну и тут как бы получается, что есть еще третье решение: добавить в язык поддержку алгебраических типов.
( Хозяйке на заметку. )
Меньше слов
Ну я и попробовал это
сделать: добавить алгебраические типы в Java'у. Оказалось, это совсем несложно, и оказалось,
что это даже работает. Смотрите, пробуйте, кому интересно. Лежит здесь:
http://code.google.com/p/ohl проект «Охламон».
Язык расширен, все старые
программы по-прежнему собираются. Реализовано все на базе Eclipse'а, а точнее это
просто Eclipse со сломаным компилятором. Но Eclipse не прилагается, качайте сами; работает с 3.4.1 и 3.5.0
(у меня работает). Можно писать программы, собирать и запускать; JVM стандартный, от 5-й версии и выше.
Добавляет switch по подтипам, все статически проверяется, в том числе
и полнота перебора (что вы не забыли вариант).
Буду очень рад любым
комментариям. Ну и вопросам конечно. Намеренно не рассказываю, как это работает. По-моему, интересная тема.
Больше слов
Давайте сразу игрушечный компилятор
напишем. Начнем с lexer'а. Что возвращает его метод peek()? Новый тип. Не простой, алгебраический. Тут он
называется enum-case (исторически
сложилось). Надо написать так:
public enum-case Tokens {
case plus(),
case minus(),
case paren_open(),
case paren_close(),
case literal(int num),
case identifier(String name)
}
(см. Lexer.java.)
Это мы перечислили токены.
Хорошо видно, что некоторые токены имеют данные, а у некоторых данных нет.
Теперь у нас есть новый
тип. Его имя пишется так: "Tokens.case". Например, создадим список токенов:
List<Tokens.case> tokenList = new ArrayList<Tokens.case>();
tokenList.add(new Tokens.literal(2));
tokenList.add(Tokens.plus); // этот типа singleton был
tokenList.add(new Tokens.literal(2));
Попробуем разобрать выражение:
private AstNode parseTerminal() throws ParserException {
switch (lexer.peek()) {
case * paren_open() {
lexer.consume();
AstNode inner = parseExpression();
if (lexer.peek() != Lexer.Tokens.paren_close) {
throw new ParserException("')' expected");
}
lexer.consume();
return inner;
}
case * literal(final int value) {
lexer.consume();
return new AstConstant() {...};
}
default * {
throw new ParserException("integer or '(' expected");
}
}
}
(См. Parser.java.)
Вроде на первый взгляд получается. Надеюсь, тут все видно. А ведь есть еще множество других интересных
фич: наследование enum-case'ов, generic'и, user type case'ы и т.д. См. Wiki. |
|
|
| C++: как яблоку ссылаться на яблоню |
[Авг. 11, 2009|07:49 am] |
Пусть один класс начинает брать на себя слишком много обязанностей.
class ArtLebedev : public DesignStudio, public BookPublisher {
int money;
Fun fun;
};
Где как, а в ОО рецепт на все один: новый класс. Постараемся, однако, не делать резких движений и на первое время сохранить близкий контакт с отпочковавшимся классом. Оформим его как вложенный и создадим поле этого типа в главном классе.
class ArtLebedev : public DesignStudio {
int money;
Fun fun;
class _PocketPublisher : public BookPublisher {
} publisher;
};
Заметим, что расположение данных в памяти при этом изменилось не сильно. Было [базовый DesignStudio, базовый BookPublisher, money, fun]. Стало [базовый DesignStrudio, money, fun, базовый BookPublisher]. Одно и то же, только порядок разный.
Однако, мы потеряли одно важное свойство. Экземпляр класса PocketPublisher больше не имеет доступа к полям money и fun главного класса.
Приходится внутри класса PocketPublihser заводить дополнительное поле, чтобы хранить в нем указатель. Это, однако, довольно глупо выглядит: в одной ячейке памяти приходится хранить адрес соседней ячейки, хотя вот же она!
Вместо этого, воспользуемся тем, что из адреса главного класса (ArtLebedev) всегда можно получить адрес вспомогательного (PocketPublisher). Значит и наоборот. Для этого нам понадобится инструмент:
template<typename HOST, typename MEMBER>
HOST* cast_to_host(MEMBER* member, MEMBER HOST::* field) {
HOST* result = reinterpret_cast<HOST*>(reinterpret_cast<char*>(member) -
reinterpret_cast<size_t>(reinterpret_cast<char*>(&(reinterpret_cast<HOST*>(0)->*field)))
);
// assert(&(result->*field) == member);
return result;
}
Зная наш собственный адрес field и название поля, в котором нас хранят member, можно получить адрес главного класса. Эта солидной длины фунцкия на самом деле всего лишь длинно записанное вычитание. Наверное, ради скорости ее имеет смысл держать inline. Теперь мы можем вполне безопасно обращаться к главному классу из вспомогательного:
void ArtLebedev::_PocketPublisher::publish_good_book() {
ArtLebedev* host = cast_to_host(this, &ArtLebedev::publisher);
host->money -= 1000;
host->fun++;
}
Функция cast_to_host статически типизирована и вполне безопасна. Можно ли сломать программу с ее помощью? Да, есть один способ: как мы видим из примера, каждый экземпляр думает, что он – поле под названием ArtLebedev::publisher; и хотя компилятор проверит соответствие типов поля и основного класса, в главном его можно обмануть: создать второе поле такого же класса; или локальную переменную; или выделить в куче.
Чтобы хоть немного подчеркнуть, что класс _PocketePublisher может быть только полем publisher, мы используем подчеркивание (оно уродует называние) и объявляем поле сразу вместе с классом. |
|
|
| Visitor 2: перезагрузка исключений |
[Авг. 16, 2008|03:24 am] |
Это опять про языки будет, осторожно.
Как об этом говорилось в предыдущих сериях, переход от базового класса к наследникам – вещь часто необходимая. Возьмем пример: базовый класс Animal и 3 наследника – Dog, Cat и Capybara. Пока мы этих животных, например, считаем, нам совершенно достаточно базового типа Animal. Теперь, если нужно получить текстовое описание зверя, без уточнения конкретного типа не обойтись. Хорошо, если в Animal можно добавить виртуальный метод: getTextDescription(). Тогда язык просто потребует реализовать его в каждом подклассе. Это просто и неинтересно.
Ладно, пусть теперь добавить виртуальный метод в Animal нельзя, и строить описание для каждого из животных должен внешний код. Естественно (естественно!), хочется, чтобы компилятор вынуждал нас обрабатывать всех трех животных, а потом не забывать и про других, если к ним добавится, например, попугай. Это был ответ, почему нельзя сделать на instanceof-ах.
Из предыдущих серий опять же было известно, что шаблон GoF под названием Visitor решает эту проблему (т.е. стоит вам добавить тип Parrot, как программа перестает компилироваться до тех пор, пока обработчики для него не будут добавлены всюду). Пользоваться им ясное дело неудобно.
Как было бы удобно?
Добавить в язык понятие списка типов. Т.е. буквально, что метод может вернуть значение одного из перечисленных типов. Ни больше, ни меньше. Например:
public [Dog, Cat, Capybara] getCurrentAnimal(); Соответственно, иметь switch, который бы позволял перебирать эти типы:
switch (pet: getCurrentAnimal()) {
case Cat:
return "кошка цвета "+pet.getColor();
case Dog:
return "собака породы "+pet.getBreed();
case Capybara:
return pet.getWeightKg()+"-килограммовая капибара";
} В этом фантастическом фрагменте переменная pet имеет разный тип в разных ветках оператора switch. Естественно, если список типов расширится, программа должна перестать собираться в этом месте до тех пор, пока новый тип также не будет обработан.
Понятно, что часто такой метод удобно добавить прямо в базовый класс:
interface Animal {
// blah-blah-blah
[Dog, Cat, Capybara] getSubtyped();
} Теперь посмотрим, как сказку можно воплотить в жизнь хотя и несколько анекдотичным образом. Дело в том, что Java в каком-то смысле действительно поддерживает такую вещь как «список типов». Надо только найти где.
Итак, если пожертвовать эффективностью, то вот как пишется этот волшебный метод и чудесный switch (только не смейтесь).
interface Animal {
void getSubtyped() throws DogEx, CatEx, CapybaraEx;
}
interface PresentAdviser {
void getHomePet() throws DogEx, CatEx;
}
public String getAnimalDescription(Animal animal) {
try {
animal.getSubtype();
throw new RuntimeException("Unreachable point");
} catch (CatEx e) {
return "кошка цвета "+ e.getCat().getColor();
} catch (DogEx e) {
return "собака породы "+ e.getDog().getBreed();
} catch (CapybaraEx e) {
return e.getCapybara().getWeightKg()+"-килограммовая капибара";
}
} Может возникнуть вопрос, что тут за классы CatEx, DogEx и CapybaraEx? Все уже видно: это checked-исключения (наследующие java.lang.Exception), состоящие, очевидно, из getter’а, поля и конструктора.
Как выглядит реализация метода getSubtyped, например, в классе Dog? Просто:
public void getSubtyped() throws DogEx {
throw new DogEx(this);
} Вот еще одно доказательство, что Java поддерживает все, что нам нужно: если вызывать метод PresentAdviser#getHomePet() и сделать обработчик для капибары, компилятор выдаст ошибку: этот метод не «возращает» такой тип.
Если серьезно, то вот минусы такого решения:
- нужно заводить оберточные классы-исключения на каждый тип (т.к. исключения не могут быть generic’ами)
- скорость работы будет низкая, потому что исключения долго конструируются
- кидать “unreachable point” исключение после вызова раздражает
- это решение не совместимо с generic’ами, т.е. список типов нельзя параметризовать.
А так все нормально. |
|
|
| Дополнительная фича для Java 5.0 |
[Авг. 22, 2006|10:06 pm] |
Этот пост - маленькое упражнение на тему языков программирования. В отличии от предыдущих постов, речь в нем идет о том, чего нет.
При использовании generic'ов - новой фичи Java 5.0, - я несколько раз столкнулся с ограничением, которое мне не понравилось.
Например, нужно было сделать простую структуру, элементы которой образовывали бы дерево:
interface Node {
java.util.Enumeration<Node> children();
Node getParent();
}
Сделав первый шаг, я собирался расширить интерфейсы, введя два вида node'ов: RedNode и BlackNode.
interface RedNode extends Node {
BlackNode getParent();
java.util.Enumeration<BlackNode> children();
}
interface BlackNode extends Node {
}
Первый тип, RedNode, должен отражать некоторые ограничения на связи между элементами. Его дети и родитель всегда определенного типа.
К сожалению, написанный код не компилируется. Проблемы с методом getParent не возникает - начиная с версии 5.0 в Java можно уточнять тип возвращаемого значения при перекрытии метода (называется "covariant return type").
Но с типом Enumeration<Node> в методе children это, к сожалению, не пройдет.
Проблема состоит в том, что Enumeration<BlackNode> нельзя неявно приводить к типу Enumeration<Node>. Хотя по смыслу это очень естественная операция.
Удивительно, но я уже добрался до темы этого поста.
Доступное и сторгое решение состоит в том, чтобы заменить всюду типы следующим образом: Enumeration<A> --> Enumeration<? extends A>.
Это будет компилироваться и правильно работать. Но, к сожалению, заменять придется действительно всюду. И это - слишком длинно.
Фича, о которой я думаю - в том, чтобы это автоматизировать. Я представляю ее себе так.
interface Enumeration<T-> {
boolean hasNext();
T nextElement();
}
Дополнительный знак "-" - условный синтаксис, обозначающий, что данный параметр generic'а должен всюду трактоваться как "? extends T".
Т.е., там где написано Enumeration<String>, на самом деле должно читаться Enumeration<? extends String>. Тогда преобразование Enumeration<String> --> Enumeration<Object> становится совершенно законным по стандарту Java 5.0.
Можно заметить интересное следствие из этого правила: тип-параметр, который был определен с модификатором "-", не может быть использован в качестве типа аргумента метода. Таким образом мы получаем дополнительную семантику: тип, который можно считывать, но нельзя записывать в рамках данного интерфейса.
Пример:
interface List<T> extends RoList<T>, WoList<T> {
T get(int pos);
void add(T element);
}
/** i.e. read-only */
interface RoList<T-> {
T get(int pos);
void inaccessibleAdd(T element);
}
/** i.e. write-only */
interface WoList<T+> {
void add(T element);
}
List<String> невозможно привести к типу List<Object> (что очень хорошо, иначе в него можно будет добавлять объекты, и совсем не String'и).
Но законно преобразование List<String> --> RoList<Object>, т.е. для чтения можно считать, что там лежат просто Object'ы.
Метод с названием inaccessibleAdd из интерфейса RoList действительно вызывать нельзя, ведь на самом деле (по правилу) тип такой: RoList<? extends Object>. Единственное значение, которое можно передать в этот метод - null.
Кроме того, законно и преобразование в другую сторону. Для этого введем второй модификатор "+": любое упоминание типа WoList<А> должно читаться как WoList<? super A>.
Тогда List<Object> --> WoList<String>. Или List<Object> --> WoList<Integer>. Это может быть нужно для методов, которые хотят заполнять списки, но не читать их.
Стоит заметить, что такое расширение синтаксиса позволяет заодно ввести поддержку read-only интерфейсов в библиотеку java.util и вообще в язык.
Если честно, все это выглядит достаточно просто и очевидно. Признаться, я удивлен, почему этого сделано не было и, насколько я понимаю, не планируется.
update: Мне разъяснили термины: указание параметра-типа как "? extends Type" называется use-site variance, а то, чего мне не хватает, называется declaration-site variance. Соответственно, Java 5.0 поддерживает первое и не поддерживает второе.
|
|
|
| Хаки: делайте для них интерфейсы |
[Май. 23, 2006|01:43 pm] |
Собственно, больше можно уже ничего не писать.
Но так как чем длиннее пост, тем лучше, постараюсь раскрыть тему. Хак в программе разрушает ее логичность и универсальность. Но, что делать, их все равно (иногда) создают. В то время, как хаков конечно надо стыдиться и всеми силами души стремиться избавиться от них, совсем не стоит их скрывать.
Допустим, есть интерфейс к подсистеме, но вас он временно не устраивает. Можно конечно начать обращаться к подсистеме в обход этого интерфейса, но я бы просто добавил в API новый метод. С префиксом или суфиксом “hack” в имени. Теперь мы никого не обманываем, интерфейс точно отражает реальную ситуацию. И значит, можно будет легко найти все точки, где программа использует хаки, когда мы наконец до них доберемся.
Противопоказания.
Конечно, если это API публичное, может и не стоит поступать так эксцентрично; но ведь большинство интерфейсов внутренние. Если в существующем API и так с трудом можно разобраться, например, оно сводится к морю int-овых констант, то добавлять туда что-нибудь новое, тем более временное я бы побоялся. |
|
|
| Объекты-сессии, как я их называю |
[Май. 15, 2006|11:13 pm] |
Бывают разные объекты. Но предполагается, что все они должны жить ровно столько времени, сколько нужны их данные. (Пока, конечно, речь не пойдет об уловках вроде экономии на времени работы конструкторов и garbage collector’а — создании pool'ов.)
Некоторые данные нужны только на время работы какого-нибудь метода. Если метод простой, то они будут храниться в его локальных переменных. Но если он вызывает другие методы, используются рекурсивные вызовы, а данных много, можно завести специальный объект, который будет существовать только во время выполнения этой сложной операции. Например, такой внутренний класс:
class FlowerSomething {
public List<Flower> pickBestFlowers() {
PickingStruct pickingStruct = new PickingStruct();
...
return pickingStruct.result;
}
private void pickAtATrot(TypeA typeA, PickingStruct pickingStruct) {
...
}
private void pickAtAGallop(TypeB typeB, PickingStruct pickingStruct) {
...
}
private static class PickingStruct {
final ArrayList<Flower> result = new ArrayList<Flower>();
final Map<This,That> mainMap = new HashMap<This,That>();
}
}
Основной public-метод pickBestFlowers создает временный объект, который передается во все private’ные вспомогательные методы. Он будет выброшен, когда операция закончится. Класс, который мы ввели, по сути выполняет роль структуры, как это было в языках C или Pascal. Поэтому мы позволяем себе обращаться к его полям напрямую (сделав его private’ным).
Можно заметить, что сгруппировав все данные, нужные для выполнения операции, в одну структуру будет логично также сгруппировать и методы. При этом структура превращается в полноценный объект. Такой объект, время жизни которого связано со временем выполнения операции, я для себя называю «объектом-сессией».
class FlowerSomething {
public List<Flower> pickBestFlowers() {
return new PickingSession().go(myType0);
}
private static class PickingSession {
List<Flower> pick(Type0 type0) {
...
return myResult;
}
private void pickAtATrot(TypeA typeA) {
...
}
private void pickAtAGallop(TypeB typeB) {
...
}
private final ArrayList<Flower> myResult = new ArrayList<Flower>();
private final Map<This,That> myMainMap = new HashMap<This,That>();
}
}
Как видно, теперь операция полностью реализуется в одном вспомогательном классе, не засоряя список полей главного класса. Данные, сколь угодно сложные, легко доступны во время выполнения этой операции, а вне ее просто не существуют.
( P.s. ) |
|
|
| Когда мы нарушаем общепринятые договоренности |
[Май. 10, 2006|03:32 pm] |
Все знают, что возвращать нулевой указатель на Enumeration/Iterator – свинство. Необходимость проверять полученный enumeration/iterator на null – это почти как новость, что this может быть нулевым: много дополнительной рутины просто так.
Однако есть случаи, когда нулевые значения законны: например в API моделей подсистемы данных (ничего не значащее условное название). Есть интерфейс, который используется в одном единственном месте, а реализуется в десятках разных классов несколькими программистами. Каждая реализация возвращает свой набор данных, который при большинстве условий – пустое множество. Это навело нас на мысль, что можно разрешить программистам упростить свой код и возвращать нулевой указатель там, где получается пустое множество.
Hапример:
...
if (isFeatureDisabled()) {
return EmptyCollectionHelper.getEmpty();
return null;
}
Data data = myService.getData(obj);
if (data == null) {
return EmptyCollectionHelper.getEmpty();
return null;
}
...
Мораль. Мы люди безпринципные (аморальные). Ради того, чтобы нескольким программистам было меньше рутинной работы, а другим было легче читать написанное, готовы пойти на то, чтобы объявить в документации для метода в интерфейсе право возвращать null и вставить одну лишнюю проверку в свой код. |
|
|
| Пусть не падают исключения при нормальной работе |
[Май. 10, 2006|02:03 pm] |
Есть разные мнения, когда могут падать исключения, а когда не должны. (Например, можно прагматично подумать о том, сколько времени уходит на запись стек-трейса при создании исключения в Java).
Я смотрю на эту проблему преимущественно в перспективе отладчика. В том, которым я пользуюсь, есть возможность перехватывать швыряние исключений. Периодически я включаю этот режим, чтобы в сыром коде (не всегда своем) видеть, как возникают ошибки (а то значений локальных переменных не видно из обычного стек-трейса). Мои любимые типы исключений для прослушивания – NullPointerException и ClassCastException.
Но иногда, как не жаль, от этого приходится отказаться. Когда кто-нибудь пишет код вроде такого:
try {
Object val = pointer.getValue();
return val.toString();
} catch (NullPointerException e) {
// oh well, pointer or val is null – ok, we expected it
return null;
}
После запуска такой программы отладчик все время будет заставлять меня приникнуть к этим строфам. Насладиться тем, как изящно можно неправильно использовать механизм исключений вместо того, чтобы поставить проверку или две.
Я предлагаю так: исключения неэкзотических типов должны бросаться только когда в этом месте необходимо внимание отладчика (человека). |
|
|
| Идея об основном потоке и выходах по ошибкам |
[Апр. 24, 2006|11:27 pm] |
Конечно, совсем не все, но многие методы устроены так: проверяем, делаем, еще проверяем, еще делаем.
Мне нравится стиль, при котором штатные действия записываются без отступа, а обработка сбоев – внутри if-блоков. Можно ведь и наоборот, но, мне кажется, таким образом хорошо разделяются основное русло выполнения и вспомогательная работа.
Вот так:
List<Link> collectLinks(Graph graph) {
if (graph == null) {
throw new IllegalArgumentException("graph is null");
}
Enumeration<Link> linkEnum = graph.allLinks();
if (!linkEnum.hasMoreElements()) {
return Collections.emptyList();
}
ArrayList<Link> result = new ArrayList<Link>();
while (linkEnum.hasMoreElements()) {
Link nextLink = linkEnum.nextElement();
if (!nextLink.isResolved()) {
continue;
}
result.add(nextLink);
}
return result;
}
А еще внутри циклов тоже. |
|
|
| Visitor часть 2. Еще немного мыслей |
[Апр. 17, 2006|02:51 am] |
В посте о шаблоне Visitor я ничего не говорил про обход каких-нибудь структур. Например, описанный там код мог располагаться в методе String diagnoseIt(Substance), который вообще работает с единственным объектом. Для меня главная идея в том, что «это абстрактное нечто может быть только этим, этим и этим» удается формализовать и потребовать, чтобы компилятор сам следил за порядком. Ведь в Java/C++ любой объект можно пытаться привести к любому другому объектному типу. Введение visitor’а позволяет явно описать, что к чему имеет смысл приводить. Мне кажется, иногда это важно.
Но часто интерфейсы с таким названием используют именно для обхода структур. Например, интерфейс IResourceVisitor из проекта Eclipse. В нем вообще всего один метод, т.о. для перебора вариантов он явно не предназначен.
С другой стороны, для меня остается открытым вопрос, имеет ли смысл интерфейс вроде SubstanceVisitor в публичном API: ведь он предназначен для того, чтобы клиентский код переставал компилироваться при внесении измненений. |
|
|
| Visitor |
[Апр. 17, 2006|02:20 am] |
Разрабатывать маленькую программу просто. Большую систему – сложнее. Ее приходится разбивать на части, и желательно так, чтобы изменения одних частей пореже приводили к изменениям в других.
Но бывает наоборот, эту независимость хочется уменьшить.
Пусть в одной из частей программы раположен такой код:
Substance substance = ...
if (substance instanceof SolidBody) {
SolidBody solidBody = (SolidBody)substance;
Shape shape = solidBody.getShape();
...
} else if (substance instanceof LiquidSubstance) {
LiquidSubstance liquidSubstance = (LiquidSubstance)substance;
double volume = liquidSubstance.getVolume();
...
} else if (substance instanceof GasSubstance) {
...
}
Отсюда кстати можно понять, что где-то объявлен интерфейс Substance, и предполагается что каждый его экземпляр принадлежит к одному из трех подтипов: SolidBody, LiquidSubstance или GasSubstance.
При написании этого кода может возникнуть два подтипа мыслей:
- а вот если потом добавится четвертый вариант, как бы его здесь не прохлопать...
- все будет хорошо.
Второй вариант абсолютно законен во многих случаях, естественнен, не приводит к созданию громоздкого кода, и к сожалению на этом придется закончить о нем.
В тех же случаях, когда вероятность появления четвертого подтипа достаточна велика, равно как и вероятность того, что код из примера забудут поправить, можно подстраховаться. Например, когда API подсистемы еще нестабильно, а код, работающий с ней уже разрабатывается в соседней команде. Примерно тогда, когда клиентский код уже написали и о нем забыли, авторы API понимают, что надо было добавить еще один подтип.
Будем добиваться, чтобы компилятор сам следил за тем, что мы перебираем все подтипы. Для этого предусмотрен GoF'овский шаблон проектирования Visitor. Инструкция:
Повторяющиеся всюду секции if/elseif/elseif заменим реализацииями специального интерфейса:
interface SubstanceVisitor {
Object visitSolidBody(SolidBody solidBody);
Object visitLiquidSubstance(LiquidSubstance liquidSubstance);
Object visitGasSubstance(GasSubstance gasSubstance);
} а код, расположенный в ветках условного оператора перенесем в соответствующие методы. Теперь добавление четвертого подтипа (ага, четвертого агрегатного состояния; покажем заодно, что любой прием можно доводить до абсурда) повлечет добавление метода в интерфейс, а значит и во все точки перебора.
Связжем базовый тип со своим visitor’ом – добавим метод в интерфейс:
interface Substance {
Color getColor();
Object accept(SubstanceVisitor visitor);
}
Реализации метод accept элементарны, сэкономим на них место.
Таким образом, мы решили проблему, немного усложнив API и, к сожалению, сильно загромоздив использующий его код.
Что мы получили взамен? Одну, но порою весьма важную выгоду: когда ребята из соседней комнаты придумают новое агрегатное состояние, мы гарантированно будем к этому готовы (т.е. кому-то придется написать недостающие методы). Да, еще адепты чистоты стиля порадуются тому, что совсем пропали явные приведения типов. |
|
|
| Возвращение коллекций, пустых и не очень |
[Мар. 27, 2006|01:37 am] |
Иногда метод должен вернуть пустую коллекцию, например типа List, Map или Set. Обычно такой тип обещает вернуть абстрактный метод, а одна из его конкретных реализаций отличается тем, что всегда возвращает пустой набор.
Самый простой вариант – написать return new ArrayList(). Это расточительно конечно, потому что по умолчанию коллекции в java выделют немного памяти про запас. ArrayList – 10 ячеек. Можно подсказать ему, что они не понадобятся: return new ArrayList(0). На данные и правда потратится 0 единиц памяти, но объект все равно будет готов к тому, что придется расширяться (создаст пустой массив и будет ждать, чтобы заменить его на массивчик побольше).
Если расширяться ему не придется никогда, самым оптимальным будет создание специального всегда-пустого объекта: java.util.Collections.emptyList(). Этот не содержит никаких данных, и вообще не создается больше одного экземпляра.
Аналогично, можно получить объекты остальных типов: Map или Set.
А еще можно создать коллекции из всего одного элемента. Они тоже не поддерживают расширение, а значит не тратят на это память. Кроме того, их удобно создавать за один ход: java.util.Collections.singletonMap(key, value).
Особо фанатствовать конечно не стоит, но кто знает, может быть в некоторых случаях эта экономия cкажется на производительности радикально.
Ну и конечно, нужно понимать, что попытка добавить в такие коллекции данные приведет к самым ужасным последствиям – UnsupportedOperationException. |
|
|
| Чего бояться: создание отложенных команд |
[Мар. 21, 2006|03:25 am] |
Стоит бороться с искушением создавать отложенные команды. Они всегда усложняют программу. ( Не говоря о том, как усложняют этот блог )
Отложенные команды совсем не стоит создавать, если результаты их работы потенциально могут понадобиться в других подсистемах.
Вот если результаты нужны только пользователю, то никаких проблем возникнуть не должно: человек просто дождется, когда результат будут ему предложен. Например, абсолютно допустимо отложить отрисовку данных на экране на потом, когда вся обработка этих данных закончится. Или отложить реакцию на некоторые нажатые клавиши до тех пор, пока не завершится текущая операция. Так устроено общение человека с машиной – он просто ждет, когда будут результаты.
Можно отложить операцию сохранения данных на диске, подразумевая, что до этого времени они могут несколько раз поменяться. Хотя в принципе это уже пограничный случай – ведь рано или поздно может статься, что результаты этих модификаций (в виде файлов на диске) понадобятся другой подсистеме. А это будет означать начало проблем. Потому что нет никакого ясного способа дождаться, когда же отложенная команда выполнится. Она висит где-то в очереди и программно узнать, что она отработала (то есть, можно забирать файлы) практически невозможно. Есть конечно дедовский метод: назначить еще одну отложенную команду, которая (предположительно) будет выполнена после той, записывающей данные на диск. Только потом третьей подсистеме понадобится сделать что-то совсем-совсем после. Несложно представить, какой цирк тогда может приехать. За удовольствие следить за отложенными командами в debugger’е платить отдельно.
Корень этих проблем в том, что очередь команд UI’я – это вообще говоря частное дело графической платформы и небольшой прослойки в программе. Эта прослойка, относящаяся к разделу “UI”, занимается в основном передачей вызовов в другие части программы.
Но API этой очереди обычно открыто всем подсистемам без разбора. Если же компонент начинает им пользоваться, то он к сожалению перестает быть мобильной и управляемой боевой единицей. Потому что управлять им мешают неизвестно когда завершающиеся отложенные действия, а без этого перебросить его в другой сценарий может оказаться невозможно.
Конечно есть случаи, когда без чего-то похожено на отложенные команды не обойтись. Но опять же, есть способы сделать это более цивилизованно, чтобы потом возникало минимум проблем. Но об этом в другой раз. :) |
|
|
| Имена и занимательная комбинаторика |
[Мар. 12, 2006|11:55 pm] |
Сколько разных нестандартных названий классов у нас было! Например, когда слово «manager» перегружалось особенно сильно, на смену приходил «administrator». Вместо «service» и «provider» появлялся загадочный «clerk». Опять «manager» или унылое «info» заменялось на «db», что должно было означать «база данных». А что делать? Популярных слов слишком мало. Если на них зациклиться, можно получить что-то такое:
package com...impl;
...
class CompileHelper {
public static CompilePerformer.CompileHelper getPerformerHelper(Param param) {
return new PerformerHelper(param);
}
private static class PerformerHelper implements CompilePerformer.CompileHelper {
...
(основано на реальных событиях) |
|
|
| Вызов виртуальных функций из конструктора |
[Мар. 9, 2006|11:30 pm] |
Как известно, вызывать виртуальные методы из конструктора – плохо. Вообще-то в Java почти все методы виртуальные, но в данном случае речь о том, когда конструктор находится в базовом классе, а виртуальный метод перекрыт в наследнике.
Мировая гармония в этом случае страдает от того, что перекрывающий метод в наследнике окажется вызван раньше, чем сработает конструктор его класса. Другими словами, последовательность будет такая:
конструктор базового класса, виртуальный метод, реализованный в наследнике, конструктор базового класса – продолжение, конструктор наследника.
Как видно, эта схема обрекает виртуальный метод работать в состоянии, когда поля его класса еще не получили свои значения. Ничего особенно страшного конечно не произойдет, просто все они будут нулевые. Эта не самая очевидная вещь особенно хорошо врезается в память, если провести N*10 минут в попытках разобраться, откуда все-таки берется загадочный NPE в простой короткой функции.
Возьмем в качестве примера неплохую задачку по ООП: написать фильтрующий Enumeration.
public abstract class FilterEnumBad implements Enumeration {
public FilterEnumBad(Enumeration innerEnum) {
myInnerEnum = innerEnum;
advance();
}
protected abstract Object accept(Object obj);
private void advance() {
while (myInnerEnum.hasMoreElements()) {
Object nextUnfiltered = myInnerEnum.nextElement();
myNext = accept(nextUnfiltered);
if (myNext != null) {
return;
}
}
myNext = null;
}
( полный текст )
В данном случае конструктор вызывает метод advance (чтобы подготовить первый элемент), а вот он в свою очередь – (чисто-)виртуальный метод accept.
С первой попытки проблема может не возникнуть:
class WithoutEmptyStringsEnum extends FilterEnumBad {
WithoutEmptyStringsEnum(Enumeration innerEnum) {
super(innerEnum);
}
protected Object accept(Object obj) {
String s = (String)obj;
if (s.length()==0) {
return null;
} else {
return obj;
}
}
}
Здесь мы просто выкидываем пустые строки, и никакие данные нам не нужны, то есть такой вариант метода accept по сути действует как статический метод. Но лишь пока это так, вся конструкция и будет работать.
Едва мы захотим немного усложнить правило,
class WithoutSomeStringsEnum extends FilterEnumBad {
WithoutSomeStringsEnum(Enumeration innerEnum, Collection nonGrata) {
super(innerEnum);
myNonGrata = nonGrata;
}
protected Object accept(Object obj) {
String s = (String)obj;
if (myNonGrata.contains(s)) {
return null;
} else {
return obj;
}
}
private final Collection myNonGrata;
}
выяснится что в методе accept падает исключение. И исправить это, не меняя базовый класс, будет нельзя.
Что можно (нужно было) поправить в базовом классе, чтобы гармония восстановилась?
- Если конструктор должен вызывать виртуальный метод, пусть это будет виртуальный метод вспомогательного объекта – того, которого мы успеем сконструировать раньше. В данном случае это значит завести отдельный интерфейс Filter.
- Можно вообще убрать сложные действия из конструктора. Но порой это неудобно и приводит к возникновению рядом с конструктором вспомогательного метода init_не_забудь_меня_вызвать.
p.s. А чтобы разбираться в подобных проблемах было еще интереснее, к версии Java 5.0 поведение немного изменилось, и в некоторых случаях поля все же успевают проинициализироваться. Так что даже полагаться на то, что программа обязательно упадет в данном вопросе нельзя. |
|
|
| Final-поля |
[Мар. 1, 2006|12:37 am] |
Ключевое слово final в Java принято использовать в двух случаях: для создания статических констант в классе (а в интерфейсе они становятся final автоматически) и при передачи данных в анонимный класс.
Жаль, что при описании полей класса слово “final” используется гораздо реже. Как известно, поле, объявленное с модификатором final менять нельзя, а инициализировать можно только в точке объявления, либо в конструкторе.
Чем класс с полями final лучше?
Становится проще зафиксировать и понять его инвариант (например, увидеть, что внутренний тип задается при создании и не меняется). Дополнительная вкусность в том, что Java следит за тем, чтобы мы никогда не забывали инициализировать такие поля.
Например, для случая двух конструкторов:
public AdvancedProjectSettings(IProject project) {
myProject = project;
myPlugin = project.getBasePlugin();
mySource="";
}
public AdvancedProjectSettings(String platformProjectSourceFolder,
BasePlugin plugin)
{
myPlugin = plugin;
mySource = platformProjectSourceFolder;
}
Поле myProject иногда остается неинициализированным, т.е. по умолчанию нулевым. Это не всегда очевидно, но легко выяснить, если присмотреться и сопоставить все присваивания в конструкторах с полями класса. Следующий вопрос - задумывал ли автор, что в некоторых случаях ссылка на проект должна остаться нулевой, или это ошибка.
Если поле myProject сделать final, то компилятор потребует от программиста дать явный ответ: вписать забытую инициализацию или просто поставить ему нулевое значение. Одно это уже будет совсем не лишним для тех, кто видит класс в первый раз. |
|
|
| Решение сложных задач. – Нейронные сети. |
[Фев. 21, 2006|03:05 pm] |
Как известно, нейронные сети применяют для решения очень сложных задач, которые к тому же непонятно, как решать. Однако не все еще знают, как совместить подход объектно-ориентированный с технологией нейронных сетей.
Первый этап – построение сети. Для этого подойдет большой программный продукт старше двух лет. Превращение обычной программы в сеть требует выделения нескольких подсистем. При этом крайне желательно дублирование функциональности между соседями. Вообще, каждая подсистема должна стремиться к максимальной универсальности. Еще, важно чтобы границы ответсвенности определялись как на политической карте – исключительно историческими причинами.
Ну и конечно, любая нейронная сеть – это всегда немножечко волшебство. Главное чистота помыслов; без искренности в устремлениях никогда не разбираться, как именно работает подсистема, ничего просто не получится.
Второй, заключительный и самый важный этап – настройка нейронной сети. Происходит это так. На вход подается напряжение: тестерам не нравится, как выглядит или ведет себя какой-то элемент. Линия напряжения пройдет через все подсистемы, участвующие в формировании этого элемента. Подсистема, в которую вносятся изменения, выбирается случайным образом. После этого один или несколько нейронов должны измениться строго таким образом, чтобы напряжение было снято. Самым многообещающим при этом является класс изменений, связанный с образованием в сети новых связей, для максимально эффективного снятия напряжения. Возникновение новых, незапланированных изначально связей не только решают поставленную задачу, но и делают сеть еще более мощной, более сложной.
Так старые экспериментальные технологии с успехом можно применить при разработке коммерческого ПО. Могут возникнуть сомнения, насколько это действительно применимо? Уверяю, это работает. |
|
|
| Как я проверяю на «не instanceof» |
[Фев. 21, 2006|02:55 pm] |
Один из приемов, подсмотренных мной в чужом коде, который я все время использую – записывать условие «не instanceof» так:
if (list instanceof RandomAccess == false) …
Отметая генетику, я объясняю это тем, что стандартная форма (! (list instanceof RandomAccess) ) очень неудобна ни для набора, ни для чтения. При чтении глаз спотыкается на обилии скобок. А при написании программы я, как известный любитель набрать скорее, стараюсь экономить на движениях, отрывающих руки от алфавитно-цифрового блока (мне кажется читатели-музыканты заметят, сколько лишних неудобных движений требуется при этом пассаже).
п.с. Не буду сочинять закрывающий абзац, чтобы не удлинять пост зря. |
|
|
| Not nullable/may be null |
[Фев. 20, 2006|02:28 am] |
Один из источников NPE (NullPointerException)– отсутствие договоренности о том, может ли метод вернуть нулевое значение или нет. В некоторых случаях это очевидно (когда интерфейсы хорошо документированы). В других – почти очевидно что не может (например, традиционно, методы, которые должны вернуть Enumeration или Iterator, не возвращают null). В третьих, наоборот, очевидно что еще как может.
В остальных ситуациях, на наш взгляд, хорошей практикой является документирование методов. Например так:
@return may be null или @return not null.
(Кстати, известный IDE Idea заявляет возможность читать подобные заголовки и следить за опасностью NullPointerException.)
Часто значение null в программе вообще не нужно, а применяется лишь если возникла проблема и вернуть больше нечего. Тут разумеется есть элемент перекладывания своей неприятностей на того, кто вызвал. Вызвавший естественно тоже редко добровольно берет на себя чужие проблемы и проверят у всех методов возращаемые значения. Кто именно будет исправлять NPE неопределено.
Чтобы придать этому всему порядок, имеет смысл документировать подходящие методы как «никогда не возвращающие нулевое значение» и действительно его никогда не возвращать. Например:
/**
@return not null
*/
Entity createStructure(Container container) {
Entity createdEntity = container.createChild(template);
if (createdEntity == null) {
throw new RuntimeException("Failed to create entity"); //$NON-NLS-1$
}
return createdEntity;
}
Мы обещали не возвращать null, и, так как не уверены в методе createChild (он не выставил такой javadoc), добавляем проверку, гарантирующую, что мы сдержим обещание. С другой стороны, то что метод createChild вернул нулевое значение – серьезная и маловероятная проблема, на которую допустимо реагировать жестко. |
|
|
| navigation |
| [ |
viewing |
| |
most recent entries |
] |
| [ |
go |
| |
earlier |
] |
| |
|
|