How do I initialize classes with lots of fields in an elegant way?

删除回忆录丶 提交于 2019-11-29 21:07:54
snickers10m

You can either use a constructor or a builder pattern or a variation of the builder pattern to fix the problem of having too many fields in your initialization step.

I'm going to extend your example a bit to prove my point of why these options are useful.

Understanding your example:

Lets say an Offer is simply a container class for 4 fields:

public class Offer {
    private int price;
    private Date dateOfOffer;
    private double duration;
    private HotelOnly hotelOnly;
    // etc. for as many or as few fields as you need

    public int getPrice() {
        return price;
    }

    public Date getDateOfOffer() {
        return dateOfOffer;
    }

    // etc.
}

As it stands in your example, to set values to these fields, you use setters:

    public void setHotelOnly(HotelOnly hotelOnly) {
        this.hotelOnly = hotelOnly;
    }

Unfortunately, this means if you need an offer with values in all of the fields, you have to do what you have:

Offers offers = new Offers();
Offer offer = new Offer();
offer.setPrice(price);
offer.setDateOfOffer(date);
offer.setDuration(duration);
offer.setHotelOnly(hotelOnly);
offers.add(offer);

Now let's look at improving this.

Option 1: Constructors!

A constructor other than the default constructor (the default constructor is currently Offer() ) is useful for initializing the values of the fields in your class.

A version of Offer using constructors would look like this:

public class Offer {
    private int price;
    private Date dateOfOffer;
    //etc.

    // CONSTRUCTOR
    public Offer(int price, Date dateOfOffer, double duration, HotelOnly hotelOnly) {
        this.price = price;
        this.dateOfOffer = dateOfOffer;
        //etc.
    }

    // Your getters and/or setters
}

Now, we can initialize it in one line!

Offers offers = new Offers();
Offer offer = new Offer(price, date, duration, hotelOnly);
offers.add(offer);

Even better, if you never use offer other than that single line: offers.add(offer); you don't even need to save it in a variable!

Offers offers = new Offers();
offers.add( new Offer(price, date, duration, hotelOnly) ); // Works the same as above

Option 2: Builder Pattern

A builder pattern is useful if you want the option of having default values for any of your fields.

The problem a builder pattern solves is the following messy code:

public class Offer {
    private int price;
    private Date dateOfOffer;
    // etc.

    // The original constructor. Sets all the fields to the specified values
    public Offer(int price, Date dateOfOffer, double duration, HotelOnly hotelOnly) {
        this.price = price;
        this.dateOfOffer = dateOfOffer;
        // etc.
    }

    // A constructor that uses default values for all of the fields
    public Offer() {
        // Calls the top constructor with default values
        this(100, new Date("10-13-2015"), 14.5, new HotelOnly());
    }

    // A constructor that uses default values for all of the fields except price
    public Offer(int price) {
        // Calls the top constructor with default values, except price
        this(price, new Date("10-13-2015"), 14.5, new HotelOnly());
    }

    // A constructor that uses default values for all of the fields except Date and HotelOnly
    public Offer(Date date, HotelOnly hotelOnly) {
        this(100, date, 14.5, hotelOnly);
    }

    // A bunch more constructors of different combinations of default and specified values

}

See how messy that can get?

The builder pattern is another class that you put inside your class.

public class Offer {
    private int price;
    // etc.

    public Offer(int price, ...) {
        // Same from above
    }

    public static class OfferBuilder {
        private int buildPrice = 100;
        private Date buildDate = new Date("10-13-2015");
        // etc. Initialize all these new "build" fields with default values

        public OfferBuilder setPrice(int price) {
            // Overrides the default value
            this.buildPrice = price;

            // Why this is here will become evident later
            return this;
        }

        public OfferBuilder setDateOfOffer(Date date) {
            this.buildDate = date;
            return this;
        }

        // etc. for each field

        public Offer build() {
            // Builds an offer with whatever values are stored
            return new Offer(price, date, duration, hotelOnly);
        }
    }
}

Now, you can not have to have so many constructors, but still are able to choose which values you want to leave default, and which you want to initialize.

Offers offers = new Offers();
offers.add(new OfferBuilder().setPrice(20).setHotelOnly(hotelOnly).build());
offers.add(new OfferBuilder().setDuration(14.5).setDate(new Date("10-14-2015")).setPrice(200).build());
offers.add(new OfferBuilder().build());

That last offer is simply one with all default values. The others are default values except the ones that I set.

See how that makes things easier?

Option 3: Variation of Builder Pattern

You can also use the builder pattern by simply making your current setters return the same Offer object. It's exactly the same, except without the extra OfferBuilder class.

Warning: As user WW states below, this option breaks JavaBeans - a standard programming convention for container classes such as Offer. So, you shouldn't use this for professional purposes, and should limit your use in your own practices.

public class Offer {
    private int price = 100;
    private Date date = new Date("10-13-2015");
    // etc. Initialize with default values

    // Don't make any constructors

    // Have a getter for each field
    public int getPrice() {
        return price;
    }

    // Make your setters return the same object
    public Offer setPrice(int price) {
        // The same structure as in the builder class
        this.price = price;
        return this;
    }

    // etc. for each field

    // No need for OfferBuilder class or build() method
}

And your new initialization code is

Offers offers = new Offers();
offers.add(new Offer().setPrice(20).setHotelOnly(hotelOnly));
offers.add(new Offer().setDuration(14.5).setDate(new Date("10-14-2015")).setPrice(200));
offers.add(new Offer());

That last offer is simply one with all default values. The others are default values except the ones that I set.


So, while it's a lot of work, if you want to clean up your initialization step, you need to use one of these options for each of your classes that have fields in them. Then use the initialization methods that I included with each method.

Good luck! Does any of this need further explanation?

I've always preferred using builder-pattern-with-a-twist because it provides much more than the basic approach of the builder pattern.

But what happens when you want to tell the user that she must call one builder method or the other, since it is crucial for the class you’re trying to build.

Think about a builder for a URL component. How would one think about the builder methods for encapsulating access to URL attributes, are they equally important, do they interact with each other, etc? While the query parameters or fragment are optional the hostname is not; you could say that protocol is also required but for that you can have a meaningful default, like http right?

Anyway, I don't know if this makes sense to your particular problem but I thought it would be worth mentioning for others to have a look at it.

Some nice answeres are already given here!

What came to my mind as an addition is Domain Driven Design. Specific the Building blocks part, with Entity, Value Object, Aggregate, Factory etc.

A nice introduction is given in Domain Driven Design - Quickly (pdf).

I just provide this answer because it was mentioned in a comment and I think it should also be a part of this enumeration of Design Patterns.


Null Object Design Pattern

Intent

The intent of a Null Object is to encapsulate the absence of an object by providing a substitutable alternative that offers suitable default do nothing behavior. In short, a design where "nothing will come of nothing"

Use the Null Object pattern when

  • an object requires a collaborator. The Null Object pattern does not introduce this collaboration--it makes use of a collaboration that already exists
  • some collaborator instances should do nothing
  • you want to abstract the handling of null away from the client

Here you find the full part of "Null Object" Design Pattern

Ideally, an object should not be concerned about instantiating its dependencies. It should only worry about things that it is supposed to do with them. Have you considered any dependency injection framework? Spring or Google's Juice are quite versatile and have a small footprint.

The idea is simple, you declare the dependencies and let the framework decide when/how/where to create them and 'inject' it into your classes.

If you don't want to use any framework, you can take design notes from them and try to emulate their design patterns and tweak it for your use-case.

Also, you can simplify things to a certain extent by making proper use of Collections. For example, what additional feature does Offers have other than storing a collection of Offer? I'm not sure what your constraints there are but, if you can make that part a bit more cleaner you would have massive gains in all places where you are instantiating the objects.

Dozer framework provides nice way to do copy values from ws object to your dto. Here is another example. Additionally if the getter/setter names are the same of both class you dont need custom converter

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!