Patrón de Architeture / Design para integración con services analíticos (rash / google analytics)

Estoy desarrollando una aplicación para iOS y estoy planeando usar un service de análisis en ella, como Flurry o Google Analytics. La cosa es: ¿qué sería un buen layout de software (acoplado suelto, altamente cohesivo, fácil de mantener) para usar estos services?

Este es un proyecto personal mío y lo estoy utilizando para estudiar nuevas tecnologías y mejores prácticas. Me tropecé con este "desafío" y realmente no puedo encontrar una mejor solución.

Ya desarrollé algunas aplicaciones mobilees que utilizan este tipo de service y, por lo general, implemento un set de patrones de layout Adapter + Factory:

  • Se crea una interfaz básica que representa un service analítico genérico.

    public interface IAnalytics { void LogEvent(string name, object param); } 
  • Cada API de Servicio (Flurry / Google Analytics / etc) está encapsulada mediante el uso de un adaptador que implementa esta interfaz

     public class FlurryService : IAnalyticsService { public LogEvent(sring name, object param) { Flurry.Log(name, param.ToString()); } } 
  • Se implementa una fábrica para que tengamos el service analítico que necesitemos en esa aplicación en particular.

     public static class AnalyticsServiceFactory { public IAnalytics CreateService(string servicename) { if(servicename == "google") { return new GoogleAnalyticsService(); } else { return new FlurryService(); } } } 
  • Por último, se crea un object "proxy" (no por el libro) para registrar events específicos de la aplicación

     public class MyAnalytics { private static IAnalyticsService _Service = AnalyticsServiceFactory.CreateService("flurry"); public static void LogUserLoggedIn(string user) { _Service.LogEvent("login", user); } public static void LogLoginFailed(string user) { _Service.LogEvent("login", user); } } 

Se trata de encapsular cada API de service y funciona muy bien, especialmente en aplicaciones que comparten código entre diferentes plataforms.

Sin embargo, queda un problema que es el logging de events (o acciones realizadas por el usuario) en sí. En todos los casos en los que he trabajado, el logging de events está codificado donde ocurra. Por ejemplo:

 public void LogIn(string userName, string pass) { bool success = this.Controller.LogIn(userName, pass); if(success) { MyAnalytics.LogUserLoggedIn(username); // Change view } else { MyAnalytics.LogLogInFailed(username); // Alert } } 

Esto parece más acoplado de lo que me gustaría que fuera, así que estoy buscando una solución mejor.

Como estoy trabajando con iOS, pensé en usar NSNotificationCenter : Cada vez que ocurre un evento, en lugar de registrarlo de inmediato, publíco una notificación en NSNotificationCenter y otro object que observa estas notifications se encarga de llamar a MyAnalytics para registrar el evento. Este layout, sin embargo, solo funciona con iOS (sin una cantidad de trabajo no trivial, es decir).

Otra forma de ver este problema es: ¿Cómo es que los juegos rastrean sus acciones para alcanzar un Xbox Achievement / Playstation Trophy?

Aquí hay un layout que normalmente utilizo para una class de logging que puede ser de varios proveedores. Permite intercambios fáciles entre analíticos, informes de fallos y proveedores beta OTA y utiliza directivas de preprocesador para que ciertos services estén activos en ciertos entornos. Este ejemplo usa CocoaLumberjack pero podría hacerlo funcionar con su marco de logging preferido.

 @class CLLocation; @interface TGLogger : NSObject + (void)startLogging; + (void)stopLogging; + (NSString *)getUdidKey; + (void)setUserID:(NSString *)userID; + (void)setUsername:(NSString *)username; + (void)setUserEmail:(NSString *)email; + (void)setUserAge:(NSUInteger)age; + (void)setUserGender:(NSString *)gender; + (void)setUserLocation:(CLLocation *)location; + (void)setUserValue:(NSString *)value forKey:(NSString *)key; + (void)setIntValue:(int)value forKey:(NSString *)key; + (void)setFloatValue:(float)value forKey:(NSString *)key; + (void)setBoolValue:(BOOL)value forKey:(NSString *)key; extern void TGReportMilestone(NSString *milestone, NSDictionary *parameters); extern void TGReportBeginTimedMilestone(NSString *milestone, NSDictionary *parameters); extern void TGReportEndTimedMilestone(NSString *milestone, NSDictionary *parameters); @end 

Ahora el file de implementación:

 #import "TGLogger.h" #import "TGAppDelegate.h" #import <CocoaLumberjack/DDASLLogger.h> #import <CocoaLumberjack/DDTTYLogger.h> #import <AFNetworkActivityLogger/AFNetworkActivityLogger.h> @import CoreLocation; #ifdef USE_CRASHLYTICS #import <Crashlytics/Crashlytics.h> #import <CrashlyticsLumberjack/CrashlyticsLogger.h> #endif #ifdef USE_FLURRY #import <FlurrySDK/Flurry.h> #endif #import <Flurry.h> @implementation TGLogger + (void)startLogging { [DDLog addLogger:[DDASLLogger shanetworkingInstance]]; [DDLog addLogger:[DDTTYLogger shanetworkingInstance]]; [[DDTTYLogger shanetworkingInstance] setColorsEnabled:YES]; [[DDTTYLogger shanetworkingInstance] setForegroundColor:[UIColor blueColor] backgroundColor:nil forFlag:LOG_FLAG_INFO]; [[DDTTYLogger shanetworkingInstance] setForegroundColor:[UIColor orangeColor] backgroundColor:nil forFlag:LOG_FLAG_WARN]; [[DDTTYLogger shanetworkingInstance] setForegroundColor:[UIColor networkingColor] backgroundColor:nil forFlag:LOG_FLAG_ERROR]; [[AFNetworkActivityLogger shanetworkingLogger] startLogging]; #ifdef DEBUG [[AFNetworkActivityLogger shanetworkingLogger] setLevel:AFLoggerLevelInfo]; #else [[AFNetworkActivityLogger shanetworkingLogger] setLevel:AFLoggerLevelWarn]; #endif #if defined(USE_CRASHLYTICS) || defined(USE_FLURRY) NSString *udid = [TGLogger getUdidKey]; TGLogInfo(@"Current UDID is: %@", udid); #endif #ifdef USE_CRASHLYTICS // Start Crashlytics [Crashlytics startWithAPIKey:TGCrashlyticsKey]; [Crashlytics setUserIdentifier:udid]; [DDLog addLogger:[CrashlyticsLogger shanetworkingInstance]]; TGLogInfo(@"Crashlytics started with API Key: %@", TGCrashlyticsKey); #endif #ifdef USE_FLURRY [Flurry setAppVersion:[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]]; [Flurry setSecureTransportEnabled:YES]; [Flurry setShowErrorInLogEnabled:YES]; [Flurry setLogLevel:FlurryLogLevelCriticalOnly]; [Flurry startSession:TGFlurryApiKey]; TGLogInfo(@"Flurry started with API Key %@ and for version %@", TGFlurryApiKey, [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]); TGLogInfo(@"Flurry Agent Version %@", [Flurry getFlurryAgentVersion]); #endif TGLogInfo(@"Logging services started"); } + (void)stopLogging { TGLogInfo(@"Shutting down logging services"); [DDLog removeAllLoggers]; } + (NSString *)getUdidKey { return [[UIDevice currentDevice] identifierForVendor].UUIDString; } + (void)setUserID:(NSString *)userID { #ifdef USE_CRASHLYTICS [Crashlytics setUserIdentifier:userID]; #endif } + (void)setUsername:(NSString *)username { #ifdef USE_CRASHLYTICS [Crashlytics setUserName:username]; #endif #ifdef USE_FLURRY [Flurry setUserID:username]; #endif } + (void)setUserEmail:(NSString *)email { #ifdef USE_CRASHLYTICS [Crashlytics setUserEmail:email]; #endif } + (void)setUserAge:(NSUInteger)age { #ifdef USE_FLURRY [Flurry setAge:(int)age]; #endif } + (void)setUserGender:(NSString *)gender { #ifdef USE_FLURRY [Flurry setGender:gender]; #endif } + (void)setUserLocation:(CLLocation *)location { #ifdef USE_FLURRY [Flurry setLatitude:location.coordinate.latitude longitude:location.coordinate.longitude horizontalAccuracy:location.horizontalAccuracy verticalAccuracy:location.verticalAccuracy]; #endif #ifdef USE_CRASHLYTICS [Crashlytics setObjectValue:location forKey:@"location"]; #endif } + (void)setUserValue:(NSString *)value forKey:(NSString *)key { #ifdef USE_CRASHLYTICS [Crashlytics setObjectValue:value forKey:key]; #endif } #pragma mark - Report key/values with crash logs + (void)setIntValue:(int)value forKey:(NSString *)key { #ifdef USE_CRASHLYTICS [Crashlytics setIntValue:value forKey:key]; #endif } + (void)setBoolValue:(BOOL)value forKey:(NSString *)key { #ifdef USE_CRASHLYTICS [Crashlytics setBoolValue:value forKey:key]; #endif } + (void)setFloatValue:(float)value forKey:(NSString *)key { #ifdef USE_CRASHLYTICS [Crashlytics setFloatValue:value forKey:key]; #endif } void TGReportMilestone(NSString *milestone, NSDictionary *parameters) { NSCParameterAssert(milestone); TGLogCInfo(@"Reporting %@", milestone); #ifdef USE_FLURRY [Flurry logEvent:milestone withParameters:parameters]; #endif } void TGReportBeginTimedMilestone(NSString *milestone, NSDictionary *parameters) { NSCParameterAssert(milestone); TGLogCInfo(@"Starting timed event %@", milestone); #ifdef USE_FLURRY [Flurry logEvent:milestone withParameters:parameters timed:YES]; #endif } void TGReportEndTimedMilestone(NSString *milestone, NSDictionary *parameters) { NSCParameterAssert(milestone); TGLogCInfo(@"Ending timed event %@", milestone); #ifdef USE_FLURRY [Flurry endTimedEvent:milestone withParameters:parameters]; #endif } @end