helospark / SparkBuilderGenerator

Eclipse plugin to generate builders
MIT License
38 stars 12 forks source link

Support for default Values #60

Closed matthiasmast closed 2 years ago

matthiasmast commented 2 years ago

Hello,

thank you for this great tool.

I didn't find an option for default values. If a value was not set in the builder the field in the class will be null even if the field was initialised with a value.

I think this would be a really helpful feature.

Use cases:

helospark commented 2 years ago

Hello @matthiasmast, Could you please share an example input class and expected generated code?

matthiasmast commented 2 years ago

Hello @helospark

I think something like this should work

import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.annotation.processing.Generated;

public class Example{

    private int intData = 5;
    private Collection<String> texts = List.of("default");
    private String noData;

    public int getIntData(){
        return this.intData;
    }

    public Collection<String> getTexts(){
        return this.texts;
    }

    public String getNoData(){
        return this.noData;
    }

    @Generated("SparkTools")
    private Example(Builder builder){
        if(builder.isIntDataSet){
            this.intData = builder.intData;
        }
        if(builder.isTextsSet){
            this.texts = builder.texts;
        }
        if(builder.isNoDataSet){
            this.noData = builder.noData;
        }
    }

    @Generated("SparkTools")
    public static Builder builder(){
        return new Builder();
    }

    @Generated("SparkTools")
    public static final class Builder{
        private int intData;
        private boolean isIntDataSet = false;
        private Collection<String> texts = Collections.emptyList();
        private boolean isTextsSet = false;
        private String noData;
        private boolean isNoDataSet = false;

        private Builder(){
        }

        public Builder withIntData(int intData){
            this.intData = intData;
            this.isIntDataSet = true;
            return this;
        }

        public Builder withTexts(Collection<String> texts){
            this.texts = texts;
            this.isTextsSet = true;
            return this;
        }

        public Builder withNoData(String noData){
            this.noData = noData;
            this.isNoDataSet = true;
            return this;
        }

        public Example build(){
            return new Example(this);
        }
    }
}

Test

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import java.util.List;

import org.junit.Test;

public class ExampleTest{

    @Test
    public void testNoData(){
        Example example = Example.builder()
                .build();
        assertEquals(5, example.getIntData());
        assertEquals(1, example.getTexts().size());
        assertNull(example.getNoData());
    }

    @Test
    public void testWithData(){
        Example example = Example.builder()
                .withIntData(10)
                .withTexts(List.of("1", "2"))
                .withNoData(null)
                .build();
        assertEquals(10, example.getIntData());
        assertEquals(2, example.getTexts().size());
        assertNull(example.getNoData());
    }

}

I think this would reflect very well the intention of the class creator if he initialised the fields with values

matthiasmast commented 2 years ago

could even be improved like this for more complex default values

import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.annotation.processing.Generated;

public class Example{

    private int intData;
    private Collection<String> texts;
    private String noData;

    private int defaultIntData(){
        return 5;
    }

    private Collection<String> defaultTexts(){
        return List.of("default");
    }

    private String defaultNoData(){
        return null;
    }

    public int getIntData(){
        return this.intData;
    }

    public Collection<String> getTexts(){
        return this.texts;
    }

    public String getNoData(){
        return this.noData;
    }

    @Generated("SparkTools")
    private Example(Builder builder){
        if(builder.isIntDataSet){
            this.intData = builder.intData;
        } else{
            this.intData = this.defaultIntData();
        }
        if(builder.isTextsSet){
            this.texts = builder.texts;
        } else{
            this.texts = this.defaultTexts();
        }
        if(builder.isNoDataSet){
            this.noData = builder.noData;
        } else{
            this.noData = this.defaultNoData();
        }
    }

    @Generated("SparkTools")
    public static Builder builder(){
        return new Builder();
    }

    @Generated("SparkTools")
    public static final class Builder{
        private int intData;
        private boolean isIntDataSet = false;
        private Collection<String> texts = Collections.emptyList();
        private boolean isTextsSet = false;
        private String noData;
        private boolean isNoDataSet = false;

        private Builder(){
        }

        public Builder withIntData(int intData){
            this.intData = intData;
            this.isIntDataSet = true;
            return this;
        }

        public Builder withTexts(Collection<String> texts){
            this.texts = texts;
            this.isTextsSet = true;
            return this;
        }

        public Builder withNoData(String noData){
            this.noData = noData;
            this.isNoDataSet = true;
            return this;
        }

        public Example build(){
            return new Example(this);
        }
    }
}

but then ordering is important

helospark commented 2 years ago

I wonder why not just copy the field assignments to the builder, like:

import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.annotation.processing.Generated;

public class Example{

    private int intData = 5;
    private Collection<String> texts = List.of("default");
    private String noData;

    public int getIntData(){
        return this.intData;
    }

    public Collection<String> getTexts(){
        return this.texts;
    }

    public String getNoData(){
        return this.noData;
    }

    @Generated("SparkTools")
    private Example(Builder builder){
        this.intData = builder.intData;
        this.texts = builder.texts;
        this.noData = builder.noData;
    }

    @Generated("SparkTools")
    public static Builder builder(){
        return new Builder();
    }

    @Generated("SparkTools")
    public static final class Builder{
        private int intData = 5;
        private Collection<String> texts = List.of("default");
        private String noData;

        private Builder(){
        }

        public Builder withIntData(int intData){
            this.intData = intData;
            return this;
        }

        public Builder withTexts(Collection<String> texts){
            this.texts = texts;
            return this;
        }

        public Builder withNoData(String noData){
            this.noData = noData;
            return this;
        }

        public Example build(){
            return new Example(this);
        }
    }
}

This seems cleaner, I guess the only thing not supported by this is the initialization of a field by private (non-static) method.

matthiasmast commented 2 years ago

that would also work. A disadvantage here would be that if you change the default values you have to remember to change them in both places or to renew the builder.

helospark commented 2 years ago

Released change in 0.0.24, please check.

matthiasmast commented 2 years ago

works fine. Thank you very much!