简体   繁体   中英

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() .

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. (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. 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). 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. See, for example, an IEnumerable :

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

Or DbDataReader :

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

Or Stream , for that matter:

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.

Neither of them is an ideal solution, where the Exception has my personal preference because it allows a single point of entry.

You could opt to implement the IEnumerable<SomeObject> interface. 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.

Basically, make an API that invites correct use and makes it hard to use wrong.

Or let me rephrase that: Make the correct usage of your API the only way to use your 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?" - 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". In other words, make the API behave in the way that the consumer would assume it would by default.

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.

Some purists might say that then perhaps the method should be called 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.

An example API in the .NET framework that behaves like this is the TextReader.ReadLine method:

Return Value:
The next line from the reader, or null if all characters have been read.

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.

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.

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. 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.

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 .

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:

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 .

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 .

If you were to document your class as "a collection of SomeObject instances" I would grab my LINQ and foreach toolkit by default. 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. 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.

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 ? 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.

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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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