简体   繁体   English

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

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

This is a question about design/best practices. 这是关于设计/最佳实践的问题。

I have the following class: 我有以下课程:

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

   public SomeObject GetNext() 
   {
      return nextObject;
   } 
} 

I consider this a bad design because the users of this class need to be aware that they need to call IsNextAvailable() before calling GetNext() . 我认为这是一个糟糕的设计,因为这个类的用户需要知道他们需要在调用GetNext()之前调用IsNextAvailable() GetNext()

However, this "hidden contract" is the only thing which I can see wrong about, that the user can call GetNext() when there is nothing avaiable. 但是,这个“隐藏的契约”是我唯一能看到错误的东西,用户可以在没有任何可用的情况下调用GetNext()。 (I would be happy if anyone can point out other scenarios in which this implementation is bad) (如果有人能指出这种实现不好的其他场景,我会很高兴)

A second implementation I thought of is that GetNext() throws an exception if nextObject is not available. 我想到的第二个实现是如果nextObject不可用,GetNext()会抛出异常。 Client will have to handle the exception, plus it can have a small impact on performance and cpu usage due to the exception handling mechanism in .net (I expect this exception to be thrown quite often). 客户端必须处理异常,并且由于.net中的异常处理机制,它可能对性能和CPU使用产生很小的影响(我希望经常抛出此异常)。 Is the exception-driven way a better approach than the previous one? 异常驱动的方式比前一种更好吗? Which is the best way? 哪种方式最好?

That's just fine. 那很好。 In fact, this two-step process is a common idiom for a bunch of .NET BCL classes. 事实上,这个两步过程是一堆.NET BCL类的常用习惯。 See, for example, an IEnumerable : 例如,参见IEnumerable

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

Or DbDataReader : 或者DbDataReader

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

Or Stream , for that matter: 或者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
    }
}

Now, your entire IsNextAvailable() / GetNext() could very well be replaced by implementing an IEnumerable<SomeObject> and thusly your API will be immediately familiar to any .NET developer. 现在,您可以通过实现IEnumerable<SomeObject>来替换整个IsNextAvailable() / GetNext() ,因此您的API将立即为任何.NET开发人员所熟悉。

Neither of them is an ideal solution, where the Exception has my personal preference because it allows a single point of entry. 它们都不是理想的解决方案,其中Exception具有我个人的偏好,因为它允许单个入口点。

You could opt to implement the IEnumerable<SomeObject> interface. 您可以选择实现IEnumerable<SomeObject>接口。 In that way you can provide an enumerator that actually does all the checking for you. 通过这种方式,您可以提供一个实际上为您完成所有检查的枚举器。

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();
    }
}

Disclaimer : This question is in hindsight asking for opinions so I'm torn between closing it (and deleting my answer) or leaving my answer here. 免责声明 :这个问题事后要求提出意见,所以我在关闭它(并删除我的答案)或在此留下我的答案之间徘徊。

In any case, this is my opinion, and only my opinion. 在任何情况下,这我的意见,只有我的意见。


You should always strive for "pit of success". 你应该始终争取“成功的坑”。

The "pit of success" is best described by Jeff Atwood: Falling into the Pit of Success : 杰夫阿特伍德最好地描述了“成功的底蕴”:陷入成功之中

The Pit of Success: in stark contrast to a summit, a peak, or a journey across a desert to find victory through many trials and surprises, we want our customers to simply fall into winning practices by using our platform and frameworks. 成功的底蕴:与峰会,高峰或穿越沙漠的旅程形成鲜明对比,通过许多尝试和惊喜找到胜利,我们希望我们的客户通过使用我们的平台和框架简单地落入获胜实践。 To the extent that we make it easy to get into trouble we fail. 如果我们容易陷入困境,我们就会失败。

The term was coined by Rico Mariani but I am unable to find a clear source for this term. 这个词由Rico Mariani创造,但我无法找到这个术语的明确来源。

Basically, make an API that invites correct use and makes it hard to use wrong. 基本上,制作一个邀请正确使用的API,并且很难使用错误。

Or let me rephrase that: Make the correct usage of your API the only way to use your API . 或者让我重申一下:正确使用API 使用API 的唯一方法

In your case, you haven't done that. 在你的情况下,你没有这样做。

Broad Explanation 广泛的解释

In the case of "is it bad design to require consumers of my API to call methods in the right order, otherwise bad/incorrect things will happen?" 对于“要求我的API的消费者以正确的顺序调用方法是否设计不好,否则会发生错误/不正确的事情?” - the answer is yes. - 答案是肯定的。 This is bad. 这不好。

Instead you should try to restructure your API so that the consumer "falls into the pit of success". 相反,你应该尝试重组你的API,以便消费者“陷入成功的困境”。 In other words, make the API behave in the way that the consumer would assume it would by default. 换句话说,使API的行为方式与消费者默认假设的方式相同。

The problem with this is that it invariably falls down to what people considers "by default". 问题在于它总是落到人们认为“默认”的地方。 Different people might be used to different behavior. 不同的人可能习惯于不同的行为。

For instance, let's say we get rid of IsNextAvailablle [sic] altogether and make GetNext return null in the case of no next available. 例如,假设我们完全摆脱了IsNextAvailablle [原文如此],并且在没有下一个可用的情况下使GetNext返回null

Some purists might say that then perhaps the method should be called TryGetNext . 一些纯粹主义者可能会说,那么也许这个方法应该被称为TryGetNext It may "fail" to produce a next item. 它可能“失败”产生下一个项目。

So here's your revised class: 所以这是你修改过的课程:

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

There should no longer be any doubts as to what this method does. 对这种方法的作用不应再有任何疑问。 It attempts to get the next object from "something". 它试图从“某事”中获取下一个对象。 It may fail, but you should also document that in the case where it fails, the consumer get null in return. 它可能会失败,但您还应记录在失败的情况下,消费者获得null作为回报。

An example API in the .NET framework that behaves like this is the TextReader.ReadLine method: .NET框架中的行为示例API是TextReader.ReadLine方法:

Return Value: 返回值:
The next line from the reader, or null if all characters have been read. 阅读器的下一行,如果已读取所有字符,则为null。

HOWEVER , if the question "is there anything else" can easily be answered, but "give me the next something" is an expensive operation then perhaps this is the wrong approach. 但是 ,如果问题“还有什么”可以很容易地回答,但“给我下一件事”是一项昂贵的操作,那么也许这是错误的做法。 For instance, if the output of GetNext is an expensive and large data structure that can be produced if one has an index into something, and the IsNextAvailablle can be answered by simply looking at the index and seeing that it is still less than 10, then perhaps this should be rethought. 例如,如果GetNext的输出是一个昂贵的大型数据结构,如果有一个索引可以生成,并且可以通过简单地查看索引并看到它仍然小于10来回答IsNextAvailablle ,那么也许这应该重新思考。

Additionally, this type of "simplification" may not always be possible. 另外,这种类型的“简化”可能并不总是可能的。 For instance, the Stopwatch class requires the consumer to start the stopwatch before reading time elapsed. 例如, Stopwatch类要求消费者在阅读时间之前启动秒表。

A better restructuring of such a class would be that you either have a stopped stopwatch or a started stopwatch. 对这样一个班级进行更好的重组将是你有一个停止的秒表或一个已启动的秒表。 A started stopwatch cannot be started. 无法启动已启动的秒表。 Let me show this class: 让我来看看这堂课:

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; }
}

This API doesn't even allow you to do the wrong things. 这个API甚至不允许你做错事。 You cannot stop a stopped stopwatch and since it has never been started you can't even read the time elapsed. 你不能停止一个停止的秒表,因为它从未开始,你甚至无法读取经过的时间。

A running stopwatch however can be paused, or you can read the elapsed time. 然而,可以暂停正在运行的秒表,或者您可以读取已用时间。 If you pause it, you can unpause it to get it running again, or you can read the elapsed time (as of when you paused it). 如果您暂停它,可以取消暂停它以使其再次运行,或者您可以读取已用时间(截至暂停时)。

This API invites correct usage because it doesn't make incorrect usage available. 此API邀请正确使用,因为它不会使错误的使用可用。

So in the broad sense, your class is bad design (in my opinion). 所以从广义上讲,你的课程是糟糕的设计(在我看来)。

Try to restructure the API so that the correct way to use it is the only way to use it . 尝试重新构建API,以便使用它的正确方法是使用它的唯一方法

Specific Case 具体案例

Now, let's deal with your specific code example. 现在,让我们来处理您的特定代码示例。 Is that bad design, and how do you improve it? 这是不好的设计,你如何改进它?

Well, as I said in a comment, if you squint slightly and replace some of the names in the class you have reimplemented IEnumerable: 好吧,正如我在评论中所说,如果你稍微眯眼并替换类中的一些名称,你已经重新实现了IEnumerable:

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

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

So your example class looks a lot like a collection. 所以你的示例类看起来很像集合。 I can start enumerating over it, I can move to the next item, one item at a time, and at some point I reach the end. 我可以开始枚举它,我可以移动到下一个项目,一次一个项目,并在某个时候我到达终点。

In this case I would simply say "don't reinvent the wheel". 在这种情况下,我只想说“不要重新发明轮子”。 Implement IEnumerable because as a consumer of your class, this is what I would you expect you to do . 实现IEnumerable,因为作为您的类的消费者, 这是我希望您做的

So your class should look like this: 所以你的课应该是这样的:

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

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

Again, this is "pit of success". 再次,这是“成功的障碍”。 If the class is in reality a collection of things, use the tools built into .NET to make it behave like any other .NET collection . 如果该类实际上是一组东西,请使用.NET中内置的工具使其行为与任何其他.NET集合一样

If you were to document your class as "a collection of SomeObject instances" I would grab my LINQ and foreach toolkit by default. 如果你要将你的类记录为“SomeObject实例的集合”,我会默认使用我的LINQ和foreach工具包。 When I get a compiler error I would start looking at the members to find the actual collection because I have a very strong sense of what a collection should be in .NET. 当我遇到编译器错误时,我会开始查看成员以查找实际的集合,因为我非常清楚.NET中的集合应该是什么。 I would be very puzzled if you reimplemented all the tools that would be able to handle IEnumerable but simply didn't make it implement this interface. 如果你重新实现了能够处理IEnumerable所有工具 ,但只是没有让它实现这个接口,我会很困惑。

So, instead of me having to write code like this: 所以,而不是我必须编写这样的代码:

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

I can write this: 我可以这样写:

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

Why should the users even have to call IsNextAvailable ? 为什么用户甚至必须调用IsNextAvailable By rights, IsNextAvailable should be private and GetNext should be the one calling it, and then throw an exception or a warning or return a null if there is nothing available. 通过权限, 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