多少工作是合理的一个对象的构造函数呢? 如果它只是初始化领域,而不是实际的数据进行任何操作,或者是它没关系把它进行一些分析?
背景:我正在写一类,这是负责解析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; }
}
是否在某些初始化代码,如解析信息,不应该在构造函数中发生的情况下,还是我只是傻和第二猜测自己?
什么是从构造分割解析的好处/缺点?
思考? 见解?
Answer 1:
我通常遵循一个简单的原则:
一切是强制性的类实例的正确存在和行为应该通过并完成到构造。
每隔活动是由其他的方法来完成。
构造函数永远不应该:
- 使用类的其他方法使用压倒一切行为的目的
- 作用于通过其方法私有属性
因为我深知这一点,虽然你是在构造函数中,对象是在是太危险了处理语无伦次,中间状态。 一些这意外的行为可能会从您的代码可以预料的,有些可能是从语言结构和编译器的决定。 永远猜不到,保持安全,是最小的。
在你的情况,我会用一个解析器:: parseHtml(文件)的方法。 解析器和解析的实例是两个不同的操作。 当你实例解析,构造函数把它在执行其任务(解析)的条件。 然后你使用它的方法来进行解析。 然后,你有两个选择:
- 要么你让解析器包含解析的结果,并给客户端检索该分析信息的接口(例如分析器:: getFooValue())。 该方法将返回NULL,如果你还没有进行解析,又或如果解析失败。
- 或者您的解析器:: parseHtml()返回一个ParsingResult例如,包含分析器发现了什么。
第二个策略授予您更好的粒度,因为解析器现在是无状态的,并且客户端需要与ParsingResult接口的方法进行交互。 Parser接口仍然圆滑和简单。 解析器类的内部往往会遵循Builder模式 。
你对此有何评论:“我觉得好像回到尚未解析任何东西(你的建议)解析器,这是失去其意义构造的实例有一个在初始化解析器没有实际解析信息的意图没有用左右。如果解析将要发生的是肯定的,我们应该尽可能早地分析和报告和错误早,如解析器的施工过程中?我觉得好像初始化无效数据解析器应该导致一个错误被抛出。”
并不是的。 如果返回解析器的一个实例,当然它会解析。 在Qt中,当你实例化一个按钮,当然它会显示出来。 但是,你有方法的QWidget ::秀()手动调用之前的东西是对用户可见。
在面向对象的任何对象有两个顾虑:初始化和操作(忽略定稿,这不是在讨论现在)。 如果保持这两个操作在一起,你们两个闹风险(具有完全的对象操作),你失去灵活性。 有很多的原因,你会()调用之前执行parseHtml你的对象的中间设置。 例如:假设你想你的分析器配置要严格或许可(如表中的给定列包含字符串,而不是一个整数所以失败)。 或登记被警告执行或结束一个新的解析每次侦听器对象(GUI认为进度条)。 这些都是可选的信息,如果你的架构提出了构造函数,做一切的übermethod,你最终有一个巨大的可选方法参数和条件的列表处理成本质上是一个雷区的方法。
“缓存不应该是一个解析器的责任。如果数据进行高速缓存,独立的缓存类都应该以提供该功能。”
在对面。 如果你知道你要在很多文件中使用的分析功能,并有一个显著的机会,这些文件将要访问,稍后再分析,这是解析器的内部责任进行智能缓存什么它已经看到了。 从客户的角度来看,如果执行这个缓存或不是,这是完全无视。 他仍然callling解析,并且仍然获得结果对象。 但它要快得多得到了答案。 我认为有比这个关注点分离的没有更好的示范。 您提高性能绝对在合同接口或没有改变整个软件架构。
但是,请注意,我不主张你不应该使用一个构造函数调用进行解析。 我只是声称这是潜在的危险,你失去灵活性。 有大量的例子在那里,其中的构造是在对象的实际活动的中心,但也有大量的相反的例子。 示例(虽然有失偏颇,它出现在C风格):在Python中,我会考虑很奇怪这样的事情
f = file()
f.setReadOnly()
f.open(filename)
而不是实际的
f = file(filename,"r")
但我相信有使用第一种方式(与第二作为糖的语法的方法)IO访问库。
编辑 :最后,请记住,虽然它很容易和兼容,在将来增加一个构造函数“捷径”,这是不可能的,如果你觉得危险的或有问题的删除此功能。 增加的界面比搬迁更容易,原因很明显。 含糖的行为必须针对你必须提供对未来行为的支持进行加权。
Answer 2:
“应该分析代码被放在一个空parseHtml()方法中和存取只有一次调用此方法返回有效值?”
是。
“之类的设计是这样的:类的构造函数解析”
这可以防止定制,扩展和 - 最重要的 - 依赖注入。
会有时候,你要做到以下几点
构建一个解析器。
添加功能解析器:业务规则,过滤器,更好的算法,策略,命令,等等。
解析。
一般来说,最好尽量少做在构造函数中,让你可以自由地扩展或修改。
编辑
“不能简单地扩展在解析它们的构造额外的信息?”
只有当他们没有任何一种需要注入的功能。 如果你想增加新的功能 - 比如构造解析树不同的策略 - 你的子类有他们解析之前还管理此功能的补充。 它可能不等于简单的super()
因为超确实太多了。
“此外,在构造解析允许我失败早”
的种类。 施工过程中失败是一个奇怪的使用情况。 施工期间未能使其难以构建这样的解析器...
class SomeClient {
parser p = new Parser();
void aMethod() {...}
}
通常一个建设失败意味着你出的内存。 有很少一个很好的理由抓建设例外,因为你无论如何注定。
你不得不建造解析器的方法体,因为它有太多复杂的参数。
总之,你已经从你的解析器的客户端删除的选项。
“这是不可取的,从这个类继承来替换算法”。
那很好笑。 认真。 这是一个离谱的要求。 没有算法是最佳的所有可能的使用情况。 通常,一个高性能的算法使用大量的内存。 客户端可能希望与使用较少的存储器更慢的一个替换算法。
您可以要求完美,但它是罕见的。 子类是常态,也不例外。 总会有人在你的“完美”的改进。 如果限制他们的子类解析器的能力,他们会干脆放弃它的东西更灵活。
“我在答案中描述完全看不出需要第2步。”
一个大胆的声明。 依赖关系,战略和相关注入设计模式是共同的要求。 事实上,他们的单元测试,一个设计,这使得它很难或往往是复杂的原来是一个不好的设计如此重要。
限制子类或扩展解析器的能力是一个糟糕的政策。
底线 。
假设什么。 写有关于它的使用情况尽可能少的假设一类。 在施工时间解析使有关客户的使用案例太多的假设。
Answer 3:
构造函数应该采取一切必要把该实例到一个可运行的,有效的,随时可以使用状态。 如果这意味着一些验证和分析,我会说这是属于那里。 只是要小心构造多少。
有可能是在你的设计中的其他地方确认适合为好。
如果输入的值是从UI来了,我会说,它应该在确保有效输入手。
如果输入的值被从输入XML流解组,我会考虑使用模式来验证它。
Answer 4:
我可能只是传递足够的初始化对象,然后有一个“解析”的方法。 这个想法是,昂贵的操作应当尽可能明显。
Answer 5:
你应该尽量保持构造从做不必要的工作。 最后,这一切都取决于类应该做什么,应该如何使用。
例如,将所有的访问器构造你的对象之后,叫什么名字? 如果没有,那么你不必要处理的数据。 此外,还有扔“毫无意义的”异常的更大的风险(哦,尝试创建解析器,我得到了一个错误,因为该文件是格式不正确的,但我也没有要求它来解析什么...)
关于第二个想法,你可能需要访问这些数据它建成后快,但你可能需要很长时间建立的对象。 这可能是在这种情况下确定。
无论如何,如果建设过程是复杂的,我建议使用一个创建模式 (工厂,建造者)。
Answer 6:
这是经验的很好的规则只在构造函数初始化的字段,否则做尽可能少的初始化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字符串作为你的类中的字段,并解析每一个你的干将被称为时间。 这很可能不会是非常有效的。
设置HTML作为对象的字段,然后引进的依赖关系parse()
它需要的构造是在完成或者右后或通过添加类似包括某种懒解析“ensureParsed()被调用您存取的头。 我不喜欢这所有的东西,因为你可以有HTML你解析后四周,你ensureParsed()
调用可编码设置所有的分析领域,从而引入副作用你消气。
你可以调用parse()
从你的构造和运行抛出异常的风险。 正如你所说,你要设置的字段来初始化Object
,所以这是真的确定。 关于Exception
,指出有传入构造函数的非法参数是可以接受的。 如果你这样做,你应该小心,以确保你明白,你的语言处理的对象的创建如上面所讨论的方式。 要使用Java示例跟进上面,你可以这样做,而不必担心,如果你确保只有private
方法(因此没有资格获得由子类覆盖)从构造函数中调用。
Answer 7:
MISKO Hevery对这个问题一个很好的故事,从单元测试的角度来看, 这里 。
Answer 8:
构造函数应该建立一个有效的对象。 如果你的情况,需要阅读和分析信息,不如这样吧。
如果该对象可被用于其他目的,而无需首先解析的信息,不是考虑制作两个构造函数,或一个单独的方法。
Answer 9:
要使用的构造应建立的对象。
所以,不管它是什么。 这可能包括采取行动的一些数据,或只设置字段。 它会从每个类别变化。
在你说话的HTML解析器的情况下,我会选择创建类,然后调用一个解析HTML方法。 这样做的原因是它给你一个未来编机会,设置的项目在类解析HTML。
Answer 10:
在这种特殊情况下,我会说有两个班在这里:解析器和解析结果。
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
?
Answer 11:
我认为当你创建一个类($ OBJ =新类),该类应该不会影响网页的一切,应该是比较低的处理。
例如:
如果你有一个用户类,应检查传入登录/注销参数,用曲奇,并将它们分配给类变量。
如果你有一个数据库类,它应该与数据库的连接,因此它是当你要开始一个查询准备。
如果你有一个类与特定形式的交易,应该去获取表单值。
在很多我的课,我检查某些参数来定义的“行动”,像添加,编辑或删除。
所有这些东西真的不影响页面,所以不会有问题,如果你创建了它们还是不太多。 他们只是在准备好时你要调用第方法。
Answer 12:
我不会做的构造解析。 我会做一切必要的验证构造函数的参数,并确保在需要时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)。
但是,因为你没有做它在构造函数中,我的建议很简单 - 没有。
如果你这样做,也可能是几年它会导致一个问题之前,如果在所有。
Answer 13:
为什么不通过解析器的构造? 这将允许您更改执行不改变模式:
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);
}
}
这应该给你改变/改善你的应用程序如何执行,而无需重写这个类有一定的灵活性。
Answer 14:
在我的情况下,HTML文件的全部内容通过字符串传递。 该字符串不再需要一旦被解析,是相当大的(几百千字节)。 因此,这将是最好不要保存在内存中。 对象不应该用于其它情况。 它被设计来解析某一页。 解析别的东西应该促使不同对象的创建来解析。
这听起来非常,仿佛你的对象是不是一个真正的解析器。 难道它只是换到一个解析器的调用,并提出在(大概)更实用时尚的结果? 正因为如此,你需要调用解析器的构造函数的对象将是一个无用的状态,否则。
我不知道的“面向对象”的部分是如何帮助在这里。 如果只有一个对象,它只能处理一个特定的页面,那么为什么它需要一个对象,目前还不清楚。 你可以在程序(即非OO)代码做到这一点很容易。
对于语言,只有拥有的对象(例如,Java),你可以只创建一个static
的类方法没有访问构造函数,然后调用解析器并返回解析出的所有值的一个Map
或类似的集合
Answer 15:
一个可能的选择是解析代码移到一个单独的功能,使构造私有,并且有静态函数解析(HTML),其构造对象,并立即呼叫解析功能。
这样,你避免与constructur解析(不一致的状态,调用问题时,覆盖的功能,...)的问题。 但是,客户端代码仍然得到所有优势(一个调用来获取解析HTML或“早”的错误)。
Answer 16:
由于相当多的都谈到的一般规则是只能做初始化在构造函数,从不使用虚拟讲的方法(你会如果你试图要注意的警告:)得到一个编译器警告)。 在你特定情况下,我不会去为parHTML方法无论是。 对象应处于有效状态时,它的构造,你应该做的东西到对象后,才能真正使用它。
个人而言,我会去一个工厂方法。 揭露没有公共构造一类,并使用一个工厂方法,而不是创建它。 让你的工厂方法做了分析和解析结果传递给一个私人/受保护的构造。
看看System.Web.WebRequest如果你想看到一些similiar逻辑的样本。
Answer 17:
我同意此海报争论在构造函数中最小的工作,真的只是把对象变成非僵尸状态,然后有像parseHTML动词功能();
有一点我想打,但我不想引起口水战,是考虑一个非异常环境的情况下。 我知道你在谈论C#,但我尽量保持我的编程模型作为类似C ++和C#之间可能。 由于种种原因,我没有用C使用异常++(想想嵌入式视频游戏节目),我使用的返回码错误。
在这种情况下,我不能在构造函数抛出异常,所以我倾向于不具有构造做任何可能会失败。 我留给存取功能。
Answer 18:
一般情况下,一个构造函数应该:
- 初始化所有字段。
- 保留生成的对象处于有效状态。
不过,我不会在你的方式使用构造函数。 解析应该使用解析结果分开。
通常,当我写一个解析器我把它写成一个单例。 我不存储在除了单一实例对象的任何领域; 相反,我只是使用的方法中的局部变量。 理论上,这些可能仅仅是静态的(类级别)的方法,但是这将意味着我不能让他们的虚拟。
Answer 19:
我个人把没有在构造函数和有一套初始化函数。 我发现标准构造方法有限制和繁琐的重用。
文章来源: How much work should the constructor for an HTML parsing class do?