簡體   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