类使用其自己的公共方法可以吗?


23

背景

我目前遇到一种情况,即我有一个设备同时发送和接收的对象。该消息具有以下几种构造:

public void ReverseData()
public void ScheduleTransmission()

ScheduleTransmission方法需要调用ReverseData时,它被称为方法。但是,有时我需要从应用程序中实例化对象的地方进行ReverseData外部调用(并且应该完全在命名空间之外添加)。

至于“接收”,我的意思是ReverseData将在object_received事件处理程序中从外部调用该请求以撤消数据。

对象调用其自己的公共方法通常可以接受吗?


5
从自己的方法调用公共方法没有错。但是,根据您的方法名称,如果ReverseData()确实对内部数据进行了反转,那么对它来说,作为公共方法听起来有点危险。如果ReverseData()在对象外部调用,然后使用ScheduleTransmission()再次调用怎么办。
卡恩

@Kaan这些不是我方法的真实名称,而是紧密相关的。实际上,“反向数据”仅反转整个字的8位,并且在我们接收和发送时完成。
史努比

问题仍然存在。如果在安排传输之前将这8位反转两次,该怎么办?这感觉像是公共界面上的一个巨大漏洞。像我在回答中提到的那样,考虑公共接口可能有助于弄清这个问题。
Daniel T.

2
@StevieV我相信这只会加剧Kaan提出的担忧。听起来好像您正在公开更改对象状态的方法,而状态主要取决于该方法被调用的次数。这在尝试跟踪整个代码中对象的状态是一个噩梦。对我来说,听起来更像是您将从这些概念状态的不同数据类型中受益,因此您可以知道任何给定代码段中的内容而不必担心。
jpmc26 2013年

2
@StevieV之类的数据和序列化数据。数据就是代码看到的东西,而SerializedData是通过网络发送的东西。然后,使反向数据(或更确切地说是序列化/反序列化数据)从一种类型转换为另一种类型。
csiz

Answers:


33

我想说这不仅可以接受,而且值得鼓励,尤其是如果您打算允许扩展。为了支持C#中类的扩展,您需要根据以下注释将方法标记为虚方法。但是,您可能需要对此进行记录,以使在重写ReverseData()更改ScheduleTransmission()的工作方式时不会感到惊讶。

这实际上取决于班级的设计。ReverseData()听起来像是类的基本行为。如果您需要在其他地方使用此行为,则可能不希望拥有其他版本。您只需要注意不要使ScheduleTransmission()的特定细节泄漏到ReverseData()中。那会造成问题。但是,由于您已经在课外使用了它,因此您可能已经想到了。


哇,对我来说真的很奇怪,但这很有帮助。也想看看别人怎么说。谢谢。
史努比

2
“所以当重写ReverseData()更改ScheduleTransmission()的工作方式时,不会有人感到惊讶”:不,不是真的。至少不是在C#中(请注意,问题具有C#标记)。除非ReverseData()是抽象的,否则无论您在子类中做什么,它始终都是将被调用的原始方法。
Arseni Mourzenko '16

2
@MainMa或virtual...如果要覆盖此方法,则根据定义,它必须为virtualabstract
Pokechu22年

1
@ Pokechu22:确实virtual也可以。根据OP的说法,在所有情况下,签名都是public void ReverseData(),因此答案中有关覆盖内容的部分会产生误导。
Arseni Mourzenko '16

@MainMa您是说,如果基类方法调用虚拟方法,除非它是通常的虚拟方式,否则不会表现出来abstract?我在abstractvs 上查找了答案virtual,但没有看到提到的内容:相当抽象只是意味着在此基础上没有给出任何版本。
JDługosz

20

绝对。

方法的可见性的唯一目的是允许或拒绝对类外部或子类内部方法的访问。公共,保护和私有方法始终可以在类本身内部调用。

调用公共方法没有错。您问题中的插图是存在两种公共方法正是您应做的情况的完美示例。

但是,您可能会仔细注意以下模式:

  1. 方法Hello()调用World(string, int, int)如下:

    Hello()
    {
        this.World("Some magic value here", 0, 100);
    }

    避免这种模式。而是使用可选参数。可选参数使发现变得更容易:键入的调用者Hello(不一定知道有一个方法可以使用默认值调用该方法。可选参数也是自记录的。World()不会向呼叫者显示实际的默认值是多少。

  2. 方法Hello(ComplexEntity)调用World(string, int, int)如下:

    Hello(ComplexEntity entity)
    {
        this.World(entity.Name, entity.Start, entity.Finish);
    }

    相反,请使用重载。相同的原因:更好的可发现性。呼叫者可以通过IntelliSense立即看到所有过载,并选择正确的过载。

  3. 一个方法仅调用其他公共方法,而不会增加任何实质性价值。

    三思而后行。您真的需要这种方法吗?还是应该删除它,让调用者调用其他方法?提示:如果该方法的名称看起来不正确或很难找到,则可能应将其删除。

  4. 一个方法验证输入,然后调用另一个方法:

    Hello(string name, int start, int end)
    {
        if (name == null) throw new ArgumentNullException(...);
        if (start < 0) throw new OutOfRangeException(...);
        ...
        if (end < start) throw new ArgumentException(...);
    
        this.World(name, start, end);
    }

    相反,World应该自己验证其参数,或者将其私有。


如果ReverseData实际上是内部的,并且在包含“对象”实例的整个命名空间中使用,是否也适用相同的想法?
史努比

1
@StevieV:是的,当然。
Arseni Mourzenko '16

@MainMa我不明白第1点和第2
-Kapol

@Kapol:谢谢您的反馈。我通过添加关于前两点的解释来编辑答案。
Arseni Mourzenko '16

即使在第一种情况下,我通常也喜欢重载而不是可选参数。如果选项使得不能使用重载或产生大量的重载,那么我将创建一个自定义类型并将其用作参数(如建议2中所述),或重构代码。
布赖恩

8

如果某件事是公开的,则任何系统都可以随时调用它。您没有理由也不能成为这些系统之一!

在高度优化的库中,您想要复制在 java.util.ArrayList#ensureCapacity(int)

确保能力

  • ensureCapacity 是公开的
  • ensureCapacity 具有所有必要的边界检查和默认值等。
  • ensureCapacity 来电 ensureExplicitCapacity

确保容量内部

  • ensureCapacityInternal 是私人的
  • ensureCapacityInternal 几乎没有错误检查,因为所有输入都来自类内部
  • ensureCapacityInternal 还打来 ensureExplicitCapacity

确保ExplicitCapacity

  • ensureExplicitCapacity 还是私人的
  • ensureExplicitCapacity没有错误检查
  • ensureExplicitCapacity 做实际的工作
  • ensureExplicitCapacity是不是从任何地方叫除非ensureCapacityensureCapacityInternal

这样,您信任的代码将获得特权(并且更快!)访问,因为您知道其输入是好的。您不信任的代码会经过严格的验证,以验证其完整性并对其进行轰炸,提供默认值或以其他方式处理错误的输入。他们两个都引导到实际工作的地方。

但是,它在ArrayList中使用,ArrayList是JDK中最常用的类之一。您的案件很有可能不需要那么复杂和严格的程度。长话短说,如果将所有ensureCapacityInternal电话替换为ensureCapacity电话,性能仍然会非常非常好。这可能是经过广泛考虑后才进行的微优化。


3

已经给出了几个很好的答案,我也同意他们的观点,是的,一个对象可以从其他方法中调用其公共方法。但是,您需要注意一些设计上的注意事项。

公共方法通常具有以下约定:“使对象处于一致状态,执行明智的操作,使对象处于(可能不同的)一致状态”。此处,“一致”可能表示,例如Lengtha的a List<T>不大于其Capacity,并且引用从0到索引处的元素Length-1不会抛出。

但是在对象的方法内部,对象可能处于不一致状态,因此,当您调用自己的公共方法之一时,它可能做错了非常大的事情,因为在编写时并未考虑到这种可能性。因此,如果您打算从其他方法中调用您的公共方法,请确保其合同“将对象置于某种状态,进行明智的操作,将对象置于(可能不同)(可能不一致)(可能不一致,但仅当初始状态状态不一致)。


1

在另一个公共方法中调用公共方法完全可以的另一个示例是CanExecute / Execute方法。我既需要验证又需要不变保存时使用它。

但总的来说,我对此始终保持谨慎。如果将一个方法a()称为内部方法b(),则意味着该方法a()是的实现细节b()。很多时候,它表明它们属于不同的抽象级别。他们俩都是公开的事实使我想知道这是否违反了单一责任原则


-5

抱歉,我将不得不不同意其他大多数“是的”答案,并说:

我不鼓励从另一个方法调用一个公共方法的类

这种做法有两个潜在的问题。

1:继承类中的无限循环

因此,您的基类从method2调用method1,但是您或其他人继承了该类,并用调用method2的新方法隐藏了method1。

2:事件,日志记录等

例如,我有一个方法Add1会触发一个事件“添加了1!”。我可能不希望Add10方法引发该事件,将其写入日志或其他任何十次。

3:线程和其他死锁

例如,InsertComplexData打开一个数据库连接,开始一个事务,锁定一个表,然后调用InsertSimpleData,打开一个连接,开始一个事务,等待表被解锁。...

我敢肯定还有更多原因,其中另一个答案是“您编辑method1并感到惊讶method2开始表现不同”

通常,如果您有两个共享代码的公共方法,最好使它们都调用私有方法,而不是一个调用另一个方法。

编辑----

让我们扩展一下OP中的特定情况。

我们没有很多细节,但是我们知道ReverseData是由某种事件处理程序以及ScheduleTransmission方法调用的。

我认为反向数据也会改变对象的内部状态

在这种情况下,我认为线程安全很重要,因此我对实践的第三个反对意见适用。

为了使ReverseData线程安全,您可以添加一个锁。但是,如果ScheduleTransmission也需要是线程安全的,则您将希望共享相同的锁。

最简单的方法是将ReverseData代码移到私有方法中,并让两个公共方法都调用它。然后,您可以将lock语句放入“公共方法”中并共享一个锁对象。

显然,您可以争论“那将永远不会发生!” 或“我可以用另一种方式对锁进行编程”,但是关于良好编码实践的重点是首先要很好地构建代码。

用学术术语来说,这肯定违反了L。公开方法不仅仅是公开接受的方法。它们也可以由其继承者修改。应该关闭您的代码以进行修改,这意味着您必须考虑在公共方法和受保护方法中所做的工作。

这是另一个:您也可能违反DDD。如果您的对象是领域对象,则其公共方法应为“领域”术语,这对企业而言具有一定意义。在这种情况下,即使以这种方式开始,“买一打鸡蛋”和“买一枚鸡蛋12次”的可能性很小。


3
这些问题听起来更像是思想欠佳的体系结构,而不是对设计原理的普遍谴责。(也许每次都调用“ 1添加”是可以的。如果不是,我们应该以不同的方式编写程序。也许如果两个方法试图互斥锁定同一资源,则它们不应互相调用,否则我们编写的效果会很差。)与其提出不良的实现方式,不如将其重点放在更坚实的论点上,这些论点将访问公共方法作为通用原则进行处理。例如,给定良好的代码,为什么不好呢?
doppelgreener

确实,如果没有,您没有放置事件的地方。同样,对于#3来说,一切都很好,直到链上的某个方法需要进行交易,然后您被搞砸了
Ewan 2016年

所有这些问题更多地反映了代码本身质量较差,而不是一般原则存在缺陷。这三种情况都是不好的代码。它们可以通过某种形式来解决“不这样做,这样做,而不是”(也许问“什么你想要什么呢?”),并没有质疑一类的一般原则调用自身的公众方法。从原理上讲,无限循环的隐含潜力是唯一可以解决此问题的方式,即使到那时也仍无法彻底解决。
doppelgreener

在我的示例中,唯一共享“代码”的是一个调用另一个方法的公共方法。如果您认为其“错误代码”很好!那是我的争论
Ewan

可以用锤子砸破自己的手并不意味着锤子是坏的。这意味着您无需执行任何会伤到您的手的事情。
doppelgreener
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.