如何使这个方法更加通用?

3
我有一个名为LoadToDictionary的方法,它接受一个字典和一个文件路径。目前,它看起来像这样:void LoadToDictionary(Dictionary<string, string> dict, string filePath)。然后,在内部,它会有很多代码将filePath中的内容提取到一个字典中。我想让它更通用,比如说可以接受一个具有int类型的字典,并且可以更改方法中的解析代码。我应该标记这个方法并稍后重写它吗?还有其他方法吗?

请添加代码,这样我们就可以更好地了解哪些地方可以改进... - Victor Alejandria
2个回答

36
过早的泛化会导致过早优化成为罪魁祸首。泛化是昂贵的,昂贵的代码应该有明确的收益来证明其正当性。因此,我建议基于特定场景增加你的方法的普适性。我可以想到许多方法来使你的方法更加普适:
  1. 不要使用字符串到字符串的字典;使用字符串到任意类型的字典。
  2. 不要使用Dictionary<...>;使用IDictionary<...>。
  3. 不要使用任何类型的字典。使用一个Action,它的操作可以将项目输入字典中。
  4. 不要使用文件名。你只是将文件名转换成流,所以最好直接使用Stream。让调用者为你打开流。
  5. 不要使用Stream。你只需要将流转换成一系列的项,所以最好使用IEnumerable<...>,并让调用者为你解析流。
我们能使这个方法变得多么通用呢?假设我们有一系列T,然后将其转换为从K到V的映射。我们需要什么?一个序列、一个键提取器、一个值提取器和一个映射编写器。
static void MakeMap<T, K, V>(this IEnumerable<T> sequence, Action<K, V> mapWriter, Func<T, K> keyExtractor, Func<T, V> valueExtractor)
{
    foreach(var item in sequence)
        mapWriter(keyExtractor(item), valueExtractor(item));
}

注意当代码变得非常通用时,它会变得非常简短。这非常普遍,因此调用站点现在看起来像一团混乱。那真的是你想要的吗?你会有效地使用所有这些普遍性吗?可能不会。你实际上有哪些场景驱使你希望更加通用?使用这些具体场景来激励你的重构。
我们能走得比这更远吗?当然可以。例如,我们可以注意到在其中具有副作用似乎很糟糕。该方法是否应该只返回一个新构建的字典?该字典的比较策略应该是什么?如果有多个具有相同键的项目怎么办?应该替换还是应该让字典实际上成为从键到值列表的映射?我们可以采取这些想法并创建一个解决所有这些问题的新方法:
public static ILookup<TKey, TElement> ToLookup<TSource, TKey, TElement>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey> comparer) { ... }

嗨,我们刚刚重新发明了ToLookup()扩展方法。有时候当你把一个方法变得足够通用时,你会发现它已经存在。


我必须承认你是对的。你提出了一些非常好的想法。我可能会重新考虑其中的一些。 - Dominic K
3
我对这个非常棒的答案点赞。我只想补充一点(纯属个人意见):如果您有一段可以更通用且可重复使用的代码,您可以通过添加一些“传递”方法来简化代码,这些方法调用该代码并提供细节,例如它正在操作一个IDictionary<T,U>,这样您的调用代码就不会变得像Eric所提到的那样异常复杂。 - Rob
1
确实是个很棒的答案。我完全同意。这就是YAGNI原则在实践中的体现。 - Matthew Perron
2
Eric,有时你的答案洞察力非常出色,内容也非常引人入胜,以至于我希望我有多个SO账户,这样我就可以给它投多次赞了。:-D - LBushkin

1

我在这里看到两个实际的问题:
1)方法签名可以是动态的,以便它期望的类型可以更改吗?
2)方法体可以是动态的,以便我的逻辑可以随时更改。

我不确定如何在不使用反射和相当多的代码生成的情况下实现这两个问题。但我相信这里有那些能够做到的人。

但根据您的需求,基本的方法重载听起来就是您真正需要的。

使用您的示例:

void LoadToDictionary(Dictionary<string, string> dict, string filePath)
{ ... code here ... }

//to have a LoadToDictionary method which accepts Dictionary<int, int>
void LoadToDictionary(Dictionary<int, int> dict, string filePath)

//To change the processing logic, you either need a new method name, 
// or to override the original in an inheriting class

void AlternateLoadToDictionary(Dictionary<string, string> dict, string filePath)
override void LoadToDictionary(Dictionary<string, string> dict, string filePath)

一个更好的选择是将解析逻辑与字典创建逻辑分开。你甚至可以从LoadDictionary方法内部调用其他方法,这样普通的重载也可以在那里工作。
void LoadToDictionary(Dictionary<string, string> dict, string filePath)
{
    string[] fileLines;

    if([yourBaseLineCondition])
    fileLines = LoadDataFromFile(filePath, false);

    else fileLines = LoadDataFromFile(filePath, true); 
}

string[] LoadDataFromFile(string filePath, bool altLogic)
{
    if(altLogic) return LoadDataFromFileAlt(filePath)
    else
    { ... your logic ... }
}

string[] LoadDataFromFileAlt(string filePath)
{ ... your alt logic... }

基本的方法重载可以给你很多灵活性。更重要的是,它允许你遵循YAGNI原则,而不会切断扩展的途径。如果需要的话,你总是可以回到你的代码中并添加LoadToDictionary<myCustomObject, myCustomObject>方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接