Code Monkey home page Code Monkey logo

codesorcerer's Introduction

BeanBuilder

Introduction

Welcome to BeanBuilder, a tool to build fluent immutable beans from templates.

Table of Contents

  • Motivation
    • Advantages of BeanBuilder Objects
    • Disadvantages of BeanBuilder Objects
  • Usage
  • Getting started
    • With Maven
    • With Gradle
  • Tutorial
    • What Is Generated
    • Creating a Simple Bean
    • Creating a Complex Bean
    • Working with 3rd Party Tools that Require Mutable Beans
    • Creating an Inheritance Hierarchy
    • Creating a Cyclic Bean
    • Updating an Existing Instance
    • Adding Custom Code
    • Using Javax Validation Annotations
    • Using Guava Predicates
    • Using Guava Functions
    • Using Guava Equivalence
    • Using Guava Orderings
  • Alternative Tools
  • Wish list
  • Authors

Motivation

There are numerous advantages to writing immutable classes, but apart from the simplest classes, implementation requires a lot of code, and for complex classes, updates are verbose. The result is a low signal-to-noise ratio, which is really a nuisance when you are being conscientious and following best practices. This tool allows one to define a template for a bean, and then generates an immutable implementation with a fluent API.

Advantages of BeanBuilder Objects

Disadvantages of BeanBuilder Objects

  • They require a separate object for each distinct value. (EJ Item 15)

Usage

This tool is intended for generating implementations of beans, pojo's, or data classes. It can be used for simple [value classes] (https://docs.oracle.com/javase/8/docs/api/java/lang/doc-files/ValueBased.html), it should not be used for service classes, classes designed to provide business logic or algorithms, or any other type of class that is not a (mostly) pure data object.

Getting started

With Maven

Include the following dependencies in your project. When you deploy your artifact(s), the actual overhead of the BeanBuilder jar is about 4k.

    <dependencies>
        <dependency>
            <groupId>com.codesorcerer</groupId>
            <artifactId>codesorcerer-processor</artifactId>
            <version>2.4.3</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.codesorcerer</groupId>
            <artifactId>codesorcerer-spells</artifactId>
            <version>2.4.3</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

The compiler plugin is required to build the source.

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <annotationProcessors>
                        <annotationProcessor>
                            com.codesorcerer.processor.CodeSorcererProcessor
                        </annotationProcessor>
                    </annotationProcessors>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

With Gradle

This is not supported yet. Any takers?

Tutorial

This section has a continuing example to show how to use this tool. The examples in this tutorial can be cut/paste into your own test project, and run to test the tool. It is recommended to read this section once in its entirety, and in future refer to the sections of interest directly.

What Is Generated

The bean builder generates several classes based on your template. Suppose you name your template FooDef. Then the following classes are generated:

  • Foo - This is the immutable implementation from the template.
  • FooMutable - This is a mutable implementation from the template, so you can play nice with frameworks that require mutable beans. Methods are provided in Foo to easily convert to and from FooMutable.
  • FooGuava - Stuck on Java7 or under? This class provides pseudo-functional code a la Google Guava.

A Note on Available Annotations

@BBBImmutable - Use this annotation on an interface you want an immutable bean generated from
@BBBJson  - Use this annotation on an interface you want a json compatible bean generated from
@BBBMutable  - Use this annotation on an interface you want a mutable bean generated from
@BBBGuava  - Use this annotation on an interface you want gauva adapters generated from
@BeautifulBean - Use this annotation on an interface you want everything above generated from

Creating a Simple Bean

David requires a person class for a project he is working on. Initially his person is very simple, having only 3 fields. He chooses the BeanBuilder tool because he expects his class to get more complex as the project grows.

@BBBImmutable
public interface PersonDef
{
    @Nonnull
    String getFirstName();
    
    String getMiddleName();

    @Nonnull
    String getLastName();
}

There are several requirements here to make this class function as expected. David uses an inteface to define the getters for his new class. The interface must be annotated with the @BeanTemplate annotation, and the name of the interface must end with Def. Every method that has a required argument in the implementation must be annotated with @javax.annotation.Nonnull.

Once David completes his template, he compiles the code, and can use the generated implementation. He can use a static factory that requires all fields to be supplied. He can do this because the PersonDef has 3 getter methods defined. With 4 or more getters, the static factory method is not available. This choice is appropriate when all fields are present.

final Person p = Person.newPerson( "Bob", "Rip", "Ross");

Notice that the order of the method arguments are as defined in the BeanTemplate.

David creates another person using the fluent interface. This is the right choice when the person has no middle name.

final Person jd = Person.buildPerson()
                .firstName( "Judy" )
                .lastName( "Dench" )
             .build();

If you try this example, you will notice that the tool forces the @Nonnull fields to be specified first, in the order defined in the BeanTemplate.

David could use the fluent interface to create Bob Ross, the static factory is the recommended usage.

final Person p = Person.buildPerson()
                .firstName( "Bob" )
                .lastName( "Ross" )
                .middleName( "Rip" )
             .build();

Notice the middle name appears after all the required fields in the interface.

Never add a Nonnull parameter more than once. The BeanTemplate tool cannot control how many times you can add an optional parameter, therefore, you should be careful that you specify optional parameters only once.

final Person p = Person.buildPerson()
                .firstName( "Bob" )
                .lastName( "Ross" )
                .middleName( "Rip" )
                .middleName( "Drat" )
             .build();

Creating a Complex Bean

David decides that he wants to extract the name to a composed bean (sub-bean), add an Address sub-bean, add a Sex represented by an enum (Male, Female), and an occupation. He updates his PersonDef template as below.

@BeanTemplate
public interface PersonDef
{
    @Nonnull
    NameDef getName();

    @Nonnull
    AddressDef getAddress();

    @Nonnull
    Sex getSex();

    String getOccupation();
}

Notice that sub-beans that are built using this tool must be referred to by the template name, for example, NameDef instead of Name.

Since there are more than 3 parameters, the tool allows only 1 way to create a new Person.

final Person p = Person.buildPerson()
				.newName()
					.firstName("Sherlocke")
					.lastName("Holmes")
				.done()
				.newAddress()
					.streetAddress("221B Baker Street")
				.done()
				.sex(Sex.MALE)
			.build();

Notice that the occupation is optional, and was not included in this instantiation.

Working with 3rd Party Tools that Require Mutable Beans

The tool offers support to convert your immutable bean to a mutable bean that plays nicely with 3rd party frameworks.

David decides he needs to convert a child to a mutable instance for use with a 3rd party tool and back again.

final PersonMutable person = p.toMutable();

// 3rd party integration omitted

final Person person2 = Person.fromMutable(person);

Creating an Inheritance Hierarchy

David decides he wants to model adults and children differently. He refactors his code.

public interface Person
{
     @Nonnull
     NameDef getName();
 
     @Nonnull
     AddressDef getAddress();
 
     @Nonnull
     Sex getSex();
}

@BeanTemplate
public interface ChildDef extends Person
{
    @Nonnull
    AdultDef getMother();
  
    @Nonnull
    AdultDef getFather();

    int getCavities();
}

@BeanTemplate
public interface AdultDef extends Person
{
    String getOccupation();
    
    List<? extends ChildDef> getChildren();
}

David has removed the @BeanTemplate annotation from the PersonDef, and turned it into a Person interface. He has created an AdultDef and ChildDef that are BeanTemplates. Notice the return type of getChildren is List<? extends ChildDef>. This is necessary since getChildren() is a producer (PECS) of ChildDef's.

David can fluently create a Child or Adult. This principle applies no matter how deeply nested the hierarchy of BeanTemplates is.

final Adult a = Adult.buildAdult()
                .newName()
                    .firstName("William")
                    .lastName("Shakespeare")
                .done()
                .newAddress()
                    .streetAddress("Stratford-upon-Avon, Warwickshire, West Midlands, England")
                .done()
                .sex(Sex.MALE)
                .children(Lists.newArrayList(susanna, hamnet, judith))
            .build();
            
final Child c = Child.buildChild()
    .newName()
        .firstName( "Hamnet" )
        .lastName("Shakespeare")
    .done()
    .newAddress()
        .streetAddress("Stratford-upon-Avon, Warwickshire, West Midlands, England")
    .done()
    .sex(Sex.MALE)
    .newMother()
        .newName()
            .firstName("Anne")
            .lastName("Hathaway")
        .done()
        .newAddress()
            .streetAddress("Stratford-upon-Avon, Warwickshire, West Midlands, England")
        .done()
        .sex(Sex.FEMALE)
    .done()
    .newFather()
        .newName()
            .firstName("William")
            .lastName("Shakespeare")
        .done()
        .newAddress()
            .streetAddress("Stratford-upon-Avon, Warwickshire, West Midlands, England")
        .done()
        .sex(Sex.MALE)
    .done()
.build();          

As expected, David cannot create a Person given the current class definitions. If David wanted to allow Person instances to exist, he could have kept the Person interface as a @BeanTemplate.

Although inheritance is supported, it is recommended to use composition over inheritance. Modelling Adult/Children as in the above example is not recommended.

Creating a Cyclic Bean

Never create a @Nonnull cycle in your @BeanTemplate.

@BeanTemplate
public interface CycleDef
{
    @Nonnull
    public CycleDef getCycle();
}

// needs an already existing cycle instance to complete
final Cycle c = Cycle.buildCycle()
                    .cycle(alreadyExistingCycleInstance)

// needs an already existing cycle instance to complete
final Cycle c = Cycle.buildCycle()
                    .newCycle()
                        .cycle(alreadyExistingCycleInstance)
										
// needs an already existing cycle instance to complete
final Cycle cycle = Cycle.newCycle(alreadyExistingCycleInstance);

If you do this, then then you cannot use the buildCycle() method without triggering an infinite cascade of buildCycle() calls. You can still use the newCycle() method, provided you have an existing Cycle instance, but how can you create it?

Updating an Existing Instance

David decides he wants to update the child's mother's middle name. He starts with the update method, to indicate to the tool that he is interested in creating an altered copy of the original child.

final Child child = c.update()
                .getMother()
                    .middleName("Mary")
                .done()
            .build();

When David has only 1 field to update, and an already existing Mother instance, he can use the withMother() method which provides the most concise syntax.

final Child updatedChild = child.withMother(a);

Think carefully before you do this. If you update a sub-bean instead of the current instance, you are probably not doing what you want, and confusing the next developer.

final Adult adult = child.getMother()
                .update()
                    .middleName( "Mary" )
                .build();

Notice here how the adult was updated and the updated copy returned. The child class is unaffected. This usage is not recommended.

Be aware of the implication of chaining with...() methods. The with...() method is shorthand for update(). ... .build(). Each time with...() is called, the entire instance is copied to a new instance with the single change.

This style results in 4 copies of Child instance child being created. While it is legible, it is not performant.

final Adult adult = child.getMother().withMiddleName("Mary");
final Child avoid = child.withMother(adult).withFirstName("David" ).withMiddleName( "Bruce").withLastName( "Banner" );

This style results in 2 copies of the Child instance being created. Consider that this syntax is potentially confusing.

final Adult adult = child.getMother().withMiddleName("Mary");
final Child idiom2 = child.withMother(adult)
        .update()
            .firstName("David")
            .middleName("Bruce")
            .lastName("Banner")
        .build();	

This style results in 1 copy of the Adult instance being created, and 1 copy of the Child instance being created. The syntax here is clear.

final Child idiom1 = child.update()
        .getMother()
            .middleName("Mary")
        .done()
        .firstName("David")
        .middleName("Bruce")
        .LastName("Banner")
    .build();

This is the recommended syntax.

Adding Custom Code

David decides to define some special behavior for his bean. He can do this by creating an abstract class with the custom implementation, instead of starting with an interface. Most methods defined in the abstract class are inherited by the generated implementation.

public abstract class PersonDef {
    @Nonnull
    public abstract NameDef getName();

    @Nonnull
    public abstract AddressDef getAddress();

    @Nonnull
    public abstract Sex getSex();

    public abstract String getOccupation();

    public abstract long getBirthDate();

    public void sayHi() {
        System.out.println("Hi " + getName());
    }
}

@BeanTemplate
public abstract class AdultDef extends PersonDef
{
    public abstract String getOccupation();

    public abstract List<? extends ChildDef> getChildren();
}

@BeanTemplate
public abstract class ChildDef extends PersonDef
{
    @Nonnull
    public abstract AdultDef getMother();

    @Nonnull
    public abstract AdultDef getFather();

    public abstract int getCavities();
}

Notice this approach has the usual drawbacks of using an abstract class instead of an interface. As expected, David cannot create a Person given the current class definitions.

David now has access to a method called sayHi().

final Adult adult = Adult.buildAdult()
// rest of adult omitted 

adult.sayHi();

Do not try to customize Object override methods. Methods like equals(Object o), hashcode() or toString() are never inherited, instead they are overridden by the custom implementation.

@BeanTemplate
public abstract class IdDef
{
    @Nonnull
    public abstract Long getId();
    
    @Override
    public int hashCode()
    {
        return 42; 
    }
    
    @Override
    public boolean equals( final Object obj )
    {
        return true;
    }
    
    @Override
    public String toString()
    {
        return "The Answer to the Ultimate Question of Life, the Universe, and Everything."
    } 
}

The custom implementations of the @Override methods are ignored by the tool, thus the output is not what one expects given this class definition.

Using Javax Validation Annotations

The tool incorporates [validation annotations] (http://docs.oracle.com/javaee/6/api/javax/validation/package-summary.html). A trivial example is included here.

David decides a child has 0-40 cavities, and that the mother and father cannot be the same person.

@BeanTemplate
public abstract class ChildDef implements PersonDef
{
    @Nonnull
    public abstract AdultDef getMother();

    @Nonnull
    public abstract AdultDef getFather();

    @Min(0)
    @Max(40)
    public abstract int getCavities();

    @AssertTrue(message="Class invariant violation : Mother and father cannot be the same person")
    private boolean isValid()
    {
        return !getMother().equals(getFather());
    }
}

The isValid() method is automatically called at construction time in the implementation, and therefore well suited to protect the invariants of this class.

David can now be sure that each child instance has 0-40 cavities. Instances c1 and c4 fail at runtime.

final Child c1 = c.withCavities(-1);
final Child c2 = c.withCavities(0);
final Child c3 = c.withCavities(40);
final Child c4 = c.withCavities(41);

Using Guava Predicates

The tool creates a separate class with static [Predicate methods] (https://code.google.com/p/guava-libraries/wiki/FunctionalExplained#Predicates). A trivial example is included here.

David can use a generated predicate to find all children with exactly 5 cavities. The predicate is created as a static method in a ChildGuava class.

final List<Child> childList = Lists.newArrayList(
        c.update().cavities(3).sex(Sex.MALE).build(),
        c.update().cavities(2).sex(Sex.MALE).build(),
        c.update().cavities(0).sex(Sex.FEMALE).build(),
        c.update().cavities(2).sex(Sex.FEMALE).build());

final List<Child> filteredList = Lists.newArrayList(Iterables.filter(childList, ChildGuava.byCavities(5)));

Using Guava Functions

The tool creates a separate class with static [Function methods] (https://code.google.com/p/guava-libraries/wiki/FunctionalExplained#Functions). A trivial example is included here.

David can use a function to transform the list of children into a list of number-of-cavities. The function is created as a static method in a ChildGuava class.

final List<Child> childList = Lists.newArrayList(
        c.update().cavities(3).sex(Sex.MALE).build(),
        c.update().cavities(2).sex(Sex.MALE).build(),
        c.update().cavities(0).sex(Sex.FEMALE).build(),
        c.update().cavities(2).sex(Sex.FEMALE).build());

final List<Long> numberOfCavitiesList = Lists.newArrayList(Iterables.transform(childList, ChildGuava.BY_CAVITIES));

Using Guava Equivalence

The tool creates a separate class with static [Equivalence] (http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/base/Equivalence.html) and [Wrapper] (http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/base/Equivalence.Wrapper.html). A trivial example is included here.

David can use any of the defined equivalences in place of equals() and hashcode().

final List<Child> childList = Lists.newArrayList(
        c.update().cavities(3).sex(Sex.MALE).build(),
        c.update().cavities(2).sex(Sex.MALE).build(),
        c.update().cavities(0).sex(Sex.FEMALE).build(),
        c.update().cavities(2).sex(Sex.FEMALE).build());

// output is 3, because 2 children have 2 cavities.
System.out.println(Sets.newHashSet(Iterables.transform(childList, ChildGuava.EQUALS_CAVITIES_WRAPPER)).size());

// output is 2, because 2 children are male, and 2 are female.
System.out.println(Sets.newHashSet(Iterables.transform(childList, ChildGuava.EQUALS_SEX_WRAPPER)).size());

Using Guava Orderings

The tool creates a separate class with static [Ordering methods] (https://code.google.com/p/guava-libraries/wiki/OrderingExplained). A trivial example is included here.

David can sort or order lists. His Ordering is like a fluent replacement for Comparator.

final List<Child> childList = Lists.newArrayList(
        c.update().cavities(3).sex(Sex.MALE).build(),
        c.update().cavities(2).sex(Sex.MALE).build(),
        c.update().cavities(0).sex(Sex.FEMALE).build(),
        c.update().cavities(2).sex(Sex.FEMALE).build());

// prints the children sorted by cavities, smallest to largest.
System.out.println(ChildGuava.ORDER_BY_CAVITIES.sortedCopy(childList));

// prints the children sorted by sex, male, then female.
System.out.println(ChildGuava.ORDER_BY_SEX .sortedCopy(childList));

Alternative Tools

[Lombok] (https://projectlombok.org)

Wish list

  • Add gradle support.
  • Methods that return a List should have a ... setter.
  • Rewrite in pure Java.

Authors (in alphabetical order)

  • David P Phillips
  • Micha J Pringle

codesorcerer's People

Contributors

wenjieliu avatar daviddphillips avatar hilco-wijbenga avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.