Случайны выбор дневника Раскрыть/свернуть полный список возможностей


Найдено 2026 сообщений
Cообщения с меткой

rambler - Самое интересное в блогах

Следующие 30  »
rss_rss_hh_new

Генерамба — кодогенератор для iOS разработки

Четверг, 11 Февраля 2016 г. 14:55 (ссылка)


image



TL;DR

Мы написали классный кодогенератор для iOS-разработки, обладающий следующими достоинствами:


  • Поддержка Swift и Objective-C,

  • Использование языка разметки liquid для создания шаблонов,

  • Гибкая система управления шаблонами,

  • Интеграция с менеджером зависимостей Cocoapods.



Больше подробностей — под катом.

Читать дальше →

https://habrahabr.ru/post/276275/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Анонс Rambler.iOS #5 — VIPER Edition

Вторник, 08 Декабря 2015 г. 13:00 (ссылка)


image



Мы строили, строили и наконец построили! Да здравствуем мы, ура!

Чебурашка
Читать дальше →

http://habrahabr.ru/post/272551/

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Конвейерное производство Android приложений

Понедельник, 26 Октября 2015 г. 19:21 (ссылка)

Многие разработчики сталкиваются с задачей создания кастомизированных приложений. Например, разработка нескольких версий одного приложения или изменения стандартного приложения под требования заказчика. Мы в Rambler&Co столкнулись с такой задачей при разработке Rambler кассы и ее брендированных версий под отдельные кинотеатры. В данной статье рассмотрим эволюцию архитектуры такого приложения, а также инструменты, которые упрощают нам жизнь.







Постановка задачи



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



Цели и конверсия


Кастомизированные приложения положительно влияют на конверсию. Основная цель пользователей в приложении – просмотр расписания любимого кинотеатра и покупка билетов. Наличие приложений позволяет сократить кол-во кликов от открытия до нажатия кнопки купить билет на 2-3 клика. А как известно, каждый клик это -N% пользователей. Так же присутствует возможность доносить до пользователей информацию об акциях и скидках.



Архитектура



Изменения касаются UI, основные сущности и функционал остаются без изменений. Фильтрация источников не требуется, выполняется со стороны API.



Изначально была задача сделать одну брендированную версию, планы по развитию были весьма туманны. Ветвление по версиям было через if. Примерно так:



if (TYPE == KASSA1) {
return 1;
} else if (TYPE == KASSA2) {
return 2;
} else {
return 3;
}


Или через switch:



switch (TYPE) {
case KASSA1:
return 1;
case KASSA2:
return 2;
default:
return 3;
}


Решение очень плохое, множество недостатков и почти нет преимуществ. Вся разработка в одной кодовой базе, огромное количество кода, сложная отладка и дебаг, проблема деплоя. Вся гамма эмоций на одной картинке:

IF, IF EVEREWHERE




Flavors


Следующей веткой эволюции стало использование Gradle Flavors + build Types

Создаются отдельные версии приложений, переключение между ними происходит в окошке build types, параллельно можно задавать конфигурации, например: debug, release, manager, testOrder и так далее. Каждая конфигурация содержит свой код и ресурсы. Одной из неприятных особенностей flavors является сложная навигация по коду в неактивной ветке. Пример конфигурации:



productFlavors {
kassaCinema1 {
applicationId "ru.rambler.cinema1"
versionName "1.3"
}
kassaCinema2 {
applicationId "ru.rambler.cinema2"
versionName "1.1"
}
}


Основные возможности:


http://habrahabr.ru/post/269429/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

По итогам Rambler.iOS #4

Вторник, 29 Сентября 2015 г. 17:59 (ссылка)





В прошлую пятницу на уютной мансарде компании Rambler&Co прошла четвертая встреча iOS разработчиков Москвы.



Докладчиков, по уже сложившейся традиции, было трое.



Максим Савушкин рассказал, как создать модульное приложение, с помощью чего минимальными усилиями осуществлять брендинг. Им были затронуты такие темы, как VIPER, Nimbus и Typhoon.

Видео

Слайды



Мы, кстати, оказались немного обескуражены вопросом о том, зачем превращать код с множеством if-else во что-то красивое и модульное: казалось, что Фаулер и его единомышленники рассказали до нас это много раз, однако, возможно, у кого-то из вас есть какие-то комментарии к той или иной стороне баррикад.



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







Видео

Слайды

Проект на GitHub



Для короткого перерыва Сергей Крапивенский подготовил раунд “Своей игры” с вопросами по около-iOsным темам. 4 участника, 25 вопросов и всего один приз, но, кажется, весело было как игрокам, так и зрителям.







После Егора все наши секреты написания бизнес-логики и грабли, по которым мы прошлись, раскрыл Герман Сапрыкин:

Видео

Слайды



Хотелось бы поблагодарить всех, кто пришел, проявил активность и поучаствовал в коротенькой «Своей Игре» и снова поздравить счастливого обладателя новенькой лицензии на AppCode. Судя по тому, что игра стала одной из самых позитивных частей нашей встречи, мы решили подумать над дальнейшим развитием подобного интерактива.



Если вы хотите выступить на следующей конференции Rambler.iOS, ждем ваших писем с предполагаемыми темами на rambler.ios@rambler-co.ru.



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



И спасибо Екатерине Коровкиной за помощь в проведении мероприятия и составление пресс-релиза!



Original source: habrahabr.ru (comments, light).

http://habrahabr.ru/post/267913/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Moscow Python Meetup в Rambler&Co

Среда, 23 Сентября 2015 г. 13:49 (ссылка)






8-го октября (четверг) в Rambler&Co мы ждем в гости Moscow Python Meetup. 1-я встреча нового сезона начнется в 19.00. На встрече нас ждут 3 доклада.



Читать дальше →

http://habrahabr.ru/post/267543/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Открытый митап Rambler.Android

Вторник, 01 Сентября 2015 г. 13:02 (ссылка)

Rambler.Android — периодические встречи Android-разработчиков, проводимые компанией Rambler&Co.



В сентябре у нас будет третья встреча, и она будет особенная. После предыдущих двух, которые были внутренними, мы решили полностью изменить формат. Участие во встрече будет открытым, и любой, кто вовлечён в Android разработку, теперь может присоединиться к нам.



Темы докладов:


  • Конвейерное производство приложений (Мельников Андрей — andrey7mel)

  • Android M: опыт личного знакомства (Щенёв Вадим — v555)

  • Vector Drawable API. Возможности применения (Осипенко Олег — basnopisets)



Время проведения: 10 сентября, 19:00

Место проведения: Москва, Варшавское ш., 9 с.1 (БЦ «Даниловские мануфактуры»).

Регистрация (количество мест ограничено)

Наш twitter: @rambler_android

Original source: habrahabr.ru (comments, light).

http://habrahabr.ru/post/265867/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Управляем зависимостями в iOS-приложениях правильно: Typhoon Tips & Tricks

Четверг, 13 Августа 2015 г. 14:27 (ссылка)





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



В этой статье мы рассмотрим следующие особенности Typhoon Framework:


  • Автоинъекция (также известная как autowiring),

  • Автоматический выбор из альтернативных реализаций одного TyphoonDefinition,

  • Особенности работы с TyphoonConfig

  • Использование TyphoonPatcher для организации интеграционных тестов,

  • Применение runtime-атрибутов при создании объектов,

  • Реализация фабрик на базе TyphoonAssembly,

  • Постпроцессинг объектов, создаваемых при помощи Typhoon,

  • Инструменты для написания асинхронных тестов.



Цикл «Управляем зависимостями в iOS-приложениях правильно»





Автоинъекции/Autowire



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

@interface RCMMessageViewController : UIViewController
#import TyphoonAutoInjection.h>

@protocol RCMMessageService;
@class RCMMessageRendererBase;

@interface RCMMessageViewController : UIViewController

@property (strong, nonatomic) InjectedProtocol(RCMMessageService) messageService;
@property (strong, nonatomic) InjectedClass(RCMMessageRendererBase) renderer;

@end


И единственная на данный момент TyphoonAssembly в приложении, указанная в Info.plist:

@implementation RCMHelperAssembly
@implementation RCMHelperAssembly

- (RCMMessageRendererBase *)messageRenderer {
return [TyphoonDefinition withClass:[RCMMessageRendererBase class]];
}

- (id )messageService {
return [TyphoonDefinition withClass:[RCMMessageServiceBase class]];
}

@end


Попробуем запустить приложение с такой конфигурацией:





Как видим, нужные зависимости были подставлены автоматически. Обращаю внимание на то, что при использовании автоинъекции не требуется писать метод, отдающий TyphoonDefinition для ViewController'а. Также стоит отметить, что такой подход работает только при создании UIViewController из TyphoonStoryboard.



Похожий подход может быть использован и при написании интеграционных тестов — вместо того, чтобы вручную создавать зависимости тестируемого объекта, можно автоматически подставить их из определенной TyphoonAssembly:

@interface RCMMessageServiceBaseTests : XCTestCase
#import TyphoonAutoInjection.h>

@interface RCMMessageServiceBaseTests : XCTestCase
@property (nonatomic, strong) InjectedProtocol(RCMMessageService) messageService;
@end

@implementation RCMMessageServiceBaseTests

- (void)setUp {
[super setUp];
[[[RCMServiceComponentsAssemblyBase new] activate] inject:self];
}

- (void)testThatServiceObtainsMessage {
// ...
}

@end


Аналогичным образом зависимости подставляются в UIViewController, созданный вручную, либо из xib.



Как и у любой технологии, у autowire есть как достоинства:


  • Экономия времени за счет отсутствия необходимости реализовывать некоторые assembly,

  • Более информативные интерфейсы объектов — сразу же видно, какие зависимости подставляются при помощи Typhoon, какие — самостоятельно,

  • Если какая-либо из автоматически подставляемых зависимостей объекта не найдена в фабрике, crash произойдет сразу же (в случае ручной подстановки это может вообще пройти незамеченным).



так и недостатки:


  • Привязка к Typhoon уходит за пределы assembly и затрагивает конкретные классы,

  • Просмотрев структуру модулей TyphoonAssembly проекта, нельзя судить о его архитектуре в целом.



Правило хорошего кода, выработанное нами в Rambler&Co — лучше потратить некоторое время и подготовить хорошо структурированные модули уровня Presentation, в которых будут содержаться definition'ы для всех ViewController'ов, а возможности autowire использовать только в интеграционных тестах. Наличие хорошо документированной при помощи TyphoonAssembly структуры проекта во многом превосходит все достоинства автоинъекции.



TyphoonDefinition+Option



В предыдущей статье мы рассматривали пример использования двух разных реализаций одной TyphoonAssembly — боевой и фейковой. Тем не менее, иногда такой подход равносилен стрельбе из пушек по воробьям — а Typhoon предоставляет нам гораздо более изящные способы решения проблемы.



Рассмотрим еще один кейс из Рамблер.Почты:

Команда QA попросила добавить в приложение специальное debug-меню, позволяющее работать с логами, узнавать текущий номер билда и прочие подобные вещи. Экран настроек — это таблица, которая собирается из коллекции ViewModel'ей отдельным классом RCMSettingsConfigurator. У этого класса две реализации — Base и Debug, которые включаются соответствующими build scheme. Перед нами встал выбор из трех вариантов реализации этой задачи:


  • Создавать конфигуратор вручную при помощи #ifdef'ов, определяющих значения препроцессорной директивы,

  • Написать две реализации assembly, создающей объекты для user story настроек,

  • Использовать категорию TyphoonDefinition+Option.



Первый вариант, конечно, не выбор настоящих ниндзя (ну не дело это, активно использовать #ifdef'ы в коде мобильного приложения). Второй вариант — это та самая вышеупомянутая пушка, нацеленная на невинных воробьев. Третий способ с одной стороны очень прост в реализации, с другой — достаточно гибко расширяется. Остановимся на нем подробнее.



Для начала посмотрим на интерфейс категории, используя методы которой, мы можем получать определенные definition'ы в зависимости от значения параметра, подставленного в поле option:

@interface TyphoonDefinition (Option)
@interface TyphoonDefinition (Option)

+ (id)withOption:(id)option yes:(id)yesInjection no:(id)noInjection;
+ (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock;
+ (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock autoInjectionConfig:(void(^)(id config))configBlock;

@end


К примеру, в рассматриваемом кейсе это выглядит следующим образом:

- (id )settingsConfigurator
- (id )settingsConfigurator {
return [TyphoonDefinition withOption:@(DEBUG)
yes:[self debugSettingsConfigurator]
no:[self baseSettingsConfigurator]];
}


Использование объекта TyphoonOptionMatcher позволяет работать и с более сложными условиями:

- (id )settingsConfiguratorWithOption:(id)option
- (id )settingsConfiguratorWithOption:(id)option {
return [TyphoonDefinition withOption:option matcher:^(TyphoonOptionMatcher *matcher) {
[matcher caseEqual:@"qa-team" use:[self qaSettingsConfigurator]];
[matcher caseEqual:@"big-bosses" use:[self bigBossesSettingsConfigurator]];
[matcher caseEqual:@"ios-dream-team" use:[self iosTeamSettingsConfigurator]];
[matcher caseMemberOfClass:[RCMConfiguratorOption class] use:[self settingsConfiguratorWithOption:option]];
[matcher defaultUse:[self defaultSettingsConfigurator]];
}];
}


Еще одна возможность — использовать параметр option в качестве ключа для поиска требуемого TyphoonDefinition:

- (id )settingsConfiguratorWithOption:(id)option
- (id )settingsConfiguratorWithOption:(id)option {
return [TyphoonDefinition withOption:option matcher:^(TyphoonOptionMatcher *matcher) {
[matcher useDefinitionWithKeyMatchedOptionValue];
}];
// При option = @"debugSettingsConfigurator" вернет definition из метода - debugSettingsConfigurator
}


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



TyphoonConfig и TyphoonTypeConverter



В самой первой статье в одном из примеров использования Typhoon я уже упоминал TyphoonConfig, использовав его для инъекции URL серверного API в один из network-клиентов. Пришло время взглянуть на него повнимательнее.



Поддерживаемые форматы конфигурационных файлов:


  • plist,

  • properties,

  • json



Примитивные типы (числа, BOOL, строки) указываются «как есть»:

{
"config": {
"defaultFontSize": 17,
"openLinksInExternalBrowser" : NO
}
}


Typhoon позволяет оперировать и некоторыми другими объектами: NSURL, UIColor, NSNumber, UIImage. В таком случае используется специальный синтаксис:

{
"config": {
"baseURL": NSURL(https:// mail.rambler.ru),
"logoImage" : UIImage(rambler-mail-logo-new)
}
}


Кроме того, при необходимости мы можем добавить собственный TypeConverter и описать в конфигурационном файле объекты любого другого класса. К примеру, мы хотим инкапсулировать все детали стиля приложения в одном объекте — RCMStyleModel:

@interface RCMStyleTypeConverter : NSObject
typedef NS_ENUM(NSUInteger, RCMStyleComponent) {
RCMStylePrimaryColorComponent = 0,
RCMStyleDefaultFontSizeComponent = 1,
RCMStyleDefaultFontNameComponent = 2
};

@interface RCMStyleTypeConverter : NSObject
@end

@implementation RCMStyleTypeConverter

- (NSString *)supportedType {
return @"RCMStyle";
}

- (id)convert:(NSString *)stringValue {
NSArray *styleComponents = [stringValue componentsSeparatedByString:@";"];

NSString *colorString = styleComponents[RCMStylePrimaryColorComponent];
UIColor *primaryColor = [self colorFromHexString:colorString];

NSString *defaultFontSizeString = styleComponents[RCMStyleDefaultFontSizeComponent];
CGFloat defaultFontSize = [defaultFontSizeString floatValue];

NSString *defaultFontName = styleComponents[RCMStyleDefaultFontNameComponent];
UIFont *defaultFont = [UIFont fontWithName:defaultFontName size:defaultFontSize];

RCMStyleModel *styleModel = [[RCMStyleModel alloc] init];
styleModel.primaryColor = primaryColor;
styleModel.defaultFontSize = defaultFontSize;
styleModel.defaultFont = defaultFont;

return styleModel;
}


И теперь стиль для приложения мы можем задавать в следующем виде:

{
"config": {
"defaultStyle": RCMStyle(#8732A9;17;HelveticeNeue-Regular),
"anotherStyle" : RCMStyle(#AABBCC;15;SanFrancisco)
}
}


Таким образом, если в одну и ту же сущность передаются сразу несколько параметров из конфигурационного файла, стоит задуматься об объединении их в отдельный модельный объект и написании для него TypeConverter'а.



TyphoonPatcher



Основное отличие интеграционных и unit тестов в том, что в первом случае мы тестируем взаимодействие отдельных модулей приложения друг с другом, а во втором — каждый конкретный модуль в отрыве от всех остальных. Так вот, Typhoon просто восхитителен для организации интеграционных тестов.



К примеру, в нашем проекте есть следующая цепочка зависимостей:

RCMPushNotificationCenter -> RCMPushService -> RCMNetworkClient



Мы хотим протестировать поведение RCMPushNotificationCenter в зависимости от различных результатов обращения к серверу. Вместо того, чтобы вручную создавать тестируемый объект, подставлять в него stub'овый RCMPushService и подменять реализации его методов, мы можем воспользоваться уже готовой инфраструктурой TyphoonAssembly:

- (void)setUp
- (void)setUp {
[super setUp];

NSArray *collaboratingAssemblies = @[[RCMClientAssembly new], [RCMCoreComponentsAssembly new]];
TyphoonAssembly *serviceComponents = [[RCMServiceComponentsAssemblyBase new] activateWithCollaboratingAssemblies:collaboratingAssemblies];
self.pushNotificationCenter = [serviceComponents pushNotificationCenter];

TyphoonPatcher *patcher = [[TyphoonPatcher alloc] init];
[patcher patchDefinitionWithSelector:@selector(networkClient) withObject:^id{
return [RCMFakeNetworkClient new];
}];
}


Объект TyphoonPatcher позволяет нам пропатчить метод, отдающий TyphoonDefinition, в любом из модулей TyphoonAssembly. В передаваемом TyphoonPatcher блоке можно не просто передавать другой инстанс класса, но и использовать mock'и, реализуемые различными фреймворками.



Runtime arguments



Typhoon позволяет инстанциировать объекты не только с заранее заданными зависимостями, но и с использованием runtime параметров. Понадобиться это может, к примеру, при реализации абстрактной фабрики. Рассмотрим пример:



У нас есть RCMMessageViewController, обязательной зависимостью которого является объект сообщения — RCMMessage:

- (void)setUp
@interface RCMMessageViewController : UIViewController

- (instancetype)initWithMessage:(RCMMessage *)message;
@property (nonatomic, strong) id messageService;

@end


Объект message неизвестен на момент регистрации TyphoonDefinition'ов при активации TyphoonAssembly — поэтому нам нужно уметь создавать его на лету. Для этого в TyphoonAssembly соответствующей user story напишем следующий метод:

- (UIViewController *)messageViewControllerWithMessage:(RCMMessage *)message
- (UIViewController *)messageViewControllerWithMessage:(RCMMessage *)message {
return [TyphoonDefinition withClass:[RCMMessageViewController class] configuration:^(TyphoonDefinition *definition) {
[definition useInitializer:@selector(initWithMessage:) parameters:^(TyphoonMethod *initializer) {
[initializer injectParameterWith:message];
}];

[definition injectProperty:@selector(messageService)
with:[self.serviceComponents messageService]];
}];
}


Вынесем этот метод в отдельный протокол, к примеру, RCMMessageControllerFactory, и проинжектим его в роутер:

- (id)foldersRouter
- (id)foldersRouter {
return [TyphoonDefinition withClass:[RCMFoldersRouterBase class] configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(messageControllerFactory)
with:self];
}];
}


И добавим в роутер реализацию создания этого контроллера:

- (void)showMessageViewControllerFromSourceController
- (void)showMessageViewControllerFromSourceController:(UIViewController *)sourceViewController
withMessage:(id )message {
RCMMessageViewController *messageViewController = [self.messageControllerFactory messageViewControllerWithMessage:message];
...
}


Стоит упомянуть и несколько ограничений этой техники:


  • Runtime аргументы обязательно должны представлять собой объекты. Примитивы при необходимости могут быть завернуты в NSValue,

  • Переданные фабрике объекты должны использоваться в своем первоначальном виде, их состояние изменять нельзя,

  • Стоит аккуратно использовать в сочетании с циклическими зависимостями. Runtime аргументы должны быть переданы всем объектам зависимости, иначе она не решится правильным образом.



Factory Definitions



В некоторых ситуациях бывает удобно зарегистрировать TyphoonDefinition, умеющий генерировать другие definition'ы. Объясню на конкретном примере:



За создание пользовательских аватарок отвечает специальная фабрика — RCMTextAvatarFactory:

@interface RCMTextAvatarFactory : NSObject
@interface RCMTextAvatarFactory : NSObject
- (RCMTextAvatar *)avatarWithName:(NSString *)name;
@end


Аватарки, создаваемые этой фабрикой, необходимо передавать в другие объекты. Реализуется это следующим образом — для начала регистрируется definition для фабрики:

- (RCMTextAvatarFactory *)textAvatarFactory
- (RCMTextAvatarFactory *)textAvatarFactory {
return [TyphoonDefinition withClass:[RCMTextAvatarFactory class]];
}


И затем регистрируются definition'ы для создаваемых этой фабрикой сущностей:

- (RCMTextAvatar *)textAvatarForUserName:(NSString *)userName
- (RCMTextAvatar *)textAvatarForUserName:(NSString *)userName {
return [TyphoonDefinition withFactory:[self textAvatarFactory] selector:@selector(avatarWithName:) parameters:^(TyphoonMethod *factoryMethod) {
[factoryMethod injectParameterWith:userName];
}];
}


Кстати, эта возможность позволяет плавно мигрировать с использования сервис-локатора, если вы этим грешили, на Typhoon. Первым шагом будет регистрация локатора в качестве фабрики, а вторым — реализация TyphoonDefinition'ов для сервисов с использованием factoryMethod'ов:

- (id )messageService
- (id )messageService {
return [TyphoonDefinition withFactory:[self serviceLocator] selector:@selector(messageService)];
}




TyphoonInstancePostProcessor/TyphoonDefinitionPostProcessor



Эти протоколы используются для создания так называемых инфраструктурных компонентов. Если assembly возвращает такой объект, он обрабатывается отлично от обычных definition’ов.



Использование TyphoonInstancePostProcessor позволяет нам вклиниться в момент возврата контейнером инстансов создаваемых зависимостей и каким-нибудь образом их обработать. К примеру, это можно использовать для логирования всех обращений к определенным объектам, скажем, к networkService'ам:



Для начала напишем простой декоратор, выводящий в лог все сообщения, посылаемые объекту:

@interface RCMDecoratedService : NSProxy
@interface RCMDecoratedService : NSProxy
+ (instancetype)decoratedServiceWith:(NSObject *)service;
@end

@interface RCMDecoratedService()
@property (strong, nonatomic) NSObject *service;
@end

@implementation RCMDecoratedService
- (instancetype)initWithService:(NSObject *)service {
self.service = service;
return self;
}

+ (instancetype)decoratedServiceWith:(NSObject *)service {
return [[self alloc] initWithService:service];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.service methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
NSLog(invocation.debugDescription);
[invocation invokeWithTarget:self.service];
}
@end


Теперь нужно создать объект, реализующий протокол TyphoonInstancePostProcessor — его задачей будет определять, какому из полученных им объектов требуется добавить дополнительное поведение, и декорировать их:

@interface RCMLoggingInstancePostProcessor : NSObject
@interface RCMLoggingInstancePostProcessor : NSObject 
@end

@implementation RCMLoggingInstancePostProcessor
- (id)postProcessInstance:(id)instance {
if ([self isAppropriateInstance:instance]) {
RCMDecoratedService *decoratedService = [RCMDecoratedService decoratedServiceWith:instance];
return decoratedService;
}
return instance;
}

- (BOOL)isAppropriateInstance:(id)instance {
if ([instance conformsToProtocol:@protocol(RCMService)]) {
return YES;
}
return NO;
}
@end


И последний шаг — зарегистрировать RCMLoggingInstancePostProcessor в одной из TyphoonAssembly. Сам объект не участвует в процессе инъекции зависимостей и живет сам по себе. Его жизненный цикл привязан ко времени жизни TyphoonComponentFactory.

@implementation RCMApplicationAssembly
@implementation RCMApplicationAssembly
- (id)loggingProcessor {
return [TyphoonDefinition withClass:[RCMLoggingInstancePostProcessor class]];
}
...
@end


Теперь все создаваемые Typhoon'ом зависимости будут проходить через RCMLoggingInstancePostProcessor — а те из них, кто реализует протокол RCMService — оборачиваться в NSProxy.



Другой инфраструктурный компонент, TyphoonDefinitionPostProcessor, позволяет обрабатывать все зарегистрированные definition'ы до того, как будут созданы описываемые ими объекты. Таким образом, мы можем любым образом конфигурировать и пересобирать переданные такому процессору TyphoonDefinition'ы:

- (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory;


В качестве примеров использования этого компонента можно привести уже упомянутые в статье TyphoonPatcher и TyphoonConfigPostProcessor.



Асинхронное тестирование



Для тех, кто по какой-то причине не может или не хочет использовать XCTestExpectation, Typhoon предлагает свой набор методов для реализации тестирования асинхронных вызовов. Рассмотрим в качестве примера тест синхронизации почтовых сборщиков:

- (void)testThatServiceSynchronizeMailBoxesList
- (void)testThatServiceSynchronizeMailBoxesList {
// given
NSInteger const kExpectedMailBoxCount = 4;
[OHHTTPStubs stubRequestsPassingTest:REQUEST_TEST_YES
withStubResponse:TEST_RESPONSE_WITH_FILE(@"mailboxes_success")];
__block NSInteger resultCount;
__block NSError *responseError = nil;

// when
[self.mailBoxService synchronizeMailBoxesWithCompletionBlock:^(NSError *error) {
responseError = error;
NSFetchedResultsController *controller = [self.mailBoxService fetchedResultsControllerWithAllMailBoxes];
resultCount = controller.fetchedObjects.count;
}];

// then
[TyphoonTestUtils waitForCondition:^BOOL{
typhoon_asynch_condition(resultCount > 0);
} andPerformTests:^{
XCTAssertNil(responseError);
XCTAssertEqual(resultCount, kExpectedMailBoxCount);
}];
}


Стандартный timeout, добавленный разработчиками — семь секунд, условие проверяется каждую секунду. Если оно не будет выполнено — тест провалится с соответствующим exception'ом. При необходимости можно использовать и свой timeout:

TyphoonTestUtils wait:30.0f secondsForCondition:^BOOL
[TyphoonTestUtils wait:30.0f secondsForCondition:^BOOL{
typhoon_asynch_condition(resultCount > 0);
} andPerformTests:^{
XCTAssertNil(responseError);
XCTAssertEqual(resultCount, kExpectedMailBoxCount);
}];




Заключение



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



В следующей части цикла мы вкратце рассмотрим две других реализации Dependency Injection контейнеров для Cocoa — Objection и BloodMagic. Ну и небольшая новость напоследок — мы с моим коллегой Германом Сапрыкиным вошли в команду разработчиков Typhoon, так что фреймворк стал еще чуть более отечественным.



Цикл «Управляем зависимостями в iOS-приложениях правильно»





Полезные ссылки





Original source: habrahabr.ru (comments, light).

http://habrahabr.ru/post/264683/

Комментарии (0)КомментироватьВ цитатник или сообщество
rss_rss_hh_new

Разработка через тестирование в iOS

Четверг, 06 Августа 2015 г. 17:04 (ссылка)

Содержание:




  • Разработка через тестирование – что это?

  • Три закона TDD

  • Примеры применения

  • Преимущества и недостатки

  • Литература и ссылки





Разработка через тестирование – что это?



Разработка через тестирование (Test-driven development) — техника разработки программного обеспечения, которая определяет разработку через написание тестов. В сущности вам нужно выполнять три простых повторяющихся шага:

— Написать тест для новой функциональности, которую необходимо добавить;

— Написать код, который пройдет тест;

— Провести рефакторинг нового и старого кода.



Мартин Фаулер










Три закона TDD



Сейчас многие знают, что TDD требует от нас писать сначала тесты, прежде чем писать production код. Но это правило — лишь верхушка айсберга. Предлагаю вам рассмотреть следующие три закона:


  1. Не пишется production код, прежде чем для него есть неработающий тест;

  2. Не пишется больше кода юнит теста, чем достаточно для его ошибки. И не компилируемость — это ошибка;

  3. Не пишется больше production кода, чем достаточно для прохождения текущего неработающего теста.



Эти три закона удерживают программиста в цикле, который длится около минуты. Тесты и production код пишутся одновременно, с тестами всего на несколько секунд впереди production кода.

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



Примеры применения



Пример 1. Сортировка модельных объектов


Рассмотрим простейший пример написания метода сортировки модельных объектов (событий). Сначала пишем интерфейс нового метода (на вход принимает хаотичный массив объектов, а возвращает соответственно отсортированный):



Интерфейс тестируемого метода:



    - (NSArray *)sortEvents:(NSArray *)events


Далее пишем тест:

Используем нотацию given, when, then для деления теста на три логических блока:


  • создание окружения, предусловий теста;

  • совершение действия, вызов тестируемого метода;

  • проверка работы тестируемой функциональности.



Тест:



    - (void)testSortEvents {
// given
id firstEvent = [self mockEventWithClass:kCinemaEventType name:@"В"];
id secondEvent = [self mockEventWithClass:kCinemaEventType name:@"Г"];
id thirdEvent = [self mockEventWithClass:kPerfomanceEventType name:@"А"];
id fourthEvent = [self mockEventWithClass:kPerfomanceEventType name:@"Б"];

NSArray *correctSortedArray = @[firstEvent, secondEvent, thirdEvent, fourthEvent];
NSArray *incorrectSortedArray = @[thirdEvent, secondEvent, fourthEvent, firstEvent];

// when
NSArray *sortedWithTestingMethodArray = [self.service sortEvents:incorrectSortedArray];

// then
XCTAssertEqualObjects(correctSortedArray, sortedWithTestingMethodArray, @"Сортировка эвентов работает неправильно");
}


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



Реализация метода:



    - (NSArray *)sortEvents:(NSArray *)events {
// Сортировка событий (Сначала кино, затем остальные, обе секции по алфавиту)
NSMutableArray *cinemaEvents = [[NSMutableArray alloc] init];
NSMutableArray *otherEvents = [[NSMutableArray alloc] init];

for (Event *event in events) {
if ([event.type isEqualToString:kCinemaEventType]) {
[cinemaEvents addObject:event];
} else {
[otherEvents addObject:event];
}
}

NSComparisonResult (^sortBlock)(Event *firstEvent, Event *secondEvent) = ^NSComparisonResult(Event *firstEvent, Event *secondEvent) {
return [firstEvent.type compare:secondEvent.type
options:NSNumericSearch];
};

[cinemaEvents sortUsingComparator:sortBlock];
[otherEvents sortUsingComparator:sortBlock];

return [cinemaEvents arrayByAddingObjectsFromArray:otherEvents];
}


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



Очень важная особенность TDD – это инверсия ответственности. При классическом подходе к разработке ПО программист после написания кода самолично проводит тестирование, гоняет код в дебаггере, проверяет работу всеч if-ов. В TDD именно тесты подтверждают работоспособность кода.







Пример 2. Маппер


Рассмотрим второй пример – маппер серверной выдачи в модельные объекты.



Маппер должен отвечать следующим требованиям:

1) мапить объекты (неожиданно)

2) обрабатывать кривую выдачу (Null, неверный тип данных, неверная структура выдачи)

3) обеспечивать консистентность данных (проверка обязательных полей)



Пишем интерфейс тестируемого метод, он будет синхронным:



Интерфейс тестируемого метода:



    - (NSArray *)mapEvents:(id)responseObject


Далее пишем первый тест, проверяющий позитивный сценарий, т.е. маппинг объектов при адекватной выдаче. Мы не пишем сразу все тесты, не пытаемся покрыть все требования, которым должен отвечать маппер, а пишем единственный тест к простейшей реализации:



Тест:



    - (void)testMapEventsResponseSuccessful {
// given
NSDictionary *responseObject = @{@"event" : @[@{@"type" : @1,
@"name" : @"test",
@"description" : @"test"}]};

// when
NSArray *events = [self.mapper mapEvents:responseObject];

// then
Event *event = events[0];
[self checkObject:event
forKeyExistence:@[@"type", @"name", @"description"]];
}


Для проверки объекта на наличие необходимых полей используем метод-хелпер на KVC:



    - (void)checkObject:(id)object
forKeyExistence:(NSArray *)keys {
for (NSString *key in keys) {
if (![object valueForKey:key]) {
XCTFail(@"Объект не содержит необходимого поля - %@", key);
}
}
}


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



Реализация метода:



    - (NSArray *)mapEvents:(id)responseObject {
NSDictionary *mappingsDictionary = @{kEventResponseKey : [self eventResponseMapping]};

RKMappingResult *mappingResult = [self mapResponseObject:responseObject
withMappingsDictionary:mappingsDictionary];

if (mappingResult) {
return [mappingResult array];
}

return @[];
}


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



Тест:



    - (void)testMapEventsResponseWithMissingMandatoryFields {
// given
NSDictionary *responseObject = @{kEventResponseKey : @[@{@"name" : @"test"}]};

// when
NSArray *events = [self.mapper mapEvents:responseObject];

// then
XCTAssertFalse([events count]);
}


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



Реализация метода:



    - (NSArray *)mapEvents:(id)responseObject {
NSDictionary *mappingsDictionary = @{kEventResponseKey : [self eventResponseMapping]};

RKMappingResult *mappingResult = [self mapResponseObject:responseObject
withMappingsDictionary:mappingsDictionary];

if (mappingResult) {
//Проверяем наличие обязательных полей в объекте
NSArray *events = [self checkMandatoryFields:[mappingResult array]];
return events;
}

return @[];
}


Исходя из первоначальных требований, наш маппер должен корректно себя вести в случае наличия Null-ов в выдаче, пишем проверяющий это тест:



    - (void)testMapEventsResponseWithNulls {
// given
NSDictionary *responseObject = @{kEventResponseKey : @[@{@"type" : @1,
@"name" : [NSNull null],
@"description" : [NSNull null]}]};

// when
NSArray *events = [self.mapper mapEvents:responseObject];

// then
Event *event = events[0];
XCTAssertNotNil(event.type, @"");
XCTAssertNil(event.name, @"");
}


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



    - (void)testMapEventsResponseWithWrongType {
// given
NSDictionary *responseObject = @{kEventResponseKey : @[@{@"type" : @"123"}]};

// when
NSArray *events = [self.mapper mapEvents:responseObject];


// then
Event *event = events[0];
XCTAssertTrue([event.type isEqual:@123]);
}




Пример 3. Сервис получения событий


Рассмотрим сервис получения модельных объектов (событий), протокол будет содержать следующие методы:



    @interface EventService : NSObject

- (instancetype)initWithClient:(id)client
mapper:(id)mapper;

- (NSArray *)obtainEventsForType:(EventType)eventType;

- (void)updateEventsForType:(EventType)eventType
success:(SuccessBlock)success
failure:(ErrorBlock)failure;
@end


Сервис будет работать с CoreData, поэтому для тестирования нам нужно будет проинициализоровать стэк CoreData в памяти. Используем для работы с CoreData библиотеку MagicalRecord, не забываем вызывать [MagicalRecord cleanUp] в teardown. В наш сервис инжектируются сетевой клиент и маппер, поэтому передаем в инициализатор их моки.



    @interface EventServiceTests : XCTestCase

@property (nonatomic, strong) EventService *eventService;
@property (nonatomic, strong) id clientMock;
@property (nonatomic, strong) id mapperMock;

@end

@implementation EventServiceTests

- (void)setUp {
[super setUp];

[MagicalRecord setDefaultModelFromClass:[self class]];
[MagicalRecord setupCoreDataStackWithInMemoryStore];

self.clientMock = OCMProtocolMock(@protocol(Client));
self.mapperMock = OCMProtocolMock(@protocol(Mapper));

self.eventService = [[EventService alloc] initWithClient:self.clientMock
mapper:self.mapperMock];
}

- (void)tearDown {
self.eventService = nil;
self.clientMock = nil;
self.mapperMock = nil;
[MagicalRecord cleanUp];

[super tearDown];
}


Добавляем тест для метода obtain — синхронное получение объектов из базы данных:



    - (void)testObtainEventsForType {
// given
Event *event = [Event MR_createEntity];
event.eventType = EventTypeCinema;

// when
NSArray *events = [self.eventService obtainEventsForType:EventTypeCinema];

// then
XCTAssertEqualObjects(event, [events firstObject]);
}


Реализация метода obtain и инициализатора сервиса:



    - (instancetype)initWithClient:(id)client
mapper:(id)mapper {
self = [super init];
if (self) {
_client = client;
_mapper = mapper;
}
return self;
}

- (NSArray *)obtainEventsForType:(EventType)eventType {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"eventType = %d", eventType];
NSArray *events = [Event MR_findAllWithPredicate:predicate];

return events;
}


Добавляем тест для метода update – запрашивает у сервера события, используя клиент, затем маппит в модель и сохраняет в базу. Стабаем клиент, чтобы он всегда возвращал success блок. Проверяем, что наш сервис обратится к мапперу и вызовет success блок:



    - (void)testUpdateEventsForTypeSuccessful {
// given
XCTestExpectation *expectation = [self expectationWithDescription:@"Callback"];
OCMStub([self.clientMock requestEventsForType:EventTypeCinema
success:OCMOCK_ANY
failure:OCMOCK_ANY).andDo(^(NSInvocation *invocation) {
SuccessBlock block;
[invocation getArgument:&block atIndex:3];
block();
});

// when
[self.eventService updateEventsForType:EventTypeCinema
success:^{
[expectation fulfill];

} failure:^(NSError *error) {

}];

// then
[self waitForExpectationsWithTimeout:DefaultTestExpectationTimeout handler:nil];
OCMVerify([self.mapperMock mapEvents:OCMOCK_ANY
forType:EventTypeCinema
mappingContext:OCMOCK_ANY]);
}


Пишем тест на сценарий ошибки метода update, стабаем клиент так, чтобы он возвращал ошибку:



    - (void)testUpdateEventsForTypeFailure {
// given
XCTestExpectation *expectation = [self expectationWithDescription:@"Callback"];
NSError *clientError = [NSError errorWithDomain:@""
code:1
userInfo:nil];

OCMStub([self.clientMock requestEventsForType:EventTypeCinema
success:OCMOCK_ANY
failure:OCMOCK_ANY).andDo(^(NSInvocation *invocation) {
ErrorBlock block;
[invocation getArgument:&block atIndex:4];
block(error);
});

// when
[self.eventService updateEventsForType:EventTypeCinema
success:^{

} failure:^(NSError *error) {
XCTAssertEqual(clientError, error)
[expectation fulfill];
}];

// then
[self waitForExpectationsWithTimeout:DefaultTestExpectationTimeout handler:nil];
}


Реализация метода update:



    - (void)updateEventsForType:(EventType)eventType
success:(SuccessBlock)success
failure:(ErrorBlock)failure {
@weakify(self);
[self.client requestEventsForType:eventType
success:^(id responseObject) {
@strongify(self);

NSManagedObjectContext *context = [NSManagedObjectContext MR_rootSavingContext];
NSArray *events = [self.mapper mapEvents:responseObject
forType:eventType
mappingContext:context];
[context MR_saveToPersistentStoreAndWait];

success();

} failure:^(NSError *error) {
failure(error);
}];
}


Преимущества и недостатки



Преимущества



  • Продумывание интерфейса до реализации

    Одно из основных преимуществ, TDD вынуждает программиста в первую очередь детально продумать работу новой функциональности, а потом уже написать ее.




  • Меньше отладки

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




  • Уверенность при изменении

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




  • Меньше ошибок

    Достигается за счет совокупности других преимуществ TDD, в первую очередь, за счет продумывания реализации заранее и покрытия тестами.




  • Документация тестами

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




  • Модульность

    Необходимость тестировать абсолютно все ветви выполнения кода, все if-ы вынуждает нас писать модульно, ибо мы просто не сможем протестировать всемогущий метод, который делает сразу 10 дел в нескольких потоках. Мы стремимся к тому, что каждый класс, каждый метод делает свою небольшую часть общей работы. (Следуя принципу single responsibility).




  • Полноценные тесты

    Полагаю, что многие писали тесты, но не все писали их по TDD и разница на самом деле ощутима. В случае написания тестов после реализации мы очень вероятно можем не учесть все сценарии выполнения метода, и в итоге получить неполноценные тесты. Мы считаем, что наш метод работает, потому что тест работает, но на самом деле в методе не покрыты тестами все if-ы, а при TDD такое невозможно, потому что if не может появиться в коде без теста.





Недостатки



  • Сложность применения (безопасность данных, UI, базы данных)

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




  • Больше времени

    Когда начинаешь писать по TDD – время разработки может увеличиться до нескольких раз. Но, буквально через несколько месяцев будет уже сложно писать без него. В перспективе при наличии опыта написания тестов и применения TDD, время на разработку будет уменьшаться. Особенно это будет ощущаться на дистанции, на долгоживущих проектах.




  • Ложное ощущение надежности, ошибка в тесте

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




  • Поддержка тестов

    Кодовая база при полном покрытии кода тестами увеличивает практически в два раза. И важно понимать, что весь этот код нужно поддерживать, рефакторить и документировать.









Разработка через тестирование поощряет простой дизайн и внушает уверенность.

TDD encourages simple designs and inspires confidence.



Кент Бек



Литература и ссылки



Kent Beck – Test-Driven Development: By example

Robert C. Martin – Clean code

www.objc.io/issue-15

en.wikipedia.org/wiki/Test-driven_development

qualitycoding.org/objective-c-tdd

agiledata.org/essays/tdd.html#WhatIsTDD

www.basilv.com/psd/blog/2009/test-driven-development-benefits-limitations-and-techniques

martinfowler.com/articles/is-tdd-dead

habrahabr.ru/post/206828

habrahabr.ru/post/216923

iosunittesting.com

Original source: habrahabr.ru (comments, light).

http://habrahabr.ru/post/263087/

Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество
avtobakero

Рамблер построит 'Uber для недвижимости'

Вторник, 28 Июля 2015 г. 15:40 (ссылка)
lenta.ru/news/2015/07/28/ra...rproperty/


Проект «Рамблер-Недвижимость» приобрел биржу недвижимости «АгентОН», которая вкупе с собственными технологиями позволит организовать онлайн-площадку для услуг по сдаче-съему и покупке-продаже жилья в режиме онлайн. Данные об объектах будут верифицированы и дополнены услугами необходимых специалистов.



 



ГЕНПРОКУРАТУРУ ПОПРОСИЛИ ПРОВЕРИТЬ ЯНДЕКС ТАКСИ,UBER И GETT TAXI

Комментарии (0)КомментироватьВ цитатник или сообщество
Ла-почка

«Афиша-Рестораны» запустила сервис бронирования столиков SmartReserve

Среда, 08 Июля 2015 г. 04:59 (ссылка)
ferra.ru/ru/techlife/news/2...rtReserve/



 «Афиша-Рестораны» запустила сервис бронирования столиков SmartReserveРазработчики проекта «Афиша-Рестораны», входящего в группу компаний Rambler&Co, объявили о запуске продукта SmartReserve (системы резервирования столов), который позволяет автоматизировать рабочее место хостес, привлечь брони из Интернета и увеличить оборачиваемость столов в ресторанах.

Читать далее...
Метки:   Комментарии (0)КомментироватьВ цитатник или сообщество

Следующие 30  »

<rambler - Самое интересное в блогах

Страницы: [1] 2 3 ..
.. 10

LiveInternet.Ru Ссылки: на главную|почта|знакомства|одноклассники|фото|открытки|тесты|чат
О проекте: помощь|контакты|разместить рекламу|версия для pda