如何以优雅的方式初始化有许多字段的类?

34

在我的应用程序中,我需要实例化许多不同类型的对象。每种类型都包含一些字段,并且需要添加到一个包含类型中。有什么优雅的方法可以做到这一点?

我的当前初始化步骤看起来像这样:

public void testRequest() {

        //All these below used classes are generated classes from xsd schema file.

        CheckRequest checkRequest = new CheckRequest();

        Offers offers = new Offers();
        Offer offer = new Offer();
        HotelOnly hotelOnly = new HotelOnly();
        Hotel hotel = new Hotel();
        Hotels hotels = new Hotels();
        Touroperator touroperator = new Touroperator();
        Provider provider = new Provider();
        Rooms rooms = new Rooms();
        Room room = new Room();
        PersonAssignments personAssignments = new PersonAssignments();
        PersonAssignment personAssignment = new PersonAssignment(); 
        Persons persons = new Persons();
        Person person = new Person();
        Amounts amounts = new Amounts();

        offers.getOffer().add(offer);
        offer.setHotelOnly(hotelOnly);

        room.setRoomCode("roomcode");
        rooms.getRoom().add(room);

        hotels.getHotel().add(hotel);
        hotel.setRooms(rooms);

        hotelOnly.setHotels(hotels);

        checkRequest.setOffers(offers);

        // ...and so on and so on
    } 

我希望能避免编写像这样的代码,因为需要单独实例化每个对象并跨多行代码初始化每个字段有点混乱(例如,需要调用new Offer(),然后setHotelOnly(hotelOnly),然后add(offer))。

有哪些优雅的方法可以代替我现在的写法?是否有可以使用的“工厂”?您是否有任何参考资料/示例避免编写此类代码?

我非常想实现干净的代码。


上下文:

我正在开发一个RestClient应用程序,用于向Webservice发送post请求。

API表示为xsd模式文件,并使用JAXB创建了所有对象。

在发送请求之前,我必须实例化许多对象,因为它们彼此之间存在依赖关系。 (一个Offer有Hotels,一个Hotel有Rooms,一个Room有Persons...而这些类是生成的)

谢谢您的帮助。



我希望以更优雅的方式替代目前的方法。是否有可用的“工厂”,或者有没有其他示例能够避免编写类似的代码?在这个RestClient应用程序中,我需要实例化很多对象,并且它们之间存在依赖关系(例如,Offer包含Hotels,Hotel包含Rooms,Room包含Persons等)。我想实现更清晰的代码。


http://www.javacodegeeks.com/2013/01/the-builder-pattern-in-practice.html - Jean-François Savard
你为什么要创建那么多对象?只是为了摆脱null,还是所有对象中都有实际内容?如果你无论如何都这样做,难道不能默认初始化所有字段(private FieldType field = new FieldType())吗?这将导致整个层次结构只有一个new - zapl
我几乎需要所有这些对象。最终,所有这些对象都有值并需要在 ´checkRequest´ 中发送。当然我可以使用默认值初始化所有字段。但这样做并不是很美观,对吧? - Patrick
也许可以添加Null Object,用于某些对象的默认初始化。 - Verhagen
6个回答

49
您可以使用构造函数、建造者模式或建造者模式的变体来解决初始化步骤中存在过多字段的问题。
我将扩展您的示例以证明这些选项为什么有用。
了解您的示例:
假设一个“Offer”只是一个包含4个字段的容器类:
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.
}

在您的示例中,要为这些字段设置值,您需要使用setter:

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

不幸的是,这意味着如果您需要在所有字段中使用值的报价,您必须执行您所拥有的操作:

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

现在让我们来看如何改进这个问题。
选项1:构造函数!
除默认构造函数(当前默认构造函数为Offer())外的其他构造函数,有助于初始化类中字段的值。
使用构造函数的Offer版本将如下所示:
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
}

现在,我们可以用一行代码来初始化它!
Offers offers = new Offers();
Offer offer = new Offer(price, date, duration, hotelOnly);
offers.add(offer);

更好的是,如果你除了这一行代码:offers.add(offer);从未使用过offer,那么你甚至不需要将它保存在一个变量中!
Offers offers = new Offers();
offers.add( new Offer(price, date, duration, hotelOnly) ); // Works the same as above

选项2:生成器模式

生成器模式对于希望为任何字段设置默认值的用户非常有用。

生成器模式解决的问题是以下混乱的代码:

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

}

看看会变得多么混乱?

建造者模式是另一个类,你将其放置在你的类内部

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);
        }
    }
}

现在,您不必拥有那么多构造函数,但仍然可以选择哪些值保持默认状态,哪些值要初始化。
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());

那最后一个提议只是带有所有默认值的一个。其他的都是除了我设置的默认值之外的默认值。
看到这样是否更加容易明白了呢?
选项3:构建者模式的变种
你也可以通过使当前的setter返回相同的Offer对象来简单地使用构建者模式。它与具有额外OfferBuilder类的完全相同。
警告:正如user WW在下面所述,此选项会破坏JavaBeans-用于包含类(如Offer)的标准编程约定。因此,不应将其用于专业目的,并且应限制其在自己的实践中的使用。
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
}

而您的新初始化代码是

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());

那个最后的提供只是具有所有默认值的一个。其他的是除了我设置的值之外都是默认值。
因此,如果您想清理初始化步骤,需要为每个具有字段的类使用这些选项之一。然后使用我提供的每种方法的初始化方法。祝你好运!这些内容需要进一步解释吗?

1
那是一个非常好的答案。只有一个问题。我已经有了生成的类,比如OfferHotel...,并且不想编辑它们。我应该创建扩展生成类的新类吗?比如public class OfferBuilder extends Offer,然后添加像Hotel这样的字段? - Patrick
是的!差不多。只要在独立的文件中创建OfferBuilder,并且它要有某种方式来设置Offer中的字段,那么就可以实现这个目标。所以Offer要么需要有setter方法或构造函数,然后OfferBuilder将使用建造者模式,添加类似于buildHotel的新字段,然后在construct()build()方法中使用这些setter方法或构造函数,在builder中返回一个新的具有与构建器中字段相等的Offer对象。听起来清楚吗? - snickers10m
如果您想将Offer作为JavaBean使用,例如实体,则我会选择选项2,因为setter应该返回void - frifle
3
好的回答。选项2是最佳选择。选项1会失控,迫使您在可以使用默认设置的情况下进行设置。选项3违反了Java Bean约定。我们在为测试设置复杂对象时广泛使用选项2。 - WW.
2
选项2是我最喜欢的。如果您正在使用Eclipse并让它生成getter/setter方法,则可以将Java代码模板更改为:${field} = ${param};返回this; - Conffusion
显示剩余2条评论

10

我一直更喜欢使用builder-pattern-with-a-twist,因为它提供的功能比基本的建造者模式更多。

但是当你想要告诉用户必须调用其中一个构建者方法时,该怎么办呢?因为这对于你正在构建的类非常重要。

考虑一个URL组件的构建器。如何考虑用于封装对URL属性的访问的构建器方法,它们是否同样重要,它们是否彼此交互等等?虽然查询参数或片段是可选的,但主机名不是;你可以说协议也是必需的,但是对于这个问题,你可以有一个有意义的默认值,比如 http,对吧?

无论如何,我不知道这是否符合您的特定问题,但我认为值得提到,让其他人看看它。


感谢Filip提供这个答案。强制字段的部分非常有用。 - Patrick

3

我不认为像这样引用DDD 直接回答了这个问题。 - Chris Moutray
阅读上下文中的答案(https://dev59.com/iFwY5IYBdhLWcg3wCD6W),其中提供了很好的示例,但缺少对DDD理论的参考。像“Offers”,“Hotels”,“Rooms”这样的类是DDD存储库类。像“Offer”,“Hotel”,“Room”这样的类是DDD实体类。为这些实体类创建“Builder”类将是创建不可变实体类的好方法。new Hotel.Builder().setName("Hampton by Hilton Amsterdam").create(); - Verhagen

1
我提供这个答案,因为它在评论中被提到,我认为它也应该成为设计模式枚举的一部分。

空对象设计模式

目的

空对象的目的是通过提供可替代的默认“什么也不做”行为来封装对象的缺失。简而言之,这是一个“无中生有”的设计。

当以下情况出现时使用空对象模式:

  • 对象需要合作者。空对象模式不会引入此协作——它利用已经存在的协作
  • 某些合作者实例应该什么都不做
  • 您想将空处理抽象化,使客户端无需关注

在这里找到“空对象”设计模式的完整部分


1
理想情况下,一个对象不应该关心实例化它的依赖项。它只需要担心它应该如何使用它们。 你考虑过使用任何依赖注入框架吗?Spring或Google's Juice非常灵活且占用空间小。
这个想法很简单,你声明依赖项,让框架决定何时/如何/在哪里创建它们并将其“注入”到你的类中。
如果你不想使用任何框架,你可以从中获取设计笔记,并尝试模仿它们的设计模式并为你的用例进行调整。
此外,你可以通过正确使用集合来简化事情。例如,Offers有什么额外的功能,除了存储Offer的集合?我不确定你的约束是什么,但如果你能使这部分更加清晰,你就可以在实例化对象的所有地方获得巨大的收益。

0

Dozer框架提供了一种很好的方式,可以将ws对象中的值复制到您的dto中。这里是另一个示例。此外,如果getter/setter名称在两个类中相同,则不需要自定义转换器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接