J'ai une hiérarchie d'objets qui devient de plus en plus complexe à mesure que l'arbre de l'héritage s'approfondit. Aucune de celles-ci n'est abstraite et toutes leurs instances ont donc un but plus ou moins sophistiqué.
Comme le nombre de paramètres est assez élevé, j'aimerais utiliser le modèle de constructeur pour définir les propriétés plutôt que de coder plusieurs constructeurs. Comme je dois répondre à toutes les permutations, les classes de feuilles de mon arbre d'héritage auraient des constructeurs télescopiques.
J'ai parcouru pour trouver une réponse ici lorsque je rencontre des problèmes lors de ma conception. Tout d’abord, laissez-moi vous donner un exemple simple et superficiel pour illustrer le problème.
public class Rabbit
{
public String sex;
public String name;
public Rabbit(Builder builder)
{
sex = builder.sex;
name = builder.name;
}
public static class Builder
{
protected String sex;
protected String name;
public Builder() { }
public Builder sex(String sex)
{
this.sex = sex;
return this;
}
public Builder name(String name)
{
this.name = name;
return this;
}
public Rabbit build()
{
return new Rabbit(this);
}
}
}
public class Lop extends Rabbit
{
public float earLength;
public String furColour;
public Lop(LopBuilder builder)
{
super(builder);
this.earLength = builder.earLength;
this.furColour = builder.furColour;
}
public static class LopBuilder extends Rabbit.Builder
{
protected float earLength;
protected String furColour;
public LopBuilder() { }
public Builder earLength(float length)
{
this.earLength = length;
return this;
}
public Builder furColour(String colour)
{
this.furColour = colour;
return this;
}
public Lop build()
{
return new Lop(this);
}
}
}
Maintenant que nous avons du code, nous allons créer une Lop
:
Lop lop = new Lop.LopBuilder().furColour("Gray").name("Rabbit").earLength(4.6f);
Cet appel ne sera pas compilé car le dernier appel chaîné ne peut pas être résolu, Builder
ne définissant pas la méthode earLength
. Ainsi, cette méthode nécessite que tous les appels soient chaînés dans un ordre spécifique, ce qui est très peu pratique, en particulier avec une arborescence hiérarchique profonde.
Maintenant, lors de ma recherche de réponse, je suis tombé sur Sous-classer une classe Java Builder qui suggère d'utiliser le modèle Modèle générique curieusement récursif . Cependant, comme ma hiérarchie ne contient pas de classe abstraite, cette solution ne fonctionnera pas pour moi. Mais l'approche repose sur l'abstraction et le polymorphisme pour fonctionner, raison pour laquelle je ne crois pas pouvoir l'adapter à mes besoins.
Une approche que j'ai actuellement adoptée consiste à remplacer toutes les méthodes de la super-classe Builder
dans la hiérarchie et à simplement procéder comme suit:
public ConcreteBuilder someOverridenMethod(Object someParameter)
{
super(someParameter);
return this;
}
Avec cette approche, je peux être assuré que je suis renvoyé et que je peux émettre des appels en chaîne. Bien que ce ne soit pas aussi pire que l'anti-motif télescopique, c'est une seconde place et je le considère un peu "hacky".
Existe-t-il une autre solution à mon problème que je ne connais pas? De préférence, une solution compatible avec le modèle de conception. Je vous remercie!
Cela est certainement possible avec la liaison récursive, mais les générateurs de sous-types doivent également être génériques et vous avez besoin de quelques classes abstraites intermédiaires. C'est un peu lourd, mais c'est quand même plus simple que la version non générique.
/**
* Extend this for Mammal subtype builders.
*/
abstract class GenericMammalBuilder<B extends GenericMammalBuilder<B>> {
String sex;
String name;
B sex(String sex) {
this.sex = sex;
return self();
}
B name(String name) {
this.name = name;
return self();
}
abstract Mammal build();
@SuppressWarnings("unchecked")
final B self() {
return (B) this;
}
}
/**
* Use this to actually build new Mammal instances.
*/
final class MammalBuilder extends GenericMammalBuilder<MammalBuilder> {
@Override
Mammal build() {
return new Mammal(this);
}
}
/**
* Extend this for Rabbit subtype builders, e.g. LopBuilder.
*/
abstract class GenericRabbitBuilder<B extends GenericRabbitBuilder<B>>
extends GenericMammalBuilder<B> {
Color furColor;
B furColor(Color furColor) {
this.furColor = furColor;
return self();
}
@Override
abstract Rabbit build();
}
/**
* Use this to actually build new Rabbit instances.
*/
final class RabbitBuilder extends GenericRabbitBuilder<RabbitBuilder> {
@Override
Rabbit build() {
return new Rabbit(this);
}
}
Il existe un moyen d'éviter d'avoir les classes de feuilles "concrètes", où si nous avions ceci:
class MammalBuilder<B extends MammalBuilder<B>> {
...
}
class RabbitBuilder<B extends RabbitBuilder<B>>
extends MammalBuilder<B> {
...
}
Ensuite, vous devez créer de nouvelles instances avec un losange et utiliser des caractères génériques dans le type de référence:
static RabbitBuilder<?> builder() {
return new RabbitBuilder<>();
}
Cela fonctionne parce que la borne liée à la variable type garantit que toutes les méthodes de, par exemple. RabbitBuilder
a un type de retour avec RabbitBuilder
, même lorsque l'argument de type est simplement un caractère générique.
Je ne suis pas un grand partisan de cela, cependant, car vous devez utiliser des caractères génériques partout, et vous ne pouvez créer une nouvelle instance qu'en utilisant le losange ou un type raw . Je suppose que vous vous retrouvez avec un peu de maladresse dans les deux cas.
Et au fait, à ce sujet:
@SuppressWarnings("unchecked")
final B self() {
return (B) this;
}
Il y a un moyen d'éviter cette distribution non contrôlée, qui consiste à rendre la méthode abstraite:
abstract B self();
Et ensuite, remplacez-le dans la sous-classe leaf:
@Override
RabbitBuilder self() { return this; }
Le problème avec cette méthode est que, bien qu’il soit plus sûr du type, la sous-classe peut retourner autre chose que this
. En gros, dans les deux cas, la sous-classe a la possibilité de faire quelque chose de mal, donc je ne vois pas vraiment la raison de préférer l'une de ces approches à l'autre.
Si quelqu'un rencontre encore le même problème, je suggère la solution suivante, conforme au modèle de conception "Préférer la composition à l'héritage".
Classe parentale
L'élément principal est l'interface que la classe parente Builder doit implémenter:
public interface RabbitBuilder<T> {
public T sex(String sex);
public T name(String name);
}
Voici la classe parente modifiée avec le changement:
public class Rabbit {
public String sex;
public String name;
public Rabbit(Builder builder) {
sex = builder.sex;
name = builder.name;
}
public static class Builder implements RabbitBuilder<Builder> {
protected String sex;
protected String name;
public Builder() {}
public Rabbit build() {
return new Rabbit(this);
}
@Override
public Builder sex(String sex) {
this.sex = sex;
return this;
}
@Override
public Builder name(String name) {
this.name = name;
return this;
}
}
}
La classe enfant
La classe enfant Builder
doit implémenter la même interface (avec un type générique différent):
public static class LopBuilder implements RabbitBuilder<LopBuilder>
Dans la classe enfant Builder
le champ référençant parentBuilder
:
private Rabbit.Builder baseBuilder;
cela garantit que les méthodes parent Builder
sont appelées dans l'enfant, mais leur implémentation est différente:
@Override
public LopBuilder sex(String sex) {
baseBuilder.sex(sex);
return this;
}
@Override
public LopBuilder name(String name) {
baseBuilder.name(name);
return this;
}
public Rabbit build() {
return new Lop(this);
}
Le constructeur de Builder:
public LopBuilder() {
baseBuilder = new Rabbit.Builder();
}
Le constructeur de la classe enfant construite:
public Lop(LopBuilder builder) {
super(builder.baseBuilder);
}
Cette forme semble presque fonctionner. Ce n'est pas très bien rangé mais on dirait qu'il évite vos problèmes:
class Rabbit<B extends Rabbit.Builder<B>> {
String name;
public Rabbit(Builder<B> builder) {
this.name = builder.colour;
}
public static class Builder<B extends Rabbit.Builder<B>> {
protected String colour;
public B colour(String colour) {
this.colour = colour;
return (B)this;
}
public Rabbit<B> build () {
return new Rabbit<>(this);
}
}
}
class Lop<B extends Lop.Builder<B>> extends Rabbit<B> {
float earLength;
public Lop(Builder<B> builder) {
super(builder);
this.earLength = builder.earLength;
}
public static class Builder<B extends Lop.Builder<B>> extends Rabbit.Builder<B> {
protected float earLength;
public B earLength(float earLength) {
this.earLength = earLength;
return (B)this;
}
@Override
public Lop<B> build () {
return new Lop<>(this);
}
}
}
public class Test {
public void test() {
Rabbit rabbit = new Rabbit.Builder<>().colour("White").build();
Lop lop1 = new Lop.Builder<>().earLength(1.4F).colour("Brown").build();
Lop lop2 = new Lop.Builder<>().colour("Brown").earLength(1.4F).build();
//Lop.Builder<Lop, Lop.Builder> builder = new Lop.Builder<>();
}
public static void main(String args[]) {
try {
new Test().test();
} catch (Throwable t) {
t.printStackTrace(System.err);
}
}
}
Bien que j'ai construit avec succès Rabbit
et Lop
(dans les deux formes), je ne peux pas à ce stade déterminer comment instancier réellement l'un des objets Builder
avec son type complet.
L'essence de cette méthode repose sur la conversion en (B)
dans les méthodes Builder
. Cela vous permet de définir le type d'objet et le type de Builder
et de le conserver dans l'objet pendant sa construction.
Si quelqu'un pouvait trouver la syntaxe correcte pour cela (ce qui est faux), je l'apprécierais.
Lop.Builder<Lop.Builder> builder = new Lop.Builder<>();
Face au même problème, j’ai utilisé la solution proposée par emcmanus à l’adresse: https://community.Oracle.com/blogs/emcmanus/2010/10/24/using-builder-pattern-subclasses
Je suis juste en train de recopier sa solution préférée ici. Disons que nous avons deux classes, Shape
et Rectangle
. Rectangle
hérite de Shape
.
classe publique Shape {
private final double opacity;
public double getOpacity() {
return opacity;
}
protected static abstract class Init<T extends Init<T>> {
private double opacity;
protected abstract T self();
public T opacity(double opacity) {
this.opacity = opacity;
return self();
}
public Shape build() {
return new Shape(this);
}
}
public static class Builder extends Init<Builder> {
@Override
protected Builder self() {
return this;
}
}
protected Shape(Init<?> init) {
this.opacity = init.opacity;
}
}
Il existe la classe intérieure Init
, qui est abstraite, et la classe intérieure Builder
, qui est une implémentation réelle. Sera utile lors de la mise en œuvre de la Rectangle
:
la classe publique Rectangle s'étend Shape { double hauteur finale privée;
public double getHeight() {
return height;
}
protected static abstract class Init<T extends Init<T>> extends Shape.Init<T> {
private double height;
public T height(double height) {
this.height = height;
return self();
}
public Rectangle build() {
return new Rectangle(this);
}
}
public static class Builder extends Init<Builder> {
@Override
protected Builder self() {
return this;
}
}
protected Rectangle(Init<?> init) {
super(init);
this.height = init.height;
}
}
Pour instancier la Rectangle
:
new Rectangle.Builder().opacity(1.0D).height(1.0D).build();
Encore une fois, une classe abstraite Init
, héritant de Shape.Init
, et une Build
qui est l'implémentation réelle. Chaque classe Builder
implémente la méthode self
, chargée de renvoyer une version correctement convertie d'elle-même.
Shape.Init <-- Shape.Builder
^
|
|
Rectangle.Init <-- Rectangle.Builder
J'ai fait quelques expériences et j'ai trouvé que cela fonctionnait assez bien pour moi ..__ Notez que je préfère créer l'instance réelle au début et appeler tous les setters de cette instance. Ceci est juste une préférence.
La principale différence avec la réponse acceptée est que
Le code:
public class MySuper {
private int superProperty;
public MySuper() { }
public void setSuperProperty(int superProperty) {
this.superProperty = superProperty;
}
public static SuperBuilder<? extends MySuper, ? extends SuperBuilder> newBuilder() {
return new SuperBuilder<>(new MySuper());
}
public static class SuperBuilder<R extends MySuper, B extends SuperBuilder<R, B>> {
private final R mySuper;
public SuperBuilder(R mySuper) {
this.mySuper = mySuper;
}
public B withSuper(int value) {
mySuper.setSuperProperty(value);
return (B) this;
}
public R build() {
return mySuper;
}
}
}
et ensuite une sous-classe ressemble à ceci:
public class MySub extends MySuper {
int subProperty;
public MySub() {
}
public void setSubProperty(int subProperty) {
this.subProperty = subProperty;
}
public static SubBuilder<? extends MySub, ? extends SubBuilder> newBuilder() {
return new SubBuilder(new MySub());
}
public static class SubBuilder<R extends MySub, B extends SubBuilder<R, B>>
extends SuperBuilder<R, B> {
private final R mySub;
public SubBuilder(R mySub) {
super(mySub);
this.mySub = mySub;
}
public B withSub(int value) {
mySub.setSubProperty(value);
return (B) this;
}
}
}
et une classe de sous-sous-marin
public class MySubSub extends MySub {
private int subSubProperty;
public MySubSub() {
}
public void setSubSubProperty(int subProperty) {
this.subSubProperty = subProperty;
}
public static SubSubBuilder<? extends MySubSub, ? extends SubSubBuilder> newBuilder() {
return new SubSubBuilder<>(new MySubSub());
}
public static class SubSubBuilder<R extends MySubSub, B extends SubSubBuilder<R, B>>
extends SubBuilder<R, B> {
private final R mySubSub;
public SubSubBuilder(R mySub) {
super(mySub);
this.mySubSub = mySub;
}
public B withSubSub(int value) {
mySubSub.setSubSubProperty(value);
return (B)this;
}
}
}
Pour vérifier que tout fonctionne correctement, j'ai utilisé ce test:
MySubSub subSub = MySubSub
.newBuilder()
.withSuper (1)
.withSub (2)
.withSubSub(3)
.withSub (2)
.withSuper (1)
.withSubSub(3)
.withSuper (1)
.withSub (2)
.build();
La contribution suivante à la conférence IEEE Refined Fluent Builder in Java fournit une solution complète au problème.
Il dissèque la question initiale en deux sous-problèmes héritage, déficience et quasi invariance, et montre comment une solution à ces deux sous-problèmes ouvre la voie à la prise en charge de l'héritage avec réutilisation du code dans le modèle de construction classique en Java.
Comme vous ne pouvez pas utiliser de génériques, la tâche principale consiste probablement maintenant à relâcher la frappe ... Je ne sais pas comment vous traitez ces propriétés, mais que se passe-t-il si vous utilisez une carte de hachage pour les stocker en tant que paires clé-valeur? Il y aura donc une seule méthode wrapper set (clé, valeur) dans le générateur (ou le constructeur pourrait ne plus être nécessaire).
L'inconvénient serait des transtypages supplémentaires lors du traitement des données stockées.
Si le cas est trop vague, vous pouvez conserver les propriétés existantes, mais utiliser une méthode d'ensemble générale, qui utilise la réflexion et recherche la méthode de définition sur la base du nom 'clé'. Bien que je pense que la réflexion serait une overkill.
La solution la plus simple serait simplement de remplacer les méthodes de définition de la classe parente.
Vous évitez les génériques, il est facile à utiliser, à étendre et à comprendre et vous évitez également la duplication de code lorsque vous appelez super.setter.
public class Lop extends Rabbit {
public final float earLength;
public final String furColour;
public Lop(final LopBuilder builder) {
super(builder);
this.earLength = builder.earLength;
this.furColour = builder.furColour;
}
public static class LopBuilder extends Rabbit.Builder {
protected float earLength;
protected String furColour;
public LopBuilder() {}
@Override
public LopBuilder sex(final String sex) {
super.sex(sex);
return this;
}
@Override
public LopBuilder name(final String name) {
super.name(name);
return this;
}
public LopBuilder earLength(final float length) {
this.earLength = length;
return this;
}
public LopBuilder furColour(final String colour) {
this.furColour = colour;
return this;
}
@Override
public Lop build() {
return new Lop(this);
}
}
}