Java方法参数太多怎么办—Part 7—可变状态变量

目录

  1. 自定义类型
  2. 引入参数对象
  3. Builder模式
  4. 重载
  5. 方法命名
  6. 方法返回值
  7. 可变状态变量

在Java方法参数过多解决方法第七篇中,我将关注使用状态变量来减少传入参数的个数。之所以到现在才提到这个方法,因为这是一种我不太喜欢的解决方法。也就是说,这个问题有多种解决方法,相对而言我会更偏好其中的几种。

在软件开发中,通过状态变量来减少方法参数,其中最普遍和最受轻视方式也许就是使用全局变量了。尽管从严格的语义角度上说,Java没有全局变量,但无论如何,实际上是可以通过Java中的公共静态结构来实现全局变量的。一种非常普遍的Java方式是使用带状态的单例

在企业级应用架构模式中,Martin Fowler写到“任何全局数据通常都是有问题的,除非可以证明它确实没有问题”。多种原因导致Java中的全局变量和其他类似的全局结构被认为是一种糟糕的做法。它们会增加开发者的维护和阅读代码的难度,不容易查找变量定义或是最近一次修改,甚至搞不清变量的意义。全局数据的本质和含义违背了数据的封装和隐藏原则。

Misko Hevery在讨论面向对象语言中的静态全局变量问题时写到

静态访问全局变量造成了使用了它们的构造函数和方法间共享依赖关系无法清楚地表示。全局变量和单例掩盖了API之间真实的依赖关系……全局变量问题的根源在于它可被全局访问。理想的情况下,一个对象应该仅仅与那些通过构造函数或方法调用传入的对象交互。

当两个对象都可以直接访问某个数据时,就没有必要将该数据从一个对象传入另一个对象。因此全局共享变量可以减少方法参数。尽管如此,如同Hevery所提到的,这有悖于面向对象设计的初衷。

随着并发程序变得越来越常见,可变状态引发的问题也日益严重。在JavaOne 2012关于Scala的演示中,Scala的发明者Martin Odersky谈到:在高并发环境下,每个可变状态都会成为障碍。他还同时指出,问题的产生是由于并发线程间访问共享可变状态带来的不确定性。

尽管我们有理由去避免使用可变状态,但在软件开发中它仍然是一个普遍方法。我想这有多种原因。编写共享可变状态的代码相当容易,而且代码也提供了简单访问方法。多年以来,一些可变状态已经被证明是有效的,所以它们变得非常普及。与此同时,在某些特定的场景中,可变状态可能是最适合的解决办法。基于此,让我们看看如何使用可变状态减少方法参数。

带状态的单例和静态变量

通常而言,Java实现单例和公共静态成员对任何运行在同一个Java虚拟机(JVM)上并且被同一个类加载器加载的代码而言都是可用的(更多细节,请参见“When is a Singleton not a Singleton?”)。

(从JVM和类加载器的角度来看)任何全局存储的数据对于在同一个JVM上运行并使用同一类加载器加载的客户端代码是可见的。因此,不需要在同一个JVM和类加载器加载的客户端代码、方法和构造函数间传递这些全局数据。

实例状态

通常static被认为是全局可见,但是实例状态变量也可以使用类似的方式减少方法间传递的参数。相比static的全局变量而言,后者的优势在于它的可见性被缩小到类的实例范围(private字段)或者子类的实例内(protected字段)。当然,如果字段为public,它的可见性会更广。但是,数据并不会对同一个JVM和类加载器内的其他代码自动可见。

下面的代码展示了如何使用状态变量减少类中两个方法传递的参数。

    /**
     * 这个简单示例展示了如何使用实例变量避免在同一个类的方法间传递参数。
     */
    public void doSomethingGoodWithInstanceVariables() {
        this.person = Person.createInstanceWithNameAndAddressOnly(
                new FullName.FullNameBuilder(new Name("Flintstone"), new Name(
                        "Fred")).createFullName(), new Address.AddressBuilder(
                        new City("Bedrock"), State.UN).createAddress());
        printPerson();
    }

    /**
     * 由于person是一个实例变量,因此可以直接输出该实例而不用作为参数传递。
     */
    public void printPerson() {
        out.println(this.person);
    }

上面的简单示例是为了说明故意设计的,但它确实说明了问题:实例变量person可以被类中其他实例方法访问到,所以不需要在这些实例方法间传递实例对象。这样就减少了内部方法潜在的变量,但同时引入了状态变量。这意味着,调用这些方法会影响同一个对象的状态变量。换句话说,不需要传递变量的代价是使用了另一个可变状态。为了对比,下面列出了另一种权衡方案(由于Person对象不是实例变量,所以需要在方法间进行传递)。

下面的示例使用参数传递而不是实例变量。

    /**
     * 使用参数传递而不是实例变量。
     */
    public void doSomethingGoodWithoutInstanceVariables() {
        final Person person = Person.createInstanceWithNameAndAddressOnly(
                new FullName.FullNameBuilder(new Name("Flintstone"), new Name(
                        "Fred")).createFullName(), new Address.AddressBuilder(
                        new City("Bedrock"), State.UN).createAddress());
        printPerson(person);
    }

    /**
     * 输出传入的Person参数。
     * 
     * @param person
     *            Instance of Person to be printed.
     */
    public void printPerson(final Person person) {
        out.println(person);
    }

上面的两段代码说明了如何通过使用实例变量减少传递的参数。一般我不建议单纯为了减少参数传递而使用实例变量。如果由于其他的原因而使用实例变量,那么通过它来减少传递参数的个数是额外带来的好处。但是我不喜欢仅仅为了去掉或减少参数增加不必要的实例变量。尽管在大规模的单线程环境下,存在增强可读性、减少参数个数而使用实例变量的情形。但是我认为在愈来愈多的多线程环境下,单纯为了增强可读性、减少参数而把类变成非线程安全是不值得的。当然我更不喜欢在不同方法间传递大量参数,但是我们可以使用参数对象(比如包范围内的类)来减少参数,并在方法间传递这个对象而不是大量参数。

构造JavaBean

在Java开发社区,JavaBean规范已经变得非常普及。许多框架都依赖遵循JavaBean规范的类,比如SpringHibernate。一些标准同样也建立于JavaBean规范的基础上,比如Java持久化API。JavaBean规范的普及有许多原因,比如易于使用、不需要额外的配置就可以使用反射的能力。

JavaBean规范的基本思想是,使用不带参数的构造函数实例化一个对象,然后通过只有一个参数的set方法设置属性字段,并通过不带参数的get方法来访问这些属性字段。下面的示例展示了这个过程。第一段代码是一个PersonBean类,它有一个不带参数的构造函数和对应的settergetter方法。代码中也包括了它使用到的其他一些JavaBean。第二段代码展示了JavaBean的调用方式。

JavaBean

public class PersonBean {
    private FullNameBean name;
    private AddressBean address;
    private Gender gender;
    private EmploymentStatus employment;
    private HomeownerStatus homeOwnerStatus;

    /** 无参构造函数 */
    public PersonBean() {
    }

    public FullNameBean getName() {
        return this.name;
    }

    public void setName(final FullNameBean newName) {
        this.name = newName;
    }

    public AddressBean getAddress() {
        return this.address;
    }

    public void setAddress(final AddressBean newAddress) {
        this.address = newAddress;
    }

    public Gender getGender() {
        return this.gender;
    }

    public void setGender(final Gender newGender) {
        this.gender = newGender;
    }

    public EmploymentStatus getEmployment() {
        return this.employment;
    }

    public void setEmployment(final EmploymentStatus newEmployment) {
        this.employment = newEmployment;
    }

    public HomeownerStatus getHomeOwnerStatus() {
        return this.homeOwnerStatus;
    }

    public void setHomeOwnerStatus(final HomeownerStatus newHomeOwnerStatus) {
        this.homeOwnerStatus = newHomeOwnerStatus;
    }
}

/**
 * Person全名的JavaBean。
 * 
 * @author Dustin
 */
public final class FullNameBean {
    private Name lastName;
    private Name firstName;
    private Name middleName;
    private Salutation salutation;
    private Suffix suffix;

    /** 无参构造函数,用来初始化JavaBean */
    private FullNameBean() {
    }

    public Name getFirstName() {
        return this.firstName;
    }

    public void setFirstName(final Name newFirstName) {
        this.firstName = newFirstName;
    }

    public Name getLastName() {
        return this.lastName;
    }

    public void setLastName(final Name newLastName) {
        this.lastName = newLastName;
    }

    public Name getMiddleName() {
        return this.middleName;
    }

    public void setMiddleName(final Name newMiddleName) {
        this.middleName = newMiddleName;
    }

    public Salutation getSalutation() {
        return this.salutation;
    }

    public void setSalutation(final Salutation newSalutation) {
        this.salutation = newSalutation;
    }

    public Suffix getSuffix() {
        return this.suffix;
    }

    public void setSuffix(final Suffix newSuffix) {
        this.suffix = newSuffix;
    }

    @Override
    public String toString() {
        return this.salutation + " " + this.firstName + " " + this.middleName
                + this.lastName + ", " + this.suffix;
    }
}

package dustin.examples;

/**
 * 美国地址(JavaBean风格)
 * 
 * @author Dustin
 */
public final class AddressBean {
    private StreetAddress streetAddress;
    private City city;
    private State state;

    /** 无参构造函数,用来初始化JavaBean */
    private AddressBean() {
    }

    public StreetAddress getStreetAddress() {
        return this.streetAddress;
    }

    public void setStreetAddress(final StreetAddress newStreetAddress) {
        this.streetAddress = newStreetAddress;
    }

    public City getCity() {
        return this.city;
    }

    public void setCity(final City newCity) {
        this.city = newCity;
    }

    public State getState() {
        return this.state;
    }

    public void setState(final State newState) {
        this.state = newState;
    }

    @Override
    public String toString() {
        return this.streetAddress + ", " + this.city + ", " + this.state;
    }
}

JavaBean的实例化和使用

public PersonBean createPerson()
{
   final PersonBean person = new PersonBean();
   final FullNameBean personName = new FullNameBean();
   personName.setFirstName(new Name("Fred"));
   personName.setLastName(new Name("Flintstone"));
   person.setName(personName);
   final AddressBean address = new AddressBean();
   address.setStreetAddress(new StreetAddress("345 Cave Stone Road"));
   address.setCity(new City("Bedrock"));
   person.setAddress(address);
   return person;
}

上面的示例说明了如何使用JavaBean。这种方式对减少类构造器参数做出了让步。相应的,构造器没有传入任何参数,而类中的每一个属性都需要合理设置。比起要在构造器中传递大量参数,JavaBean的一个优点是增强了代码的可读性。因为理论上每个set方法都以易于理解的方式进行命名。

JavaBean很容易理解,也确实达到了减少构造函数参数的目的。但是,它同样存在某些缺点。其中一个缺点就是,需要有大量冗长的客户端代码来实例化对象,并且每次只能设置对象的一个属性。这种方式很容易忘记设置某个必须的属性,因为除了JavaBean规范外,对于编译器来说没有办法强制设置所有必须的属性。最具破坏性的也许就是,上面的代码实例化了多个对象,而这些对象从实例化开始时到最后一个set方法被调用一直处于不完整状态。在这期间,对象处于“未定义”或者“不完整”状态。set方法的存在表示类的属性不能是final,整个对象都极易变化。

对于JavaBean的普遍使用,几位资深专家对它价值已经提出了质疑。在Allen Holub富有争议的“Why getter and setter methods are evil”文章中,开篇毫不避讳的提到:

尽管gettersetter方法在Java代码中很常见,但他们并不是特别面向对象。实际上,它们会破坏你代码的可维护性。而且,大量的gettersetter方法意味着你的程序从面向对象的角度来看不一定设计合理。

同样,对于JavaBean的gettersetter方式,Josh Bloch较温和但很有说服力的提到:“JavaBean本身就存在严重的缺陷” (Effective Java, Second Edition, Item #2)。也正是在这种情况下,Bloch推荐在对象构造中使用builder模式。

我并不反对在框架自身要求,并且有理由证明要求正确性的情况下使用JavaBean的getset方式。并且在一些情况非常适合使用JavaBean,比如与数据源的交互和应用程序获取数据。但是我并不喜欢简单地使用JavaBean来避免参数传递问题。我宁愿使用其他方式来解决这个问题,比如builder模式。

使用可变状态变量的好处和优点

本文中我介绍了减少方法或构造函数参数的不同方法,它们的共同点都是通过对外暴露可变状态变量来减少或消除方法或构造函数参数。这些方法的优点是,通常它们都比较简单,而且通常可读性都比较好(除了全局变量比较难于理解)、易于编写和使用。当然,在本文看来它们最大的优点就是消除了参数传递的需求。

使用可变状态变量的代价和缺点

本文提到方法的共同点是它们都对外暴露了可变状态变量。如果代码运行在高并发环境中,这么做将会付出非常大的代价。当对象的状态变量暴露给外界同时任何人都可以随意修改时,将会导致某种程度的不确定性。一旦出错将很难定位究竟是哪块代码错误地进行了修改,或是未能做出必要的修改(比如在生成一个新的实例化对象的时候未能调用set方法)。

结论

尽管存在缺陷,但是本文中提到的某些方法仍然被普遍使用。这其中有多种原因,比如在目前流行的框架中已经得到普遍的使用(强制用户使用某种方式,或者为其他代码开发提供示例)。其他的原因还包括,初始开发时设计相对简单或考虑不周。一般而言,在实际开发中,我更喜欢在设计和实现上多花些精力,尽量使用builder模式减少可变状态变量。然而,在某些场景中确实可以通过可变状态变量来有效地减少参数传递,而且不会引入其他已知风险。我的想法是,Java开发者应该谨慎考虑使用任何存在可变状态变量的Java类。并确保在使用可变状态变量时,认真考虑带来的易变性,或是提供充足的理由表明值得为此付出相应的代价。

原文链接: dzone 翻译: ImportNew.com - 杨欣
译文链接: http://www.importnew.com/8926.html
[ 转载请保留原文出处、译者和译文链接。]

关于作者: 杨欣

(新浪微博:@shiwoyx

查看杨欣的更多文章 >>



相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部