OOP背后的基本思想是数据和行为(基于数据)是不可分割的,并且它们与类对象的思想联系在一起。对象具有与之配合使用的数据和方法(以及其他数据)。显然,根据OOP的原理,只是数据(如C结构)的对象被视为反模式。

到目前为止一切顺利。

问题是我注意到我的代码最近似乎越来越朝着这种反模式的方向发展。在我看来,我越努力实现隐藏在类与松散耦合设计之间的信息,我的类就越有可能成为纯数据无行为类和所有行为无数据类的混合体。

我通常以一种使类对其他类的存在的意识最小化并且对其他类的接口的知识最小化的方式来设计类。我特别以自上而下的方式执行此操作,较低级别的类不了解较高级别的类。例如:

假设您有通用的纸牌游戏API。您有班级Card。现在,此Card类需要确定对玩家的可见性。

一种方法是在boolean isVisible(Player p)类上具有Card

另一种方法是在boolean isVisible(Card c)类上具有Player

我特别喜欢第一种方法,因为它可以将有关较高级别的Player类的知识授予较低级别的Card类。

我选择了第三个选项,其中有一个Viewport类,给定一个Player和一张卡片列表,可以确定哪些卡片可见。

这种方法会抢夺可能的成员函数的CardPlayer类。一旦完成了除卡片可见性之外的其他工作,您将剩下CardPlayer类,它们仅包含数据,因为所有功能都是在其他类中实现的,这些类大多是没有数据的类,只是方法,例如上述Viewport。 br />
这显然与OOP的基本思想背道而驰。

哪种方法正确?我应该如何进行使类的相互依赖性最小化和假定的知识和耦合性最小化的任务,而又不会陷入怪异的设计中,其中所有低层类仅包含数据,而高层类包含所有方法?有没有人对类设计有任何第三种解决方案或观点可以避免整个问题?

P.S.这是另一个示例:

假设您有不可变的DocumentId类,只有一个BigDecimal id成员和该成员的吸气剂。现在您需要在某处有一个方法,给定一个DocumentId从数据库中为此ID返回Document

您是否:


Document getDocument(SqlSession)方法添加到DocumentId课上,突然介绍了有关您的持久性的知识("we're using a database and this query is used to retrieve document by id"),用于访问数据库的API等。同样,该类现在也需要持久性JAR文件才可以编译。
使用方法Document getDocument(DocumentId id)添加其他类,使DocumentId类为死类,无行为,类似结构。


评论

您在这里的一些前提是完全错误的,这将使回答基本问题变得非常困难。尽可能使您的问题简洁明了,您将获得更好的答案。

“这显然违反了OOP的主要思想”-不,不是,而是普遍的谬论。

我想问题在于,过去存在着不同的“对象定向”流派-它最初是由像艾伦·凯(Alan Kay)这样的人表达的(请参阅geekswithblogs.net/theArchitectsNapkin/archive/2013/09/08/ …),Rational的那些人(en.wikipedia.org/wiki/Object-directional_analysis_and_design)在OOA / OOD的上下文中教授了这种方法。

我想说,这是一个很好的问题,而且贴得很好-与其他一些评论相反。它清楚地表明了大多数关于如何构造程序的建议是多么幼稚或不完整-以及执行它有多么困难,并且在许多情况下,无论人们有多少努力去做正确的设计,正确的设计是多么难以实现。尽管对特定问题的一个明显答案是多方法,但设计的基本问题仍然存在。

谁说无行为的课程是反模式?

#1 楼

您所描述的被称为贫血域模型。与许多OOP设计原则(如Demeter定律等)一样,仅为了满足规则而倒弯也不值得。

拥有价值袋是没有错的,只要它们不会使整个景观杂乱无章,并且不依赖其他对象来为自己做家务即可。

如果您有一个单独的类仅用于修改Card的属性,那肯定是代码的味道-如果可以合理地期望它自己照顾它们。

但是真正知道Card是哪个工作?

为什么要实施Player而不是Card.isVisibleTo(Player p)?或相反亦然?

是的,您可以尝试像这样做一样制定一些规则-例如Player.isVisibleTo(Card c)Player(?)更高级别-但这并不是那么简单的猜测,我随着时间的流逝,它可能导致在CardisVisibleTo类上都实现Card的糟糕的设计折衷方案,我认为这是不可行的。没有。为什么这样?因为我已经想象到Player返回与player1.isVisibleTo(card1)不同的值的可耻日子,所以我认为-这是主观的-设计上应该做到这一点。

纸牌和玩家的共同可见性最好由某些人来控制某种上下文对象-是card1.isVisibleTo(player1).ViewportDeal

这不等于具有全局功能。毕竟,可能会有很多并发游戏。请注意,同一张卡可以同时在许多桌子上使用。我们应该为每个王牌成员创建许多Game实例吗?

我仍然可以在Card上实现isVisibleTo,但是将上下文对象传递给它,并使Card委托查询。编程接口以避免高耦合。

至于第二个示例-如果文档ID仅包含Card,为什么还要为其创建包装器类?

我想说的是,您需要的只是一个BigDecimal

顺便说一句,虽然Java不存在,但C#中却有DocumentRepository.getDocument(BigDecimal documentID);

请参阅


http://msdn.microsoft.com/en-us/library/ah19swz4.aspx
http://msdn.microsoft.com/en-us/ library / 0taef578.aspx

供参考。这是一种高度面向对象的语言,但没人能从中大做文章。

评论


只是有关C#中的结构的注释:它们不是典型的结构,就像您在C中所知道的那样。实际上,它们也支持具有继承,封装和多态性的OOP。除了某些特性外,主要区别在于运行时将实例传递给其他对象时如何处理它们:结构是值类型,类是引用类型!

–阿斯拉特
2014年4月2日在15:05



@Aschratt:结构不支持继承。结构可以实现接口,但是实现接口的结构与类对象的行为不同。虽然可以使结构的行为类似于对象,但结构的最佳用例是当人们想要某种行为像C结构,而被封装的对象是基元或不可变的类类型。

–超级猫
2014年4月5日在23:19

+1为何说明“这不等于具有全局功能”。其他人并没有解决这个问题。 (尽管如果您有多个卡片组,则全局功能对于同一张卡的不同实例仍将返回不同的值)。

–alexis
2014年4月6日10:05



@supercat这值得一个单独的问题或一个聊天会话,但是我目前对以下两个都不感兴趣:-(您说(在C#中)“实现接口的结构与执行同样操作的类对象不同”。我同意还有其他行为上的差异要考虑,但是接口iObj =(Interface)obj;之后的代码中的AFAIK,iObj的行为不受obj的结构或类状态的影响(除非如果指定,它将是盒装副本)它是一个结构)。

–马克·赫德
2014年7月2日,2:17

您说:“我们会为每个黑桃王牌创建许多Card实例吗?”你不会吗

– BVernon
19年11月12日在4:53

#2 楼


OOP背后的基本思想是数据和行为(基于数据)是不可分割的,并且它们与类对象的思想联系在一起。


犯了一个普遍的错误,即认为类是OOP中的基本概念。类只是实现封装的一种特别流行的方法。但是我们可以允许它滑动。


假设您有通用的纸牌游戏API。您有班级卡。现在,此类卡需要确定对玩家的可见性。


GOOD HEAVENS NO。当您在玩Bridge时,您是否该问七只心,何时该将假人的手从只为假人知道的秘密更改为所有人都知道?当然不是。



一种方法是在Card类上使用boolean isVisible(Player p)。
另一种方法是使用boolean isVisible(Card c) )放在Player类上。


两人都很恐怖;不要做任何一个。玩家和纸牌都不负责实施Bridge的规则!


相反,我选择了第三个选项,其中有一个Viewport类,给定一个Player和一张牌列表,可以确定哪些牌可见。


我以前从未玩过带有“视口”的卡片,所以我不知道该类应该封装什么。我玩过带有几副纸牌,一些玩家,一张桌子和一副Hoyle的纸牌。


但是,这种方法抢夺了Card和Player类可能的成员函数。


好!


一旦完成了除卡片可见性之外的其他工作,您将剩下Card和Player类,这些类仅包含数据,因为所有功能都是在其他类中实现的,这些类大多是带有没有数据,只有方法,例如上面的视口。这显然与OOP的基本思想背道而驰。


没有; OOP的基本思想是对象封装了它们的关注点。在您的系统中,卡并不太在乎。也不是玩家。这是因为您正在准确地建模世界。在现实世界中,与游戏相关的纸牌属性极其简单。我们可以用从1到52的数字替换卡片上的图片,而无需很大程度地改变游戏的玩法。我们可以用标有北,南,东和西的人体模型代替这四个人,而无需大幅度改变游戏的玩法。玩家和纸牌是纸牌游戏中最简单的东西。规则是很复杂的,因此代表规则的类就是应该出现复杂性的地方。

现在,如果您的玩家之一是AI,则其内部状态可能会非常复杂。但是,人工智能无法确定它是否可以看到卡片。规则决定了这一点。

这就是我设计系统的方式。

首先,如果游戏中有多个卡组,则纸牌会非常复杂。您必须考虑以下问题:玩家可以区分相同等级的两张卡吗?如果第一个玩家玩了七个心中的一个,然后发生了一些事情,然后第二个玩家玩了七个心中的一个,那么三个玩家可以确定它是相同的七个心吗?请仔细考虑这一点。但是除了这种担心,卡片应该非常简单。它们只是数据。

接下来,玩家的本质是什么?玩家消耗一系列可见的动作并产生一个动作。

规则对象是协调所有这些的对象。规则产生一系列看得见的动作并通知玩家:


玩家一,十个心已被玩家三交给了。
玩家二,一张卡片已由第三名玩家移交给第一名玩家。

然后要求玩家采取行动。


第一,你想做什么?
玩家一说:高音fromp。
第一人,这是非法动作,因为高音的fromp产生了无法防御的诱惑。
第一人,你想做什么?
玩家一说:丢掉黑桃皇后。
玩家二,丢掉黑桃皇后。

以此类推。将您的机制与策略分开。游戏的策略应封装在策略对象中,而不是纸牌中。卡只是一种机制。

评论


@gnat:来自Alan Kay的相反观点是:“实际上,我用术语“面向对象”来形容,我可以告诉你我没有C ++。” JavaScript浮现在脑海。

–埃里克·利珀特
2014年4月2日在17:59



@gnat:我同意今天的JS并不是OOP语言的一个很好的例子,但是它表明人们可以很容易地构建没有类的OO语言。我同意Eiffel和C ++中的OO-ness的基本单元都是类。我不同意的观点是,类是OO的必要条件。 OO的必要条件是封装行为并通过定义良好的公共接口相互通信的对象。

–埃里克·利珀特
2014年4月2日在18:29

我同意@EricLippert,类不是OO的基础,继承也不是主流所能说的。数据,行为和责任的封装已经完成。除了Javascript,还有一些基于原型的语言,它们是面向对象的,但没有类。专注于对这些概念的继承尤其是一个错误。也就是说,类是组织行为封装的非常有用的方法。您可以将类视为对象(在原型语言中则相反),这会使行变得模糊。

–施韦恩
2014年4月2日19:50



这样考虑:在现实世界中,一张实际的卡牌会表现出什么行为?我认为答案是“无”。卡上还有其他东西。在现实世界中,纸牌本身实际上只是信息(4个俱乐部),没有任何内在行为。该信息(也称为“纸牌”)的使用方式取决于100%,最高取决于“规则”和“玩家”。相同的纸牌可由任意数量的不同玩家用于无限(很好,也许不是很)多种不同的游戏。卡只是卡,它拥有的只是属性。

– Craig
2014年4月3日在21:15

@蒙塔吉斯特:让我再澄清一下。考虑一下C。我想您会同意C没有类。但是,您可以说结构是“类”,可以创建函数指针类型的字段,可以构建vtable,可以创建用于建立vtable的称为“构造函数”的方法,以便某些结构彼此“继承”,等等。您可以在C中模拟基于类的继承。也可以在JS中模拟。但是,这样做意味着在尚不存在的语言之上构建一些东西。

–埃里克·利珀特
2014年4月4日在20:59

#3 楼

您认为数据和行为的耦合是OOP的中心思想是正确的,但是还有很多。例如,封装:OOP /模块化编程使我们能够将公共接口与实现细节分开。在OOP中,这意味着数据永远不应公开访问,而只能通过访问器使用。按照这个定义,没有任何方法的对象确实是没有用的。

除了访问器之外没有其他方法的类本质上是一个过于复杂的结构。但这还不错,因为OOP使您可以灵活地更改内部细节,而结构没有。例如,代替将值存储在成员字段中,可以每次重新计算它。或更改支持算法,并且必须跟踪其状态。

虽然OOP具有明显的优势(尤其是相对于普通过程编程而言),但为“纯” OOP。有些问题无法很好地映射到面向对象的方法,而其他范式则更容易解决。遇到此类问题时,请不要坚持使用劣等方法。


请考虑以面向对象的方式计算斐波那契数列。我想不出一个明智的方法。简单的结构化编程为该问题提供了最佳解决方案。

您的isVisible关系属于这两个类,或者都不属于这两个类,或者实际上属于上下文。无行为记录是功能或过程编程方法的典型代表,似乎最适合您的问题。

static boolean isVisible(Card c, Player p);
没问题,

Card没问题,没有超出ranksuit访问器的方法。



评论


@UMad是的,这正是我的意思,这没有错。对当前的工作使用正确的 language 范例。 (顺便说一下,除了Smalltalk之外,大多数语言都不是纯粹的面向对象的。例如,Java,C#和C ++支持命令式,结构化,过程化,模块化,函数式和面向对象的编程。所有这些非OO范例都是有原因的。 :以便您可以使用它们)

–阿蒙
2014年4月2日在13:11



有一种明智的面向对象的方法来做斐波那契,在整数实例上调用fibonacci方法。我希望强调您的观点,即OO是关于封装的,即使在看起来很小的地方也是如此。让整数找出如何完成这项工作。以后您可以改进实现,添加缓存以提高性能。与函数不同,方法遵循数据,因此所有调用方都可以从改进的实现中受益。也许以后会添加任意精度的整数,它们可以像普通整数一样透明地对待,并且可以对性能进行微调的斐波那契方法。

–施韦恩
2014年4月2日19:33



@Schwern如果斐波那契是抽象类Sequence的子类,则序列可被任何数字集使用,并负责存储种子,状态,缓存和迭代器。

–乔治·瑞斯(George Reith)
2014年4月2日20:39



我没想到“纯正的OOP斐波那契”在书呆子狙击中如此有效。尽管有一定的娱乐价值,但请停止在这些评论中进行任何圆形讨论。现在,让我们共同为变革做些有建设性的事情!

–阿蒙
2014年4月2日在21:07

将fibonacci用作整数方法是很愚蠢的,只是您可以说它是OOP。这是一个函数,应将其视为函数。

–user253751
2014年4月3日在5:14



#4 楼


OOP背后的基本思想是数据和行为(基于数据)是不可分割的,并且它们与类对象的思想联系在一起。对象具有与之配合使用的数据和方法(以及其他数据)。显然,根据OOP的原理,只是数据(如C结构)的对象被视为反模式。 (...)这显然与OOP的基本思想背道而驰。


这是一个棘手的问题,因为它基于很多错误的前提:


OOP是唯一有效的编写方法的想法代码。
OOP是一个定义明确的概念。它变得如此流行,以至于很难找到两个人可以就OOP的内容达成共识。
OOP是关于绑定数据和行为的想法。
一切都是/应该是抽象的想法。

我不会多谈#1-3,因为每个人都可以产生自己的答案,并且会引起很多基于观点的讨论。但是我发现“ OOP是关于耦合数据和行为”的想法特别麻烦。它不仅导致#4,而且还导致一切都应该是方法的想法。

定义类型的操作与使用该类型的方式之间存在差异。能够检索i元素对于数组的概念至关重要,但是排序只是我可以选择处理的许多事情之一。排序不需要是“仅创建一个仅包含偶数元素的新数组”的方法。

OOP是关于使用对象的。对象只是实现抽象的一种方法。抽象是一种避免代码中不必要耦合的方法,而不是最终目的。如果您仅通过套房和等级的值来定义卡的概念,则可以将其实现为简单的元组或记录。没有任何其他代码部分可以依赖的非必需细节。有时您根本没有什么可隐藏的。

您不会将isVisible用作Card类型的方法,因为对于您的卡片概念来说,可见并不是必需的(除非您有非常特殊的卡片,它们可能会变成半透明或不透明...)。它应该是Player类型的方法吗?好吧,这可能也不是球员的素质。它应该是某些Viewport类型的一部分吗?再次取决于定义视口的方式以及检查卡可见性的概念对于定义视口是否必不可少。

isVisible很可能应该只是一个自由功能。

评论


+1是常识,而不是无意识的开车。

–右键
2014年4月4日13:48



从我读过的书中,您链接的文章看起来像是我一段时间以来没有读过的一本坚实的书。

– Arthur Hv
2014年4月4日在16:12



@ArthurHavlicek如果您不理解代码示例中使用的语言,则很难遵循,但我发现它很有启发性。

–Doval
14年4月4日在16:36

#5 楼


显然,根据OOP的原理,只是数据的对象(如C结构)被视为反模式。


不,它们不是。 Plain-Old-Data对象是一个完全有效的模式,我希望它们在任何处理数据的程序中都需要在程序的不同区域之间进行持久化或通信。

虽然您的数据层在从Player表中读取数据时可能会假装完整的Players类,但它可能只是一个通用的数据库,返回带有表中字段的POD,这它传递到您程序的另一个区域,该区域将播放器POD转换为具体的Player类。

使用数据对象(无论是键入还是未键入)在您的程序中可能没有意义,但这样做没有意义使它们成为反模式。如果它们有意义,请使用它们,否则请不要使用。

评论


不要不同意您所说的话,但这根本无法回答问题。就是说,我将问题归咎于答案。

– pdr
2014年4月2日,12:50

确切地说,即使在现实世界中,卡片和文档也只是信息的容器,任何无法处理的“模式”都应被忽略。

– JeffO
2014年4月2日在12:51

Plain-Old-Data对象是一个完全有效的模式,我不是说不是,而是说当它们填充应用程序的整个下半部分时是错误的。

– RokL
2014年4月2日在12:59

#6 楼

我个人认为域驱动设计有助于使这个问题更清晰。我问的问题是,我该如何向人类描述纸牌游戏?换句话说,我在建模什么?如果我要建模的东西确实包含“视口”一词和与其行为相匹配的概念,那么我将创建视口对象并使其在逻辑上应做的事情。

如果我不这样做的话我的游戏中没有视口的概念,我认为这是我所需要的,因为否则代码“感觉不对”。对于将其添加到我的域模型中,我三思而后行。

模型一词意味着您正在构建事物的表示形式。我警告不要放置一个代表抽象内容的类,而不是要代表的东西。

我将进行编辑以补充说,在代码的另一部分中可能需要视口的概念,如果您需要与显示器连接。但是用DDD术语来说,这将是基础架构方面的问题,并且将存在于域模型之外。

评论


利珀特(Lippert)的上述回答是该概念的更好示例。

– RibaldEddie
2014年4月3日在9:44

#7 楼

我通常不做自我宣传,但是事实是我在博客上写了很多有关OOP设计的文章。总结几页:您不应该从类开始设计。从接口或API以及形状代码开始,有更大的机会提供有意义的抽象,适合规格并避免使用不可重用的代码来膨胀具体的类。

这如何应用于Card-Player问题:创建一个如果将ViewPortCard视为两个独立的库,则Player的抽象是有意义的(这意味着Player有时不带Card使用)。但是,我倾向于认为Player可以容纳Cards,并应该为它们提供Collection<Card> getVisibleCards ()访问器。就创建可理解的代码关系而言,这两种解决方案(ViewPort和mine)都比提供isVisible作为CardPlayer的方法要好。

类外的解决方案要好得多, DocumentId。几乎没有动机使(基本上是整数)依赖于复杂的数据库库。

评论


我喜欢你的博客。

– RokL
2014年4月7日在8:24

#8 楼

我不确定眼前的问题是否在正确的水平上得到了回答。我曾敦促论坛中的智者在这里积极思考问题的核心。

U Mad正在提出一种情况,他认为按照他对OOP的理解进行编程通常会导致问题。

我认为这个话题与是否要在Card vs Player中定义isVisible略有关系。这只是一个示例,尽管很幼稚。

尽管如此,我还是请有经验的人来看一下眼前的问题。我认为,U Mad提出了一个很好的问题。我知道您会将规则和有关的逻辑推到自己的对象上。但据我了解,问题是


是否可以使用简单的数据持有人构造(类/结构;我不在乎它们针对此问题建模)真的没有提供太多功能吗?
如果是,对它们进行建模的最佳或首选方法是什么?
如果没有,我们如何将这个数据计数器部件合并到更高的API类中(包括行为)?

我的观点:

我认为您是在问以下问题:在面向对象的编程中很难做到的粒度。以我的少量经验,我不会在模型中包括一个本身不包含任何行为的实体。如果必须的话,我可能已经使用了一种结构,该结构旨在保留这种抽象,而不像具有封装数据和行为的类的类。

评论


问题是问题(和您的答案)是关于“一般”如何做事的。事实是,我们从不“一般”地做事。我们总是做特定的事情。有必要检查我们的特定事物并根据我们的要求对其进行衡量,以确定我们的特定事物是否适合该情况。

–约翰·桑德斯(John Saunders)
2014年4月3日4:40在

@JohnSaunders我在这里理解您的智慧并在一定程度上表示同意,但是在解决问题之前也仅需要概念上的方法。毕竟,这里的问题并不像看起来那样开放。我认为这是任何面向对象设计人员在OOP最初使用中都面临的有效的面向对象问题。你拿什么如果有帮助,我们可以讨论构建您选择的示例。

– Harsha
2014年4月3日4:48在

我已经放学超过35年了。在现实世界中,我发现“概念方法”几乎没有价值。在这种情况下,我发现经验是比Meyers更好的老师。

–约翰·桑德斯(John Saunders)
2014年4月3日,下午4:53

我不太了解数据类与行为区别类。如果正确地抽象对象,则没有区别。想象一下带有getX()函数的Point。您可以想象它正在获得它的属性之一,但是它也可以从磁盘或Internet读取它。获取和设置是行为,拥有可以做到这一点的类是完全可以的。数据库只能获取和设置数据

– Arthur Hv
2014年4月4日在6:24



@ArthurHavlicek:知道一个班级不会做什么通常和知道它将做什么一样有用。在其合同中指定某些内容,使其仅充当可共享的不可变数据持有人,或充当不可共享的可变数据持有人,这很有用。

–超级猫
14年4月4日在22:44

#9 楼

OOP中常见的混乱根源在于以下事实:许多对象封装了状态的两个方面:它们所了解的事物以及有关它们的事物。关于对象状态的讨论经常忽略后一个方面,因为在对象引用混杂的框架中,没有通用的方法来确定任何有关其引用曾经暴露于外界的对象可能了解什么。

我建议使用CardEntity对象将卡的那些方面封装在单独的组件中可能会有所帮助。一个组成部分与卡上的标记有关(例如“钻石王”或“熔岩爆炸;玩家有AC-3闪避几率,否则会受到2D6伤害”)。一个可能与状态的独特方面有关,例如位置(例如,它在甲板上,在Joe的手中或在Larry面前的桌子上)。可能涉及三分之一的人可以看到它(也许没有人,也许一个玩家,或者也许很多玩家)。为了确保所有内容保持同步,可能不会将卡的位置封装为简单字段,而是将其封装为CardSpace对象;将卡移动到某个空间时,可以给它一个适当的CardSpace对象的引用;它将把自己从旧空间中移出并放到新空间中。)

分别封装“谁知道X”和“ X知道”应该有助于避免很多混乱。有时需要小心避免内存泄漏,尤其是在有许多关联的情况下(例如,如果新卡可以存在而旧卡消失了,则必须确保不要将应丢弃的卡永久地附着在任何长期存在的物体上) ),但是如果对对象的引用的存在将构成其状态的相关部分,则对象本身明确地封装此类信息是完全适当的(即使它将实际管理它的工作委派给其他某个类)。 />

#10 楼


但是,这种方法抢夺了Card和Player类可能的成员函数。


那不好/不明智的建议是什么?

要使用与您的卡示例类似的类比,请考虑一个Car,一个Driver,并且需要确定Driver是否可以驱动Car

确定,所以您决定您不希望Car知道Driver是否具有正确的车钥匙,并且由于某些未知原因,您还决定不希望Driver知道Car类别(您并未充分充实此知识)以及您最初提出的问题)。因此,您有一个中间类,类似于Utils类,其中包含带有业务规则的方法,以便为上述问题返回boolean值。

我认为这很好。中级班级可能现在只需要检查车钥匙,但是可以重构以考虑驾驶员是否在酒精的影响下或者在反乌托邦的未来拥有有效的驾驶执照,以检查DNA生物特征。通过封装,将这三个类并存在一起确实没有什么大问题。