Java方法中的参数太多,第7部分:可变状态

在我的系列文章的第七篇中,有关解决Java方法或构造函数中过多参数问题 ,我着眼于使用状态来减少传递参数的需要。 我等到本系列的第七篇文章来解决这个问题的原因之一是,它是我最不喜欢的减少传递给方法和构造函数的参数的方法之一。 也就是说,这种方法有多种口味,我绝对更喜欢某些口味。

在所有软件开发中,使用状态减少参数的方法中最著名,最被嘲笑的方法可能是使用全局变量 。 尽管从语义上讲Java没有全局变量可能是准确的,但事实是,无论是好是坏,都可以通过公共静态构造在Java中实现全局变量等效项 。 在Java中实现此目标的一种特别流行的方法是通过有状态的Singleton

马丁·福勒(Martin Fowler)在《企业应用程序架构模式》中写道:“任何全球数据在被证明无辜之前总是有罪的。” 出于多种原因 ,Java中的全局变量和“类全局”构造被认为是不好的形式。 它们会使开发人员难以维护和读取代码来知道值的定义位置,最后更改甚至是来源。 就其本质和意图而言,全局数据违反了封装和数据隐藏的原理。

MiškoHevery 面向对象的语言编写了有关静态全局变量问题的以下文章:


静态访问全局状态并不会向那些使用全局状态的构造函数和方法的读者澄清那些共享的依赖关系。 Global State和Singletons使API依赖其真正的依赖关系。 …全局状态的根本问题是它可以全局访问。 在理想情况下,一个对象应该只能与直接传递给其他对象(通过构造函数或方法调用)的其他对象进行交互。

具有全局可用状态减少了对参数的需求,因为如果两个对象已经可以直接访问该数据,则无需将其传递给另一个对象。 但是,正如Hevery所说,这完全与面向对象设计的意图正交。

随着并发应用变得越来越普遍,可变状态也是一个日益严重的问题。 在他关于Scala的JavaOne 2012演示中Scala的创建者Martin Odersky指出,在高度并发的世界中,“拥有的每一个可变状态都是一种责任”,并补充说,问题是“由并发线程访问共享的可变状态引起的不确定性。 ”

尽管有避免避免可变状态的原因,但它仍然是软件开发中普遍使用的方法。 我认为这样做的原因有很多,其中包括:写可变状态共享代码非常容易,可变共享代码确实提供了访问的便利。 某些类型的可变数据很受欢迎,因为这些类型的可变数据已经被教授和学习多年了。 最后,可变状态可能是最合适的解决方案,这是三遍。 出于最后一个原因并且为了完整起见,我现在看一下可变状态的使用如何减少方法必须期望的参数数量。

有状态的单例和静态变量

一个Java实现辛格尔顿和其他公共Java 静态字段通常可用于同一范围内的任何Java代码的Java虚拟机 (JVM)和加载同一 类加载器 [对于更多细节 ,请参见当是一个Singleton不是单身? ]。

通用存储的任何数据(至少从JVM / classloader角度而言)已可供同一JVM中的客户端代码使用并由相同的类加载器加载。 因此,无需在同一JVM / classloader组合中的客户端与方法或构造函数之间传递数据。

实例状态

尽管“静态”被认为是“全局可用的”,但也可以类似的方式使用较窄的实例级状态,以减少在同一类的方法之间传递参数的需求。 相对于全局变量,此方法的一个优点是可访问性仅限于类的实例( 私有字段)或类的子实例( 私有字段)。 当然,如果这些字段是public ,则可访问性是相当开放的,但是相同的数据不会自动提供给同一JVM / classloader中的其他代码。

下一个代码清单演示了状态数据如何以及有时用于减少给定类内部的两个方法之间对参数的需求。

用于避免传递参数的实例状态示例
/**
    * Simple example of using instance variable so that there is no need to
    * pass parameters to other methods defined in the same class.
    */
   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();
   }

   /**
    * Prints instance of Person without requiring it to be passed in because it
    * is an instance variable.
    */
   public void printPerson()
   {
      out.println(this.person);
   }

上面的示例经过精心设计和简化,但确实说明了这一点:实例变量person可以由同一类中定义的其他实例方法访问,因此不需要在这些实例方法之间传递实例。 这确实减少了潜在的public可访问性意味着可以由外部方法使用)内部方法的签名,但同时也引入了状态,现在意味着所调用的方法会影响同一对象的状态。 换句话说,不必传递参数的好处是以另一个可变状态为代价的。 为了进行比较,权衡的另一面需要传递Person实例,因为它不是实例变量,因此需要传递该实例。

传递参数而不使用实例变量的示例
/**
    * Simple example of passing a parameter rather than using an instance variable.
    */
   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);
   }

   /**
    * Prints instance of Person that is passed in as a parameter.
    * 
    * @param person Instance of Person to be printed.
    */
   public void printPerson(final Person person)
   {
      out.println(person);
   }

前两个代码清单说明可以通过使用实例状态来减少参数传递。 我通常更喜欢不要仅使用实例状态来避免传递参数。 如果出于其他原因需要实例状态,那么减少传递的参数是一个不错的附带好处,但是我不喜欢引入不必要的实例状态来简单地删除或减少参数数量。 尽管有时候在大型单线程环境中,精简参数的可读性可能已经证明了实例状态的合理性,但我觉得从精简参数中获得的轻微可读性并不值得在越来越多的非线程安全类中付出代价多线程的世界。 我仍然不喜欢在同一个类的方法之间传递大量参数,但是我可以使用parameters对象 (也许使用package-private scope class )来减少这些参数的数量并将该参数对象传递给周围而不是大量的参数。

JavaBean样式构造

JavaBeans约定/样式已在Java开发社区中变得非常流行。 诸如Spring FrameworkHibernate之类的许多框架都依赖于遵循JavaBeans约定的类,并且某些标准(如Java Persistence API)也围绕JavaBeans约定构建。 JavaBeans风格流行的原因有很多,包括它的易用性以及针对遵循此约定的此代码使用反射的能力,以避免进行额外的配置。

JavaBean样式的基本思想是使用无参数构造函数实例化对象,然后通过单参数“ set”方法设置其字段,然后通过无参数“ get”方法访问其字段。 在下面的代码清单中将对此进行演示。 第一个清单显示了一个简单的PersonBean类示例,其中包含无参数构造函数以及getter和setter方法。 该代码清单还包括它使用的一些JavaBeans样式的类。 该代码清单后跟使用该JavaBean样式类的代码。

JavaBeans样式类的示例
public class PersonBean
{
   private FullNameBean name;
   private AddressBean address;
   private Gender gender;
   private EmploymentStatus employment;
   private HomeownerStatus homeOwnerStatus;

   /** No-arguments constructor. */
   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;
   }
}

/**
 * Full name of a person in JavaBean style.
 * 
 * @author Dustin
 */
public final class FullNameBean
{
   private Name lastName;
   private Name firstName;
   private Name middleName;
   private Salutation salutation;
   private Suffix suffix;

   /** No-args constructor for JavaBean style instantiation. */
   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;

/**
 * Representation of a United States address (JavaBeans style).
 * 
 * @author Dustin
 */
public final class AddressBean
{
   private StreetAddress streetAddress;
   private City city;
   private State state;

   /** No-arguments constructor for JavaBeans-style instantiation. */
   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;
   }
}
JavaBeans样式实例化和填充的示例
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;
   }

刚刚显示的示例演示了如何使用JavaBeans样式方法。 这种方法做出了一些让步,以减少将大量参数传递给类的构造函数的需要。 相反,不会将任何参数传递给构造函数,并且必须设置所需的每个单独属性。 JavaBeans样式方法的优点之一是,与具有大量参数的构造函数相比,可读性得到了增强,因为希望每个“ set”方法都以可读的方式命名。

JavaBeans方法易于理解,在构造函数的情况下,绝对可以达到减少冗长参数的目的。 但是,这种方法也有一些缺点。 一个优点是很多繁琐的客户端代码,它们一次实例化对象并设置其属性。 忽略设置必需属性的方法很容易,因为在不离开JavaBeans约定的情况下,编译器无法强制设置所有必需参数。 也许最具破坏性,在最后一个代码清单中实例化了多个对象,这些对象从实例化到调用最终“设置”方法的时间,处于不同的不完整状态。 在这段时间内,对象处于真正的“未定义”或“不完整”状态。 “设置”方法的存在必然意味着该类的属性不能为final ,从而使整个对象高度可变。

关于Java中JavaBeans模式的普遍使用 ,一些可靠的作者对它的价值提出质疑 。 艾伦·霍鲁布(Allen Holub)的有争议的文章为什么getter和setter方法是邪恶的,开始时没有任何保留:


尽管getter / setter方法在Java中很常见,但它们并不是面向对象(OO)的。 实际上,它们会破坏代码的可维护性。 而且,存在大量的getter和setter方法是一个危险信号,即从OO角度来看,该程序不一定设计得很好。

乔什·布洛赫(Josh Bloch)用较不那么有力且更柔和的说服力谈到JavaBeans getter / setter风格:“ JavaBeans模式具有其自身的严重缺点”( Effective Java ,第二版, 项目#2 )。 在这种情况下,Bloch推荐使用构建器模式代替对象构建。

当我出于其他原因选择的框架需要JavaBeans get / set样式并且使用该框架的理由证明了我的理由时,我不反对使用JavaBeans get / set样式。 在某些方面,JavaBeans样式类也特别适合,例如与数据存储区进行交互并保存数据存储区中的数据以供应用程序使用。 但是,我不喜欢使用JavaBeans样式实例化一个问题,只是为了避免传递参数。 为此,我更喜欢其他方法之一,例如builder。

优势与优势

我在本文中介绍了减少方法或构造函数的参数数量的不同方法,但它们也有相同的权衡:暴露可变状态以减少或消除必须传递给方法或构造函数的参数数量。到构造函数。 这些方法的优点是简单,通常可读(尽管“全局变量”可能很难读取)以及易于首次编写和使用。 当然,从本文的角度来看,它们的最大优点是它们通常消除了传递任何参数的需要。

成本与劣势

此职位分享的所有方法的特点是易变状态的暴露。 如果在高度并发的环境中使用代码,则可能导致极高的成本。 当对象状态暴露给任何人随意修改时,存在一定程度的不可预测性。 可能很难知道哪个代码进行了错误的更改或未能进行必要的更改(例如,在填充新实例化的对象时未能调用“ set”方法)。

结论

甚至我之前介绍过的某些减少参数的方法(例如自定义类型和参数对象)都可以以(可选)可变状态的方式实现,但这些方法不需要可变状态。 相反,本文中涉及的将参数减少为方法和构造函数的方法确实需要可变状态。

尽管存在缺陷,但本文中介绍的某些方法还是非常受欢迎的。 这可能是出于多种原因,包括在流行框架中的普遍使用(迫使框架的用户使用该样式,还为其他人提供了自己的代码开发示例)。 这些方法之所以受欢迎的其他原因是,相对容易的初始开发以及使用这些方法进行设计时似乎(欺骗性)相对较少的思考。 总的来说,我更愿意花更多的设计和实现工作来使用构建器,而在可行时使用较少可变的方法。 但是,在某些情况下,这些可变状态方法可以很好地减少传递的参数数量,并且不会带来比已经存在的更多的风险。 我的感觉是,Java开发人员应仔细考虑使用任何可变的Java类,并确保可变性是所希望的,或者是由于使用可变状态方法的原因而证明是合理的成本。


翻译自: https://www.javacodegeeks.com/2013/11/too-many-parameters-in-java-methods-part-7-mutable-state.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值