对象构造函数做多少工作是合理的?它应该只是初始化字段而不是实际对数据执行任何操作,还是可以让它执行某些分析?
背景 我正在编写一个类,负责解析HTML页面并根据解析的信息返回各种信息。类的设计使得类的构造函数执行解析,如果发生错误则抛出异常。初始化实例后,解析后的值无需通过访问器进行进一步处理即可使用。类似的东西:
public class Parser {
public Parser(final String html) throws ParsingException {
/* Parsing logic that sets private fields */
/* that throws an error if something is erroneous.*/
}
public int getNumOfWhatevers() { return private field; }
public String getOtherValue() { return other private field; }
}
在设计课程后,我开始怀疑这是否是正确的OO练习。解析代码是否应放在void parseHtml()
方法中,并且一旦调用此方法,访问器只返回有效值?我觉得我的实现是正确的,但是我不禁觉得有些OO纯粹主义者可能因为某种原因发现它不正确,并且以下的实现会更好:
public class Parser {
public Parser(final String html) {
/* Remember html for later parsing. */
}
public void parseHtml() throws ParsingException {
/* Parsing logic that sets private fields */
/* that throws an error if something is erroneous.*/
}
public int getNumOfWhatevers() { return private field; }
public String getOtherValue() { return other private field; }
}
是否存在初始化代码(例如解析信息)不应出现在构造函数中的情况,或者我只是在愚蠢地猜测自己?
从构造函数中拆分解析有什么好处/缺点?
思考?见解?
答案 0 :(得分:36)
答案 1 :(得分:18)
“解析代码是否应放在void parseHtml()方法中,并且访问者只有在调用此方法后才返回有效值?”
是
“类的设计是这样的,'类构造函数进行解析”
这可以防止自定义,扩展,以及 - 最重要的 - 依赖注入。
有时您想要执行以下操作
构建解析器。
向解析器添加功能:业务规则,过滤器,更好的算法,策略,命令等等。
解析。
通常,最好在构造函数中尽可能少地执行,以便您可以自由扩展或修改。
修改强>
“扩展程序无法简单地解析其构造函数中的额外信息吗?”
只有当他们没有任何需要注入的功能时。如果要添加功能 - 例如构建解析树的不同策略 - 您的子类必须在解析之前管理此功能添加。它可能不等于简单super()
,因为超类做得太多了。
“另外,在构造函数中解析可以让我早点失败”
有点儿。在施工期间失败是一个奇怪的用例。在构造期间失败使得构造这样的解析器变得困难......
class SomeClient {
parser p = new Parser();
void aMethod() {...}
}
通常施工失败意味着你的记忆力不足。很少有理由抓住施工异常,因为无论如何你都注定要失败。
你被迫在方法体中构建解析器,因为它有太复杂的参数。
简而言之,您已从解析器的客户端中删除了选项。
“从此类继承以替换算法是不可取的。”
这很有趣。认真。这是一个令人发指的声明。对于所有可能的用例,没有算法是最佳的。通常,高性能算法会占用大量内存。客户端可能希望将算法替换为使用较少内存的较慢算法。
你可以声称完美,但这种情况很少见。子类是常态,而不是例外。有人会永远改善你的“完美”。如果你限制了他们对解析器进行子类化的能力,那么他们只需将其丢弃以获得更灵活的东西。
“我没有看到答案中描述的第2步。”
一个大胆的声明。依赖性,策略和相关的注射设计模式是常见的要求。实际上,它们对于单元测试非常重要,因为设计使其变得困难或复杂,通常会成为一种糟糕的设计。
限制子类化或扩展解析器的能力是一个糟糕的策略。
底线。
什么也不做。写一个关于它的用例尽可能少的假设的类。在构建时进行解析会对客户端用例做出太多假设。
答案 2 :(得分:5)
我可能只是通过足够的初始化对象,然后有一个'解析'方法。这个想法是昂贵的操作应该尽可能明显。
答案 3 :(得分:5)
构造函数应该尽一切可能将该实例置于可运行,有效,可立即使用的状态。如果这意味着一些验证或分析,我会说它属于那里。请注意构造函数的作用。
您的设计中可能还有其他地方也可以验证验证。
如果输入值来自用户界面,我会说它应该确保有效输入。
如果输入值是从传入的XML流中解组的,我会考虑使用模式来验证它。
答案 4 :(得分:3)
您应该尝试让构造函数不要做不必要的工作。最后,这一切都取决于班级应该做什么,以及应该如何使用。
例如,在构造对象后是否会调用所有访问器?如果没有,那么您已经不必要地处理了数据。此外,抛出“无意义”异常的风险更大(哦,在尝试创建解析器时,我收到错误,因为文件格式错误,但我甚至没有要求它解析任何内容......)
第二个想法,您可能需要在构建数据后快速访问此数据,但您可能需要很长时间才能构建对象。在这种情况下可能没问题。
无论如何,如果构建过程很复杂,我建议使用creational pattern(工厂,构建器)。
答案 5 :(得分:3)
仅仅初始化构造函数中的字段是一个很好的经验法则,否则尽可能少地初始化Object
。使用Java作为示例,如果在构造函数中调用方法,则可能会遇到问题,尤其是在子类化Object
时。这是因为,由于对象实例化中的操作顺序,直到超级构造函数完成后才会计算实例变量。如果您尝试在超级构造函数的过程中访问该字段,则会抛出Exception
假设你有一个超类
class Test {
Test () {
doSomething();
}
void doSomething() {
...
}
}
你有一个子类:
class SubTest extends Test {
Object myObj = new Object();
@Override
void doSomething() {
System.out.println(myObj.toString()); // throws a NullPointerException
}
}
这是一个特定于Java的示例,虽然不同的语言以不同的方式处理这种排序,但它可以驱动这一点。
编辑作为评论的答案:
虽然我通常会回避构造函数中的方法,但在这种情况下,您有几个选项:
在构造函数中,将HTML字符串设置为类中的字段,并在每次调用getter时进行解析。这很可能效率不高。
将HTML设置为对象上的字段,然后引入对parse()
的依赖,需要在构造函数完成后立即调用它,或者通过添加包含某种延迟解析像访问者头部的'ensureParsed()'之类的东西。我不喜欢这一点,因为你可以在解析后得到HTML,并且你的ensureParsed()
调用可以被编码以设置所有解析的字段,从而为你的getter引入副作用。
您可以从构造函数中调用parse()
,冒着抛出异常的风险。正如您所说,您正在设置字段以初始化Object
,所以这很好。关于Exception
,声明传递给构造函数的非法参数是可以接受的。如果这样做,您应该小心确保您了解语言处理对象创建的方式,如上所述。为了跟进上面的Java示例,如果确保在构造函数中只调用private
方法(因此不符合子类的覆盖条件),则可以毫无顾虑地执行此操作。
答案 6 :(得分:3)
从单元测试的角度来看,Misko Hevery有一个关于这个主题的好故事,here。
答案 7 :(得分:2)
构造函数应该创建一个有效的对象。如果在您的情况下需要阅读和解析信息,那么就是这样。
如果对象可以用于其他目的而不首先解析信息,那么考虑制作两个构造函数或单独的方法。
答案 8 :(得分:1)
构造函数应该设置要使用的对象。
无论如何。这可能包括对某些数据采取措施或仅设置字段。它将从每个班级改变。
如果你说的是Html Parser,我会选择创建类,然后调用Parse Html方法。这样做的原因是它为您提供了在课堂上设置解析Html的项目的机会。
答案 9 :(得分:1)
在这种特殊情况下,我会说这里有两个类:解析器和解析结果。
public class Parser {
public Parser() {
// Do what is necessary to construct a parser.
// Perhaps we need to initialize a Unicode library, UTF-8 decoder, etc
}
public virtual ParseResult parseHTMLString(final string html) throws ParsingException
{
// Parser would do actual work here
return new ParseResult(1, 2);
}
}
public class ParseResult
{
private int field1;
private int field2;
public ParseResult(int _field1, int _field2)
{
field1 = _field1;
field2 = _field2;
}
public int getField1()
{
return field1;
}
public int getField2()
{
return field2;
}
}
如果您的解析器可以处理部分数据集,我怀疑将其他类添加到混合中是合适的。可能是PartialParseResult
?
答案 10 :(得分:0)
通常,构造函数应该:
但是,我不会以你的方式使用构造函数。解析应该与使用解析结果分开。
通常,当我编写解析器时,我将其写为单例。我不存储除单个实例之外的对象中的任何字段;相反,我只在方法中使用局部变量。从理论上讲,这些可能只是静态(类级别)方法,但这意味着我无法将它们变为虚拟。
答案 11 :(得分:0)
我同意这里的海报争论构造函数中的最小工作,实际上只是将对象置于非僵尸状态,然后有动词函数,如parseHTML();
我想提出的一点,虽然我不想引起火焰战争,但考虑的是非例外环境。我知道你在谈论C#,但我试图让我的编程模型在c ++和c#之间保持尽可能相似。由于各种原因,我不使用C ++中的异常(想想嵌入式视频游戏编程),我使用返回代码错误。
在这种情况下,我不能在构造函数中抛出异常,所以我倾向于没有构造函数做任何可能失败的事情。我把它留给了访问者功能。
答案 12 :(得分:0)
很多人评论说,一般规则是只在构造函数中进行初始化,而从不使用虚拟方法(如果你试着注意那个警告,你会收到编译器警告:))。在具体情况下,我也不会选择parHTML方法。一个对象在构造时应该处于有效状态,你必须先对对象做一些事情才能真正使用它。
就个人而言,我会选择工厂方法。公开没有公共构造函数的类,而是使用工厂方法创建它。让你的工厂方法进行解析并将解析后的结果传递给私有/受保护的构造函数。
如果你想看一些类似逻辑的样本,请看看System.Web.WebRequest。
答案 13 :(得分:0)
一个可能的选择是将解析代码移动到单独的函数,使构造函数为私有,并具有构造对象的静态函数解析(html)并立即调用解析函数。
这样就可以避免在constructur中解析问题(不一致状态,调用重写函数时出现问题,......)。但客户端代码仍然具有所有优势(一次调用获取解析的html或'早期'错误)。
答案 14 :(得分:0)
在我的情况下,整个内容 HTML文件通过String传递。 该字符串不再需要一次 它被解析并且相当大(a 几百千字节)。所以它会 最好不要把它留在记忆中。该 对象不应该用于其他 案例。它旨在解析一个 某页。解析别的东西 应该提示创建一个 不同的对象来解析它。
听起来好像你的对象不是真正的解析器。它只是将一个调用包装到解析器中并以(可能)更有用的方式呈现结果吗?因此,您需要在构造函数中调用解析器,否则您的对象将处于无用状态。
我不确定“面向对象”部分在这里有何帮助。如果只有一个对象并且它只能处理一个特定的页面,那么就不清楚它为什么需要成为一个对象。您可以在程序(即非OO)代码中轻松完成此操作。
对于只有对象(例如Java)的语言,您只需在没有可访问构造函数的类中创建static
方法,然后调用解析器并返回{{1}中的所有已解析值或类似的集合
答案 15 :(得分:0)
为什么不将解析器传递给构造函数?这将允许您在不更改模型的情况下更改实现:
public interface IParser
{
Dictionary<string, object> ParseDocument(string document);
}
public class HtmlParser : IParser
{
// Properties, etc...
public Dictionary<string, object> ParseDocument(string document){
//Do what you need to, return the collection of properties
return someDictionaryOfHtmlObjects;
}
}
public class HtmlScrapper
{
// Properties, etc...
public HtmlScrapper(IParser parser, string HtmlDocument){
//Set your properties
}
public void ParseDocument(){
this.myDictionaryOfHtmlObjects =
parser.ParseDocument(this.htmlDocument);
}
}
这可以让您灵活地更改/改进应用程序的执行方式,而无需重写此类。
答案 16 :(得分:0)
我认为当你创建一个类($ obj = new class)时,该类根本不应该影响页面,并且应该处理相对较低。
例如:
如果你有一个用户类,它应该检查传入的登录/注销参数,以及cookie,并将它们分配给类变量。
如果您有数据库类,它应该建立与数据库的连接,以便在您开始查询时准备就绪。
如果您有一个处理特定表单的类,则应该获取表单值。
在我的很多课程中,我会检查某些参数来定义“动作”,例如添加,编辑或删除。
所有这些都不会真正影响页面,所以如果你创建它们就没那么重要了。当你要打电话给第一种方法时,它们就已经准备好了。
答案 17 :(得分:0)
我不会在构造函数中进行解析。我将做所有必要的事情来验证构造函数参数,并确保在需要时可以解析HTML。
但是如果HTML在他们需要的时候没有被解析的话,我会让访问器方法进行解析。解析可以等到那个时间 - 它不需要在构造函数中完成。
建议的代码,供讨论之用:
public class MyHtmlScraper {
private TextReader _htmlFileReader;
private bool _parsed;
public MyHtmlScraper(string htmlFilePath) {
_htmlFileReader = new StreamReader(htmlFilePath);
// If done in the constructor, DoTheParse would be called here
}
private string _parsedValue1;
public string Accessor1 {
get {
EnsureParsed();
return _parsedValue1;
}
}
private string _parsedValue2;
public string Accessor2 {
get {
EnsureParsed();
return _parsedValue2;
}
}
private void EnsureParsed(){
if (_parsed) return;
DoTheParse();
_parsed = true;
}
private void DoTheParse() {
// parse the file here, using _htmlFileReader
// parse into _parsedValue1, 2, etc.
}
}
将这些代码放在我们面前,我们可以看到在构造函数中进行所有解析并按需执行此操作之间几乎没有什么区别。有一个布尔标志的测试,标志的设置,以及每个访问器中对EnsureParsed的额外调用。如果没有列出额外的代码,我会感到惊讶。
这不是什么大不了的事,但我倾向于在构造函数中尽可能少地做。这允许建筑需要快速的场景。毫无疑问,这些都是你没有考虑过的情况,比如反序列化。
同样,这不是一个大问题,但你可以避免在构造函数中完成工作,并且在其他地方完成工作并不昂贵。我承认,这并不像你在构造函数中做网络I / O(当然,除非传入UNC文件路径),并且你不必在构造函数中等待很长时间(除非那里是网络问题,或者你推广该类,以便能够从文件以外的地方读取HTML,其中一些可能很慢。)
但是因为你不必在构造函数中这样做,我的建议很简单 - 不要。
如果你这样做,可能需要数年时间才会出现问题,如果有的话。
答案 18 :(得分:-1)
我个人在构造函数中没有放任何东西,并且有一组初始化函数。我发现标准构造函数方法具有有限且繁琐的重用。