问题描述
我编写了下面的代码,如您所见,在构造函数中,我调用了一些方法来执行某些操作。 现在我要问的是,从构造函数中调用这些方法是一个好习惯还是将这些方法声明为public并从类info中实例化一个对象,让对象调用这些方法? 这有什么好的做法?
码:
class Info {
public RoadInfo(String cityName, double lat, double lng) throws FileNotFoundException, SAXException, IOException, XPathExpressionException {
// TODO Auto-generated constructor stub
this.cityname = cityName;
this.lat = lat;
this.lng = lng;
this.path = "c:"+File.separatorChar+this.cityname+".xml";
System.out.println(path);
this.initXPath();
this.method1()
this.method2()
..
this.expr = "//node[@lat='"+this.lat+"'"+"]/following-sibling::tag[1]/@v";
this.xPath.compile(this.expr);
String s = (String) this.xPath.evaluate(this.expr, this.document, XPathConstants.STRING);
System.out.println(s);
}
1楼
TLDR在我看来,使用构造函数内部的方法是糟糕设计的标志。 如果你不是在寻找设计建议,那么答案是“没有任何问题,从技术上讲,只要你避免调用非最终方法”应该没问题。 如果您正在寻找设计建议,请参阅下文。
我认为你的示例代码根本不是好的做法。 在我看来,构造函数应该只接收与它相关的值,并且不需要对这些值执行任何其他初始化。 你无法测试你的构造函数是否与所有这些额外的步骤一起“工作” - 你所能做的就是构造对象并希望一切都以正确的状态结束。 此外,您的构造函数最终会有多个更改原因,这违反了SRP。
class Info {
public RoadInfo(String cityName, double lat, double lng) throws FileNotFoundException, SAXException, IOException, XPathExpressionException {
// TODO Auto-generated constructor stub
this.cityname = cityName;
this.lat = lat;
this.lng = lng;
this.path = "c:"+File.separatorChar+this.cityname+".xml";
System.out.println(path);
this.initXPath();
this.method1()
this.method2()
..
this.expr = "//node[@lat='"+this.lat+"'"+"]/following-sibling::tag[1]/@v";
this.xPath.compile(this.expr);
String s = (String) this.xPath.evaluate(this.expr, this.document, XPathConstants.STRING);
System.out.println(s);
}
所以,例如,这个构造函数正在加载一个文件,在XPath中解析它。如果我想创建一个RoadInfo
对象,我现在只能通过加载文件并且不必担心抛出异常来实现。
这个类现在变得非常难以进行单元测试,因为现在你不能孤立地测试this.initXPath()
方法,例如 - 如果this.initXPath()
, this.method1()
或this.method2()
有任何失败,那么每个测试用例都会失败。
坏!
我希望它看起来像这样:
class RoadInfoFactory {
public RoadInfo getRoadInfo(String cityName, double lat, double lng) {
String path = this.buildPathForCityName(cityName);
String expression = this.buildExpressionForLatitute(lat);
XPath xpath = this.initializeXPath();
XDocument document = ...;
String s = (String) xpath.evaluate(expression, document, XPathConstants.STRING);
// Or whatever you do with it..
return new RoadInfo(s);
}
}
别介意你在这里至少有5个职责。
- 构建与OS无关的路径
- 为纬度/经度构建XPath表达式
- 创建XPath文档
-
检索
s
- 无论是什么 -
创建新的
RoadInfo
实例
这些责任中的每一个(除了最后一个)都应该分成它们自己的类(IMO),并让RoadInfoFactory
它们全部编排在一起。
2楼
构造函数的目的是建立类不变量,即将新创建的对象置于允许客户端使用它们的状态。 通常,对象在构造之后依赖于额外的初始化是不好的做法。 您要避免的是将这些内容写入文档中:
...在创建类
X
的实例后,记得总是调用initX()
,否则会发生不好的事情!
虽然在某些情况下很难避免它,但构造函数可能变得非常混乱。 例如,在构造函数中加载外部文件是有问题的。
在这些情况下,您可以做两件事:
-
重写您的构造函数,因此它需要文件的内容而不是名称。
让呼叫者进行加载。
主要区别在于您需要调用者在创建对象之前执行某些操作,并且您可以使用构造函数的签名来表达它:
public RoadInfo(String cityName, Document cityDatabase, double lat, double lng) {...}
当然,你可以更进一步,直接需要s
的值,让调用者进行XPath搜索。 请注意,所有这些步骤都会使班级承担单一责任,这被视为一件好事。 -
但是现在您需要调用者在构建
RoadInfo
之前执行许多步骤。 这是您可以使用工厂的地方,这些工厂也会执行额外的初始化并返回完全构建的RoadInfo
对象。
但最重要的是构造函数不能调用正在构造的可被覆盖的对象的任何方法 。
调用私有方法是好的,呼吁公众对方法this
是不是一个好主意,除非方法或类本身被标记为final
。
如果你调用这样的方法,那么重写方法的类总是有可能会破坏你的功能,比如在构造完成之前将this
暴露给外部世界。
这是一个例子:
public abstract class Foo {
public Foo(String param) {
if (this.processParam(param) == null)
throw new IllegalArgumentException( "Can't process param.");
}
protected abstract processParam(String param);
}
public class Bar extends Foo {
public Bar(String param) {super(param);}
protected processParam(String param) {
SomeOtherClass.registerListener(this); // Things go horribly wrong here
return null;
}
}
如果现在调用new Bar("x")
, Foo
的构造函数将抛出异常,因为它认为参数无效。
但Bar.processParam()
this
引用泄露给SomeOtherClass
,可能允许SomeOtherClass
使用甚至不存在的Bar
实例。
3楼
更典型地,需要大量初始化的类将通过工厂方法提供给客户端。
构造函数通常限制太多 - 一个随机的例子是无法使用try-catch
包围super
或this
开始调用。
如果提供公共工厂方法,则可以将构造函数设为私有。 构造函数只能像分配最终字段一样轻松完成工作,工厂接管。 从长远来看,这是一个更具前瞻性的设计。 许多公共图书馆不得不打破他们早期的API来引入允许其代码增长的工厂。
4楼
从构造函数中调用一些方法是一个好习惯吗?
可悲的是,这是唯一的好答案,这取决于对象 。
如果该对象旨在保存信息,那么答案必定可能不是,尽量避免它,因为一个对象应该只做 。
但是,如果对象是执行函数,那么通过调用方法等确保它已准备好执行该函数。例如,如果它是数据库连接,那么您可能希望连接到数据库构造时间,或者至少在连接池中注册自己。
它,但是,好的做法是推迟任何潜在的慢的东西,你可以推迟,直到你需要它。 在我的数据库示例中,您可能希望推迟与数据库的实际连接,但您肯定会在连接池中注册连接。
可悲的是 - 相反问题的答案:
从构造函数中调用一些方法是不好的做法?
也是因为类似的原因取决于对象 。
5楼
没有好的做法,只是你不应该做的坏习惯。
在构造函数中调用方法时,这里有一些危险:
1)该方法可以被覆盖,并且它的子类实现打破了由构造函数保护的类的约束,该工具不受你的控制。
class T {
int v;
T() {
v = getValue();
}
int getValue() {
return 1;
}
}
class Sub extends T {
@Override
int getValue() {
return -1;
}
}
当你调用new T()
,T's v假设为1,但是当你创建一个new Sub()
,'v'将被设置为-1,这可能会破坏T的约束,并且这会在不知不觉中发生。
2)半结构物体泄露,而它的状态可能是非法的。
class T {
int a, b;
T(C c) {
// status of "this" is illegal now, but visible to c
c.calc(this);
a = 1;
b = 2;
}
}
class C {
int calc(T t) {
return t.a / t.b;
}
}
3)更多我不知道的东西......
如果你可以阻止所有这些,你可以做你想要的。
6楼
- (尽量不要抛出异常。所以构造函数可以很好地初始化一个字段。)
- 不要在构造函数中调用可覆盖的方法。
关于构造函数中可覆盖方法调用的缺陷:
(子)构造函数的评估是:
-
“归零”所有字段(
0, null, 0.0, false
) - 调用超级构造函数(如果不在代码中则隐式)
- 调用所有字段初始化(字段声明= ...)
- 做其余的构造函数代码
所以:
class A {
A() { f(); }
protected void f() { }
}
class B implements A {
String a;
String b = null;
String c = "c";
B() {
//[ a = null, b = null, c = null; ]
//[ super();
// B.f();
//]
//[ b = null; ]
//[ c = "c"; ]
// "nullA" - null - "c"
System.out.printf("end %s - %s - %s%n", a, b, c);
}
@Override
protected void f() {
System.out.printf("enter f : %s - %s - %s%n", a, b, c);
// null - null - null
a += "A";
b += "B";
c += "C";
// "nullA" - "nullB" - "nullC"
System.out.printf("leave f : %s - %s - %s%n", a, b, c);
}
}
这种行为在水域中非常混乱,并且在这里进行由字段初始化立即覆盖的分配。
在构造函数中经常看到的正常调用是一个setter,它可能有一些规范化代码。
使该setter public final void setX(X x);
。