Pull to refresh

Comments 21

Интересно.
В голову сразу приходят примеры, когда хочется создать подобный класс:
— инициализация класса A с использованием объекта класса B в ситуации, когда A и B ничего не знают друг о друге;
— функции над объектами какого-нибудь класса, лежащие за пределами его области ответственности. Например, сохранение чисто математического объекта в формате, заданном внешним приложением. Сам объект про этот формат и его требования ничего не знает.
Как тут обойтись без полтергейстов?
В первом случае, когда нет никакой связи между А и B, то предполагается наличие C, который выступает в роли посредника? Если да, то в такой ситуации мне приходят примеры таких паттернов, как Адаптер, например. Но у таких посредников свое конкретное назначение, которое укладывается в общую схему (согласно которой, например А не должен ничего знать о B).

Во втором случае (на мой взгляд) такое решение опять же определяется конкретной потребностью и несет свою полезную нагрузку. Например, это ведь может быть класс Converter, который принимает этот математический объект и (например) сохраняет его в формате, необходимом для внешней системы. Например в API LibreOffice есть интерфейс XStoreable, который инициализируется XComponent и сохраняет документы в поддерживаемые форматы. XComponent при этом ничего не знает о формате документов MS Office.

В описании же антипаттерна указывается, что классы эти необязательны, а ваши примеры вполне жизненны.
Сам объект про этот формат и его требования ничего не знает

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

  • If it is a unary operator, implement it as a member function.
  • If a binary operator treats both operands equally (it leaves them unchanged), implement this operator as a non-member function.
  • If a binary operator does not treat both of its operands equally (usually it will change its left operand), it might be useful to make it a member function of its left operand’s type, if it has to access the operand's private parts.
  • Если это унарный оператор, реализуйте его как внутренний метод.
  • Если это бинарная операция, которая рассматривает оба операнда неизменными, реализуйте ее как внешнюю функцию
  • Если же бинарная операция модифицирует, например, левый операнд, будет полезным реализовать ее как внутренним метод для типа левого операнда [...].

И более яркий пример — внешнее доопределение оператора сдвига << для случая std::ostream — влезть в ostream мы не можем, а нативно выводить наши объекты, тем не менее, очень хотим.

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

Еще один пример — вещественнозначный вектор.

Чтобы ¨научить" такой вектор умножаться на комплексное число, с выдачей комплекснозначного вектора, мы будем вынуждены натолкать в класс «вещественный вектор» совершенно нерелевантные ему «комплексные» методы.
Это проблемы и особенности конкретного языка (С++).

По поводу std::ostream: в языках, которые изначально разрабатывались как объектно-ориентированные (Java/C#), в любом классе есть метод ToString(), так что преобразование в строку в ООП-языках находится внутри класса.

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

По поводу вещественнозначного вектора: можно написать приведение вещественнозначного вектора к комплекснозначному, а умножение поместить в класс комплекснозначного вектора.
То, что ООП в C++ ослаблено — второй конец палки под названием «программы на C++ могут выполняться весьма компактно и шустро (если их разработчик хорошо знает свое ремесло)».

Полная инкапсуляция иногда идет вразрез с желанием написать действительно быструю программу, принуждая программиста к написанию программ, которые большую часть машинного времени воспроизводят деятельность производственной артели «Реванш» небезызвестных Ильфа и Петрова:
Основной технологический процесс артели \"Реванш\"
В задней комнате находилось производство. Там стояли две дубовые бочки с манометрами и водомерными стеклами, одна — на полу, другая — на антресолях. Бочки были соединены тонкой клистирной трубкой, по которой, деловито журча, бежала жидкость. Когда вся жидкость переходила из верхнего сосуда в нижний, в производственное помещение являлся мальчик в валенках. Не по-детски вздыхая, мальчик вычерпывал ведром жидкость из нижней бочки, тащил ее на антресоли и вливал в верхнюю бочку. Закончив этот сложный производственный процесс, мальчик уходил в контору греться, а из клистирной трубки снова неслось всхлипыванье: жидкость совершала свой обычный путь — из верхнего резервуара в нижний.


Посмотрим на пример с выводом в поток: У нас стоит явная задача выдать данные либо на терминал, либо в файл. При этом, данных может быть достаточно много (например, вектор из 1'000'000 элементов), поэтому тратить время и память на создание очевидно временной строковой переменной смысла мало.

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

Жертвовать простотой\читабельностью\архитектурой можно в любом языке, необязательно используя С++ и необязательно нарушая ООП (например, при выводе в поток данных гигантского объекта, можно запрашивать у объекта данные порциями, используя паттерн iterator). При этом быстродействие необязательно будет максимальным при использовании именно С++. В Java/C# используется JIT-компилятор, который «на лету» компилирует программу и оптимизирует её под конкретный процессор пользователя, в результате чего программа медленнее запускается но быстрее работает (что особенно полезно в серверных приложениях, которые запускаются раз в сутки и работают весь день), быстродействию тут может помешать разве что периодически запускаемый сборщик мусора.
Имхо, статья была про проектирование, а про проектирование не стоит говорить в контексте «а как быть, если нужно написать код для микроконтроллера?». В микроконтроллерах понятность кода как ценность находится на самом последнем месте. Не в микроконтроллерах не нужно оптимизировать всё подряд — нужно искать узкие места.
Спасибо за статью.
Во время прочтения возник такой вопрос: в MVC — классы Controller'ы будут являться полтергейстами?
Если вы про веб — то зависит от нагруженности. Если делать по принципу «контроллер на область ответственности» — не будут. А если делать как любят — «контроллер на страницу» — то получится очень даже похоже.
А если делать как любят — «контроллер на страницу» — то получится очень даже похоже.

Ну вот есть у нас какая-то особая страница (а точнее тип страницы), которая состоит из специфически настроенных блоков. Ну допустим, страница дифов в Гите. Как её настроить то корректно? Ну, допустим, будет какой-то такой псевдокод:

class DiffPage extends Page {

  public void Launch () {
    Register( new Blocks.MasksDescription() )
    Register( new Blocks.SplitDiff( this.model ) )
    Register( new Blocks.Comments( this.id ) )
  }

}


Где иначе этот код должен быть?
Это — код страницы, а не контроллера.

Но даже для контроллера такое нормально — ведь все эти данные реально нужно отобразить. Вот если рядом появятся методы AddComment, AddDiff и AddMask — которые будут дублироваться на каждой второй странице — значит архитектура была выбрана неверно.
Вот если рядом появятся методы AddComment, AddDiff и AddMask — которые будут дублироваться на каждой второй странице

Но это же уже нарушение DRY, зачем отдельный антипаттерн?

Это — код страницы, а не контроллера

Ну это своего рода контроллер страницы. Суть же не в названии, можно было бы назвать и DiffPageController.
Пожалуйста. В статье стоило скорее написать полтергейст-«контроллер», чтобы подчеркнуть, что таковым он является номинально, так как эти классы только «задумывались как классы-контроллеры». MVC — это паттерн. Там контроллер решает свою задачу в рамках расслоения по уровням и убрать его, не нарушив решения, полученного при использовании MVC (на мой взгляд), не получится.
Прочитал перевод, прочитал в оригинале, прочитал википедию, нашёл вялый вопрос на SO, всё равно не понял. Не возьмётся ли кто-то предложить короткий пример?
Да, с примерами действительно все плохо. Как бы не оказалось это чисто-теоретической вещью, ведь многие на форумах просят привести конкретные примеры (сам тоже пытался найти) но их нет (или плохо искал).

Не уверен, насколько предложенный пример соответствует антипаттерну, но вроде по всем параметрам он подходит под описание:
Пример на С++
#include <iostream>

using namespace std;

//объект воздействия
class Terminal
{
public:
    static void promptLogin();
};
void Terminal::promptLogin()
{
    cout << "Prompt login";
}
//полтергейст
class Controller
{
public:
    void startLogin();
};

void Controller::startLogin()
{
    Terminal::promptLogin();
}
//субъект действия
class User
{
public:
    void doLogin();
};
void User::doLogin()
{
    Controller controller; //время жизни заканчивается в doLogin
    controller.startLogin();
}

int main()
{
    User user;
    user.doLogin();
    return 0;
}



О, спасибо. Если я правильно понял, можно предложить более простую иллюстрацию:
Пример на Java
class NumberAdder {
  private final int a;
  private final int b;

  public NumberAdder(int a, int b) {
    this.a = a;
    this.b = b;
  }

  public int addNumbers() {
    return a + b;
  }
}

// в вызывающем коде:
...
NumberAdder numberAdder = new NumberAdder(2, 3);
int result = numberAdder.addNumbers();

// или даже:
...
int result = new NumberAdder(2, 3).addNumbers();


Несколько раз встречал такой подход в коде коллег. На вопрос «зачем?» внятного ответа ни разу не получил, предпочитали молча переделать.
Интересная игра фамилий — первое упоминание о «призраках» сделал Майкл Акройд (Michael Akroyd), а одну из главный ролей в «Охотниках за приведениями» сыграл Дэн Экройд (Daniel Edward «Dan» Aykroyd).
image
Есть подозрение, что часть проблем из-за людей которые учились программированию на Java а потом перешли на С++. У Java отсутствует возможность писать методы вне класса, что часто приводит к выше описанным странностям архитектуры.
Отсутствие возможности писать методы все класса еще не означает, что нельзя написать статический метод и притвориться, что он вне класса.
Не означает. Но тут ключевое слово «учились», а не были опытными Java разработчиками. :)
Тема интересная, но перевод _представляет_ немало переводческих ошибок:

>Big DoIt Controller Class
А где собственно перевод? Как варианты для doIt могу предложить «мастера на все руки» и «контроллер всякого».

>был впервые _представлен_
«озвучен» хотя бы

>_представляет_ концепцию
показывает / демонстрирует

>в другом, более постоянном (с большим временем жизни) классе
«в классе с большим временем жизни». Тут скорее даже не в классе, а в объекте.

>Цыганские вагоны
тут уже со строчной + не вагоны, а повозки.

>каждый раз, когда они «появляются»;
лишнее «они»

>выбор _пути_ решения
«способа»

>пути навигации
В рамках статьи можно смело заменить «(межклассовыми) связями».

>рассмотрим пример на рисунке.
и никакого перевода ((

>на подобии
наподобие

>которые инкапсулированы _нем_
«в нём»

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

>разработчики проектируют _также_, как они реализуют систему
«разработчики пишут код и проектируют одновременно».

>«by the seat of their pants»
Это даже в словарях проходит как «по наитию», но тут подойдёт и «по велению левой пятки»

>Появление антипаттерна зачастую признается именно с архитектурной точки зрения, и посредством эффективного управления стоит учитывать эту точку зрения сразу.
«Если он не был обнаружен сразу на уровне архитектуры, то позже должен быть обнаружен на уровне логики взаимодействия». Как-то так.

>Менеджеры должны заботиться о том, чтобы объектно-ориентированные архитектуры оценивались квалифицированными архитекторами
«Руководители должны следить, чтобы опытные специалисты контролировали качество архитектуры на всех этапах».

Ну и запятых перед «а» и «но» нехватает.
Sign up to leave a comment.

Articles