Сегодня я хотел бы рассказать о том, как мне помог в проекте плохой код.
Кому интересно, прошу под кат.
Достался мне в наследство проект на сопровождение. Не знаю кем писался, но этому человеку икается и по сей день. Суть такая — есть приложение для сбора фотоотчетов в магазине и их отправке руководству на сервак.
Все вроде просто, но были постоянные жалобы сотрудников на падение приложения при фотографировании. Анализ кода показал, что при вызове intent приложения камеры, Android убивал приложение из-за нехватки ресурсов и по возвращению результата в активити, последняя пересоздавалась и не содержала нужных для работы данных. Пересоздание активити просто не обрабатывалось. Ну да, есть же configChanges *sarcasm*
Шаг 1. Обработка пересоздания
Помучавшись 2 дня, поправив код, выкинув повторяющиеся фрагменты я принялся тестировать. Закинул в эмулятор taskkiller, запустил камеру, убил процесс. Фоткаю — красота!!! Все красиво, фотка есть. Нажимаю сохранить и… бабах) Приложение упало.
Шаг 2. Singleton — злое зло
Причина падения — нет данных о текущем отчете. Оказалось, что в приложении был чудо-синглтон, который хранил в себе ВСЕ! Абсолютно все критические данные, необходимые для работы приложения и даже больше. Это и токен авторизации, и 40 полей состояния отчета и ссылки на классы, описывающие отчет и геоданные юзера и еще пару коллекций Bitmap. В одно мгновение мы лишились всего, чего только можно было лишиться.
Результат был описан заказчику. Рефакторинг оценен, но столько денег у них не оказалось. При этом слезно попросили что-то сделать и наставить костылей.
Шаг 3. Меняем Singleton
Все, что пришло в голову — исправить синглтон так, чтобы он стал надежным и при этом абсолютно не поменялся его интерфейс. Придумано было следующее — все что только можно запихнуть в SharedPreferenses сохранялось туда.
public String getAuthToken() {
return getStingFromPref(AUTH_TOKEN);
}
.....
// и так далее.
Грубо, тупо, зато не нужно менять код во всем проекте и наш Singleton стал более устойчив (остались коллекции Bitmap, с которыми пока ничего не сделал). Все объекты, которые только можно сериализовать были сериализованы и тоже запихнулись в SharedPreferences.
Приложение ушло на тест, в прод и падения прекратились на 70% устройств. На остальных после отправки результата с камеры, пересоздавалась не текущая, а предыдущая активити в стеке. Для меня это пока загадка. Гуру — объясните в комментах.
Суть такая — Активити А стартует Активити Б с получением результата. Активити Б запускает intent камеры с получением результата.
Когда в процессе работы камеры процесс убивался, по созданию фотки пересоздавалась активити А. У активити Б не вызывался ни onCreate, ни onActivityResult. (P.S. Android 4.4)
goоgle и stackowerflow ответа не дали, пришлось идти другим путем и писать больше плохого кода.
Если пересоздать не получается, можно запретить убивать, подумал я и перешел к шагу 4.
Шаг 4. Финальный. Foreground-мусоровоз
Как же заставить Android не убивать процесс? Мои мысли были такими: «Если сказать операционке, что у нас есть важный foreground сервис, выполняющий важные нам вычисления ровно столько времени, сколько мы фоткам товар. Это определенно может сработать».
Так родился в проекте foreground-мусоровоз. Он гонял цикл от 0 до 60 и в теле засыпал на секунду. Как только приходил результат с камеры — сервис убивался. Приложение было собрано и отдано на тестирование.
Результат — KitKat оказался умнее и прибивал процесс. То-есть не изменилось ничего.
Я уже было отчаялся, когда мне в голову пришла еще одна мысль: «А если тупо сохранить ссылку на активити в синглтоне и вызвать startActivityForResult прямо из foreground-мусоровоза?». Я никогда так не делал и, надеюсь, больше не придется.
Приложение собрано, отдано в тест и… о чудо, процесс больше не закрывался.
Вместо заключения. Заказчик предупрежден, что у него плохое приложение и я сделал его еще хуже, но оно работает так как этого ждут сотрудники компании.
Напоследок. Расскажите про свой самый «плохой код» в карьере. Давайте поднимем друг другу настроение.
Комментарии (70)
SerafimArts
26.11.2015 02:52+1Расскажите про свой самый «плохой код» в карьере
Сознательное программирование я начинал с PHP (несознательное с VB6). Вам точно хочется, что бы я рассказал о том что там творилось? =)
rboots
26.11.2015 04:06+1Код называют «плохим», как правило, по двум причинам:
— если опытный разработчик видит написанный новичком код, содержащий концптуальные ошибки
— если новичёк видит код, написанный опытным разработчиком, и не понимает используемых паттернов и принципов, на которых он построен, а видит вместо этого хаос
Не буду категоричен, но я бы усомнился какая причина актуальна в вашем случае. Вынесение данных из классов с логикой в отдельную модель и одна модель вместо нескольких не кажутся такой плохой идеей.kolipass
26.11.2015 06:39+4Есть общепринятое мнение, что классы-боги это плохо. Я понял из статьи, что lavelas столкнулся как раз лицом к лицу с богом).
Suvitruf
26.11.2015 07:20+2Осталось только определить критерии, когда класс становится классом-богом.
FractalizeR
26.11.2015 10:42+3Вероятно, когда нарушает SRP. Тогда останется только определить, что является «обязанностью» класса, верно? ;)
EngineerSpock
26.11.2015 15:27Верно. Когда есть множественное нарушение SRP. Понятие обязанности класса в целях оценки удовлетворения SRP заключается в количестве причин для изменения класса, где причинами являются акторы или аспекты изменений. На самом деле, кол-во ответственностей класса можно всегда подсчитать конкретно.
VolCh
26.11.2015 22:09Далеко не всегда. Нельзя сказать точно, сколько причин для изменения может быть у класса в общем случае.
EngineerSpock
27.11.2015 11:26Почти всегда можно. Другое дело, что нам не всегда следует удовлетворять SRP. Всё зависит от вероятности изменений по тем или иным аспектам, а вероятности эти обычно определяются эвристически. Невозможно везде удовлетворять SRP и собсна делать код PURE SRP.
p.s. Ну, не буду утверждать, что можно в 100% случаев подсчитать кол-во обязанностей, но хотелось бы увидеть класс, где это невозможно.
xoxol_89
26.11.2015 07:13-4Плюс хранение важных данных в Синглтоне — тоже весьма частое заблуждение новичков.
Z80A
26.11.2015 09:52+5Крайне категоричное и ничем не подтвержденное высказывание.
xoxol_89
26.11.2015 11:03Что именно категоричное и не подтвержденное?
То, что хранение данных в синглтоне — плохая идея? Или то, что это частое заблуждение новичков?Mikhail_dev
26.11.2015 11:10То, что хранение данных в синглтоне плохая идея.
Мы довольно активно используем синглтоны, потому как они:
1. Удобны, особенно как обсерверы
2. Редко выгружаются (ибо загрузка происходит класслодером)
3. Являются хорошим кешем.
И то, что они выгружаются… И что с того? в конструкторе инициализацию прописал и всё встанет на свои места. Критические данные кешируем на ПЗУ при необходимости, откуда мы их берем при инициализации. Что не так то?
Mikhail_dev
26.11.2015 11:11-3То, что хранение данных в синглтоне плохая идея.
Мы довольно активно используем синглтоны, потому как они:
1. Удобны, особенно как обсерверы
2. Редко выгружаются (ибо загрузка происходит класслодером)
3. Являются хорошим кешем.
И то, что они выгружаются… И что с того? в конструкторе инициализацию прописал и всё встанет на свои места. Критические данные кешируем на ПЗУ при необходимости, откуда мы их берем при инициализации. Что не так то?xoxol_89
26.11.2015 11:45Наверное, я неправильно сделал акцент.
Я имел в виду хранение в синглтоне Критически важных данных, которые не проинициализируются в конструктору. Например, какой-нибудь sessionId, полученный после логина.
В том, что синглтоны удобны, особенно в Андроиде, где они никак не привязаны к жизненному циклу активити, я не спорю и полностью согласен. Как согласен и с тем, что удобны они и для хранения восстанавливаемых данных.Mikhail_dev
26.11.2015 12:37Не совсем понял. Ну вот есть сессия у нас, она не может быть сохранена. Окей, приложение умерло вместе с ней. Дальше при запросе данных сессии в синглтоне, мы смотри, не null ли она к примеру, и если да, то производим необходимые действия.
lavelas
26.11.2015 12:51Вот Вам и проблема синглтона для Андроида. Что делать, если мы запустили выбор файла и пока была открыта галерея наше приложение закрылось и по возвращению у нас нет сессии, перекинуть на окно логина? Круто, я в чате шлю файл другу, но вместо этого меня выкидывает из сессии. Очень крутое поведение.
Это нужно в onSaveInstanceState сохранить id сессии и при ресторе его обратно закинуть в синглтон?Mikhail_dev
26.11.2015 13:10Вот Вам и проблема синглтона для Андроида. Что делать, если мы запустили выбор файла и пока была открыта галерея наше приложение закрылось и по возвращению у нас нет сессии, перекинуть на окно логина? Круто, я в чате шлю файл другу, но вместо этого меня выкидывает из сессии. Очень крутое поведение.
В чем проблема то? Всё зависит от кейса. Я сейчас же говорил про кейс к примеру тех же банковских приложений, когда сессия будет считаться невалидной, и её не надо сохранять. Но можно и сохранить, это ВООБЩЕ не проблема. Взяли и сохранили в том же синглтоне. Что не так?i_user
26.11.2015 14:06Чтоб вам этот синглтон и классы его использующие покрыть регрессионными тестами, чтобы оценили, почему плохо.
Удобно — очень удобно, конечно. Но это код, который годится только на «написать и забыть» — потому что гарантировать, что
а. он работает
б. я что-то изменил, а что-то там где-то еще не отломалось — не просто сложно, а практически невозможно.
То есть код для фриланса, с которым, конечно, можно мириться, но гордиться и защищать — не стоит)Mikhail_dev
26.11.2015 14:40тесты — это единственное, ибо покрыть тестами сложно.
что-то изменили и сломалось? дык кто виноват?? вы вызываете метод у объекта и этот метод должен ровно то, что от него просят. если вы его сломали, то не важно, в синглтоне он или нет, он не работает.
и да, я не горжусь и не защищал, а говорю где его можно использовать. только с тестами проблема. или есть еще что?i_user
26.11.2015 14:45+3Важно, что в синглтоне — потому что синглтон — это shared state протянутый через все приложение.
Если ты изменил этот стейт — то не факт что все потребители этого синглтона ожидают это изменение и правильно на него среагируют. И проверить, что ты ничего не сломал можно только либо «регрессионным тестированием» мануальных QA, либо верой, что «я же четкий, я ж не сломал»
В этом и фундаментальное отличие синглтона от несинглтона — область видимости обычного объекта позволяет локализовать потенциальные места для возникновения ошибки, а синглтона — нет.Mikhail_dev
26.11.2015 15:08Я полностью согласен. И я понимаю этот минус.
Просто в статье, в комментариях пишут что мол плохо, что антипаттерн, но всякие доводы что там несколько раз может быть инициализирован, что данные в нём теряются и подобное — смешны. К этому и вёл я.
Если такая уж тема пошла, то скажите, как быть с глобальными данными? Есть к примеру у нас довольно часто изменяющиеся данные, которые мы должны получать в разных частях приложения, будь то сервис или UI. Сохранять в базу — это будет оверхед, ибо данные очень часто изменяются (к примеру датчики телефона, данные с NMEA протокола и т.д.). Постоянно дёргать ПЗУ будет неправильно. Как эту задачу решить элегантно? Я использую для этого Singleton.i_user
26.11.2015 15:15+2Для этого неплохо подходит реактивщина и DI — в одном месте создается сервис, который содержит в себе ваши часто изменяющиеся данные — а дальше во все заинтересованные части приложения рассовываются стримы — иными словами неизменяемые «обсерверы», которые сработают в момент, когда эти данные обновились — и — отдельно — в заинтересованные сущности, которые могут менять эти данные — инжектятся «синки» — иными словами лямбды, в которые можно засунуть новое значение.
Таким образом у нас нет способа получить значение этих данных, кроме как среагировав на него и нет способа изменить его, кроме как через этот синк.
Хранить же эти данные можно и так как описали вы. Важно — как вы предоставляете доступ к этим данным.
lavelas
26.11.2015 15:28Вы можете мне объяснить такой момент:
«Если ты изменил этот стейт — то не факт что все потребители этого синглтона ожидают это изменение и правильно на него среагируют.» Вы отвечаете «Я полностью согласен. И я понимаю этот минус»
А если стейт изменился, потому что «несколько раз может быть инициализирован, что данные в нём теряются» — это смешной довод.
Как так??Mikhail_dev
26.11.2015 16:06Речь про изменение кода, а не про рабочее уже приложение. Т.е. когда человек взял и изменил что-то в коде синглтона, то он не может быть уверен в работоспособности конструкции, ибо синглтон может затрагивать множество классов во всём приложении. Это называется сильносвязанным приложением.
А когда приложение работает и там синглтон работает в стиле обсервера, когда активити подписывается и отписывается к примеру в метода onResume, onPause, то тут никаких потерь нет, ибо синглтон сам по себе инициируется класслодером, и если класслодер умирает, то умирает и процесс приложения, то значит умерли и все Activity (лишь только Task может жить).lavelas
26.11.2015 18:34Речь про изменение кода, а не про рабочее уже приложение. Т.е. когда человек взял и изменил что-то в коде синглтона, то он не может быть уверен в работоспособности конструкции, ибо синглтон может затрагивать множество классов во всём приложении. Это называется сильносвязанным приложением.
А когда приложение работает и там синглтон работает в стиле обсервера, когда активити подписывается и отписывается к примеру в метода onResume, onPause, то тут никаких потерь нет, ибо синглтон сам по себе инициируется класслодером, и если класслодер умирает, то умирает и процесс приложения, то значит умерли и все Activity (лишь только Task может жить).
А пардон, понял этот момент.
Но блин, разве Вы не согласны, что реальна ситуация, когда из синглтона просто возвращаются те данные, которых ты не ждешь? Какой-то сервис параллельный их сменил, хотя бы.
lavelas
26.11.2015 12:221. Удобны, особенно как обсерверы
Позвольте полюбопытствовать, как? Поделитесь опытом.
В целом не буду с Вами спорить, на вкус и цвет все фломастеры разные.Mikhail_dev
26.11.2015 12:34Есть к примеру Singleton, к которому подписываются и отписываются Activity/Fragments/Service при изменении нашего местоположения. Это один из кейсов.
>В целом не буду с Вами спорить, на вкус и цвет все фломастеры разные.
В целом в споре рождается истина, так что давайте =)
EndUser
26.11.2015 05:09Если не считать ошибок по незнанию (а как бы я тогда догадался, что пишу ошибку?), то самой неприятной ошибкой по невнимательности была короткая транзакция — список неопределённой (хотя и небольшой длины) коммитился из дельфей в оракул построчно. Одной транзакцией список обрабатывать не получалось, так как на самом деле операций было много. Писать журнал/счётчик обработанного я не догадался.
В общем день, когда программа свалилась в неопределённом месте обработки списка всё-таки свершился. Поиск точной позиции краха и восстановление информации заняли несколько часов на продакшне.
Z80A
26.11.2015 09:55+11Singleton — злое зло.
Книжек умных перечитали?lavelas
26.11.2015 10:35+1Ну как Вам сказать. Это мое личное мнение.
Вы же понимаете, что в приложениях для Android абсолютно нормально при очередном вызове getInstance() получить новый экземпляр класса? К этому нужно быть готовым.Mikhail_dev
26.11.2015 12:39Вы же понимаете, что в приложениях для Android абсолютно нормально при очередном вызове getInstance() получить новый экземпляр класса? К этому нужно быть готовым.
А вот с этого момента поподробнее. С чего вы решили что вы получите новый экземпляр класса?lavelas
26.11.2015 14:10+1А вот с этого момента поподробнее. С чего вы решили что вы получите новый экземпляр класса?
Вот Вам пару вариантов:
1. Допустим Вы инициализировали статический Singleton в стартовой активити. Когда она будет выгружена, Вы потеряете ссылку на Singleton и получите новый инстанс.
2. Вы инициализировали статический Singleton в унаследованном от Application классе. Ваш юзер ходит по окнам и все хорошо и прекрасно. Потом ему приходит СМС и он переключается на ее чтение, а параллельно у него запущен скайп, играет музыка и в смс ему прислали ссылку, по которой он открыл браузер. Android выгружает Ваш процесс и когда юзер возвращается в приложение Вы так же получите новый инстанс.
P.S. Я не говорю, что этот паттерн можно или нельзя использовать в андроиде. Но я точно знаю, что его использование может сыграть злую шутку.
Конечно, Вы можете сказать, о том, что:
Но можно и сохранить, это ВООБЩЕ не проблема. Взяли и сохранили в том же синглтоне. Что не так?
Это и не есть проблема. Проблема в том, что вполне реально что-то не учесть.Mikhail_dev
26.11.2015 16:191. Допустим Вы инициализировали статический Singleton в стартовой активити. Когда она будет выгружена, Вы потеряете ссылку на Singleton и получите новый инстанс.
я слышал про то что если инициировать статическую ссылку в Activity, то это чревато выгрузкой и его. Но можно ведь и не в Activity инициировать его? Например в классе, унаследованном от Application, который живёт всегда, пока живо приложение?
2. Вы инициализировали статический Singleton в унаследованном от Application классе. Ваш юзер ходит по окнам и все хорошо и прекрасно. Потом ему приходит СМС и он переключается на ее чтение, а параллельно у него запущен скайп, играет музыка и в смс ему прислали ссылку, по которой он открыл браузер. Android выгружает Ваш процесс и когда юзер возвращается в приложение Вы так же получите новый инстанс.
Ну что же с того? Процесс выгрузился и всё потерялось, что не сохранилось. Это логично, и так должно быть. Ведь умер не синглтон, а умерло всё приложение (поправка: процесс). При умирании UI процесса как бы всё имирает в UI.
Это и не есть проблема. Проблема в том, что вполне реально что-то не учесть.
Это не проблема, в том контексте, о котором вы говорите, потому что вы всегда должны быть готовы, что ваше приложение выгрузится. Приложение, а не ваш объект синглтона. не путайте это. У вас может умереть процесс, что равно умиранию приложения.
Проблема синглтона в том, что их сложно поддерживать и сложно тестировать.
Ivan22
26.11.2015 10:21+2Мир полон костылей — это факт. Вот я представляю, что приезжаю на СТО и мастер мне что-там долго говорит, говорит, говорит и спрашивает потом — ну что будем капиталить и через месяц и 3000$ — будет как новенькая!!! Или говорит я тут, фигак, фигак за 15 минут, заплатку на 100 рублей поставлю и можно дальше ездить, хотя концептуально это не правильно. Что я выберу!!?????
vbif
26.11.2015 10:45+7Но если я поставлю заплатку, вам нужно будет постоянно следить за температурой двигателя — если она превысит 90 градусов, заплатка отвалится и весь антифриз вытечет за пол-минуты. А ещё эта заплатка цепляет рулевую тягу, поэтому нельзя поворачивать руль влево больше чем на пол-оборота.
Loki3000
26.11.2015 12:14+2Не совсем так: при каких-то условиях двигатель может перегреться, а рулевая тяга может зацепиться за заплатку. Но пользователь практически в 100% случаев с этим не столкнется. Зато столкнется следующий мастер, когда его попросят обслужить двигатель.
Lamaster
26.11.2015 10:49+3Была одна история с кодом, за который мне до сих пор стыдно:
Надо было посчитать приблизительные сроки доставки товара в почтовой службе. Но так как исходников не было, то пришлось декомпилировать DLL'ку с помощью dotPeek (очень рекомендую). В процессе полетели bool пеерменные, которые преобразовались в int, а обратно при компиляции не сконвертировались в bool. Но так как многие проблемные места были обёрнуты try'ями без логирования, то ошибка нашлась не сразу. Но это всё последующие умозаключения. Сама проблема заключалась вот в чём: было несколько файликов с пунтами доставки и временем/стоимостью доставки в каждом регионе. Для новосозданных регионов (в частности Крым, Симферополь, Севастополь, etc.) этой информации не было, а для некоторых старых информация в рантайме терялась (детали не помню, просто что-то не заработало). Поэтому для таких случаев, когда невозможно посчитать сроки доставки всё равно необходимо было выдавать хоть какие нибудь цифры. Первая мысль была про рандом, но сроки доставки должны совпадать для каждых двух пунктов соответственно, а не разниться при каждом обновлении страницы. Поэтому было решено сложить названия пунктов, взять от них длину, делённую по модулю на определённое значение и прибавить минимальный коэффициент. Все данные подбирались опытным путём.
- 26.11.2015 11:15
Вот допустил ли я ошибку при проектировании, когда заложил int в качестве ключа для записей (состояние датчиков авто) или другой, ответственный, когда пропустил приближение трагедии.
Но буквально в этот вторник записей стало больше 2147483647.
Беда не пришла одна: при обновлении БД кончилось место на дисках.
В итоге — 9 часов даунтайма (в рабочее время) и, после, DDOS от авто с архивными данными за это время.
Частичная потеря данных за 1,5 часа с момента как закончился int, до момента обнаружения и осознания с остановкой всего.
P.S.: в течение месяца ожидается окончание int и для другого типа данных, надеемся хоть тут не облажатьсяJustRoo
26.11.2015 13:43Зависит от ТЗ. Если там была корректно указана скорость добавления записей в таблицу, то это однозначно факап.
guessss_who
26.11.2015 14:45+3Самый плохой код в карьере? О, это очень просто. Пришлось как-то исправлять ошибку в одном PHP-шном проекте, который генерировал отчеты о работе бригад по ремонту оборудования.
Все данные для отчетов были в БД Oracle. Готовый отчет представлял собой HTML-ную таблицу. В чем нюанс? HTML-код строк этой таблицы формировался внутри SQL-запроса, путем конкатенации строк с HTML-тегами и результатов извлеченных из оракловых табли. SQL-запрос был внутри PHP-шного кода. PHP-шный код, ошметками, был внутри HTML-я (заголовок таблицы и т.п.).
Кстати, причина, из-за которой вообще пришлось трогать это говно: Oracle ругался когда в результате конкатенации HTML-кода и результатов из таблиц формировалась строка >4K (или даже >8K, уже не помню точно).
ncix
26.11.2015 15:40Покаюсь и я.
В одном крупном продуктовом проекте для бизнеса, который я начинал как тимлид-руководитель, кто-то кинул идею о важности добавления новых таблиц чуть ли не на лету под новые хотелки каждого клиента. И я предложил то что мне казалось тогда гениальным решением — засунуть все метаданные в гигантский XML, по которому будут строиться SQL-запросы, в том числе на генерацию БД. И в довесок еще и весь интефейс в XML описали, чтоб значит все таблички и формочки генерились единообразно «на лету».
А потом заказчик очень захотел чтоб все это работало под Firebird и MSSQL. Умный гетерогенный генератор запросов оброс кучейфишечеккостыликов для всевозможных ситуаций и разных СУБД. Все это осложнялось неоптимальной структурой данных, доставшихся от смежного проекта. Генератор подчас выдавал запросы длиной в несколько страниц, на отладку и разбор которых уходили долгие часы.
Напоследок ко всему этому прикрутили синхронизатор распределенных баз данных со своими запросами, очередями, транзакциями и оптимизацией запросов на лету. И конечно же все это было завернуто в многопоточную логику с сетевым взаимодействием. И оно даже работало, хотя и нестабильно. И это было крайне тяжело отлаживать и исправлять. Но я таки дотянул проект до пилотного внедрения.
Проект тянулся очень долго, кто-то из разработчиков уволился, кто-то вырос в тимлиды, команда сильно обновилась. И новые ребята просто не смогли до конца понять и принять всю эту сложность. А после моего увольнения продавили идею переписать почти всё с нуля. Наверное, вспоминают меня недобрым словом.
IRainman
27.11.2015 00:04Да, суперклассы это ужас. Кстати, в качестве альтернативного решения foreground-мусоровозу существует ещё такой хак: создание иконки приложения в трее, такие приложения тоже в фоне не закрываются.
P.S. О своём самом-самом суровом случае расскажу если вспомню действительно выдающийся и интересный ибо пока ничего интересного в голову не приходит, а о «рядовом унынии» рассказывать не хочется.lavelas
30.11.2015 15:09в качестве альтернативного решения foreground-мусоровозу существует ещё такой хак: создание иконки приложения в трее, такие приложения тоже в фоне не закрываются.
Это и есть foreground-сервис. Просто этот я назвал мусоровоз, потому что в нем был мусор :)
lavelas
Не ожидал, что народ начнет минусовать мою историю :(
Всем захабренным поясняю, если вам как фрилансеру говорят почини (что равносильно перепиши с нуля), но мы тебе заплатим $100 от силы то ты:
1. Как фрилансер — пошлешь (работы часов на 200)
2. Как человек пожалеешь и постараешься сделать за 20 часов франкинштейна (поверьте, там хуже некуда).
Bringoff
100$ за 20 часов? Печальная история, конечно.
Alexufo
а удовольствие куда дели?
crmMaster
Нет удовольствия в копании чужого дерьма.
masterL
Это получается 800 долларов за 160 часов (рабочий месяц) — вполне обычная зарплата программиста не в столице.
Bringoff
Фриланс — это не офис. Нормальных фрилансеров с рейтом меньше 10$/h я еще не встречал.
VolCh
Рейт (реальный) фрилансера зависит прежде всего от его
наглостиумения продавать себя.taliban
Чудом вставили волшебное слов «нормальный» и вышли чистым из воды. Рейт фрилансера зависит от опыта работы, умения и наглости. Есть полно с рейтом меньше 10$/h.
Bringoff
Может, где-то их и полно (на каком-то fl.ru), но если обратиться к статистике, то в Украине, допустим, средний рейт 21-22$/h. То есть, чтобы среднее число было таким, тех, кто имеет рейт, скажем, 5$/h, должно быть довольно мало. Или тех, кто берет больше 30, много)
taliban
Можно мне ссылочку на пруф, статистику по разным местам, например? Ато Вы не очень тянете на человека которому можно доверять безоговорочно :)
Bringoff
Фу, ну что за переход на личности? Вот dou.ua/lenta/articles/freelance-eastern-europe/
VolCh
Не похоже, что там анализируются российские биржи фриланса.
Bringoff
А я о чем говорил?
Есть у меня небольшой опыт работы на отечественных биржах — это того не стоит. Разве что если с английским туго, но тогда вообще печаль.
taliban
Спасибо, отличная статистика, ребята хорошо поработали :)
Lure_of_Chaos
Мы не хотим Вам заплатить за огромную работу, пожалуйста, поработайте еще столько же «за еду»!
Офигеть. Как у Вас гордости не хватило вообще их послать нафиг с тем, что у них было? Или это такая маленькая месть — преумножение их говнокода своим?
VolCh
Фрилансеру почти всё равно сделать заказ на 160 часов или на 20, если он заявил свой рейт. В принципе обычная практика при любом аутсорсе предлагать заказчику хотя бы два варианта: качественно и быстро.