Недавно перечитывал книгу Бертрана Мейера "Объектно-ориентированное конструирование программных систем".
Там все функции классов разделяются на три категории: "конструктор", "аксессор" и "модификатор".
А как же быть с функциями, которые выполняют отложенную\ленивую загрузку\инициализацию?
get
{
if (_myWidget == null)
{
_myWidget = Widget.Load(_myWidgetID);
}
return _myWidget;
}
Получается, что это "неправильные" функции?
И ими нельзя пользоваться?
Ведь действительно, когда смотришь на название такой функции, она вводит в заблуждение.
Может быть есть смысл разделить ее на две функции?
Типа так:
prepare
{
if (_myWidget == null)
{
_myWidget = Widget.Load(_myWidgetID);
}
}
get
{
return _myWidget;
}
И в клиентском коде везде явно вызывать две функции при необходимости:
object.prepare();
object.get();
Хотя, так наверно тоже неправильно, потому что клиент и не должен ничего знать про "отложенную" инициализацию.
Как же быть?
Как лучше делать в таких случаях?
Re: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, Maniacal, Вы писали:
M>Ну, это же практически синглтон, только не обязательно глобальный, как ему положено быть.
Возможно, такая реализация Синглтона — это тоже не правильно?
В моем вопросе речь идет о "теории", на которой должны строиться классы\архитектура.
Если теорию нарушать — то это рано или поздно приведет к проблемам.
Вот такая реализация Синглтона — тоже ведь нарушает теорию.
Re: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, zelenprog, Вы писали:
Z>Недавно перечитывал книгу Бертрана Мейера "Объектно-ориентированное конструирование программных систем". Z>Там все функции классов разделяются на три категории: "конструктор", "аксессор" и "модификатор".
Z>А как же быть с функциями, которые выполняют отложенную\ленивую загрузку\инициализацию? Z>Получается, что это "неправильные" функции? Z>И ими нельзя пользоваться?
Нет, получается, что у Мейера неправильная классификация. Либо на нее надо смотреть шире.
Можно (и стоит) учитывать только внешний API класс/функции. Нам совсем не интересно, что происходит под капотом.
Тогда эта функция является "асессором"
Но встречаются совсем странные функции, и при этому очень полезные, get_or_create()
Куда такие относить — еще более непонятно в такой классификации
Best regards, Буравчик
Re[3]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, zelenprog, Вы писали:
Z>В моем вопросе речь идет о "теории", на которой должны строиться классы\архитектура. Z>Вот такая реализация Синглтона — тоже ведь нарушает теорию.
Синглтон это вообще надстройка для отказа от глобальных переменных без отказа от них. А глобальные переменные в классическом случае = неправильная архитектура и костыли.
Re: Мутные функции отложенной (ленивой) загрузки\инициализации
Z>Добрый день!
Z>Недавно перечитывал книгу Бертрана Мейера "Объектно-ориентированное конструирование программных систем". Z>Там все функции классов разделяются на три категории: "конструктор", "аксессор" и "модификатор".
Z>А как же быть с функциями, которые выполняют отложенную\ленивую загрузку\инициализацию? Z>Получается, что это "неправильные" функции? Z>И ими нельзя пользоваться?
Да
Z>Ведь действительно, когда смотришь на название такой функции, она вводит в заблуждение. Z>Может быть есть смысл разделить ее на две функции? Z>И в клиентском коде везде явно вызывать две функции при необходимости:
Нет, это называется "двухшаговая инициализация" — источник многих ошибок.
Z>Хотя, так наверно тоже неправильно, потому что клиент и не должен ничего знать про "отложенную" инициализацию.
Проблема не в этом, а в том, что "инициализация", как и любое другое действие, которое может завершиться неуспехом, должно быть явно видно в исходниках.
Есть общие правила: "гет-свойства не должны бросать исключения", "конструкторы не должны бросать исключения" (в C++ это просто ошибка)
Z>Как же быть? Z>Как лучше делать в таких случаях?
Решение простое — factory method
class C {
protected C(Widget widget) {
this.widget = widget;
}
static C? FromWidgetId(it widgetId)
{
var w = Widget.Load(widgetId);
if w != null? new C(w) : null;
}
}
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициализации
Б>Нет, получается, что у Мейера неправильная классификация.
Ну в книге все довольно логично.
Да и на практике все функции укладываются в эту классификацию. Кроме функций отложенной инициализации.
Б>... Либо на нее надо смотреть шире.
Шире — это как?
Б>Можно (и стоит) учитывать только внешний API класс/функции. Нам совсем не интересно, что происходит под капотом.
Так ведь его классификация и относится именно к внешнему описанию функции.
Там у него изначально класс описывается как АТД — абстрактный тип данных, реализация которого неизвестна.
Б>Тогда эта функция является "асессором"
Получается, что ее нельзя так назвать, так как она изменяет сам объект.
Б>Но встречаются совсем странные функции, и при этому очень полезные, get_or_create() Б>Куда такие относить — еще более непонятно в такой классификации
Эта странная — "отложенное" создание, по сути тоже самое что и отложенная инициализация.
Re[4]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, Maniacal, Вы писали:
M>Синглтон это вообще надстройка для отказа от глобальных переменных без отказа от них. А глобальные переменные в классическом случае = неправильная архитектура и костыли.
Это конечно оффтоп, но все-таки интересно...
А как построить правильную архитектуру без глобальных переменных?
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициали
Здравствуйте, gandjustas, Вы писали:
Z>>Ведь действительно, когда смотришь на название такой функции, она вводит в заблуждение. Z>>Может быть есть смысл разделить ее на две функции? Z>>И в клиентском коде везде явно вызывать две функции при необходимости: G>Нет, это называется "двухшаговая инициализация" — источник многих ошибок.
А как же быть, если действительно какой-то ресурс нужен в зависимости от некоторых условий?
А условия могут сформироваться только в процессе выполнения программы.
G>Проблема не в этом, а в том, что "инициализация", как и любое другое действие, которое может завершиться неуспехом, должно быть явно видно в исходниках.
В исходниках каких? В исходниках клиента?
Просто в исходниках реализации это и так видно.
Но тут возникает противоречие.
С одной стороны клиент, если клиенту нужен ресурс, который возвращается функцией из используемого клиентом объекта, то клиент и не должен знать в какой момент происходит инициализация этого ресурса.
С другой стороны, такая "неявная" инициализация изменяет используемый клиентом объект, но клиент про это ничего не знает.
G>Решение простое — factory method G>
G>class C {
G> protected C(Widget widget) {
G> this.widget = widget;
G> }
G> static C? FromWidgetId(it widgetId)
G> {
G> var w = Widget.Load(widgetId);
G> if w != null? new C(w) : null;
G> }
G>}
G>
Хм... Но по сути это ведь тоже самое.
В клиентском коде как будет выглядеть вызов этой фабрики?
Здравствуйте, zelenprog, Вы писали:
Z>А как же быть, если действительно какой-то ресурс нужен в зависимости от некоторых условий? Z>А условия могут сформироваться только в процессе выполнения программы.
Как написал выше
G>>Проблема не в этом, а в том, что "инициализация", как и любое другое действие, которое может завершиться неуспехом, должно быть явно видно в исходниках.
Z>В исходниках каких? В исходниках клиента?
В вызывающем коде
Z>С другой стороны, такая "неявная" инициализация изменяет используемый клиентом объект, но клиент про это ничего не знает.
Это верно только когда инициализация не может быть не успешна, а это далеко не всегда так.
Z>Хм... Но по сути это ведь тоже самое.
Не совсем
Z>В клиентском коде как будет выглядеть вызов этой фабрики?
Не то же самое, так как метод может вернуть не только ссылку на объект, но и какие-то сложные значения.
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, gandjustas, Вы писали:
G>Есть общие правила: "гет-свойства не должны бросать исключения", "конструкторы не должны бросать исключения" (в C++ это просто ошибка)
Что делать если объект не сконструировался? Методы valid и прочие проверки на NULL худшее что может быть, а потом его ещё и уничтожить надо, а если в деструкторе как-то хитро уничтожается всё, то и ожидание чего-то может быть, или другие проблемы.
Sic luceat lux!
Re: Мутные функции отложенной (ленивой) загрузки\инициализации
Как правило, это ошибка проектирования. Хорошо написанный софт проходит через две фазы: фазу инициализации, когда создаются объекты и устанавливаются зависимости между ними, и фазу выполнения.
Бывает ленивая инициализация, когда инициализирующую функцию оборачивают в кеш, но эта ленивость не должна быть выражена в коде напрямую, а завернута в DI фреймворк, монады или другой неинтрузивный механизм.
Синглтоны делают ленивую инициализацию неуправляемой, т.к. а) зависимости синглотона неявные и могут быть вызваны из любого места кода б) нельзя контролировать ленивость-неленивость.
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, scf, Вы писали:
scf>Как правило, это ошибка проектирования ...
Значит, Бертран Мейер прав.
scf>Бывает ленивая инициализация, когда инициализирующую функцию оборачивают в кеш, но эта ленивость не должна быть выражена в коде напрямую, а завернута в DI фреймворк, монады или другой неинтрузивный механизм.
А разве "обернутость" ленивой инициализации исправит ошибку проектирования?
scf>Синглтоны делают ленивую инициализацию неуправляемой, т.к. а) зависимости синглотона неявные и могут быть вызваны из любого места кода б) нельзя контролировать ленивость-неленивость.
Как надо сделать с точки зрения правильного проектирования?
Re[3]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, Kernan, Вы писали:
K>Здравствуйте, gandjustas, Вы писали:
G>>Есть общие правила: "гет-свойства не должны бросать исключения", "конструкторы не должны бросать исключения" (в C++ это просто ошибка) K>Что делать если объект не сконструировался?
Если в конструкторе проблема, то должно быть исключение. Других вариантов нет.
Если метод, возвращающий объект, то можно сделать опциональный тип, вернуть null итд
Re[3]: Мутные функции отложенной (ленивой) загрузки\инициализации
Да. Т.к. такая ленивость становится не фичей кода (которая имеет некоторую стоимость реализации и поддержки), а фичей платформы.
Z>Как надо сделать с точки зрения правильного проектирования?
Серебряной пули, к сожалению, пока не изобрели и ответ зависит от ЯП, библиотек и навыков программирования. Но есть общее правило: сервисные классы должны быть иммутабельными и должны получать свои зависимости через конструктор. Как именно передавать — здесь уже возможны варианты. Можно отказаться от ленивости и вызывать конструкторы руками, можно использовать любимый фреймворк на аннотациях, можно lazy values прикрутить, а можно всё завернуть в монады.
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициализации
scf>Бывает ленивая инициализация, когда инициализирующую функцию оборачивают в кеш, но эта ленивость не должна быть выражена в коде напрямую, а завернута в DI фреймворк, монады или другой неинтрузивный механизм.
Ага, чтоб уж наверняка был happy debugging. Порой очень хочется заставить всех любителей наворотить сложность на пустом месте заниматься отладкой чужого софта, написанного примерно так же.
Re[3]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, zelenprog, Вы писали:
Z>В моем вопросе речь идет о "теории", на которой должны строиться классы\архитектура. Z>Если теорию нарушать — то это рано или поздно приведет к проблемам. Z>Вот такая реализация Синглтона — тоже ведь нарушает теорию.
Это своего рода отложенный конструктор.
Кодом людям нужно помогать!
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, gandjustas, Вы писали:
G>Есть общие правила: "гет-свойства не должны бросать исключения", "конструкторы не должны бросать исключения" (в C++ это просто ошибка)
впервые слышу. Вообще в языках с исключениями это стандартная практика — кинуть исключение из конструктора. Из деструктора да — бросать не стоит, но из конструктора — на усмотрение программиста.
G>Решение простое — factory method G>
G>class C {
G> protected C(Widget widget) {
G> this.widget = widget;
G> }
G> static C? FromWidgetId(it widgetId)
G> {
G> var w = Widget.Load(widgetId);
G> if w != null? new C(w) : null;
G> }
G>}
G>
но тогда ленивость ушла в никуда. Я ведь хочу получить объект С без создания widget.
Re[4]: Мутные функции отложенной (ленивой) загрузки\инициализации
Z>>А разве "обернутость" ленивой инициализации исправит ошибку проектирования?
scf>Да. Т.к. такая ленивость становится не фичей кода (которая имеет некоторую стоимость реализации и поддержки), а фичей платформы.
А какая разница?
Объясни пожалуйста подробнее.
Почему ленивость как фича кода хуже, чем фича платформы с точки зрения проектирования?
Ведь клиент в любом из этих случаев не узнает про эту ленивость. А именно это незнание клиента и является кривым проектированием.
Z>>Как надо сделать с точки зрения правильного проектирования?
scf>... Можно отказаться от ленивости и вызывать конструкторы руками ...
Где вызвать? В коде клиента?
Тогда это нарушает инкапсуляцию.
Re[3]: Мутные функции отложенной (ленивой) загрузки\инициализации
scf>>Бывает ленивая инициализация, когда инициализирующую функцию оборачивают в кеш, но эта ленивость не должна быть выражена в коде напрямую, а завернута в DI фреймворк, монады или другой неинтрузивный механизм.
SD>Ага, чтоб уж наверняка был happy debugging. Порой очень хочется заставить всех любителей наворотить сложность на пустом месте заниматься отладкой чужого софта, написанного примерно так же.
А как сделать, чтобы не наворачивать сложности?
Объясни плиз.
Мне очень хочется разобраться в этом вопросе.
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, gandjustas, Вы писали:
G>Есть общие правила: "гет-свойства не должны бросать исключения", "конструкторы не должны бросать исключения"
А это вообще реально на практике? Как минимум stack overflow или out of memory могут быть при вызове практически любого метода.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, zelenprog, Вы писали:
Z>А как же быть с функциями, которые выполняют отложенную\ленивую загрузку\инициализацию?
Это обычный аксессор. Эти категории относятся к наблюдаемому поведению. С т.з. клиентов этого объекта поведение ленивой загрузки не должно влиять на внешнее наблюдаемое состояние.
Т.е. грубо говоря в коде:
obj.anything();
по поведению никак не должно отличаться от
obj.get();
obj.anything();
тогда get — это аксессор. Неважно как он устроен внутри.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[2]: Мутные функции отложенной (ленивой) загрузки\инициализации
Z>>А как же быть с функциями, которые выполняют отложенную\ленивую загрузку\инициализацию?
·>Это обычный аксессор. Эти категории относятся к наблюдаемому поведению. ....
Мне кажется, это неверно.
Если функция является аксессором, это значит, что объект гарантирует свою неизменность при вызове этой функции.
Чтобы клиент был уверен в том, что при вызове такой функции "внутри" объекта ничего не изменится.
То есть аксессор — это не наблюдаемое проведение, а гарантия "внутренней" неизменяемости.
Re[3]: Мутные функции отложенной (ленивой) загрузки\инициализации
Здравствуйте, zelenprog, Вы писали:
Z>Если функция является аксессором, это значит, что объект гарантирует свою неизменность при вызове этой функции.
Неизменность именно с т.з. поведения, а не эквивалентности байтов в памяти.
Z>Чтобы клиент был уверен в том, что при вызове такой функции "внутри" объекта ничего не изменится.
Зачем? Чтобы что?
Z>То есть аксессор — это не наблюдаемое проведение, а гарантия "внутренней" неизменяемости.
Для клиента кроме наблюдаемого поведения ничего нет. См: инкапсуляция. Что происходит внутри он не знает и знать не может.
Суть всего этого действа в том, чтобы клиент мог делать какие-то рассуждения о поведении своего кода на основании знания контрактов. Т.е. например, если у тебя идёт подряд несколько методов, про которые ты знаешь что они акцессоры, то ты их можешь свободно переставлять местами как угодно, ставить новые, удалять и т.п., зная что поведение будет эквивалентным, o.getX() ... o.getY() == o.getY() ... o.getX().
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re: Мутные функции отложенной (ленивой) загрузки\инициализации