Используйте фабрику и шаблон построителя, чтобы создать какой-нибудь класс наилучшим образом - PullRequest
0 голосов
/ 01 августа 2020

У меня есть фабричный класс, и я использую построитель для создания класса для фабричного метода класса. В приведенном ниже примере я заменяю пользовательский построитель на StringBuilder на упрощенный пример.

Перед тем, как создать построитель для класса, поведение будет таким же. Я не хочу писать повторяющийся код, поэтому я создаю базовый фабричный класс для инкапсуляции метода и производного метода или делегирую его строителю.
Таким образом, дочерний класс может переопределять метод для подключения строителя и управления строителем.

Полный код.

public abstract class FactoryBase
{
    protected delegate void HookSomeStringHandler(StringBuilder builder);

    protected HookSomeStringHandler OnHookSomeStringHandler;

    /// <summary>
    /// You can override <see cref="InnerHookSomeString"/> to hook builder.
    /// </summary>
    public string GetSomeStringA()
    {
        var sb = new StringBuilder();
        sb.Append(GetType().Name); // need all child class name              
        InnerHookSomeString(sb);   // hook StringBuilder to append some string
        return sb.ToString();
    }

    /// <summary>
    /// Child class can override this to hook StringBuilder <see cref="GetSomeStringA"/>
    /// </summary>
    protected virtual void InnerHookSomeString(StringBuilder builder)
    {
    }

    /// <summary>
    /// You can override method to hook stringBuilder or using delegate action to hook stringBuilder. 
    /// </summary>
    public virtual string GetSomeStringB(Action<StringBuilder> outerHook)
    {
        var sb = new StringBuilder();
        sb.Append(GetType().Name);  // need all child class name              
        outerHook?.Invoke(sb);      // hook StringBuilder to append some string
        return sb.ToString();
    }

    /// <summary>
    /// Use register delegate to hook stringBuilder. <see cref="OnHookSomeStringHandler"/>
    /// </summary>
    public string GetSomeStringC()
    {
        var sb = new StringBuilder();
        sb.Append(GetType().Name);                // need all child class name              
        OnHookSomeStringHandler?.Invoke(sb);      // hook StringBuilder to append some string
        return sb.ToString();
    }
}

public class ChildA : FactoryBase
{
    public ChildA()
    {
        OnHookSomeStringHandler += (sb) =>
        {
            // TODO do something by GetSomeStringC
        };
    }

    protected override void InnerHookSomeString(StringBuilder builder)
    {
        // TODO do something by GetSomeStringA
    }

    public override string GetSomeStringB(Action<StringBuilder> outerHook)
    {
        return base.GetSomeStringB((sb) =>
        {
            // TODO do something by GetSomeStringB
        });
    }
}

Примечание. Строителю в GetSomeString не нужно каждый раз добавлять строку или что-то делать, поэтому я не использую абстрактный метод для принудительного применения дочернего класса, который должен быть отменен.

У меня есть три идеала для этой ситуации.

  1. GetSomeStringA используйте InnerHookSomeString, чтобы зацепить StringBuilder и дочерний класс может управлять построителем, но этот способ записи, возможно, пользователь не знает этого подхода, поэтому необходимо использовать тег <see cref>.

  2. GetSomeStringB использовать переопределение для перехвата StringBuilder и может подключать конструктор снаружи, но такой способ записи выглядит некрасиво.

  3. GetSomeStringC аналогично GetSomeStringA, это делается путем регистрации делегата и необходимости использовать тег и для запроса пользователя.

Какой из e выше трех методов лучше поддерживать или читать?
Есть ли у кого-нибудь идея или предложение получше?

1 Ответ

1 голос
/ 03 августа 2020

Это зависит от ваших намерений. В целом все три решения - плохой дизайн. Также, учитывая предоставленный вами контекст, похоже, что термин или имя 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);
}

Обратите внимание, что оба решения работают на одном уровне деталей: все параметры относятся к сообщению журнала, а не к деталям внутренней реализации, например как сообщение сохраняется. В этом контексте разумный уровень подробностей, касающихся самой регистрации, будет параметром конфигурации для управления тем, какой приемник данных использовать, например, электронную почту или базу данных.

...