繁体   English   中英

在从同一个类调用第二个方法之前强制客户端调用方法是不好的做法?

[英]Is it a bad practice to force the client to call a method before calling a second one from the same class?

这是关于设计/最佳实践的问题。

我有以下课程:

class MyClass 
{
   public bool IsNextAvailablle() 
   { 
      // implementation
   }

   public SomeObject GetNext() 
   {
      return nextObject;
   } 
} 

我认为这是一个糟糕的设计,因为这个类的用户需要知道他们需要在调用GetNext()之前调用IsNextAvailable() GetNext()

但是,这个“隐藏的契约”是我唯一能看到错误的东西,用户可以在没有任何可用的情况下调用GetNext()。 (如果有人能指出这种实现不好的其他场景,我会很高兴)

我想到的第二个实现是如果nextObject不可用,GetNext()会抛出异常。 客户端必须处理异常,并且由于.net中的异常处理机制,它可能对性能和CPU使用产生很小的影响(我希望经常抛出此异常)。 异常驱动的方式比前一种更好吗? 哪种方式最好?

那很好。 事实上,这个两步过程是一堆.NET BCL类的常用习惯。 例如,参见IEnumerable

using(var enumerator = enumerable.Enumerator())
{
    while(enumerator.MoveNext())
    {
        // Do stuff with enumerator.Current
    }
}

或者DbDataReader

using(var dbDataReader = dbCommand.ExecuteReader())
{
    while(dbDataReader.Read())
    {
        // Do stuff with dbDataReader
    }
}

或者Stream ,就此而言:

var buffer = new byte[1024];

using(var stream = GetStream())
{
    var read = 0;
    while((read = stream.Read(buffer, 0, buffer.Length)))
    {
        // Do stuff with buffer
    }
}

现在,您可以通过实现IEnumerable<SomeObject>来替换整个IsNextAvailable() / GetNext() ,因此您的API将立即为任何.NET开发人员所熟悉。

它们都不是理想的解决方案,其中Exception具有我个人的偏好,因为它允许单个入口点。

您可以选择实现IEnumerable<SomeObject>接口。 通过这种方式,您可以提供一个实际上为您完成所有检查的枚举器。

class MyClass : IEnumerable<SomeObject>
{
    private bool IsNextAvailablle()
    {
        // implementation
    }

    private SomeObject GetNext()
    {
        return nextObject;
    }

    public IEnumerator<SomeObject> GetEnumerator()
    {
        while (IsNextAvailablle())
        {
            yield return GetNext();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

免责声明 :这个问题事后要求提出意见,所以我在关闭它(并删除我的答案)或在此留下我的答案之间徘徊。

在任何情况下,这我的意见,只有我的意见。


你应该始终争取“成功的坑”。

杰夫阿特伍德最好地描述了“成功的底蕴”:陷入成功之中

成功的底蕴:与峰会,高峰或穿越沙漠的旅程形成鲜明对比,通过许多尝试和惊喜找到胜利,我们希望我们的客户通过使用我们的平台和框架简单地落入获胜实践。 如果我们容易陷入困境,我们就会失败。

这个词由Rico Mariani创造,但我无法找到这个术语的明确来源。

基本上,制作一个邀请正确使用的API,并且很难使用错误。

或者让我重申一下:正确使用API 使用API 的唯一方法

在你的情况下,你没有这样做。

广泛的解释

对于“要求我的API的消费者以正确的顺序调用方法是否设计不好,否则会发生错误/不正确的事情?” - 答案是肯定的。 这不好。

相反,你应该尝试重组你的API,以便消费者“陷入成功的困境”。 换句话说,使API的行为方式与消费者默认假设的方式相同。

问题在于它总是落到人们认为“默认”的地方。 不同的人可能习惯于不同的行为。

例如,假设我们完全摆脱了IsNextAvailablle [原文如此],并且在没有下一个可用的情况下使GetNext返回null

一些纯粹主义者可能会说,那么也许这个方法应该被称为TryGetNext 它可能“失败”产生下一个项目。

所以这是你修改过的课程:

class MyClass 
{
   public SomeObject TryGetNext() 
   {
      return nextObject; // or null if none is available
   } 
}

对这种方法的作用不应再有任何疑问。 它试图从“某事”中获取下一个对象。 它可能会失败,但您还应记录在失败的情况下,消费者获得null作为回报。

.NET框架中的行为示例API是TextReader.ReadLine方法:

返回值:
阅读器的下一行,如果已读取所有字符,则为null。

但是 ,如果问题“还有什么”可以很容易地回答,但“给我下一件事”是一项昂贵的操作,那么也许这是错误的做法。 例如,如果GetNext的输出是一个昂贵的大型数据结构,如果有一个索引可以生成,并且可以通过简单地查看索引并看到它仍然小于10来回答IsNextAvailablle ,那么也许这应该重新思考。

另外,这种类型的“简化”可能并不总是可能的。 例如, Stopwatch类要求消费者在阅读时间之前启动秒表。

对这样一个班级进行更好的重组将是你有一个停止的秒表或一个已启动的秒表。 无法启动已启动的秒表。 让我来看看这堂课:

public class StoppedStopwatch
{
    public RunningStopwatch Start()
    {
        return new RunningStopwatch(...);
    }
}

public class RunningStopwatch
{
    public PausedStopwatch Pause()
    {
        return new PausedStopwatch(...);
    }

    public TimeSpan Elapsed { get; }
}

public class PausedStopwatch
{
    public RunningStopwatch Unpause()
    {
        return new RunningStopwatch(...);
    }

    public TimeSpan Elapsed { get; }
}

这个API甚至不允许你做错事。 你不能停止一个停止的秒表,因为它从未开始,你甚至无法读取经过的时间。

然而,可以暂停正在运行的秒表,或者您可以读取已用时间。 如果您暂停它,可以取消暂停它以使其再次运行,或者您可以读取已用时间(截至暂停时)。

此API邀请正确使用,因为它不会使错误的使用可用。

所以从广义上讲,你的课程是糟糕的设计(在我看来)。

尝试重新构建API,以便使用它的正确方法是使用它的唯一方法

具体案例

现在,让我们来处理您的特定代码示例。 这是不好的设计,你如何改进它?

好吧,正如我在评论中所说,如果你稍微眯眼并替换类中的一些名称,你已经重新实现了IEnumerable:

class MyClass                          interface IEnumerable
{                                      {
   public bool IsNextAvailablle()          public bool MoveNext()
   {                                       {
      // implementation
   }                                       }

   public SomeObject GetNext()             public SomeObject Current
   {                                       {
      return nextObject;                       get { ... }
   }                                       }
}                                      }

所以你的示例类看起来很像集合。 我可以开始枚举它,我可以移动到下一个项目,一次一个项目,并在某个时候我到达终点。

在这种情况下,我只想说“不要重新发明轮子”。 实现IEnumerable,因为作为您的类的消费者, 这是我希望您做的

所以你的课应该是这样的:

class MyClass : IEnumerable<SomeObject>
{
    public IEnumerator<SomeObject> GetEnumerator()
    {
        while (... is next available ...)
            yield return ... get next ...;
    }

    public IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
} 

再次,这是“成功的障碍”。 如果该类实际上是一组东西,请使用.NET中内置的工具使其行为与任何其他.NET集合一样

如果你要将你的类记录为“SomeObject实例的集合”,我会默认使用我的LINQ和foreach工具包。 当我遇到编译器错误时,我会开始查看成员以查找实际的集合,因为我非常清楚.NET中的集合应该是什么。 如果你重新实现了能够处理IEnumerable所有工具 ,但只是没有让它实现这个接口,我会很困惑。

所以,而不是我必须编写这样的代码:

var c = new MyClass();
while (c.IsNextAvailablle())
{
    var item = c.GetNext();
    // process item
}

我可以这样写:

var c = new MyClass();
foreach (var item in c)
    // process item

为什么用户甚至必须调用IsNextAvailable 通过权限, IsNextAvailable应该是私有的, GetNext应该是调用它的那个,然后抛出异常或警告或者如果没有可用的话返回null。

public SomeObject GetNext() 
{
   if(IsNextAvailable())
       return nextObject;
   else
       throw new Exception("there is no next"); // this is just illustrative. By rights exceptions shouldn't be used for this scenario
} 

private bool IsNextAvailable() 
{ 
   // implementation
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM