Шаблон для класса, который делает только одно

24

Допустим, у меня есть процедура, которая делает вещи :

void doStuff(initalParams) {
    ...
}

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

class StuffDoer {
    private someInternalState;

    public Start(initalParams) {
        ...
    }

    // some private helper procedures here
    ...
}

И тогда я называю это так:

new StuffDoer().Start(initialParams);

или вот так:

new StuffDoer(initialParams).Start();

И это то, что кажется неправильным. При использовании .NET или Java API я никогда не звоню new SomeApiClass().Start(...);, что заставляет меня подозревать, что я делаю это неправильно. Конечно, я мог бы сделать конструктор StuffDoer закрытым и добавить статический вспомогательный метод:

public static DoStuff(initalParams) {
    new StuffDoer().Start(initialParams);
}

Но тогда у меня был бы класс, внешний интерфейс которого состоит только из одного статического метода, который также кажется странным.

Отсюда мой вопрос: есть ли устоявшаяся модель для этого типа классов,

  • иметь только одну точку входа и
  • не имеют «внешне распознаваемого» состояния, т. е. состояние экземпляра требуется только во время выполнения этой одной точки входа?
Heinzi
источник
Я был известен тем, что делал, например, bool arrayContainsSomestring = new List<string>(stringArray).Contains("somestring");когда все, о чем я заботился, - это конкретная часть информации, а методы расширения LINQ недоступны. Работает нормально, и вписывается в if()условия без необходимости прыгать через обручи. Конечно, вам нужен язык для сбора мусора, если вы пишете такой код.
CVN
Если это не зависит от языка , зачем добавлять java и c # ? :)
Steven Jeuris
Мне любопытно, какова функциональность этого «одного метода»? Это очень поможет в определении наилучшего подхода в конкретном сценарии, но, тем не менее, интересный вопрос!
Стивен Джеурис
@StevenJeuris: я сталкивался с этой проблемой в различных ситуациях, но в данном случае это метод, который импортирует данные в локальную базу данных (загружает их с веб-сервера, выполняет некоторые манипуляции и сохраняет их в базе данных).
Хайнци
1
Если вы всегда вызываете метод при создании экземпляра, почему бы не сделать его логикой инициализации внутри конструктора?
NoChance

Ответы:

29

Существует шаблон с именем Method Object, в котором вы выделяете один большой метод с большим количеством временных переменных / аргументов в отдельный класс. Вы делаете это, а не просто извлекаете части метода в отдельные методы, потому что им нужен доступ к локальному состоянию (параметры и временные переменные), и вы не можете совместно использовать локальное состояние, используя переменные экземпляра, потому что они будут локальными для этого метода (и методы, извлеченные из него) только и не будут использоваться остальной частью объекта.

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

скоро
источник
1
Это звучит так же, как то, что я делаю. Спасибо, по крайней мере, у меня есть имя для этого сейчас. :-)
Хайнци
2
Очень актуальная ссылка! Это пахнет как анти-шаблон для меня. Если ваш метод такой длинный, возможно, его части можно было бы извлечь в многократно используемые классы или вообще улучшить их рефакторинг? Я также не слышал об этом шаблоне раньше, и я не могу найти много других ресурсов, которые заставляют меня поверить, что он обычно не используется так часто.
Стивен Джеурис
1
@ StevenJeuris Не думаю, что это анти-паттерн. Анти-паттерн будет заключаться в том, чтобы загромождать рефакторизованные методы параметрами партии вместо сохранения этого состояния, которое является общим для этих методов, в переменной экземпляра.
Альфредо Осорио
@AlfredoOsorio: Ну, это мешает переработанным методам с большим количеством параметров. Они живут в объекте, так что это лучше, чем передавать их все, но это хуже, чем рефакторинг для повторно используемых компонентов, которым требуется только ограниченное подмножество параметров.
Ян Худек
13

Я бы сказал, что рассматриваемый класс (с использованием единой точки входа) хорошо спроектирован. Это легко использовать и расширять. Довольно твердый.

То же самое для no "externally recognizable" state. Это хорошая инкапсуляция.

Я не вижу никаких проблем с кодом, как:

var doer = new StuffDoer(initialParams);
var result = doer.Calculate(extraParams);
jgauffin
источник
1
И, конечно, если вам не нужно использовать то же самое doerснова, это сводится к var result = new StuffDoer(initialParams).Calculate(extraParams);.
CVN
Расчет должен быть переименован в doStuff!
Шабун
1
Я бы добавил, что если вы не хотите инициализировать (новый) ваш класс там, где вы его используете (возможно, вы хотите использовать Factory), у вас есть возможность создать объект где-то еще в вашей бизнес-логике и затем передать параметры напрямую. на единственный публичный метод у вас есть. Другой вариант - использовать Factory и иметь метод make, который получает параметры и создает ваш объект со всеми параметрами через его конструктор. Чем вы вызываете свой публичный метод без параметров, откуда вы хотите.
Паткос Чаба
2
Этот ответ слишком упрощен. Есть StringComparerкласс , который вы используете , как new StringComparer().Compare( "bleh", "blah" )хорошо продуманы? А как насчет создания государства, когда в этом нет нужды? Хорошо, поведение хорошо инкапсулировано (по крайней мере, для пользователя класса, подробнее об этом в моем ответе ), но это не делает его «хорошим дизайном» по умолчанию. Что касается вашего примера «результат деятельности». Это имеет смысл, только если вы когда-либо будете использовать doerотдельно от result.
Стивен Джеурис
1
Это не одна из причин существования, это one reason to change. Различие может показаться неуловимым. Вы должны понять, что это значит. Он никогда не создаст один метод классов
jgauffin
8

С точки зрения твердых принципов ответ jgauffin имеет смысл. Однако не стоит забывать об общих принципах проектирования, например, сокрытии информации .

Я вижу несколько проблем с данным подходом:

  • Как вы сами указали, люди не ожидают использовать ключевое слово «new», когда созданный объект не управляет каким-либо состоянием . Ваш дизайн отражает его намерения. Люди, использующие ваш класс, могут запутаться в том, какое состояние он управляет, и могут ли последующие вызовы метода привести к другому поведению.
  • С точки зрения человека, использующего класс, внутреннее состояние хорошо скрыто, но когда вы хотите внести изменения в класс или просто понять его, вы делаете вещи более сложными. Я уже много писал о проблемах, которые я вижу с методами разделения, просто чтобы уменьшить их , особенно при перемещении состояния в область видимости класса. Вы изменяете способ использования вашего API только для того, чтобы иметь меньшие функции! Это, на мой взгляд, определенно заходит слишком далеко.

Некоторые связанные ссылки

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

Другая важная ссылка, относящаяся к этой теме: «Разделите ваши программы на методы, которые выполняют одну идентифицируемую задачу. Сохраните все операции в методе на одном уровне абстракции » - Кент Бек Кей здесь "тот же уровень абстракции". Это не означает «одна вещь», как это часто интерпретируется. Этот уровень абстракции полностью зависит от контекста, для которого вы разрабатываете.

Так каков правильный подход?

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

new TupleList<Key, int>
{
    { Key.NumPad1, 1 },
            ...
    { Key.NumPad3, 16 },
    { Key.NumPad4, 17 },
}
    .ForEach( t =>
    {
        var trigger = new IC.Trigger.EventTrigger(
                        new KeyInputCondition( t.Item1, KeyInputCondition.KeyState.Down ) );
        trigger.ConditionsMet += () => AddMarker( t.Item2 );
        _inputController.AddTrigger( trigger );
    } );

Поскольку сам «локальный» код внутри ForEachне используется повторно нигде, я могу просто сохранить его в точном месте, где он имеет значение. Изложение кода таким образом, что код, опирающийся друг на друга, сильно сгруппирован, делает его более читабельным, на мой взгляд.

Возможные альтернативы

  • В C # вы можете использовать вместо этого методы расширения. Поэтому оперируйте аргументом непосредственно, передавая этот метод «одно».
  • Посмотрите, действительно ли эта функция не принадлежит другому классу.
  • Сделайте это статической функцией в статическом классе . Скорее всего, это наиболее подходящий подход, который также отражен в общих API, на которые вы ссылались.
Стивен Джеурис
источник
Я нашел возможное название для этого анти-паттерна, как обрисовано в общих чертах в другом ответе, Полтергейстах .
Стивен Джеурис
4

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

Скотт Уитлок
источник
Вот Это Да! Тебе удалось разобраться в этом гораздо более кратко, чем я. :) Благодарность. (конечно, я иду немного дальше, где мы можем не согласиться, но я согласен с общей предпосылкой)
Стивен Джеурис
2
new StuffDoer().Start(initialParams);

Существует проблема с невежественными разработчиками, которые могут использовать общий экземпляр и использовать его

  1. несколько раз (хорошо, если предыдущее (может быть частичное) выполнение не портит последующее выполнение)
  2. из нескольких потоков (не в порядке, вы явно сказали, что он имеет внутреннее состояние).

Таким образом, для этого требуется явная документация, что он не является поточно-ориентированным и если он легкий (его создание быстрое, не связывает ни внешние ресурсы, ни много памяти), то можно создать экземпляр на лету.

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

Если он имеет дорогостоящую инициализацию, может быть (не всегда) полезно подготовить его инициализацию один раз и использовать его несколько раз, создав другой класс, который будет содержать только состояние и сделает его клонируемым. Инициализация создаст состояние, которое будет храниться в doer. start()будет клонировать его и передать его внутренним методам.

Это также учитывает другие вещи, такие как сохранение состояния частичного выполнения. (Если на это уйдет много времени, а внешние факторы, например, сбой в электроснабжении, могут прервать выполнение.) Но эти дополнительные причудливые вещи обычно не нужны, поэтому не стоят этого.

user470365
источник
Хорошие моменты. Надеюсь, ты не возражаешь против уборки.
Стивен Джеурис
2

Помимо моего более детального, индивидуального ответа , я считаю, что следующее наблюдение заслуживает отдельного ответа.

Есть признаки того, что вы можете следовать анти-паттерну Полтергейста .

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

Симптомы и последствия

  • Избыточные навигационные пути.
  • Переходные ассоциации.
  • Классы без гражданства.
  • Временные, краткосрочные объекты и классы.
  • Классы с одной операцией, которые существуют только для «инициации» или «вызова» других классов посредством временных ассоциаций.
  • Классы с «подобными элементам управления» именами операций, такими как start_process_alpha.

Рефакторированный раствор

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

Стивен Джеурис
источник