Если то, что делает класс, достаточно просто, чтобы мы могли одновременно тестировать его публичные и приватные методы, тогда мы можем просто протестировать публичные методы.
Если приватный метод становится настолько сложным, что между публичным и приватным методом нам нужно слишком много комбинаций тестов, тогда пришло время разделить приватный метод на его собственный класс. Публичный публичный метод нарушил бы инкапсуляцию класса. Даже если мы не добавим метод в interface
, общедоступный метод класса все равно добавит метод в открытый интерфейс самого класса .
Итак, если у нас есть это:
public class ClassWithComplexPrivateMethod
{
public void DoSomething(int value)
{
PrivateMethodThatNeedsItsOwnTests(value);
}
private string PrivateMethodThatNeedsItsOwnTests(int value)
{
// This method has gotten really complicated!
return value.ToString();
}
}
Мы можем изменить рефакторинг на что-то вроде этого:
public interface IRepresentsWhatThePrivateMethodDid
{
string MethodThatNeedsItsOwnTests(int value);
}
public class RefactoredClass
{
private readonly IRepresentsWhatThePrivateMethodDid _dependency;
public RefactoredClass(IRepresentsWhatThePrivateMethodDid dependency)
{
_dependency = dependency;
}
public string DoSomething(int value)
{
return _dependency.MethodThatNeedsItsOwnTests(value);
}
}
А теперь новый класс реализует IRepresentsWhatThePrivateMethodDid
.
Теперь, когда мы тестируем реорганизованный класс, мы имитируем IRepresentsWhatThePrivateMethodDid
и пишем отдельные модульные тесты для любых классов, которые реализуют IRepresentsWhatThePrivateMethodDid
.
Может показаться противоречием утверждение о том, что разоблачение приватного метода как публичного нарушает инкапсуляцию, а разоблачение его как отдельного класса - нет. Есть два отличия:
- Реорганизованный класс не зависит от нового класса, содержащего то, что раньше было закрытым методом. Это зависит от нового интерфейса.
- Взаимодействие между рефакторированным классом и интерфейсом все еще скрыто в его методах. Другие классы, которые вызывают его публичные методы, не «знают» о том, как он использует свою зависимость. (На самом деле, другие классы, скорее всего, будут зависеть от абстракций, а не напрямую от переработанного класса.)
Также легко увлечься этим и слишком рано ввести отдельную зависимость, когда мы могли бы протестировать класс - включая его частные методы - с помощью открытых методов. Я делал это много раз, и это может привести к большому количеству ненужных интерфейсов и дополнительных классов. Там нет совершенства, только наши лучшие усилия, чтобы сбалансировать его.
Еще одна мысль: мы склонны использовать интерфейсы для представления зависимостей, но мы не обязаны это делать. Если то, что мы извлекаем, было только одним закрытым методом, то, возможно, мы можем представить его с делегатом или Func
вместо этого, например так:
public class RefactoredClass
{
private readonly Func<int, string> _dependency;
public RefactoredClass(Func<int, string> dependency)
{
_dependency = dependency;
}
public string DoSomething(int value)
{
return _dependency(value);
}
}
Или мы можем использовать делегата, который мне нравится больше, чем Func
, потому что он указывает, что делает функция.