Привет всем кто забрел на наш канал.🎉 Этот канал для тех, кто интересуется разработкой под android или вообще программированием.
💻 На этом канале мы разбираем разные фундаментальные штуки из мира android, jvm и немного computer science. Эти вещи помогают понять все остальное, вроде современых фреймворков, магию библиотек и общих подходов.
🧐На канале будет мало новостей с мира android, для этого уже есть Android Broadcast, мы тут собрались чтобы смотреть глубже. Интересно будет тем, кто любит сложные штуки, и хочет понять как они работают. Помогать нам будут два наших персонажа, Хукума и Манифесто.
💻 На этом канале мы разбираем разные фундаментальные штуки из мира android, jvm и немного computer science. Эти вещи помогают понять все остальное, вроде современых фреймворков, магию библиотек и общих подходов.
🧐На канале будет мало новостей с мира android, для этого уже есть Android Broadcast, мы тут собрались чтобы смотреть глубже. Интересно будет тем, кто любит сложные штуки, и хочет понять как они работают. Помогать нам будут два наших персонажа, Хукума и Манифесто.
🔥9👍4❤1
Начнем серию постов с темы: «как работает UI с точки зрения многопоточности🧵».
Представьте прогу с одним потоком, все легко и прозрачно, не нужно думать про многопоточность и про все её проблемы🦄. Однако есть минус, пока прога работает, пользователь просто ждет и ничего не может сделать (напоминает Россию).
Ок, увеличим количество потоков, теперь приложение отзывчивое, и можно делать задачи параллельно, в чем проблема?
Проблема в том, что у нас есть UI слой, на котором мы двигаем и рисуем кнопки, возникает вопрос, как его обновлять из разных потоков? В голову приходит два решения:
☝️Просто сделать все View многопоточными и тогда их можно будет обновлять откуда угодно и не париться.
✌️Выделить для UI один специальных поток, и обновлять только через него.
🦾 Лучшие инженеры в свое время пытались сделать 1-й вариант и все попытки провалились. Как показала практика почти нереально сделать так, чтобы View была многопоточной, было бы безумно сложно не словить
🧐 Представьте что вы на View делаете переменную
😅 Со специальным потоком гораздо проще, но вопрос по прежнему актуальный, как менять UI из других потоков? На помощь приходит старый добрый паттерн Consumer/Producer. В частности в Android он реализован за счет троицы пацанов
Представьте прогу с одним потоком, все легко и прозрачно, не нужно думать про многопоточность и про все её проблемы🦄. Однако есть минус, пока прога работает, пользователь просто ждет и ничего не может сделать (напоминает Россию).
Ок, увеличим количество потоков, теперь приложение отзывчивое, и можно делать задачи параллельно, в чем проблема?
Проблема в том, что у нас есть UI слой, на котором мы двигаем и рисуем кнопки, возникает вопрос, как его обновлять из разных потоков? В голову приходит два решения:
☝️Просто сделать все View многопоточными и тогда их можно будет обновлять откуда угодно и не париться.
✌️Выделить для UI один специальных поток, и обновлять только через него.
🦾 Лучшие инженеры в свое время пытались сделать 1-й вариант и все попытки провалились. Как показала практика почти нереально сделать так, чтобы View была многопоточной, было бы безумно сложно не словить
DeadLock
. 🧐 Представьте что вы на View делаете переменную
isAnimationRunning
, а теперь она может меняться из разных потоков, сейчас она true
, а через мгновение уже false
. А если просто навесить по всюду synchronized
, то очень сильно просядет производительность.😅 Со специальным потоком гораздо проще, но вопрос по прежнему актуальный, как менять UI из других потоков? На помощь приходит старый добрый паттерн Consumer/Producer. В частности в Android он реализован за счет троицы пацанов
Looper, Handler, MessageQueue
, о которых так любят спрашивать на собесах и о которых мы поговорим далее…👍14👏2❤1
#android #ui
{1/4} Что такое Looper, как работает и что делает?
🙌 Представьте себе бесконечный цикл, допустим
Это и есть вся суть Looper. Просто бесконечный цикл который получает из очереди сообщения и их выполняет.
Чтобы создать Looper нужно вызвать метод Looper.prepare(). После этого метод Looper.prepare() сохраняет созданный объект в статическое поле типа ThreadLocal.
Реализация инициализации лупера довольна простая, и при этом позволяет в любом месте программы и из любого треда получить лупер, связанный с текущим тредом. Статический метод Looper.myLooper() просто достает лупер из переменной ThreadLocal.
Далее мы запускаем Looper при помощи метода Looper.loop() он уходит в бесконечный цикл, который мы обсудили выше. В следующих постах обсудим что за сообщения, и кто их посылает.
{1/4} Что такое Looper, как работает и что делает?
🙌 Представьте себе бесконечный цикл, допустим
for(;;){}
. Далее представим, что в этом цикле мы читаем из некоторой очереди Queue<Runnable> значения и выполняем их, получается что-то вроде:
Queue<Runnable> queue;
for(;;){
final Runnable runnable = queue.take();
runnable.run();
}
Это и есть вся суть Looper. Просто бесконечный цикл который получает из очереди сообщения и их выполняет.
Чтобы создать Looper нужно вызвать метод Looper.prepare(). После этого метод Looper.prepare() сохраняет созданный объект в статическое поле типа ThreadLocal.
Реализация инициализации лупера довольна простая, и при этом позволяет в любом месте программы и из любого треда получить лупер, связанный с текущим тредом. Статический метод Looper.myLooper() просто достает лупер из переменной ThreadLocal.
Далее мы запускаем Looper при помощи метода Looper.loop() он уходит в бесконечный цикл, который мы обсудили выше. В следующих постах обсудим что за сообщения, и кто их посылает.
👍23
#android #ui
{2/4} В прошлом посте мы поговорили про Looper, там упоминалась некоторая очередь Queue<Runnable>. Давай-те подробнее о ней поговорим.
В реальности есть два отличия:
☝️- это не просто очередь из Collection, это отдельный класс, который так и называется MessageQueue
✌️- внутри очереди не просто Runnable, а специальные объекты, которые называются Message
Начнем с класса Message. В классе есть много полей, но нас сейчас интересует только 3️⃣ это callback, when и next.
callback – тот самый Runnable, который будет исполнен Looper'ом
next - ссылка на следующее сообщение
when - просто поле типа long, которое является 🕑временем, когда это сообщение должно быть выполнено
MessageQueue – простой односвязный список. Если заглянуть в MessageQueue то увидим, что там просто одно поле mMessages типа Message. У каждого Message есть ссылка на следующее сообщение Message.next. Другими словами, MessageQueue хранит только ссылку на первое сообщение.
Сообщения в MessageQueue отсортированы по возрастанию значения поля Message.when. Looper вызывает метод MessageQueue.next() в цикле, и получает отсортированное сообщение, которое нужно выполнить, если же очередь пуста, метод MessageQueue.next() блокирует цикл до тех пор, пока сообщение не появится.
Чтобы положить сообщение в очередь нужно вызвать метод MessageQueue.enqueueMessage(). Метод MessageQueue.enqueueMessage() проходит по очереди, проверяя значение Message.when каждого из сообщений и вставляет новое сообщение в положенное место очереди.
Как создается сообщение? Вручную сообщение лучше не создавать, для создания лучше использовать метод Message.obtain(). Message.obtain() возвращает объект message из пула, который представляет собой связный список максимальным размером 5️⃣0️⃣ сообщений. Если все сообщения пула используются, то Message.obtain() создает и возвращает новый объект Message.
{2/4} В прошлом посте мы поговорили про Looper, там упоминалась некоторая очередь Queue<Runnable>. Давай-те подробнее о ней поговорим.
В реальности есть два отличия:
☝️- это не просто очередь из Collection, это отдельный класс, который так и называется MessageQueue
✌️- внутри очереди не просто Runnable, а специальные объекты, которые называются Message
Начнем с класса Message. В классе есть много полей, но нас сейчас интересует только 3️⃣ это callback, when и next.
callback – тот самый Runnable, который будет исполнен Looper'ом
next - ссылка на следующее сообщение
when - просто поле типа long, которое является 🕑временем, когда это сообщение должно быть выполнено
MessageQueue – простой односвязный список. Если заглянуть в MessageQueue то увидим, что там просто одно поле mMessages типа Message. У каждого Message есть ссылка на следующее сообщение Message.next. Другими словами, MessageQueue хранит только ссылку на первое сообщение.
Сообщения в MessageQueue отсортированы по возрастанию значения поля Message.when. Looper вызывает метод MessageQueue.next() в цикле, и получает отсортированное сообщение, которое нужно выполнить, если же очередь пуста, метод MessageQueue.next() блокирует цикл до тех пор, пока сообщение не появится.
Чтобы положить сообщение в очередь нужно вызвать метод MessageQueue.enqueueMessage(). Метод MessageQueue.enqueueMessage() проходит по очереди, проверяя значение Message.when каждого из сообщений и вставляет новое сообщение в положенное место очереди.
Как создается сообщение? Вручную сообщение лучше не создавать, для создания лучше использовать метод Message.obtain(). Message.obtain() возвращает объект message из пула, который представляет собой связный список максимальным размером 5️⃣0️⃣ сообщений. Если все сообщения пула используются, то Message.obtain() создает и возвращает новый объект Message.
👍4🔥3
{3/4} В этом посте используется код на Kotlin, просьба Java дИдов не пугаться!
Понемногу у нас с вами складывается картина того, каким образом работает UI в android. Мы разобрали что такое Looper, что такое Message и MessageQueue. Узнали, что в Looper посылаются задачи через MessageQueue. Возникает вопрос❓, как именно послать задачу в Looper, так как прямого доступа к MessageQueue у нас нет. Тут на сцену выходит Handler.
Прежде чем начнем разбирать Handler введем 2️⃣ понятия:
☝️поток consumer - поток, который ждет сообщения, тот, который вызывал Looper.loop().
✌️поток producer - поток, который создает сообщения и посылает их потоку consumer через Handler.
Поток consumer и поток producer могут быть одним потоком, как это может быть разберем далее.
Каждый Android разработчик хотя бы раз в своих проектах использовал Handler. Как уже сказано выше, нужен он для того, чтобы посылать задачи из потока producer в поток consumer.
У Handler есть несколько конструкторов, интересные пожалуй только 2️⃣ это:
☝️дефолтый коструктор без аргументов Handler()
✌️конструктор с аргументом типа Looper Handler(looper: Looper)
Когда используют конструктор без Looper, Handler пытается найти его через Looper.myLooper() и если его не находит, то падает❌.
Допустим в Activity.onCreate вы вызовете
Конструктор с входным параметром в виде Looper предпочтительнее, так как тогда вы явно задаете Looper🔁, в который будут post’иться сообщения. С ним все довольно очевидно, на вход нужно подать Looper в который будут посылаться задачи.
Теперь посмотрим на слудующий кусок кода:
Для начала попробуйте ответить сами, что будет выведено в лог❓ В логах мы увидим следующую последовательность:
- "hello from onCreate thread main"
- "hello from handler thread main"
Когда вызываем метод post, переданный Runnable оборачивается в Message и через MessageQueue подается на Looper consumer потока. Однако система все методы жизненного цикла активности тоже вызывает через Looper потока Main🧶, даже сам метод onCreate() вызывался примерно так:
Следовательно, когда мы вызываем
Этот пример очень сильное упрощение того, что происходит на самом деле, но суть та же. Это тот случай, когда поток consumer и поток producer это один и тот же поток. Именно этим фактом, обуславливается асинхронность в UI. Когда мы создаем транзакцию для показа фрагмента, после метода commit эта транзакция также кладется в MessageQueue через Handler, и выполняется позже Looper’ом Main Thread🧶, также происходит и с показом новой🆕 Activity и многими другими вещами вроде анимаций и т.п.
Стоит упомянуть еще одну интересную особенность, так уж вышло, что огромное количество багов связаных с View на Android можно решить просто отложив задачу
Помните мы разбирали, что у Message есть специальное поле when, так вот, когда вызываем post у Handler, там используется SystemClock.uptimeMillis()), наподобие System.getCurrentTimeMillis(), а когда вызываем postDelayed, то входной аргумент delay прибавляется к SystemClock.uptimeMillis()) и записывается в поле when, а дальше магия сортировки, которая обсуждалась туть.
Понемногу у нас с вами складывается картина того, каким образом работает UI в android. Мы разобрали что такое Looper, что такое Message и MessageQueue. Узнали, что в Looper посылаются задачи через MessageQueue. Возникает вопрос❓, как именно послать задачу в Looper, так как прямого доступа к MessageQueue у нас нет. Тут на сцену выходит Handler.
Прежде чем начнем разбирать Handler введем 2️⃣ понятия:
☝️поток consumer - поток, который ждет сообщения, тот, который вызывал Looper.loop().
✌️поток producer - поток, который создает сообщения и посылает их потоку consumer через Handler.
Поток consumer и поток producer могут быть одним потоком, как это может быть разберем далее.
Каждый Android разработчик хотя бы раз в своих проектах использовал Handler. Как уже сказано выше, нужен он для того, чтобы посылать задачи из потока producer в поток consumer.
У Handler есть несколько конструкторов, интересные пожалуй только 2️⃣ это:
☝️дефолтый коструктор без аргументов Handler()
✌️конструктор с аргументом типа Looper Handler(looper: Looper)
Когда используют конструктор без Looper, Handler пытается найти его через Looper.myLooper() и если его не находит, то падает❌.
Допустим в Activity.onCreate вы вызовете
Handler().post{ doSmth() }
- тут создается Handler, который через метод Looper.myLooper() получает Looper который привязан к Main Thread🧶, что аналогично записи Handler(Looper.getMainLooper()).post{ doSmth() }
. Но если попытаемся тоже самое сделать на потоке без Looper, конструктор упадет💣:
Thread {
val handler = Handler() // - тут мы упадем
❌, так как у этого потока нет Looper
🔁, подробнее смотри в посте про Looper
handler.post { doSmth() }
}.start()
Конструктор с входным параметром в виде Looper предпочтительнее, так как тогда вы явно задаете Looper🔁, в который будут post’иться сообщения. С ним все довольно очевидно, на вход нужно подать Looper в который будут посылаться задачи.
Теперь посмотрим на слудующий кусок кода:
fun onCreate() { // этот метод вызывает система
Handler().post {
Log.d("hello from handler thread ${Thread.currentThread().name}")
}
Log.d("hello from onCreate thread ${Thread.currentThread().name}")
}
Для начала попробуйте ответить сами, что будет выведено в лог❓ В логах мы увидим следующую последовательность:
- "hello from onCreate thread main"
- "hello from handler thread main"
Когда вызываем метод post, переданный Runnable оборачивается в Message и через MessageQueue подается на Looper consumer потока. Однако система все методы жизненного цикла активности тоже вызывает через Looper потока Main🧶, даже сам метод onCreate() вызывался примерно так:
val activity = getCurrentActivity()
val handler = Handler()
handler.post {
activity.onCreate()
}
Следовательно, когда мы вызываем
Handler().post{ Log.d("hello from handler thread ${Thread.currentThread().name}") }
эта задача кладется в очередь и выполнится после того, как завершится метод onCreate().Этот пример очень сильное упрощение того, что происходит на самом деле, но суть та же. Это тот случай, когда поток consumer и поток producer это один и тот же поток. Именно этим фактом, обуславливается асинхронность в UI. Когда мы создаем транзакцию для показа фрагмента, после метода commit эта транзакция также кладется в MessageQueue через Handler, и выполняется позже Looper’ом Main Thread🧶, также происходит и с показом новой🆕 Activity и многими другими вещами вроде анимаций и т.п.
Стоит упомянуть еще одну интересную особенность, так уж вышло, что огромное количество багов связаных с View на Android можно решить просто отложив задачу
Handler().postDelayed(100) { //doSmth }
.Помните мы разбирали, что у Message есть специальное поле when, так вот, когда вызываем post у Handler, там используется SystemClock.uptimeMillis()), наподобие System.getCurrentTimeMillis(), а когда вызываем postDelayed, то входной аргумент delay прибавляется к SystemClock.uptimeMillis()) и записывается в поле when, а дальше магия сортировки, которая обсуждалась туть.
👍24❤1🔥1
{4/4} Разобрав тему Handler стоит упомянуть одну❕интересную особенность работы с Handler через View.
У каждой View в Android также есть методы
Если AttachInfo не равен null, тогда Message просто кладется в Handler который есть у этого самого AttachInfo, т.е в Handler главного потока, тот который
Если же AttachInfo в данный момент равен null, т.е View еще не приатачена к компоненту, то Message кладется в специальную очередь, которая уникальна для каждой View. Затем, когда View приатачится к компоненту, система пробежится по этой очереди и запустит все Messages, которые были в очереди в Handler.
Почему это важно знать ❗️.
Когда работаем с Handler через View нужно вручную удалить все задачи, которые еще не выполнены, иначе компонент может утечь💧 или просто упасть💣, так как View уже не будет. Это актуально для длительных задач, если запускаем задачу с delay меньше секунды, то можно забить.
Однако, если мы делаем что-то вроде
Поэтому для длительных задач 🕰 (которые бывают очень редко) сохраняем Runnable(тот который пихаем в метод
У каждой View в Android также есть методы
post()
, postAtTime()
, postDelayed()
, аналогичные тем, что есть у Handler, но работают немного прикольнее. Они сперва проверяют есть ли в данный момент AttachInfo, или по-другому, приатачена ли View к компоненту, например к Activity. Если AttachInfo не равен null, тогда Message просто кладется в Handler который есть у этого самого AttachInfo, т.е в Handler главного потока, тот который
Handler(Looper.getMainLooper())
Если же AttachInfo в данный момент равен null, т.е View еще не приатачена к компоненту, то Message кладется в специальную очередь, которая уникальна для каждой View. Затем, когда View приатачится к компоненту, система пробежится по этой очереди и запустит все Messages, которые были в очереди в Handler.
Почему это важно знать ❗️.
Когда работаем с Handler через View нужно вручную удалить все задачи, которые еще не выполнены, иначе компонент может утечь💧 или просто упасть💣, так как View уже не будет. Это актуально для длительных задач, если запускаем задачу с delay меньше секунды, то можно забить.
Однако, если мы делаем что-то вроде
refreshlayout.postDelayed(4000) { refreshlayout.isEnabled = false }
, и при этом сами не очищаем очередь View вызвав refreshlayout.removeCallbacks(runnable)
то можем упасть 💣, так как задача может быть вызвана даже когда уйдем с этого экрана. Поэтому для длительных задач 🕰 (которые бывают очень редко) сохраняем Runnable(тот который пихаем в метод
post()
)в поле Fragment/Activity и удаляем его ручками на onDestroyView/onDestroy через метод removeCallbacks(runnable).👍22❤1
#ui #android
Большой респект тем, кто прочитал первую серию постов. Серию можно перечитывать перед собесами, и тогда вам не будет равных по вопросу Handler, Looper и MessageQueue.
Здесь что-то вроде оглавления:
- Пост про Looper
- Пост про MessageQueue
- Пост про Handler
- Дополнение к посту про Handler
Большой респект тем, кто прочитал первую серию постов. Серию можно перечитывать перед собесами, и тогда вам не будет равных по вопросу Handler, Looper и MessageQueue.
Здесь что-то вроде оглавления:
- Пост про Looper
- Пост про MessageQueue
- Пост про Handler
- Дополнение к посту про Handler
👍32
Итак инвариант, что это и зачем это знать? В подкасте подлодка, на одном из выпусков про обязательные знания программиста, пришедший эксперт сказал интересную вещь. Он сказал, что каждый сеньор должен знать, что такое инвариант и знать как ответить на такой вопрос на собеседовании. Чтож давай-те разберем, что это, чтобы не ударить в грязь лицом🤦♂️.
Начнем с истории, само понятие инвариант, пришло к нам из математики. Инвариант в математике - это выражение которое сохраняет свое значение, аля если у нас есть функция
💻 Вернемся в программирование. В программировании инвариантом называют предикат (читай некоторое условие) который всегда истинный. Другими словами если функция инвариантна, значит некоторое условие сохраняется до вызова функции и после вызова функции. Если класс инвариантен, значит его состояние всегда удовлетворяет какому-то условию. Для еще большого понимания, есть языки программирования, в которых понятие инварианта вшито в синтаксис языка, вот пример язык D
Думаю из кода все очевидно, в блоке
🦾 Мы используем для разработки такие языки как java или kotlin, в которых нет такой фичи как
Делая это, мы даем гарантии для других разработчиков по использованию наших классов. Если кто-то начнет творить фигню код просто упадет и сразу можно будет найти ошибку.
Начнем с истории, само понятие инвариант, пришло к нам из математики. Инвариант в математике - это выражение которое сохраняет свое значение, аля если у нас есть функция
y = x + 2
, то при x = 3
, y
всегда будет равен 5
. Не будет такого, что сейчас он 5
, а завтра 6
, не нифига, условие непоколебимое как моя преподша по матану на экзамене 👩🏫. 💻 Вернемся в программирование. В программировании инвариантом называют предикат (читай некоторое условие) который всегда истинный. Другими словами если функция инвариантна, значит некоторое условие сохраняется до вызова функции и после вызова функции. Если класс инвариантен, значит его состояние всегда удовлетворяет какому-то условию. Для еще большого понимания, есть языки программирования, в которых понятие инварианта вшито в синтаксис языка, вот пример язык D
class Date {
int day;
int hour;
invariant() {
assert(1 <= day && day <= 31);
assert(0 <= hour && hour < 24);
}
}
Думаю из кода все очевидно, в блоке
invariant
, задаем условия, которые всегда должны быть истины. Если попытаемся присвоить полю day
значение 32
, код просто упадет с ошибкой. Это дает нам гарантии того, что используя данный класс в его полях всегда будет корректное значение. 🦾 Мы используем для разработки такие языки как java или kotlin, в которых нет такой фичи как
invariant
, следовательно нам с вами это нужно делать руками. Чаще всего это реализуется так, что мы делаем проверку значений поля класса, которые хотим поменять в функции перед выполнением кода функции и после. Если проверка прошла то ок, если нет, то падаем. Делая это, мы даем гарантии для других разработчиков по использованию наших классов. Если кто-то начнет творить фигню код просто упадет и сразу можно будет найти ошибку.
👍11❤1
Я долго думал, чтобы такого не сложного рассказать по архитектуре и не придумал ничего умнее.
Композия или наследование?
Представим, что у нас есть такой класс Developer:
И мы хотим сделать класс Devops, который умеет запускать докер и помимо этого, чтобы он еще и умел пить кофе как Developer. Очевидный путь это создать класс Devops написать функцию по запуску докера, а функцию пить кофе просто скопировать из Developer.
Однако получится дублирование кода, что довольно скверно и несет кучу проблем в будущем. Как решить это дерьмо? Есть два варианта: Наследованиe и Композиция, разберем каждый.
👨👩👧 Наследованиe. Если класс
Вроде бы все круто, но возникает сложность. Допустим мы не хотим, чтобы Devops умел писал код (ведь
🪆Композиция, это когда одно из полей класса
мы избавились от дублирования кода и при этом в классе
Возникает вопрос зачем нам тогда нужно наследование, ведь все вокруг трубят, что лучше использовать композицию а не наследование?
Самым правильным ответом на этот вопрос будет: это зависит от вашего случая.
Наследование стоит выбирать тогда, когда у двух классов есть отношение
Композицию стоит выбирать когда есть отношение
Некоторые практических советов как выбрать и итоги:
👉При использовании стороних библиотек, стоит унаследоваться только от абстрактных классов или интерфейсов. Во всех других случаях лучше использовать композицию. Это связано с тем, что простые классы могуть меняться, и эти изменения могут сильно стрельнуть.
👉Если вы сами делаете либу, то делайте ваши классы закрытыми для наследования. Давайте возможность клиентам наследоваться только от ваших абстактных классов и интерфейсов.
👉Если класс
👉Если класс
Композия или наследование?
Представим, что у нас есть такой класс Developer:
Developer{
fun writeCode()
fun drinkCoffee()
}
И мы хотим сделать класс Devops, который умеет запускать докер и помимо этого, чтобы он еще и умел пить кофе как Developer. Очевидный путь это создать класс Devops написать функцию по запуску докера, а функцию пить кофе просто скопировать из Developer.
Однако получится дублирование кода, что довольно скверно и несет кучу проблем в будущем. Как решить это дерьмо? Есть два варианта: Наследованиe и Композиция, разберем каждый.
👨👩👧 Наследованиe. Если класс
Devops
наследует класс Developer
, значит все открыте методы и поля Developer окажутся в объекте Devops
:class Devops : Developer{
fun launchDocker()
}
val devops = Devops()
devops.writeCode()
devops.launchDocker()
Вроде бы все круто, но возникает сложность. Допустим мы не хотим, чтобы Devops умел писал код (ведь
Devops
это не человек а идеалогия, но кофе пить можно!). В таком случае лучше использовать композицию.🪆Композиция, это когда одно из полей класса
Devops
является классом Developer
. Эначит мы сначала конструируем объект Developer
, а потом устанавливаем этот объект в поле объекта Devops
. А Devops
уже будет обращаться к методам из класса Developer
:class Devops(
private val developer:Developer
){
fun launchDocker()
fun drinkCoffee(){
developer.drinkCoffee()
}
}
val devops = Devops()
devops.drinkCoffee()
devops.launchDocker()
мы избавились от дублирования кода и при этом в классе
Devops
нет метода drinkCoffee()
.Возникает вопрос зачем нам тогда нужно наследование, ведь все вокруг трубят, что лучше использовать композицию а не наследование?
Самым правильным ответом на этот вопрос будет: это зависит от вашего случая.
Наследование стоит выбирать тогда, когда у двух классов есть отношение
является
. Например есть класс Promotion (Акция) и есть класс NewYearPromotion (Новогодняя акция) очевидно, что у них есть отношение является
, так как NewYearPromotion это просто другая разновидность Promotion и тут нужно наследование.Композицию стоит выбирать когда есть отношение
использует
. Допустим есть класс Car и класс Wheel, явно Car использует
Wheel, а не является Wheel значит тут нужна композиция.Некоторые практических советов как выбрать и итоги:
👉При использовании стороних библиотек, стоит унаследоваться только от абстрактных классов или интерфейсов. Во всех других случаях лучше использовать композицию. Это связано с тем, что простые классы могуть меняться, и эти изменения могут сильно стрельнуть.
👉Если вы сами делаете либу, то делайте ваши классы закрытыми для наследования. Давайте возможность клиентам наследоваться только от ваших абстактных классов и интерфейсов.
👉Если класс
B
расширяет и является классом A
то наследование.👉Если класс
B
только использует часть функционала класса A
то композиция.👍14❤3
Начинаем цикл про некоторые проблемы многопоточности.
Как таковых проблем существует куча но мы разберем самые основные:
👉 Visibility
👉 Atomicity
👉 Reordering
👉 Happens-before
👉 Deadlock
Как таковых проблем существует куча но мы разберем самые основные:
👉 Visibility
👉 Atomicity
👉 Reordering
👉 Happens-before
👉 Deadlock
👍4😁3
🙈 Сегодня поговорим про проблему Visibility. Чтобы понять суть проблемы разберем один синтетический пример.
Все программы, которые мы пишем, используют как минимум несколько потоков. Один отвечает за отображение другие за походы в сеть, в файловую систему и много чего еще. Даже когда вы пишете код на js и кажется будто поток всегда один, на самом деле их несколько просто за вас эту работу делает браузер.
Вернемся к проблеме, у нас несколько потоков и есть переменная с которой эти потоки работают. Эта переменная может изменятся из нескольких потоков. Небольшой кусок кода на kotlin:
В коде мы запускаем 100 потоков, каждый из потоков увеличивает значение переменной number на 1. Затем мы ждем завершения всех потоков и выводим значение переменной.
❓Вопрос что будет в поле number? Правильный ответ вообще хз. Может быть 100, а может и 98 и 101 можете попробовать сами🙃
Почему так проиходит, почему иногда программа глючит? Чтобы это понять придется погрузится в то, как устроенны процессоры. 💻
Почти все процессоры, даже мобильные имеют несколько ядер, это нужно потому как повышать герцы мы больше не можем и приходится их ускорять путем паррелизации задач.
Пока одно ядро показывает вам видос на YouTube другое ядро в это время ищет вирусы, третье что-то качает с сети и т.п.
У каждого ядра есть кеши, L1..L4. Каждый кеш имеет свой размер и свою скорость записи/чтения. L1 супер быстрое чтение/запись и очень маленький размер ~ 32 Кб. L4 более медленная чтение/запись, но размер уже по больше ~ 16 МБ байт. Естественно размеры зависят он конкретного процессора.
Эти кеши нужны чтобы процессор не ходил в оперативу каждый раз. Это еще одна оптимизация для ускорения, так как ходить в кеш гораздо быстрее, ведь он находится в самом процессоре.
Теперь возвращаемся к нашему примеру, у нас создается 100 потоков, и предположим у системы 4 ядра, значит скорее всего параллельно будут работать 4 потока.
🙌И теперь следите за руками, когда процессор работает с переменной number, это значение он сначала кладет в кеш L1, затем спустя какое-то время это значение из кеша попадает в оперативную память. Увеличение значения переменной просиходит в 3 шага, получить переменную, увеличить на один и записать переменную.
Допустим поток X взял переменную пока её значение было 1, затем он увеличил её до 2 и записал это значение. Это значение сначало записалось в кеш, и только спустя какое-то время оно попадет в оперативную память.
В это время другой поток Y тоже хочет проделать аналогичную операцию. Он также идет в оперативную память, получает значение 1, так как поток X работает на другом ядре и еще не записал значение в оперативную память. Поток Y получает получает значение 1 увеличивает его на 1 и записывает 2, аналогично потоку X.
В итоге должно было получиться 3, но получилось 2 из-за того, что потоки не видят того, что делают другие потоки, или видят значение но с опозданием. 🕰
Это сильное упращение того, что проиcходит в реальности, однако суть таже. Это фундаментальная проблема Visibility в многопоточности. О том как решается эта проблема поговорим в отдельном посте.
Все программы, которые мы пишем, используют как минимум несколько потоков. Один отвечает за отображение другие за походы в сеть, в файловую систему и много чего еще. Даже когда вы пишете код на js и кажется будто поток всегда один, на самом деле их несколько просто за вас эту работу делает браузер.
Вернемся к проблеме, у нас несколько потоков и есть переменная с которой эти потоки работают. Эта переменная может изменятся из нескольких потоков. Небольшой кусок кода на kotlin:
fun main(){
val threadList = mutableList<Thread>()
var number = 0
repeat(100){
threadList+=thread {
number++
}
}
thread.forEach { it.join()}
print(number)
}
В коде мы запускаем 100 потоков, каждый из потоков увеличивает значение переменной number на 1. Затем мы ждем завершения всех потоков и выводим значение переменной.
❓Вопрос что будет в поле number? Правильный ответ вообще хз. Может быть 100, а может и 98 и 101 можете попробовать сами🙃
Почему так проиходит, почему иногда программа глючит? Чтобы это понять придется погрузится в то, как устроенны процессоры. 💻
Почти все процессоры, даже мобильные имеют несколько ядер, это нужно потому как повышать герцы мы больше не можем и приходится их ускорять путем паррелизации задач.
Пока одно ядро показывает вам видос на YouTube другое ядро в это время ищет вирусы, третье что-то качает с сети и т.п.
У каждого ядра есть кеши, L1..L4. Каждый кеш имеет свой размер и свою скорость записи/чтения. L1 супер быстрое чтение/запись и очень маленький размер ~ 32 Кб. L4 более медленная чтение/запись, но размер уже по больше ~ 16 МБ байт. Естественно размеры зависят он конкретного процессора.
Эти кеши нужны чтобы процессор не ходил в оперативу каждый раз. Это еще одна оптимизация для ускорения, так как ходить в кеш гораздо быстрее, ведь он находится в самом процессоре.
Теперь возвращаемся к нашему примеру, у нас создается 100 потоков, и предположим у системы 4 ядра, значит скорее всего параллельно будут работать 4 потока.
🙌И теперь следите за руками, когда процессор работает с переменной number, это значение он сначала кладет в кеш L1, затем спустя какое-то время это значение из кеша попадает в оперативную память. Увеличение значения переменной просиходит в 3 шага, получить переменную, увеличить на один и записать переменную.
Допустим поток X взял переменную пока её значение было 1, затем он увеличил её до 2 и записал это значение. Это значение сначало записалось в кеш, и только спустя какое-то время оно попадет в оперативную память.
В это время другой поток Y тоже хочет проделать аналогичную операцию. Он также идет в оперативную память, получает значение 1, так как поток X работает на другом ядре и еще не записал значение в оперативную память. Поток Y получает получает значение 1 увеличивает его на 1 и записывает 2, аналогично потоку X.
В итоге должно было получиться 3, но получилось 2 из-за того, что потоки не видят того, что делают другие потоки, или видят значение но с опозданием. 🕰
Это сильное упращение того, что проиcходит в реальности, однако суть таже. Это фундаментальная проблема Visibility в многопоточности. О том как решается эта проблема поговорим в отдельном посте.
❤5👍3🔥1
Очень давно наткнулся на интересную вещь. Один чувак сделал матрицу компетенций для разработчика. В ней представлены многие области в которых должен разбираться человек, который претендует на звание инженера. Матрица разбита по уровням, т.е насколько глубоко должен шарить джун, мидл и сеньор (такой, как Манифесто).
От себя могу добавить, что к матрице стоит подходить очень cкeптически, много спорных вещей, как например, что крепкий мидл должен писать используя TDD и знать минимум 4-5 платформ. Однако её можно рассматривать как некоторый идеал к которому можно хотя немного стремиться.
От себя могу добавить, что к матрице стоит подходить очень cкeптически, много спорных вещей, как например, что крепкий мидл должен писать используя TDD и знать минимум 4-5 платформ. Однако её можно рассматривать как некоторый идеал к которому можно хотя немного стремиться.
👍3
Следующая проблема многопоточности Atomicity.
Суть в том, что в платформе, с которой вы работаете, те операции, которые как вам кажется выполняются за одну операцию, на самом деле могут выполняться в несколько операций.
Потому как большинство тут джависты, будем рассуждать на примере платформы JVM (я тупо в других не шарю...)
Начнем с простого примера:
Взглянем подробнее на операцию
📖 считать значение переменной number;
1️⃣ увеличить значение на 1;
✒️ записать новое значение в переменную number.
Все это довольно очевидно и наверняка в головах у вас звучит "Спасибо Кэп 👨✈️", но не спешите меня осуждать. Перейдем к примеру прикольнее:
Смотрим на операцию
"Что за хуефокус❓" – Вопрос, который мог возникнуть в вашей голове и чтобы на него ответить давайте разбираться.
Начнем с того, что это зависит от конкретной JVM и окружения, в которой она выполняется. Как мы знаем программы написанные на языке java можно запускать везде, где есть JVM. Мы пишем на java, а JVM уже умеет работать с конкретной платформой.
И вот тут начинается веселье, в примере мы используем Long. В большинстве языков – Long это целочисленное значение, под которое выделяется 64 бита (для Double кстати тоже).
💻 Процессоры у нас бывают 32х и 64х разрядные. Представим, что JVM работает в системе с 32х разрядным процессором. Вот незадача у нас переменная с размером в 64 бита, а процессор 32х разрядный, как он тогда вообще может записать значение в переменную типа Long? 🤔
Правильно, в 2 этапа, сначала первые 32 бита, затем вторые 32 бита. Смекаете к чему я веду и почему это является проблемой многопоточности? Потому как может быть ситуация, где один поток запишет первые 32 бита, а другой вторые 32 бита. В итоге получим такой баг, который без знания этих основ можно искать очень долго 🔎.
Вот так, как всегда проблема обозначена, а решение я пока попридержу.
Суть в том, что в платформе, с которой вы работаете, те операции, которые как вам кажется выполняются за одну операцию, на самом деле могут выполняться в несколько операций.
Потому как большинство тут джависты, будем рассуждать на примере платформы JVM (я тупо в других не шарю...)
Начнем с простого примера:
fun main(){
var number: Int = 0
number++
print(number)
}
Взглянем подробнее на операцию
number++
– за сколько шагов она делается? В прошлом посте мы уже разобрали, что тут происходит 3️⃣ операции: 📖 считать значение переменной number;
1️⃣ увеличить значение на 1;
✒️ записать новое значение в переменную number.
Все это довольно очевидно и наверняка в головах у вас звучит "Спасибо Кэп 👨✈️", но не спешите меня осуждать. Перейдем к примеру прикольнее:
fun main(){
val number: Long = 0
number = 42
print(number)
}
Смотрим на операцию
number = 42
сколько шагов делается тут? Иииии наш любимый ответ хз 🤷♂️, может быть в одну операцию, но может и в две) "Что за хуефокус❓" – Вопрос, который мог возникнуть в вашей голове и чтобы на него ответить давайте разбираться.
Начнем с того, что это зависит от конкретной JVM и окружения, в которой она выполняется. Как мы знаем программы написанные на языке java можно запускать везде, где есть JVM. Мы пишем на java, а JVM уже умеет работать с конкретной платформой.
И вот тут начинается веселье, в примере мы используем Long. В большинстве языков – Long это целочисленное значение, под которое выделяется 64 бита (для Double кстати тоже).
💻 Процессоры у нас бывают 32х и 64х разрядные. Представим, что JVM работает в системе с 32х разрядным процессором. Вот незадача у нас переменная с размером в 64 бита, а процессор 32х разрядный, как он тогда вообще может записать значение в переменную типа Long? 🤔
Правильно, в 2 этапа, сначала первые 32 бита, затем вторые 32 бита. Смекаете к чему я веду и почему это является проблемой многопоточности? Потому как может быть ситуация, где один поток запишет первые 32 бита, а другой вторые 32 бита. В итоге получим такой баг, который без знания этих основ можно искать очень долго 🔎.
Вот так, как всегда проблема обозначена, а решение я пока попридержу.
👍6