Это зависит от ваших намерений. В целом все три решения - плохой дизайн. Также, учитывая предоставленный вами контекст, похоже, что термин или имя Factory не подходит. Я не вижу создаваемых экземпляров. Я просто вижу какую-то сборку струн. Этот класс должен называться SomeStringCreator
. Присвоение имени классу ... Factory подразумевает, что тип является реализацией шаблона Factory, например, присвоение имени классу ... Builder будет означать, что класс реализует шаблон Builder.
Для лучшего понимания предположим, что мы хотим реализовать класс Logger
. Этот регистратор имеет метод publi c Log(string message)
. Внутренне Logger
может направлять вывод в указанный c приемник данных, например, в файл или базу данных. Клиент Logger
- обычный разработчик, который хочет записать сообщение. Но разработчикам / наследникам разрешено расширять или изменять поведение Logger
, например, изменять приемник данных.
Если вы намереваетесь иметь базовый класс abstract , который обеспечивает / инкапсулирует какое-то общее поведение, тогда 2) и 3) не работают ( хорошо).
abstract
class означает, что класс не будет обеспечивать готовое к использованию поведение. Отсутствующие logi c должны быть реализованы наследником, хотя некоторые basi c logi c уже предоставляются через элементы private
, protected
или virtual
. Если класс готов к использованию, то он не будет объявляться abstract
и будет предоставлять только virtual
членов, для которых требуется расширяемость.
2) Это решение демонстрирует расширяемое поведение с помощью параметра метода publi c, делая поведение publi c:
// Forces the caller to mix high-level and low-level details in a high-level context
public void Log(string message, Action<string> persistMessage)
{
var formattedMessage = AddHeaderToMessage(message);
persistMessage.Invoke(formattedMessage);
}
. В этом примере вызывающая сторона вашего API должна заботиться о внутреннем устройстве (low- level), т.е. logi c, используемый для достижения цели класса, которая заключается в регистрации сообщения (высокий уровень). Это не то, для чего предназначен базовый класс (делегировать внутренние компоненты API c publi) или вообще то, как должен быть разработан API чистого класса.
Внутренние элементы (logi c как класс достигает своей цели ) должен быть скрыт (private
или protected
). Это инкапсуляция. Logi c (подробности низкого уровня) класса не следует вводить в качестве параметра метода, если метод предназначен для работы в контексте высокого уровня. В нашем примере клиент хочет только зарегистрировать сообщение, а не реализовать или предоставить реализацию logi c сохраняемости. Он не хочет смешивать ведение журнала (высокий уровень) и реализацию регистратора (низкий уровень).
3) Не очень удобно. Обратите внимание, что обычно базовый класс всегда должен предоставлять полезный logi c по умолчанию для достижения своей цели. Это означает, что делегат должен быть как минимум инициализирован. Что делает делегат плохим выбором, так это то, что он не является ожидаемым при обеспечении расширяемости. Разработчик всегда ищет виртуальные методы для переопределения. Делегаты хороши, чтобы позволить вызывающему / клиенту определять обратные вызовы.
1) В контексте класса, который предназначен для расширения наследником, решение 1) является правильным. Но ваша текущая реализация подвержена ошибкам. Обратите внимание, что обычно базовый класс всегда должен предоставлять полезный logi c по умолчанию для достижения своей цели (в противном случае используйте интерфейс). Базовый класс abstract
должен объявлять все необходимые члены для достижения sh цели, а также abstract
, чтобы заставить наследника предоставить реализацию или предоставить реализацию virtual
по умолчанию:
// WRONG
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Will fail silently, if the inheritor forgets to override this member
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string message)
{
}
Либо предоставьте реализацию по умолчанию:
// Right
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Can't fail, because the base class provides a default implementation
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string message)
{
// Default implementation
SaveToFile(message);
}
Или сделайте член abstract
:
// Right
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Can't fail, because the inheritor is forced by the compiler to override this member
PersistMessage(formattedMessage);
}
protected abstract void PersistMessage(string message);
Или позвольте нереализованному члену генерировать исключение. Используйте это решение только в том случае, если два предыдущих решения не работают, поэтому обычно не используйте его. Дело в том, что исключение генерируется только во время выполнения, в то время как отсутствующее переопределение класса abstract
генерирует ошибки времени компиляции:
// Right
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Forced to fail at run-time, because the default implementation
// will throw a NotImplementedException (non-silent fail)
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string message)
{
throw new NotImplementedException();
}
Если вы хотите сделать класс расширяемым для клиента при взаимодействии с API, то, конечно, 2) - это решение go с. Например, если вы хотите, чтобы клиент мог изменять форматирование зарегистрированного сообщения, например, какие заголовки или теги использовать или их порядок появления, тогда вы можете разрешить методу принимать связанный logi c или конфигурацию в качестве параметра. . Этот параметр может быть делегатом, объектом конфигурации или строкой формата, в которой используются заполнители, например "<timestamp><callerContext><errorLevel> - <message>"
:
public void Log(string message, string formatPattern)
{
var formattedMessage = AddHeaderToMessage(message, formatPattern);
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string formattedMessage)
{
SaveToFile(message);
}
. Чтобы API был чистым, рассмотрите возможность предоставления свойств publi c и / или перегрузки конструктора. для настройки экземпляра, например, с помощью объекта / параметра делегата или конфигурации:
// Constructor
public Logger(string formatPattern)
{
_formatPattern = formatPattern;
}
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message, _formatPattern);
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string formattedMessage)
{
SaveToFile(message);
}
Обратите внимание, что оба решения работают на одном уровне деталей: все параметры относятся к сообщению журнала, а не к деталям внутренней реализации, например как сообщение сохраняется. В этом контексте разумный уровень подробностей, касающихся самой регистрации, будет параметром конфигурации для управления тем, какой приемник данных использовать, например, электронную почту или базу данных.