projectlombok / lombok

Very spicy additions to the Java programming language.
https://projectlombok.org/
Other
12.9k stars 2.39k forks source link

@Factory - new annotation - proposal - to automatically produce a factory class. #1408

Open wheredevel opened 7 years ago

wheredevel commented 7 years ago

@Factory is a new annotation I would like to propose .

Many times, I find it very convenient to have all siblings of a parent class/interface/annotation a) enlisted (great for code assistance) and b) ready for construction. In vanilla Java - I have to either remember all those siblings, or use IDE's method do rediscover them over-and-over again, or create and maintain a corresponding Factory class manually. The Factory class is responsible for building (assuming @Build annotation) instances of those (non-abstract) siblings.

It would be much more convenient to have that Factory class, created by Lombok! (Moreover, to turn the parent class itself into the siblings factory!)

I show this in the example below.

------------------------
@Factory
class abstract Transformer {
}

------------------------
@Factory
class abstract Autobot extends Transformer {
}

------------------------
@Builder
class Optimus extends Autobot {
}

@Builder
class Bumblebee extends Autobot {
}

...

------------------------
@Factory
class abstract Decepticon extends Transformer {
}

------------------------
@Builder
class Megatron extends Decepticon {
}

@Builder
class Soundwave extends Decepticon {
}

...

//
// vs. Vanilla Java
//

class Autobot {

  ... 

  public static Optimus.Builder optimus() {
   return Optimus.builder();
  }

  public static Bumblebee.Builder bumblebee() {
   return Bumblebee.builder();
  }

  ...

}

class Decepticon {

  ...

  public static Megatron.Builder megatron() {
   return Megatron.builder();
  }

  public static Soundwave.Builder soundwave() {
   return Soundwave.builder();
  }

  ...

}

class Transformer {

  ... 

  public static Optimus.Builder optimus() {
   return Optimus.builder();
  }

  public static Bumblebee.Builder bumblebee() {
   return Bumblebee.builder();
  }

  public static Megatron.Builder megatron() {
   return Megatron.builder();
  }

  public static Soundwave.Builder soundwave() {
   return Soundwave.builder();
  }

  ...

}

------------------------
public class Example {

 public static void main() {
  //
  // Autobots
  //
  Autobot optimus = Autobot.optimus().build();
  Autobot bumblebee = Autobot.bumblebee().build();
  //
  // Decepticons
  //
  Decepticon megatron = Decepticon.megatron().build();
  Decepticon soundwave = Decepticon.soundwave().build();
  //
  // Transformers
  //
  Transformer[] transformers = {
    Transformer.optimus().build(),
    Transformer.bumblebee().build(),
    Transformer.megatron().build(),
    Transformer.soundwave().build(),
  //
  //...
  //
 }
}

------------------------

Sources:
List of The Transformers

FilipMalczak commented 7 years ago

I like the idea, though I see need for some details.

As far as I can see, handling of @Factory(T) annotation would look like this:

  1. find all instantiable subclasses/implementations of T
  2. for each subclass: 2.1. for each public constructor 2.1.1. generate public static method named the same way as the subclass and with the same argument list as constructor (alternatively, as you've shown - use builders, it doesn't matter for example's case)

The problem is in (1) - where do you look for them? a. in the same package as annotated class (what with inner classes? should we look only through the same scope, so only in outer class, or maybe in the most outer class and all below, or the package in which the most outer class is?) b. in currently compiled sources c. in the whole classpath d. add argument to annotation: @Factory(T, scanPackages: String[], scanClassPackages: Class[]) in similiar fashion as in Spring Boot; if both scan... argument are missing, we may then fall back to some variant of (a), (b) or (c).

I would propose (d) with fallback to (b) (though this may be tricky, so maybe to (a) - with the scope of whole package and its subpackages). This way it would be configurable, and would have an easy to understand fallback.


Another idea for this pattern would be grouping common constructor parameters, but it would need some questions answered too and I'm not really sure if that's a good idea:

@Value
class abstract Autobot extends Transformer {
    int healthPoints;
}

@Builder
@Value
class Optimus extends Autobot {
}

@Builder
@Value
class Bumblebee extends Autobot {
}

...

------------------------
@Value
class abstract Decepticon extends Transformer {
    boolean lovableVillain;
}

@Builder
@Value
class Megatron extends Decepticon {
}

@Builder
@Value
class Soundwave extends Decepticon {
}

...

------------------------
@Factory(Autobot.class)
class Autobots {}

@Factory(Decepticon.class)
class Decepticons {}

//
// vs. Vanilla Java
//

class Autobots {
    private final int healthPoints;

    //constructor

  public static Optimus.Builder optimus() {
   return Optimus.builder().healthPoints(healthPoints);
  }

  public static Bumblebee.Builder bumblebee() {
   return Bumblebee.builder().healthPoints(healthPoints);
  }

  // ...

}

class Decepticons {
   private final boolean lovableVillain;

   //constructor

  public static Megatron.Builder megatron() {
   return Megatron.builder().lovableVillain(lovableVillain);
  }

  public static Soundwave.Builder soundwave() {
   return Soundwave.builder().lovableVillain(lovableVillain);
  }

  // ...

}
wheredevel commented 7 years ago

@FilipMalczak, I agree with you considerations.

I think scanPackages annotation attribute is a good idea. Though, I'd propose includePackages/excludePackages and includeClasses/excludeClasses naming schema.

I'd vote for a fallback to (b) - because those sources are being processed anyway.

wheredevel commented 6 years ago

Any progress?

punkratz312 commented 6 years ago

+1

bobdrad commented 6 years ago

+1

brant-hwang commented 5 years ago

+1

janrieke commented 5 years ago

I'm just a contributor and not speaking for the dev team, but I don't think this feature will ever make it into Lombok. The reason is that it requires resolution, and that will make it very hard to implement and create a high maintenance burden. When Lombok runs, you simply cannot get info about subclasses, and there is no such thing as "currently compiled sources", because the compilation is not yet finished.

wheredevel commented 5 years ago

@janrieke , @FilipMalczak already brought it up above: https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-307555590

And I suggested this: https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-307557568
At the end of the day, we can simplify it down to just a list of subclasses (let's say, of attribute).
Even in this case, adding each subclass and its constructor method would reduce 3 lines of boilerplate code:

// Vanilla
class Superclass {
  ...
     public static Subclass.Builder subclass() {
       return Subclass.builder();
     }
  ...
}

class Subclass extends Superclass {
  ...
}

// Lombok
@Factory(of={Subclass.class})
class Superclass {
  ...
}

@Builder
class Subclass extends Superclass {
  ...
}

After all, accessor annotations reduce the same number of code lines - and they are the bread and butter of Lombok.

janrieke commented 5 years ago

You have to think of "no resolution" as if you cannot inspect anything of a referenced class. You don't know any methods or constructors of that class, you even cannot say if the class exists at all. All this is processed at a later stage of the compiler, and it's to late for Lombok then.

This means that everything you have to generate must be computable using only lexical/syntactical information from the annotated class. So you not only have to annotate the class name, but also the method name or constructor and all its parameters. This would create the most ugly Lombok annotation ever, and you would simply move the reduced lines to the annotation.

Of course you could return a builder, but this is usually not what a factory does, and it requires the referenced class to have one, which you cannot know. And as the name of the builder() can be customised, you have provide that parameter too.

wheredevel commented 5 years ago

@janrieke : You have to think of "no resolution" as if you cannot inspect anything of a referenced class. You don't know any methods or constructors of that class, you even cannot say if the class exists at all. All this is processed at a later stage of the compiler, and it's to late for Lombok then. ...

Absolutely fine with me.
Generating a constructor method, by template, for each enlisted subclass requires "no resolution" at all!!!
See my comment above for an example: https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-439678883

janrieke commented 5 years ago

BTW: The generated code you propose is not an example of the factory pattern as described by the GoF. Typically you have an interface as return type, because you want to allow to redefine which class to instantiate. Including this would further increase the annotations parameters.

wheredevel commented 5 years ago

@janrieke : BTW: The generated code you propose is not an example of the factory pattern as described by the GoF. Typically you have an interface as return type, because you want to allow to redefine which class to instantiate. Including this would further increase the annotations parameters.

I don't know if anyone would ever want it to become a fully-blown GoF.
I'm seeing @Factory annotation as simply: "Add here constructor methods for enlisted subclasses of the annotated class."
This simple feature is very important in virtually all projects I do.

randakar commented 5 years ago

Out of curiosity: Why exactly do you use this pattern so much? What is the value of that pattern?

On Sun, Nov 18, 2018 at 5:24 PM Pavel Bekkerman notifications@github.com wrote:

BTW: The generated code you propose is not an example of the factory pattern as described by the GoF. Typically you have an interface as return type, because you want to allow to redefine which class to instantiate. Including this would further increase the annotations parameters.

I don't know if anyone would ever want of to become a fully-blown GoF. I'm seeing @factory https://github.com/factory annotation as a simply: "Add here constructor methods for enlisted subclasses of the annotated class." This simple feature is very important in virtually all projects I do.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-439705214, or mute the thread https://github.com/notifications/unsubscribe-auth/AAKCRd4Ah2SRlBAaQKZ0q5Kk0RdF3ZUjks5uwYm_gaJpZM4NspHj .

-- "Don't only practice your art, but force your way into it's secrets, for it and knowledge can raise men to the divine." -- Ludwig von Beethoven

wheredevel commented 5 years ago

@randakar: Out of curiosity: Why exactly do you use this pattern so much? What is the value of that pattern?

Regarding the "value"

As my example above shows:

------------------------
public class Example {

 public static void main() {
  //
  // Autobots
  //
  Autobot optimus = Autobot.optimus().build();
  Autobot bumblebee = Autobot.bumblebee().build();
  //
  // Decepticons
  //
  Decepticon megatron = Decepticon.megatron().build();
  Decepticon soundwave = Decepticon.soundwave().build();
  //
  // Transformers
  //
  Transformer[] transformers = {
    Transformer.optimus().build(),
    Transformer.bumblebee().build(),
    Transformer.megatron().build(),
    Transformer.soundwave().build(),
  //
  //...
  //
 }
}

In other words: 1) make a discovery of a "contract" implementors as easy as possible at the codding time. 2) hide details of their construction

Note: 2 - is of less importance than 1, but nice to have.

Regarding the frequency of use

I can't reveal details of my projects, of course.
But, empirically, every project has Interfaces and their implementations, right?

As an example, let's say, I have a BinaryExpression (abstract class) and plenty of its subclasses: Addition, Subtraction, Multiplication, Division, Modulo, etc. etc.
So, ideally, I'd like my IDE to auto-suggest this:

BinaryExpression.
                 addition()
                 subtraction()
                 multiplication()
                 division()
                 modulo()
                 ...

And, it would be silly of me to expect it would auto-suggest this:

new 
    ??????

(...actually why not?!?!)

new BinaryExpression
    Addition
    Subtraction
    Multiplication
    Division
    Modulo
    ...

Another example, from the apps world.
Let's say, I have a Controller (interface) and its implementors: UserController, DataController, etc.. And also Service (interfaces) and its implementors: UserService, DataService, etc.
Now if I'm in the Spring world - great! I can both instantiate and tie controllers and services together.
Otherwise I would have to do that myself. Than the Factory approach would be of help, of course.

Now, as for "value" Point 1 above - I would really love the package auto-scanning for implementors of interest.
But, as the discussion above goes, it's a tougher task for Lombok team: https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-439678883 .
So I'm fine with manually maintaining this implementors list. For now... :)

randakar commented 5 years ago

Right. Well, I get why you find that useful. But I think that once you know the name of the class you want to instantiate the usefulness of this kinda ends. You're right. The IDE should be able to discover implementers of an interface somehow. And honestly, doing it with inheritance? That kind of sucks, if only because usually inheritance is the wrong tool for the job. (almost always, in fact. There are a few narrow cases where it's better than interfaces or delegate objects, but not many).

On Tue, Nov 20, 2018 at 9:51 PM Pavel Bekkerman notifications@github.com wrote:

  • Regarding the "value" As my example above shows:

------------------------public class Example {

public static void main() { // // Autobots // Autobot optimus = Autobot.optimus().build(); Autobot bumblebee = Autobot.bumblebee().build(); // // Decepticons // Decepticon megatron = Decepticon.megatron().build(); Decepticon soundwave = Decepticon.soundwave().build(); // // Transformers // Transformer[] transformers = { Transformer.optimus().build(), Transformer.bumblebee().build(), Transformer.megatron().build(), Transformer.soundwave().build(), // //... // } }

In other words:

  1. make a discovery of a "contract" implementors as easy as possible at the codding time.
  2. hide details of their construction

Note: 2 - is of less importance than 1, but nice to have.

  • Regarding the frequency of use I can't reveal details of my projects of course. But, empirically, every project has Interfaces and their implementations, right?

As an example, let's say, I have a BinaryExpression (abstract class) and plenty of its subclasses: Addition, Subtraction, Multiplication, Division, Modulo, etc. etc. So, ideally, I'd like my IDE to auto-suggest this:

BinaryExpression. addition() subtraction() multiplication() division() modulo() ...

And, it would be silly of me to expect it would auto-suggest this:

new ??????

(...actually why not?!?!)

new BinaryExpression Addition Subtraction Multiplication Division Modulo ...

Another example, from the apps world. Let's say, I have a Controller (interface) and its implementors: UserController, DataController, etc.. And also Service (interfaces) and its implementors: UserService, DataService, etc. Now if I'm in the Spring world - great! I can both instantiate and tie controllers and services together. Otherwise I would have to do that myself. Than the Factory approach would be of help, of course.

Now, as for Point 1 - I would really love the package auto-scanning for implementors of interest. But, as the discussion above goes, it's a tougher task for Lombok team: #1408 (comment) https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-439678883 . So I'm fine with manually maintaining this implementors list. For now... :)

Out of curiosity: Why exactly do you use this pattern so much? What is the value of that pattern? … <#m-4146766576779420309> On Sun, Nov 18, 2018 at 5:24 PM Pavel Bekkerman @.***> wrote: BTW: The generated code you propose is not an example of the factory pattern as described by the GoF. Typically you have an interface as return type, because you want to allow to redefine which class to instantiate. Including this would further increase the annotations parameters. I don't know if anyone would ever want of to become a fully-blown GoF. I'm seeing @factory https://github.com/factory https://github.com/factory annotation as a simply: "Add here constructor methods for enlisted subclasses of the annotated class." This simple feature is very important in virtually all projects I do. — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub <#1408 (comment) https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-439705214>, or mute the thread https://github.com/notifications/unsubscribe-auth/AAKCRd4Ah2SRlBAaQKZ0q5Kk0RdF3ZUjks5uwYm_gaJpZM4NspHj . -- "Don't only practice your art, but force your way into it's secrets, for it and knowledge can raise men to the divine." -- Ludwig von Beethoven

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/rzwitserloot/lombok/issues/1408#issuecomment-440424982, or mute the thread https://github.com/notifications/unsubscribe-auth/AAKCRZ2qqUHDk_6J2LD0xnclUjZ0xFcfks5uxGtFgaJpZM4NspHj .

-- "Don't only practice your art, but force your way into it's secrets, for it and knowledge can raise men to the divine." -- Ludwig von Beethoven

burdoto commented 5 years ago

+1

In my case, it would be very helpful to have the option of a "counter" field; one that increments every time an instance is created. Currently, I have this solution: https://github.com/burdoto/VBAN-API/blob/master/src/main/java/de/kaleidox/vban/packet/VBANPacketHead.java#L144, implementing https://github.com/burdoto/VBAN-API/blob/master/src/main/java/de/kaleidox/util/model/Factory.java#L22. This allows me to include an incrementing number in every new instance created by the factory.