2015-02-07 7 views
4

Я пытаюсь разработать проект в Groovy, и я просматривал свой код и пытался найти области, которые я мог бы заменить чем-то более идиоматически Groovy, пока не найду решение для another issue I've been having.Есть ли способ сделать аннотацию @Builder для неизменяемых классов?

Я начал более подробно изучать аннотации трансформации АСТ - они помогли значительно сократить количество кода, который мне приходится писать в некоторых местах. Однако у меня возникла проблема с использованием аннотации groovy.transform.builder.Builder с одним из моих неизменяемых классов значений. Источник этой аннотации размещен here.

Проблема заключается в том, что, по-видимому, аннотации заставляют строителя устанавливать значения сборщика напрямую, а не хранить копию значений и передавать их конструктору сборщика. Это приводит к ReadOnlyPropertyException, когда вы пытаетесь использовать его с неизменяемыми классами.

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

ExternalStrategy выглядит как наиболее перспективный из четырех, и вы можете найти на нем SSCCE, детализируя проблему here.

Исходный код из примера также приводится ниже:

import groovy.transform.Immutable 
import groovy.transform.builder.Builder as GBuilder 
import groovy.transform.builder.ExternalStrategy 

/* 
* Uncommenting the below causes a failure: 
* 'groovy.lang.ReadOnlyPropertyException: Cannot set readonly property: value for class: Value' 
*/ 
//@Immutable 
class Value { 

    @GBuilder(forClass = Value, prefix = 'set', builderStrategy = ExternalStrategy) 
    static class Builder { } 

    int value 
    String toString() { "Value($value)" } 
} 

def builder = new Value.Builder() 
println builder.setValue(1).build() 

Там также, как представляется, соответствующее обсуждение JIRA по этому вопросу here.

Редактировать
Я попытался с помощью ответа CFrick в ниже, используя InitializerStrategy вместо ExternalStrategy.

Теперь все компилируется, но я получаю следующие ошибки во время выполнения, когда я пытаюсь выполнить мои тесты:

java.lang.IllegalAccessError: tried to access class com.github.tagc.semver.version.BaseVersion from class com.github.tagc.semver.version.BaseVersion$com.github.tagc.semver.version.BaseVersionInitializer 
    at java.lang.Class.getDeclaringClass(Class.java:1227) 
    at java.beans.MethodRef.set(MethodRef.java:46) 
    at java.beans.MethodDescriptor.setMethod(MethodDescriptor.java:117) 
    at java.beans.MethodDescriptor.<init>(MethodDescriptor.java:72) 
    at java.beans.MethodDescriptor.<init>(MethodDescriptor.java:56) 
    at java.beans.Introspector.getTargetMethodInfo(Introspector.java:1163) 
    at java.beans.Introspector.getBeanInfo(Introspector.java:426) 
    at java.beans.Introspector.getBeanInfo(Introspector.java:173) 
    at com.github.tagc.semver.version.VersionFactory.createBaseVersion(VersionFactory.groovy:34) 
    at com.github.tagc.semver.test.util.TestSetup.<clinit>(TestSetup.groovy:77) 
    at java.lang.Class.forName(Class.java:344) 
    at com.github.tagc.semver.version.SnapshotDecoratorSpec.#decoratedVersion should be considered equal to patch-bumped #releaseVersion snapshot(SnapshotDecoratorSpec.groovy:24) 

Вслед затем серией исключений, как следующее:

java.lang.NoClassDefFoundError: Could not initialize class com.github.tagc.semver.test.util.TestSetup 
    at java.lang.Class.forName(Class.java:344) 
    at com.github.tagc.semver.version.SnapshotDecoratorSpec.#decoratedVersion should be considered equal to minor-bumped #releaseVersion snapshot(SnapshotDecoratorSpec.groovy:36) 

То, что я сейчас это BaseVersion класс, как следующее:

/** 
* A concrete, base implementation of {@link com.github.tagc.semver.version.Version Version}. 
* 
* @author davidfallah 
* @since v0.1.0 
*/ 
@Immutable 
@Builder(prefix = 'set', builderStrategy = InitializerStrategy) 
@PackageScope 
final class BaseVersion implements Version { 
    // ... 

    /** 
    * The major category of this version. 
    */ 
    int major = 0 

    /** 
    * The minor category of this version. 
    */ 
    int minor = 0 

    /** 
    * The patch category of this version. 
    */ 
    int patch = 0 

    /** 
    * Whether this version is a release or snapshot version. 
    */ 
    boolean release = false 

    // ... 
} 

завода по производству экземпляров этих элементов:

/** 
* A factory for producing base and decorated {@code Version} objects. 
* 
* @author davidfallah 
* @since v0.5.0 
*/ 
class VersionFactory { 

    // ... 

    /** 
    * Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed 
    * with the given parameters. 
    * 
    * @param major the major category value of the version instance 
    * @param minor the minor category value of the version instance 
    * @param patch the patch category value of the version instance 
    * @param release the release setting of the version instance 
    * @return an instance of {@code BaseVersion} 
    */ 
    static BaseVersion createBaseVersion(int major, int minor, int patch, boolean release) { 
     return new BaseVersion(major, minor, patch, release) 
    } 

    /** 
    * Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed 
    * with the given parameters. 
    * 
    * @param m a map of parameter names and their corresponding values corresponding to the 
    *  construction parameters of {@code BaseVersion}. 
    * 
    * @return an instance of {@code BaseVersion} 
    */ 
    static BaseVersion createBaseVersion(Map m) { 
     return new BaseVersion(m) 
    } 

    /** 
    * Returns an instance of {@link com.github.tagc.semver.version.BaseVersion BaseVersion} constructed 
    * with the given parameters. 
    * 
    * @param l a list of parameter values corresponding to the construction parameters of {@code BaseVersion}. 
    * 
    * @return an instance of {@code BaseVersion} 
    */ 
    static BaseVersion createBaseVersion(List l) { 
     return new BaseVersion(l) 
    } 

    /** 
    * Returns a builder for {@link com.github.tagc.semver.version.BaseVersion BaseVersion} to specify 
    * the construction parameters for the {@code BaseVersion} incrementally. 
    * 
    * @return an instance of {@code BaseVersion.Builder} 
    */ 
    static Object createBaseVersionBuilder() { 
     return BaseVersion.builder() 
    } 

    // ... 
} 

тест спецификация класс для Version объектов:

/** 
* Test specification for {@link com.github.tagc.semver.version.Version Version}. 
* 
* @author davidfallah 
* @since 0.1.0 
*/ 
@Unroll 
class VersionSpec extends Specification { 

    static exampleVersions = [ 
     VersionFactory.createBaseVersion(major:1, minor:2, patch:3), 
     VersionFactory.createBaseVersion(major:0, minor:0, patch:0), 
     VersionFactory.createBaseVersion(major:5, minor:4, patch:3), 
     VersionFactory.createBaseVersion(major:1, minor:16, patch:2), 
     VersionFactory.createBaseVersion(major:4, minor:5, patch:8), 
     ] 

    // ... 
} 

и другие классы, которые пытаются создавать экземпляры BaseVersion, которые упущение, такие как TestSetup.

+0

'@ Immutable' является окончательным по умолчанию. не могли бы вы предоставить минимальную версию кода? это трудно понять, как сейчас. также почему '@ PackageScope'? – cfrick

+0

@cfrick Я постараюсь собрать что-то вместе и обновить вас, когда закончите. '@ PackageScope', потому что лучше всего ограничить область как можно больше. Я хочу, чтобы интерфейс 'Version' был общедоступным, но конкретные реализации были приватными пакетами, чтобы клиенты были вынуждены проходить через« VersionFactory »для их создания (статические заводские методы часто лучше конструкторов [Effective Java Item 1]). Я не играл достаточно с этим еще, чтобы убедиться, что это лучший подход, но я попробую что-то другое, если это не сработает. – Tagc

+0

@cfrick При создании SSCCE я обнаружил, что неправильно использовал подход Initializer. Я исправил свою ошибку, и теперь она работает нормально, поэтому я отмечаю ваш ответ как принятый. Тем не менее, мне не очень нравится подход к инициализатору (особенно потому, что он требует избавления от '@ PackageScope'), поэтому я просто придерживаюсь старомодного подхода на самом деле в исходном коде, который у меня был до. Спасибо за вашу помощь. – Tagc

ответ

3

Ваш код здесь не удается, потому что выбранная стратегия там в основном делает:

def v = new Value().with{ setValue(1); return it } 

и это не может быть сделано на @Immutable объектов.

Согласно docs, существует только InitializerStrategy, что может явно справиться с @Immutable.

Вы можете использовать InitializerStrategy в сочетании с @Canonical и @Immutable. Если в аннотации @Builder нет явных включений или исключений атрибутов аннотации, но ваша @Canonical аннотация, то из @Canonical будет повторно использоваться для @Builder.

E.g.

import groovy.transform.* 
import groovy.transform.builder.* 

@Immutable 
@ToString 
@Builder(prefix='set', builderStrategy=InitializerStrategy) 
class Value { 
    int value 
} 

def builder = Value.createInitializer().setValue(1) 
assert new Value(builder).toString()=='Value(1)' 

В зависимости от того, что вы до, это rahter уродливый синтаксис, и вы могли бы быть лучше только с помощью карты на основе c'tors. Даже без, например, @TypeChecked a new Value(vlaue: 666) сгенерирует ошибку, и уходящие параметры (для класса с несколькими свойствами) оставят их null.

+0

Спасибо за ответ. «В зависимости от того, что вы делаете, это верный уродливый синтаксис, и вам может быть лучше, если вы просто используете карточные c'tors». Это возможность, но, как я уже упоминал в обсуждении JIRA, использование строителя способствует совместимости с Java. Ваш подход делает все компилируемым, поэтому он, кажется, идет по правильному пути, и по этой причине я его поддержал. Однако теперь я получаю 'IllegalAccessError', поэтому, боюсь, я не могу принять этот ответ прямо сейчас, так как другие могут столкнуться с этой проблемой и с этим подходом. Я обновил оригинальный пост. – Tagc

Смежные вопросы