пятница, 5 мая 2017 г.

«Декораторы, декораторы, декораторы» ч.1



Сегодня будет разговор о декораторах... потому что о них всегда заходит речь и так или иначе, мы к ним всегда к приходим, когда рассматриваем правильное Объектно-ориентированное программирование (в стиле ElegantObject).

автор Егор Бугаенко - разработчик с хорошим практическим опытом, все примеры взяты из его реальных проектов:
  • Jcabi.com - библиотека
  • takes.org - webфреймворк по принципам EO
  • rultor.com - для деплоймента
Декоратор - design паттерн

Пример: BufferedInputStream оборачивает InputSream, т.е. ведет себя точно также, как и исходный объект, но добавляет новую функциональность (буферизацию).

Есть поток данных из файла FileInputStream и метод read, но мы берем этот объект и дополнительно заворачиваем его в BufferedInputStream и там у нас тоже есть метод read, который тоже читает, но у него есть дополнительные штуки, например он буфферизирует. 

Так можно заворачивать еще и еще.

Когда мы завернули InputStream в bufferedInputStream то нам также доступен метод read, но метод уже не будет каждый раз обращаться к файлу, теперь он заберет из него, например 1000 байт, и будет нам отдавать информацию уже из буфера, Но главное, что для чего-то внешнего он будет себя вести точно также как и InputStream.

Мы конечно можем просто передать объект InputStream внутрь другого какого-то объекта для обработки и назвать это все както по другому... ну тогда это будет уже не декоратор, а какая-токомпозиция объектов.

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

apache.commons-io
это популярная и мощная библиотека, должны все пользоваться, там огромное количество utility классов, против которых я выступаю, но тем не менее, которые могут вам сильно могут оптимизировать работу со стримами, работу со входящими/выходящими потоками, файлами и всем остальным.

там есть класс TeeinputStram (Tee - разветвление) - классический декоратор, который инкапсулирует в себе 2 стрима один входящий, другой исходящий.

Например, я хочу читать какой-то соккет и тут же обрабатывать и куда-то вываливать, например, в byteArrayOutputStream для последующей обработки  - вот для эго TeeinputStram и нужен  - он инкапсулирует в себя 2 объекта, а ведет себя как обычный InputStreaam

И тот, кто его использует со стороны, он даже не знает что кроме чтения read это тут же все сохраняется где-то.

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

Что нельзя назвать декоратором

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

Слои декораторов взаимозаменяемы, они не знают детали реализации друг друга мы можем InputStream завернуть в BufferedInputStream потом в TeemInputStraeam а потом снова в Buffered... Они все одинаковые, один другого не умнее. Хотя об этом не говорится в википедии, но для себя принял, что декоратор должен быть без экстра методов.

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

Альтернативы декораторам

нужны, чтобы добавить новый функционал в объект
например чтение с файловой системы, и там лучше не побайтово а порциями, не по байтам а по 10 кб
и я не хочу чтобы он за каждый read уходил в файловую систему

и как мне это сделать, вот у меня есть InputStream, как мне это сделать?
мне говорят, код работает медленно, я смотрю в код, действительно...
у меня read в цикле и каждый read целый поход в файловую систему. Что мне делать?

1.Флажок 

Другими словами, загнать функционал внутрь, объект станет умнее, был такой, а теперь еще и будет уметь, например, буфферизировать по флажку. Причем еще круче будет, если я загоню туда сеттер setBuferedOn = true;

Думаю многие понимают,что будет ужасный дизайн и так делать нельзя. Главное - это потому что если использовать такой подход, то размер класса все время будет расти. Сегодня это буфферизация, завтра - это логгирование, послезавтра это фильтры, после-послезавтра - это трансформация данных и мы будем каждый раз вставлять все новую и новую функциональность. В итоге у нас класс будет на 5000 строк, сотня конфигурационных параметров у каждого из которых будет вот такая длина по имени

Так обычно в апаче или в Springe  и бывает. Огромное количество кода внутри... классы растут в размере до гигантских масштабов и новый код все вставляется и вставляется внутрь. 

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

2.Extended - наследование


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

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

Примеры декораторов


1. буферизация - была рассмотрена выше;

2. повторный retry
Обычно в качестве фреймворка используется appache+Spring, но Егор Бугаенко написал свой, полегче и более объектно-ориентированный фреймворк j-kabi.com

В нем есть Wire - интерфейс с одним методом send, который должен установить коннект с url в сети, достать от туда данные и вернуть respond. Просто и понятно. Ну, а что, если сфейлилось, оборвался коннект, сокет упал? это же сеть, всякое может быть.

Wire, оборачивает аппач клиент, и работает через него

И теперь нужно добавить функционал, если мы с крешились, то нужно пробовать еще раз. Для этого создан декоратор retryWire - класс, который инкапсулирует другой Wire. Инкапсулирует в один единственный параметр Wire origin;

Дальше к единственному методу Send придет запрос и он делает простой не сложный цикл While на три попытки, до окончательного креша.
try
Обращается к инкапсулированному Wire, делает send,
если будет exception, то catch ошибки и запись лог,
а также увеличивается счетчик попыток ++attempt;

Если мы не с крешились, то идет работа и возвращается нужный ответ


Логика примитивная. Фишка в том, что это сделано через декоратор. И в этом его красота. потому что сюда мы можем подставить абсолютно любой Wire... и апачевский, и любой другой, вот, например, еще HTTP запрос через Jdk клиент (единственная причина почему приходится пользоваться аппаческим, в том что jdk не понимает patch запрос, остальные да, а этот нет, по крайней мере в Java7, ну а так он даже лучше, т.к. lowLevel)

Точно также любой другой код, который нестабильно работает, удобно завернуть в декоратор, чтобы обеспечить несколько попыток. Можно создать даже конфигурацию и через конструктор определять три параметра: оригинальный объект, количество попыток и время между попытками. Можно запоминать какие exceptions были (записать в массив, а затем после исчерпания количества попыток вывалить это все в отчет).

3.Логгинг

есть класс Rebound - который слушает события на GitHab, который обращается к серверу по событиям которые там происходят и присылает кусок кода на Json и я по разному реагирую на эти события  (коммит - одно, issue - другое)

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


Я могу в каждый из ребаундов зайти и написать лог, лог, лог... но это дублирование кода. Вместо этого я пишу декоратор RbLogged он инкапсулирует Rebaund и у него один единственный метод react.

К нему приходит JSon
он засекает время
потом вызывает оригинальный реакт
а потом пишет в лог оригинальный инкапсулированный объект

все. примитивный маленький класс, который осуществляет логгинг того что происходит в задекоррированном объекте и сам он является Rebound, т.е. его еще раз можно задекорировать, еще что-то наложить, ретрай там, еще и еще... Для логгинга декораторы подходят и красиво и элегантно получается


4.Защита от exception

Rultor - чат-бот, для общения через ГитХаб,  он слушает все обращения к нему и отвечает.

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


Как раз для этого и был создан декоратор SafeComment. Он инкапсулирует в себя оригинальный комент, и, когда вызывается Json, он его перехватывает. Вызывает оригинальный метод JSon Если возникает ошибка, то он вместо оригинального JSona подставляет фейковый с пустым текстом.. конечно, с поясняющим сообщением и возвращает Json. Т.е. ситуация проработана и проблемы не будет.

Такой декоратор делает опасный, токсичный объект безопасным. Не полностью, а то будет, например, AutOfMenory, конечно такой exception он пробросит дальше, но вот в конкретно изложенном случае он справится.


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

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


4.Фильтрация. 
На примере Jcabi - email

Для отправки почты JDK - сложно, можно намучиться, есть еще appache - проще, но он не объектно ориентированный, один огромный utility класс в 2000 строк, который нужно "set, set, set, а потом send", но его не возможно конфигурировать.

Итак, есть почтальон (Postman), у него один единственный метод send, методу передается конверт (Envelope) и он должен доставить его. Задача в том, что нужно чтобы не все конверты доставлялись. Почтальон же, все что ему не давай будет тут же отправлять через SMTP. И вот нужно сделать, чтобы он не отправлял черновики. В JDK есть флажок, который устанавливает для письма тип Draft (черновик).

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


Игнорирование, фильтрация чего-то что приходит на вход.

Например у вас OutputStream и вы хотите например каждую букву А игнорировать . Хотя это не реальный проект, а вот с тем чтобы бы черновики реально не уходили - это реальный. Класс PostNoDraft - не отправлять черновиков.

5.Валидация: Работать с объектом, только если он готов к работе.

XML - это данные вместе с форматированием

При этом, можно просто, как хочется написать этот xml а можно по некоторой схеме. И в этой схеме например может быть сказано что элемент главного уровня называется гараж, а в нем есть автомобиль (car), т.е. нельзя туда вставить карандаш(pencil), там всегда гараж и в гараже машины.

Структура этого документа описывается в XSD схеме (например, есть еще и другие схемы). Так вот я хочу чтобы не было ошибок и мои документы проверялись на соответствие схеме.

У меня есть интерфейс XML в билиотеке jkabi, там есть несколько методов (5). Я создаю XML какого-то класса и он сразу какой он есть с гаражом, с машинами и т.п. и далее я этот документ передаю кому-то для работы, но я не хочу чтобы он с ним начинал работать пока документ не проверится.

Можно вызвать какой-о утилитный метод проверки. Что типа XMLvalidationUtils с методом validate() и каждый раз я буду отправлять туда и он там будет валидироваться, но это не объектный подход. Объектный же подход - сделать декоратор, который ведет себя как XML документ, выглядит точно также, но первое что он делает - это проверяет на наличие ошибок, сравнивает документ с этой схемой, находит ошибки и выбрасывает эксепшн, если такие ошибки есть.

StrictXML - строгий XML


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

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

Хотя конечно есть таая мысль, что чем раньше мы сфейлимся тем лучше, тем раньше мы поймем от куда пришла проблема, Даюесть такой подход, но мне ближе другая мысль что конструирование объекта и работа объекта - это 2 жестко разделенные области. Сборка объекта и работа объекта - это две области, которые мы должны жестко разделять. Иначе если они смазываются, нам сложнее понимать причины фейлов

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

6.Синхронизация


Есть интерфейс Item, и в нем всего 2 метода, но не treadsafe. Если вызвать метод path из двух потоков, то возникнет какая-то проблема. Но в месте с тем нет желания менять код реализации... Поэтому лучшее решение - декоратор, который всего лишь добавляет одну строчку synchronized.


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

Даже целый класс Project, а из него достаются Item'ы и нужно его синхронизировать. Так, вот, сначала заворачивается в декоратор сам Project (декоратор SyncProject), а потом когда вытаскивается Item, он тоже делается synchronized. Так, все дерево получается синхронизированным. Это удобно. 

7. Кэширование

Хранилище данных в облаке Amazon.s3 очень удобно. Можно создать документ загнать его туда, потом использовать. Стоит это копейки (для коротких файлов). Была создана объектно-ориентированная библиотека для облегчения работы с документами в хранилище (т.к. API самого хранилища процедурное).

Итак, создан интерфейс объекта который находится в хранилище и у него есть ряд методов, в частности есть методы read и write.

Так вот, часто нужно чтобы когда создан объект, который будет представлять интересы документа, и потом делается read, reaad, read из него...  было сделано так, чтобы не было чтения каждый раз с хранилища, потому что это долго. Речь идет о кешировании. При первом read - чтение из хранилища, а потом при всех остальных обращениях, чтение из памяти. И, соответственно, когда сделается write, кеш сбрасывается в хранилище и очищается.

Как это сделать? Вот, большой метод read, если пихать сюда дополнительный функционал, код станет еще больше. Это не правильный подход. Правильный - создать декоратор, который только кеширует.


Причем для методов, которые должны быть закешированы, использовались аннотации @Cacheable.

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

Более сложный подход с Guava. Вообще java программисты должны обязательно знать эту библиотеку от Google. До Java 8 она вообще всегда использовалась. С Java 8 немного меньше, Это такая центральная библиотека в Java программировании, там много классов и инструментов, которые мы(программисты) должны использовать. Там один из инструментов - кеш. такое несложное кеширование  в памяти. Здесь точно так же используется Wire по аналогии с тем, что мы рассматривали раньше. То что так же есть метод send, мы этот send перехватываем и на основании принятого решения идем либо в кеш либо обходимся без него и напрямую достаем из инкапсулированного объекта.

Использую CacheLoader - кеширование в памяти, хорошая штука.
Идея все та же - мы кешируем в декораторе. Мы кешируем не в объекте, а с наружи

8.Spying on stream, counting

Поток InputStream читаю из него и пишу сразу же в другом направлении.

У нас есть стрим сначала(из сокета)
Далее он заворачивается в другой InputStream
Далее используется CountingInputStream - это из апачевской библиотеки, он работает как обычный InputStream пропускает через себя все запросы. Но потом мы можем вызвать функцию getCount, которая скажет нам потом сколько байт пролетело пока мы читали InputStream. Довольно элегантно, для того чтобы посчитать сколько пролетело байт. Мы работаем, а декоратор следит за тем что происходит и пишет счетчик. Мы негде не пишем эти процессы и даже не знаем как он все это делает, нам это и не нужно. Мы получает результат. Вставили декоратор внутрь в код. и он наблюдает за нашим кодом....


Хотя конечно,  там есть экстра метод. т.е. это не классический декоратор.



Комментариев нет:

Отправить комментарий