我编写了下面的代码,如您所见,在构造函数中,我调用了一些方法来执行某些操作。现在我要问的是,从构造函数中调用这些方法是一个好习惯还是将这些方法声明为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);
}
答案 0 :(得分:6)
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个职责。
s
- 无论是RoadInfo
实例这些责任中的每一个(除了最后一个)都应该分成他们自己的类(IMO),并且RoadInfoFactory
将它们全部编排在一起。
答案 1 :(得分:5)
构造函数的目的是建立类不变量,即将新创建的对象置于允许客户端使用它们的状态。一般来说,一个对象在构造之后依赖额外的初始化是不好的做法。您要避免的是将这些内容写入文档:
...创建类
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
实例。
答案 2 :(得分:4)
更典型地,需要大量初始化的类将通过工厂方法提供给客户端。构造函数通常过于严格 - 随机的例子是无法用super
围绕this
或try-catch
开场调用。
如果提供公共工厂方法,则可以将构造函数设为私有。构造函数只能像分配最终字段一样轻松完成工作,工厂接管。从长远来看,这是一个更具前瞻性的设计。许多公共图书馆不得不打破他们早期的API来引入允许其代码增长的工厂。
答案 3 :(得分:3)
从构造函数中调用一些方法是一个好习惯吗?
可悲的是,对此唯一好的答案是这取决于对象。
如果该对象旨在保留信息,则答案必须可能不是,请尽量避免,因为对象应该只是do one thing。
但是,如果对象是执行函数那么一定要确保它已准备好通过调用方法等来执行该函数。例如,它是一个数据库连接那么您可能希望在构造时连接到数据库,或者至少在连接池中注册自己。
然而,优秀的做法是推迟你可以推迟的任何可能缓慢的东西,直到你需要它为止。在我的数据库示例中,您可能希望推迟与数据库的实际连接,但您肯定会在连接池中注册连接。
可悲的是 - 相反问题的答案:
从构造函数调用某些方法是错误练习吗?
由于类似原因,依赖于对象。
答案 4 :(得分:2)
没有好的做法,只是你不应该做的坏习惯。
当你在构造函数中调用一个方法时,这里有一些危险:
1)该方法可以被覆盖,并且它的子类实现会破坏由构造函数保护的类约束,该工具不受你的控制。
class T {
int v;
T() {
v = getValue();
}
int getValue() {
return 1;
}
}
class Sub extends T {
@Override
int getValue() {
return -1;
}
}
此处,当您致电new T()
时,假设您为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)更多我不知道的事情......
如果你可以阻止所有这些,你可以做你想做的事。
答案 5 :(得分:0)
关于构造函数中可覆盖方法调用的缺陷:
(子)构造函数的评估是:
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);
。