Mobile Client Server-Anwendungsarchitektur


    К добавлению внешнего сервера рано или поздно приходит любой сложный проект. Причины, при этом, бывают совершенно различные. Одни, загружают дополнительные сведения из сети, другие, синхронизируют данные между клиентскими устройствами, третьи- переносят логику выполнения приложения на сторону сервера. Как правило, к последним относятся большинство «деловых» приложений. По мере отхода от парадигмы «песочницы», в которой все действия выполняются только в рамках исходной системы, логика выполнения процессов переплетается, сплетается, завязывается узлами настолько, что становится трудно понять, что является исходной точкой входа в процесс приложения. В этом момент, на первое место выходит уже не функциональные свойства самого приложения, а его архитектура, и, как следствие, возможности к масштабированию.
    Заложенный фундамент позволяет либо создать величественный архитектурный ансамбль, либо «накурнож» — избушку на куриных ножках, которая рассыпается от одного толчка «доброго молодца» коих, за время своего существования повидала видимо — невидимо, потому что, глядя на множественные строительные дефекты заказчик склонен менять не исходный проект, а команду строителей.
    Планирование — ключ к успеху проекта, но, именно на него выделяется заказчиком минимальный объем времени. Строительные паттерны — туз в рукаве разработчика, который покрывает неблагоприятные комбинации где время — оказывается решающим фактором. Взятые за основу работающие решения позволяют сделать быстрый старт, чтоб перейти к задачам, кажущиеся заказчику наиболее актуальными (как-то покраска дымоходной трубы, на еще не возведенной крыше).
    In diesem Artikel werde ich versuchen, das Prinzip des Aufbaus eines skalierbaren Systems für mobile Geräte darzulegen, das 90-95% der Client-Server-Anwendungen abdeckt und den maximalen Abstand von der sakramentalen "Messerschneide" bietet.


    Während ich an der Fertigstellung dieses Artikels arbeitete, wurde ein ähnlicher Artikel im Hub veröffentlicht ( http://habrahabr.ru/company/redmadrobot/blog/246551/ ). Ich bin nicht mit allen Akzenten des Autors einverstanden, aber im Allgemeinen widerspricht meine Vision nicht und überschneidet sich nicht mit dem dort präsentierten Material. Der Leser kann bestimmen, welcher der Ansätze flexibler und relevanter ist.



    Die allgemeine Struktur der Client-Server-Interaktion auf der Serverseite wird hier dargestellt: www.4stud.info/networking/lecture5.htmlWir sind jedoch mehr an der gleichen Sichtweise auf Kundenseite interessiert, und in dieser Hinsicht gibt es keinen Unterschied zwischen einer zweigliedrigen und einer nüchternen Architektur: Hier
    ist es wichtig, zwei Dinge zu verstehen:
    1. Möglicherweise verwenden viele Kunden ein Konto, um mit dem Norden zu kommunizieren.
    2. Jeder Client verfügt in der Regel über einen eigenen lokalen Speicher. *


    * In einigen Fällen kann der lokale Speicher mit der Cloud und entsprechend mit jedem Client synchronisiert werden. Da dies ein Sonderfall ist und die Architektur der Anwendung größtenteils nicht beeinträchtigt, lassen wir ihn weg.

    Da einige Entwickler versuchen, die "Serverseite" loszuwerden, basieren einige Anwendungen auf der Synchronisierung ihres Speichers in der "Cloud". Das heißt, sie haben in der Tat auch ein Zwei-Link-System, aber mit der Übertragung der Architektur seiner Bereitstellung auf die Ebene des Betriebssystems. In einigen Fällen ist eine solche Struktur gerechtfertigt, aber ein solches System lässt sich nicht so leicht skalieren, und seine Fähigkeiten sind sehr begrenzt.



    Allgemeine Anwendungsstruktur

    Auf der primitivsten Abstraktionsebene besteht eine serverorientierte Anwendung aus den folgenden Architekturebenen:
    1. Der Kern der Anwendung, die Systemkomponenten enthält, die für die Benutzerinteraktion nicht verfügbar sind.
    2. Grafische Benutzeroberfläche
    3. Komponenten wiederverwenden: Bibliotheken, visuelle Komponenten und mehr.
    4. Umgebungsdateien: AppDelegate, .plist usw.
    5. Anwendungsressourcen: Grafikdateien, Sounds, erforderliche Binärdateien.

    Die wichtigste Voraussetzung für den Aufbau einer stressresistenten Architektur ist die Trennung des Systemkerns von der GUI, sodass einer ohne den anderen erfolgreich funktionieren kann. In der Zwischenzeit gehen die meisten RAD-Systeme von der entgegengesetzten Botschaft aus - Formen bilden das Gerüst des Systems und Funktionen bauen Muskeln auf. Dies bedeutet in der Regel, dass die Anwendung nicht durch ihre Oberfläche eingeschränkt wird. Und die Schnittstelle nimmt sowohl aus Sicht des Benutzers als auch aus Sicht der Klassenhierarchie eine eindeutige Interpretation an.



    Kernel Der

    Kern der Anwendung besteht aus folgenden Schichten:
    1. (Startebene) Die Startebene, die den Workflow für den Start der Programmausführung definiert.
    2. (Netzwerkschicht) Eine Netzwerkschicht, die einen Mechanismus für die Transportinteraktion bereitstellt.
    3. (API-Schicht) Eine API-Schicht, die ein einheitliches Befehlssystem für die Interaktion zwischen einem Client und einem Server bereitstellt.
    4. (Netzwerk-Cache-Schicht) Eine Netzwerk-Cache-Schicht, die die Netzwerkinteraktion zwischen Client und Server beschleunigt.
    5. (Validation Items Layer) Netzwerkvalidierungsschicht
    6. (Network Items Layer) Schicht der über das Netzwerk übertragenen Dateneinheit
    7. (Datenmodell) Ein Datenmodell, das die Interaktion von Datenelementen ermöglicht.
    8. (Lokale Cacheschicht) Eine lokale Cacheschicht, die den lokalen Zugriff auf bereits empfangene Netzwerkressourcen ermöglicht.
    9. (Workflow-Schicht) Eine Workflow-Schicht, die für eine bestimmte Anwendung spezifische Klassen und Algorithmen enthält.
    10. (Lokaler Speicher) Lokaler Speicher

    Eine der Hauptaufgaben für die Entwickler des Systems besteht darin, das voneinander unabhängige Funktionieren dieser Schichten sicherzustellen. Jede Schicht sollte nur die Leistung der ihr zugewiesenen Funktionen gewährleisten. In der Regel sollte eine Ebene, die sich auf einer höheren Ebene der Hierarchie befindet, keine Vorstellung von den Besonderheiten der Implementierung anderer Ebenen haben.

    Betrachten Sie den Lösungsprozess aus der Sicht von Junior- und Senior-Entwicklern.
    Ziel: Schreiben Sie ein Programm "Währungsrechner", das Daten aus dem Netzwerk empfängt, und erstellen Sie eine Grafik der Wechselkurse.
    Junior:
    1) Aufgrund der Aussage des Problems wissen wir, dass die Bewerbung aus den folgenden Teilen besteht:
    1. Formular für mathematische Operationen (Addition, Subtraktion)
    2. Graph Anzeigeform
    3. Zusätzliche Formulare: Begrüßungsbildschirm, ungefähr.

    2) Wir machen die Abhängigkeit von Formen wie folgt: Die Berechnungsform ist die Hauptform in unserer Anwendung. Es wird eine Begrüßungsform gestartet, die nach einer bestimmten Zeitspanne ausgeblendet wird, die Form von Graphen und etwa durch Anklicken einer bestimmten Schaltfläche.
    3) Anzeigedauer des Begrüßungsbildschirms - entspricht der Zeit, die zum Laden von Daten aus dem Netzwerk benötigt wurde.
    4) Da das Herunterladen aus dem Netzwerk nur während der Anzeige des Begrüßungsformulars erfolgt, wird der Datenlade-Code in das Formular eingefügt und beim Ausfüllen des Formulars zusammen mit dem Formular aus dem Speicher gelöscht.

    Wie funktional ist diese Anwendung? Ich denke, dass niemand Zweifel daran hat, dass Sie dieses Problem im Moment mit Delphi oder Visual Studio lösen können. Die Verwendung von Xcode macht dies etwas schwieriger, aber Sie können es auch ohne große Anstrengung tun. Mit dem Aufkommen des Prototyps treten jedoch allmählich Skalierbarkeitsprobleme auf. Es wird deutlich, dass Sie zum Anzeigen des Diagramms Daten für den vorherigen Zeitraum speichern müssen. Kein Problem, Sie können ein Data Warehouse in das Diagrammformular einfügen. Daten können jedoch von unterschiedlichen Anbietern und in unterschiedlichen Formaten stammen. Darüber hinaus können arithmetische Operationen mit verschiedenen Währungen ausgeführt werden, was bedeutet, dass es notwendig ist, ihre Auswahl sicherzustellen. Eine solche Auswahl in Bezug auf die Form von Diagrammen zu treffen, ist etwas unlogisch, obwohl es möglich ist, was wir in der Grafik anzeigen, hängt von diesen Einstellungen ab. Das heisst, Wenn wir zusätzliche Parameter in das Einstellungsfenster einfügen, müssen wir sie irgendwie über das Hauptformular in das Grafikfenster übertragen. In diesem Fall wäre es logisch, eine lokale Variable zu erstellen, in der die übergebenen Parameter gespeichert werden und über das Hauptformular von einem Formular auf ein anderes Formular zugegriffen werden kann. Na und so weiter. Die Argumentationskette kann sehr lange aufgebaut werden, und die Komplexität der Interaktionen wird zunehmen.

    Senior: Die
    Erklärung des Problems ermöglicht die Unterscheidung mehrerer Teilaufgaben, die durch separate Klassen beschrieben werden können:
    1) Herunterladen von Daten aus dem Netzwerk.
    1. Überprüfung der empfangenen Daten
    2. Speichern von Daten in einem dauerhaften Speicher.
    3. Datenberechnung.
    4. Zusatzoperation
    5. Subtraktionsoperation
    6. Filtern von Daten nach festgelegten Kriterien (Anwendungseinstellungen)
    7. Anwendungsstartklasse.

    2) Stellen Sie den zugehörigen Betrieb der Schnittstelle sicher, die aus den folgenden Hauptformularen besteht:
    1. Hauptregler (möglicherweise unsichtbar)
    2. Berechnungsformular
    3. Diagrammform
    4. Splash and About
    5. Formular für optionale Einstellungen.

    3) Nachdem die Anwendung zur Ausführung gestartet wurde, wird die Erstellung (Instanz) des Objekts, das für das Laden der Daten verantwortlich ist (in den allermeisten Fällen asynchron), ausgeführt und der Prozess beginnt. Der Hauptcontroller der Anwendung zeigt einen Begrüßungsbildschirm an und bildet zu diesem Zeitpunkt ein Formular, das seinen Platz beim Ausblenden des Begrüßungsformulars einnimmt.
    4) Am Ende des Datenladens werden ein Validatorobjekt und ein lokales Speicheranbieterobjekt erstellt. Wenn die Daten die erforderliche Validierung bestanden haben, können sie an den lokalen Speicheranbieter übertragen werden.
    5) Um das Diagramm anzuzeigen, werden ein lokales Speicherobjekt und ein Dateneinstellungsobjekt erstellt. Dateneinstellungen werden an den lokalen Speicheranbieter übertragen, um Daten mit installierten Filtern abzurufen.
    6) Zur Durchführung der Berechnungen werden ein Taschenrechnerobjekt und Operationsobjekte angelegt. Die vom Formular empfangenen Daten werden an das Rechnerobjekt und eines der beiden Operationsobjekte übertragen, die genau wissen, wie die Berechnungen ausgeführt werden.

    Natürlich erfordert dieser Ansatz mehr Programmieraufwand und dementsprechend nimmt er zunächst mehr Zeit in Anspruch. Anhand der Unteraufgaben wird jedoch deutlich, dass die Parallelisierung zunächst einfach ist - während ein Entwickler gerade einen Kernel erstellt -, erstellt und debuggt der andere die Benutzeroberfläche. Der Kernel kann sicher in der Konsole arbeiten, die Benutzeroberfläche wird im Gerät angeklickt und unter anderem können unabhängige Komponententests an beide Teile geschraubt werden. Ein weiterer Vorteil ist zweifellos, dass der zweite Ansatz wesentlich skalierbarer ist. Bei einer Überarbeitung der Funktionalität des Projekts werden Änderungen um ein Vielfaches schneller vorgenommen, da es einfach keinen einschränkenden Rahmen für die visuelle Darstellung gibt. In den visuellen Formularen selbst (GUIs) wird das erforderliche Minimum basierend auf den im Kernel vorhandenen Aufgaben angezeigt.



    Startschicht:
    In iOS startet eine Anwendung mit dem Starten eines Delegatenklassenobjekts. Sie dient zum Empfangen und Übertragen von Systemaufrufen an die Anwendung sowie zur Erstkonfiguration der Anwendungs-GUI. Alle Algorithmen und Mechanismen, die nicht mit dem Start der Anwendung oder dem Empfang von Nachrichten vom System zusammenhängen, müssen in separate Klassen eingeteilt werden. Unmittelbar nach Abschluss der Erstkonfiguration sollte die Steuerung an die Klasse übertragen werden, die die restlichen Anwendungskonfigurationsvorgänge ausführt: Autorisierung, Neukonfiguration der Schnittstelle in Abhängigkeit von den Bedingungen, Laden der Erstdaten, Erhalt der erforderlichen Token usw. Ein typischer Fehler, den Entwickler begehen, ist der monströse Spaghetti-Code, der in AppDelegate gehostet wird. Es ist verständlich: Fast alle Beispiele für externe Frameworks haben einen eigenen Code, um das Verständnis zu erleichtern. Unglückliche Programmierer verschwenden keine Zeit mit Umgestaltungen und kopieren einfach "was auch immer". Die Situation ist recht typisch für diejenigen, die die integrierte Vorlage zum Erstellen von CoreData verwenden.
    Oft sieht man dort die Implementierung folgender Funktionen:
    1. Einrichten und Verwalten von Facebook-Sitzungen
    2. Tab-Manager einrichten, wenn die Anwendung UITabbarController unterstützt.
    3. CoreData löschen und Daten speichern, wenn Hintergrund eingegeben wird.
    4. Aktualisierungen prüfen und initialisieren
    5. Benachrichtigung externer Statistikserver
    6. Datenmodellsynchronisation
    Eine viel elegantere Lösung wäre, die Startton-Singleton-Klasse zu erstellen, die in AppDelegate eingehenden Daten zu übertragen und die Datenverdünnung nach Work-Prozessen zu starten: für CoreData - eine Flow-Klasse, für soziale Netzwerke - eine andere.

    Netzwerkschicht:
    Stellt die grundlegenden Algorithmen für die Transportschicht zum Senden von Nachrichten vom Client an den Server und zum Abrufen der erforderlichen Informationen von diesem bereit. In der Regel können Nachrichten in den Formaten JSON und Multipart übertragen werden, obwohl es sich in einigen exotischen Fällen im Allgemeinen um XML oder einen Binärdatenstrom handeln kann. Zusätzlich kann jede Nachricht einen Header mit Overhead-Informationen haben. Dort kann beispielsweise die Dauer des Request / Response-Speichers im Application-Cache beschrieben werden.
    Network Layer hat keine Ahnung, welche Server von der Anwendung verwendet werden oder über welches Befehlssystem. Die Behandlung von Netzwerkverbindungsfehlern erfolgt durch virtuelle Methoden auf den folgenden Anwendungsebenen. Die Aufgabe dieser Schicht besteht lediglich darin, die Verarbeitungsmethode aufzurufen und vom Netzwerk empfangene Informationen an diese zu übertragen.
    Bevor Informationen direkt vom Netzwerk angefordert werden, fragt die Netzwerkschicht den lokalen Cache ab. Wenn dort eine Antwort eingeht, wird diese sofort an den Benutzer zurückgegeben.
    Der Inhalt dieser Ebene hängt maßgeblich davon ab, welche Transporttechnologie Ihnen am nächsten liegt. Im Arsenal des Entwicklers sind die folgenden Optionen am gefragtesten:
    • Socket ist der einfachste Ansatz, der synchrone und asynchrone Anforderungen umfasst und mit TCP- und UDP-Verbindungen arbeiten kann. Es ermöglicht Ihnen fast alles, erfordert jedoch ein hohes Maß an Konzentration auf die Aufgabe, nicht zu viel Ausdauer und eine große Menge an Code.
    • WebSocket ist ein Ansatz, der auf der Verwendung von Headern über TCP basiert. Details können hier nachgelesen werden: habrahabr.ru/post/79038 Mobile Entwicklung wird nicht oft verwendet, da es nicht flexibel genug ist und immer noch eine ziemlich große Menge Code benötigt, um es zu unterstützen.
    • WCF ist wahrscheinlich der fortschrittlichste Mechanismus, aber mit einem so gravierenden Minus, dass alle Vorteile überwiegen. Der von Microsoft entwickelte Ansatz basiert auf der Erstellung einer Proxy-Klasse, die die Beziehung zwischen der Anwendungslogik und dem entfernten Norden vermittelt. Es funktioniert "mit einem Paukenschlag", wenn es möglich ist, eine Proxy-Klasse basierend auf WSDL-Schemata ( en.wikipedia.org/wiki/Web_Services_Description_Language ) zu generieren , was, gelinde gesagt, nicht trivial ist. Darüber hinaus muss diese Klasse nach jeder Aktualisierung der Server-API neu generiert werden. Und wenn dies für Visual Studio-Entwickler mit der Leichtigkeit von Zephyr erledigt wird, ist dies für iOS-Entwickler eine absolut unmögliche Aufgabe, selbst für diejenigen, die MonoTouch in der Entwicklung verwenden.
    • REST ist ein zuverlässiger, bewährter Kompromiss aller oben genannten Ansätze ( en.wikipedia.org/wiki/REST ). Natürlich muss ein Teil der Fähigkeiten jedes Ansatzes aufgegeben werden, aber dies geschieht schnell und äußerst effizient mit einem Minimum an Aufwand.


    GitHub enthält viele Bibliotheken, mit denen Sie REST-Verbindungen verwenden können. AFNetworking ist für iOS die beliebteste.

    REST basiert auf der Verwendung von GET-, POST-, PUT-, HEAD-, PATCH- und DELETE-Anforderungen. Ein solcher Zoo heißt RESTFul ( habrahabr.ru/post/144011 ) und wird in der Regel nur verwendet, wenn eine universelle API für die Arbeit mit mobilen Anwendungen, Websites, Desktops und Raumstationen in einem Bundle geschrieben wurde.
    Die überwiegende Mehrheit der Anwendungen beschränkt das Befehlssystem auf zwei Typen, GET und POST, obwohl nur einer ausreicht - POST.
    Die GET-Anforderung wird als Zeichenfolge gesendet, die Sie im Browser verwenden, und die Parameter für die Anforderung werden getrennt durch die Zeichen "&" übergeben. Die POST-Anforderung verwendet auch die „Browserzeichenfolge“, die Parameter sind jedoch im unsichtbaren Nachrichtentext verborgen. Die letzten beiden Aussagen tauchen in Verzweiflung bei denen auf, die noch nie zuvor auf Anfragen gestoßen sind. In Wirklichkeit wurde die Technologie so weit ausgearbeitet, dass sie für den Entwickler völlig transparent ist und Sie sich nicht mit solchen Nuancen befassen müssen.
    Oben wurde beschrieben, was an den Server gesendet wird. Aber was vom Server kommt, ist viel interessanter. Wenn Sie AFNetworking verwenden, erhalten Sie auf der Serverseite Folgendes: In der Regel nennen iOS-Entwickler das JSON-basierte serialisierte Wörterbuch, dies ist jedoch nicht ganz richtig. True JSON hat ein etwas komplexeres Format, aber in seiner reinen Form ist es fast nie notwendig, es zu verwenden. Es gibt jedoch einen Unterschied, den Sie kennen müssen - es gibt Nuancen.
    Wenn Sie mit einem auf Microsoft Windows Server installierten Dienst arbeiten, wird dort höchstwahrscheinlich WCF verwendet. Ab Windows Framework 4 können Clients, die nur das REST-Protokoll unterstützen, den Zugriff vollständig transparent und deklarativ gestalten. Sie müssen nicht einmal Zeit damit verschwenden, Erklärungen zur API zu erhalten - die Dokumentation zum Befehlssystem wird automatisch von IIS (Microsoft-Webserver) generiert.

    Der folgende Code muss mindestens zur Implementierung von Network Layer mithilfe von AFNetworking 2 unter Objective-C verwendet werden.
    Listing 1
    ClientBase.h

    #import "AFHTTPRequestOperationManager.h"
    NS_ENUM(NSInteger, REQUEST_METHOD)
    {
        GET,
        HEAD,
        POST,
        PUT,
        PATCH,
        DELETE
    };
    @interface ClientBase : AFHTTPRequestOperationManager
    @property (nonatomic, strong) NSString *shortEndpoint;
    - (void)request:(NSDictionary *)data andEndpoint:(NSString *)endpoint andMethod:(enum REQUEST_METHOD)method success:(void(^)(id response))success fail:(void(^)(id response))fail;
    @end
    


    ClientBase.m

    #import "ClientBase.h"
    @implementation ClientBase
    - (void)request:(NSDictionary *)data andEndpoint:(NSString *)endpoint andMethod:(enum REQUEST_METHOD)method success:(void(^)(id response))success fail:(void(^)(id response))fail
    {
        self.requestSerializer = [AFJSONRequestSerializer serializer];
        if(data == nil)
            data = @{};
       AFHTTPRequestOperation *operation = [self requestWithMethod:method path:endpoint parameters:data success:success fail:fail];
       [operation start];
    }
    - (AFHTTPRequestOperation *)requestWithMethod:(enum REQUEST_METHOD)method path:endpoint parameters:data success:(void(^)(id response))success fail:(void(^)(id response))fail{
        switch (method)
        {
            case GET:
                return [self requestGETMethod:data andEndpoint:endpoint success:success fail:fail];
            case POST:
                return [self requestPOSTMethod:data andEndpoint:endpoint  success:success fail:fail];
            default:
                return  nil;
        }
    }
    - (AFHTTPRequestOperation *)requestGETMethod:(NSDictionary *)data andEndpoint:(NSString *)endpoint success:(void(^)(id response))success fail:(void(^)(id response))fail
    {
        return [self GET:endpoint
              parameters:data
                 success:^(AFHTTPRequestOperation *operation, id responseObject) {
                    [self callingSuccesses:GET withResponse:responseObject endpoint:endpoint data:data success:success fail:fail];
                     [KNZHttpCache cacheResponse:responseObject httpResponse:operation.response];
                 } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                     NSLog(@"\n\n--- ERROR: %@", operation);
                     NSLog(@"\n--- DATA: %@", data);
                    [self callingFail:fail error:error];
                }];
    }
    - (AFHTTPRequestOperation *)requestPOSTMethod:(NSDictionary *)data andEndpoint:(NSString *)endpoint success:(void(^)(id response))success fail:(void(^)(id response))fail {
        return [self POST:endpoint
               parameters:data
                  success:^(AFHTTPRequestOperation *operation, id responseObject) {
                     [self callingSuccesses:POST withResponse:responseObject endpoint:endpoint data:data success:success fail:fail];
                  } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                      NSLog(@"\n\n--- ERROR: %@", operation);
                      NSLog(@"\n--- DATA: %@", data);
                     [self callingFail:fail error:error];
                 }];
    }
    - (void)callingSuccesses:(enum REQUEST_METHOD)requestMethod withResponse:(id)responseObject endpoint:(NSString *)endpoint data:(NSDictionary *)data success:(void(^)(id response))success fail:(void(^)(id response))fail {
        if(success!=nil)
            success(responseObject);
    }
    - (void)callingFail:(void(^)(id response))fail error:(NSError *)error {
        if(fail!=nil)
            fail(error);
    }
    @end
    



    Dies reicht aus, um Netzwerk-GET- und -POST-Nachrichten zu senden. Zum größten Teil müssen Sie diese Dateien nicht mehr anpassen.

    API-Schicht:
    Beschreibt REST-Befehle und nimmt die Auswahl des Hosts vor. Die Layer-API ist völlig unabhängig von Kenntnissen über die Implementierung von Netzwerkprotokollen und anderen Funktionen der Anwendung. Technisch kann es vollständig ersetzt werden, ohne dass Änderungen an der restlichen Anwendung vorgenommen werden müssen.

    Die Klasse wird von ClientBase geerbt. Der Klassencode ist so einfach, dass er nicht einmal vollständig angegeben werden muss - er besteht aus einer einheitlichen Beschreibung der API:

    Listing 2
    #define LOGIN_FACEBOOK_ENDPOINT @"/api/v1/member/login/facebook/"
    #define LOGIN_EMAIL_ENDPOINT @"/api/v1/member/login/email/"
    - (void)loginFacebook:(NSDictionary *)data success:(void(^)(id response))success fail:(void(^)(id response))fail {
        [self request:data andEndpoint:LOGIN_FACEBOOK_ENDPOINT andMethod:POST success:success fail:fail];
    }
    - (void)loginEmail:(NSDictionary *)data success:(void(^)(id response))success fail:(void(^)(id response))fail {
        [self request:data andEndpoint:LOGIN_EMAIL_ENDPOINT andMethod:POST success:success fail:fail];
    }
    


    Wie das Sprichwort sagt: "Nichts mehr."

    Netzwerk-Cache-Schicht:
    Diese Cache-Schicht wird verwendet, um die Netzwerkkommunikation zwischen Client und Server auf iOS-SDK-Ebene zu beschleunigen. Die Auswahl der Antworten wird von einer Partei vorgenommen, die außerhalb der Kontrolle des Systems liegt und keinen Rückgang des Netzwerkverkehrs garantiert, sondern diesen beschleunigt. Weder von der Anwendung noch vom System aus kann auf Daten oder Implementierungsmechanismen zugegriffen werden. Es verwendet SQLite-Speicher.

    Der dafür erforderliche Code ist zu einfach, um ihn in keinem Projekt zu verwenden, das Zugriff auf das Netzwerk hat:
    Listing 3
    
    #define memoCache 4 * 1024 * 1024
    #define diskCache 20 * 1024 * 1024
    #define DISK_CACHES_FILEPATH @"%@/Library/Caches/httpCache"
    - (void)start {
        NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:memoCache
                                                             diskCapacity:diskCache
                                                                 diskPath:nil];
        [NSURLCache setSharedURLCache:URLCache];
    }
    


    Sie müssen es einmal von überall in der Anwendung aufrufen. Zum Beispiel von der Startschicht.

    Ebene Validierungselemente:
    Das Format der vom Netzwerk empfangenen Daten hängt mehr von den Entwicklern des Servers ab. Die Anwendung ist physisch nicht in der Lage, die Verwendung des ursprünglich angegebenen Formats zu steuern. Bei komplex strukturierten Daten ist die Fehlerkorrektur in ihrer Komplexität mit der Entwicklung der Anwendung selbst vergleichbar. Das Vorhandensein von Fehlern ist wiederum mit einem Absturz der Anwendung behaftet. Durch die Verwendung des Datenüberprüfungsmechanismus wird das Risiko von Fehlverhalten erheblich reduziert. Die Validierungsschicht besteht aus JSON-Schemata für die meisten Anforderungen an den Server und einer Klasse, die die empfangenen Daten mit dem geladenen Schema vergleicht. Wenn das empfangene Paket nicht mit dem Schema übereinstimmt, wird es von der Anwendung abgelehnt. Der aufrufende Code erhält eine Fehlermeldung. Eine ähnliche Benachrichtigung wird im Konsolenprotokoll aufgezeichnet. Außerdem, Ein Serverbefehl kann aufgerufen werden, um einen Bericht über einen Fehler an die Serverseite zu senden. Die Hauptsache ist, einen Ausweg aus der Rekursion zu schaffen, wenn der Befehl zum Senden einer solchen Nachricht auch eine Art Fehler verursacht (4xx oder 5xx).
    Es ist sinnvoll, folgende Daten an den Server zu senden:
    • Für welches Konto ist ein Fehler aufgetreten.
    • Welches Team hat den Fehler verursacht?
    • Welche Daten wurden auf den Server übertragen.
    • Welche Antwort wurde vom Server empfangen.
    • UTC Zeit *
    • Statuscode des Teams. Bei Validierungsfehlern ist es immer 200.
    • Ein Schema, das die Serverantwort nicht erfüllt.


    * Die UTC-Zeit ist die Zeit, zu der der Befehl aufgerufen wurde und nicht, als die Antwort an den Server zurückgegeben wurde. In der Regel stimmen sie überein, aber da die Anwendung möglicherweise über einen Anforderungswarteschlangenmechanismus verfügt, können theoretisch Monate zwischen dem Aufrufen eines fehlgeschlagenen Befehls und dem Aufzeichnen eines Datensatzes durch den Server vergehen.
    Es wird davon ausgegangen, dass JSON-Anforderungsschemata von Serverentwicklern bereitgestellt werden, nachdem neue API-Befehle implementiert wurden.

    Jedes Programm sowie jedes Team ist verpflichtet, bestimmte zuvor vereinbarte Kriterien zu erfüllen. Im obigen Beispiel sollte die Serverantwort zwei Haupt- und ein optionales Feld enthalten.
    "Status" ist erforderlich. Enthält eine OK- oder ERROR-ID (oder einen HTTP-Code vom Typ "200").
    "Grund" erforderlich Enthält eine Textbeschreibung der Fehlerursache, falls diese aufgetreten ist. Andernfalls ist dieses Feld leer.
    "Daten" ist optional. Enthält das Ergebnis des Befehls. Im Fehlerfall fehlt.
    Beispielschaltung:
    Listing 4
    {
        "title": "updateconfig",
        "description": "/api/v1/member/updateconfig/",
        "type":"object",
        "properties":
        {
            "reason":
            {
                "type":"string",
                "required": true
            },
            "status":
            {
                "type":"string",
                "required": true
            },
            "data":
            {
                "type":"object"
            }
        },
        "required": ["reason", "status"]
    }
    


    Dank der von Maxim Lunin entwickelten Bibliothek wurde es sehr einfach. ( habrahabr.ru/post/180923 )

    Der Validierungsklassencode ist unten angegeben
    Listing 5
    ResponseValidator.h
    
    #import "ResponseValidator.h"
    #import "SVJsonSchema.h"
    @implementation ResponseValidator
    + (instancetype)sharedInstance
    {
        static ResponseValidator *sharedInstance;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedInstance = [[ResponseValidator alloc] init];
        });
        return sharedInstance;
    }
    #pragma mark - Methods of class
    + (void)validate:(id)response endpoint:(NSString *)endpoint success:(void(^)())success fail:(void(^)(NSString *error))fail
    {
        [[м sharedInstance] validate:response endpoint:endpoint success:success fail:fail];
    }
    + (NSDictionary *)schemeForEndpoint:(NSString *)endpoint
    {
        NSString *cmd = [[ResponseValidator sharedInstance] extractCommand:endpoint];
        return [[ResponseValidator sharedInstance] validatorByName:cmd];
    }
    #pragma mark - Methods of instance
    - (void)validate:(id)response endpoint:(NSString *)endpoint success:(void(^)())success fail:(void(^)(NSString *error))fail
    {
        NSString *cmd        = [self extractCommand:endpoint];
        NSDictionary *schema = [self validatorByName:cmd];
        SVType *validator    = [SVType schemaWithDictionary:schema];
        NSError *error;
        [validator validateJson:response error:&error];
        if(error==nil)
        {
            if(success!=nil)
                success();
        }
        else
        {
            NSString *result = [NSString stringWithFormat:@"%@ : %@", cmd, error.description];
            if(fail!=nil)
                fail(result);
        }
    }
    - (NSString *)extractCommand:(NSString *)endpoint
    {
        NSString *cmd = [endpoint.stringByDeletingLastPathComponent lastPathComponent];
        return cmd;
    }
    - (NSDictionary *)validatorByName:(NSString *)name
    {
        static NSString *ext = @"json";
        NSString *filePath   = [[NSBundle mainBundle] pathForResource:name ofType:ext];
        NSString *schema     = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
        if(schema == nil)
            return nil;
        NSData *data         = [schema dataUsingEncoding:NSUTF8StringEncoding];
        NSError *error;
        NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
        return result;
    }
    @end
    


    Der Validierungsaufruf ist ziemlich einfach:
    Listing 6
        [ResponseValidator validate:responseObject endpoint:endpoint success:^{
    /*
    	Валидация прошла успешно, вызываем конвейер обработки команды
    */
       } fail:^(NSString *error) {
    /*
    	Валидация провалена. Можем что-то сделать, а можем просто игнорировать результат. Зависит от религиозных предпочтений.
    */
       }];
    


    Layer "Netzwerkelemente":
    Auf diesem Layer werden Daten aus JSON einer deserialisierten Darstellung zugeordnet. Diese Ebene wird verwendet, um Klassen zu beschreiben, die objekt- oder objektrelationale Transformationen implementieren. Es gibt eine große Anzahl von Bibliotheken im Netzwerk, die objektrelationale Transformationen implementieren. Zum Beispiel das JSON-Modell ( github.com/icanzilb/JSONModel ) oder dieselbe Maxim Lunin-Bibliothek. Allerdings ist nicht alles so rosig. Sie lösen keine Mapping-Probleme.

    Lassen Sie uns erklären, was Mapping ist:
    Angenommen, es gibt zwei Abfragen, die identische Daten in der Struktur zurückgeben. Zum Beispiel Benutzer der Anwendung und Freunde des Benutzers, die Felder wie "Bezeichner" und "Benutzername" haben. Das Problem ist, dass Serverentwickler in einer Anforderung die Felder "id", "username" und in der zweiten "ident", "user_name" übergeben können. Eine solche Diskrepanz kann eine ganze Reihe von Problemen haben:
    1. Ein deserialisiertes Datenobjekt in Objective-C kann bei Verwendung von CoreData kein ID-Feld haben
    2. Serialisierte Daten in den Feldern id und ident können entweder eine Zeichenfolge oder eine NSNumber enthalten. Bei der Anzeige auf der Konsole gibt es keinen Unterschied zwischen den beiden Zahlen, aber. Ihr Hashcode ist unterschiedlich, und das Wörterbuch nimmt die Bedeutung dieser Felder unterschiedlich wahr.
    3. Die Unterschiede zwischen den Feldnamen liegen in der Verantwortung des Servers, und Serverentwickler können möglicherweise einfach keinen Kontakt herstellen, um ihre Namen durch einheitliche, für Cliententwickler bequeme Namen zu ersetzen.

    Es gibt keine universelle Lösung für diese Probleme, aber sie sind nicht so komplex, dass ein erheblicher intellektueller Aufwand erforderlich ist.

    Lokale Cache-Ebene:



    Die Aufgaben für diese Ebene sind:
    1. Vom Netzwerk heruntergeladene Bilder zwischenspeichern.
    2. Server Request / Response Caching
    3. Das Bilden einer Anforderungswarteschlange in Abwesenheit eines Netzwerks und eines Offlinebenutzers funktioniert.
    4. Überwachen Sie zwischengespeicherte Daten und löschen Sie abgelaufene Daten.
    5. Benachrichtigung der Anwendung über die Unfähigkeit, Informationen über das angegebene Objekt vom Netzwerk zu erhalten.

    Im Allgemeinen ist diese Ebene das Thema eines separaten großen Artikels. Es gibt jedoch eine Reihe von Nuancen, die Entwickler berücksichtigen sollten.
    Zum Zwischenspeichern von Abfragen können Sie die Prozeduren aus Listing 1 leicht aktualisieren. Ich empfehle dringend, dafür virtuelle Methoden zu verwenden. Der Einfachheit halber wird jedoch ein direkter Aufruf der Klassenmethode gezeigt:
    Listing 7
    - (void)request:(NSDictionary *)data andEndpoint:(NSString *)endpoint andMethod:(enum REQUEST_METHOD)method success:(void(^)(id response))success fail:(void(^)(id response))fail queueAvailable:(BOOL)queueAvailable
    {
        self.requestSerializer = [AFJSONRequestSerializer serializer];
        if(data == nil)
            data = @{};
       // Returning cache response.
        NSDictionary *cachedResponse = [HttpCache request:endpoint];
        if(cachedResponse !=nil)
        {
            [self callingSuccesses:method withResponse:cachedResponse endpoint:endpoint data:data success:success fail:fail];
            return;
        }
       AFHTTPRequestOperation *operation = [self requestWithMethod:method path:endpoint parameters:data success:success fail:fail];
        [self consoleLogRequest:data operation:operation];
        [operation start];
    }
    - (AFHTTPRequestOperation *)requestPOSTMethod:(NSDictionary *)data andEndpoint:(NSString *)endpoint success:(void(^)(id response))success fail:(void(^)(id response))fail {
        return [self POST:endpoint
               parameters:data
                  success:^(AFHTTPRequestOperation *operation, id responseObject) {
                     [self callingSuccesses:POST withResponse:responseObject endpoint:endpoint data:data success:success fail:fail];
                      [HttpCache cacheResponse:responseObject httpResponse:operation.response];
                  } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                      NSLog(@"\n\n--- ERROR: %@", operation);
                      NSLog(@"\n--- DATA: %@", data);
                     [self callingFail:fail error:error];
                 }];
    }
    



    In der HttpCache-Klasse gibt es neben Methoden zum Speichern von Abfrageergebnissen eine weitere interessante Methode:

    Listing 8
    #define CacheControlParam @"Cache-Control"
    #define kMaxAge @"max-age="
    - (NSInteger)timeLife:(NSHTTPURLResponse *)httpResponse {
        NSString *cacheControl = httpResponse.allHeaderFields[CacheControlParam];
        if(cacheControl.length > 0)
        {
            NSRange range = [cacheControl rangeOfString:kMaxAge];
            if(range.location!=NSNotFound)
            {
                cacheControl = [cacheControl substringFromIndex:range.location + range.length];
                return cacheControl.integerValue;
            }
        }
        return 0;
    }
    


    Hiermit können Sie Schlüsselinformationen aus dem Serverantwortheader extrahieren, die angeben, wie viele Sekunden das empfangene Paket abläuft (das Datum läuft ab). Mit diesen Informationen können Sie Daten in den lokalen Speicher schreiben. Wenn Sie dieselbe Anforderung wiederholen, lesen Sie einfach die zuvor empfangenen Daten. Wenn die Methode 0 zurückgibt, können solche Daten nicht geschrieben werden.
    So kann auf dem Server geregelt werden, was genau auf dem Client zwischengespeichert werden soll. Es ist zu beachten, dass Standardheaderfelder verwendet werden. Ein Fahrrad ist also vom Standard her nicht erfunden.

    Mit einer weiteren kleinen Änderung an Listing 1 kann das Warteschlangenproblem leicht behoben werden:
    Listing 9
    - (void)request:(NSDictionary *)data andEndpoint:(NSString *)endpoint andMethod:(enum REQUEST_METHOD)method success:(void(^)(id response))success fail:(void(^)(id response))fail queueAvailable:(BOOL)queueAvailable
    {
        self.requestSerializer = [AFJSONRequestSerializer serializer];
        if(data == nil)
            data = @{};
       if(queueAvailable)
        {
           [HttpQueue request:data endpoint:endpoint method:method];
        }
       AFHTTPRequestOperation *operation = [self requestWithMethod:method path:endpoint parameters:data success:success fail:fail];
       [operation start];
    }
    


    Die HttpQueue-Klasse prüft, ob derzeit eine Netzwerkverbindung besteht, und schreibt die Anforderung in das Repository, wobei der Zeitpunkt der Anforderung auf Millisekunden genau festgelegt wird. Wenn die Verbindung wieder hergestellt wird, werden die Daten aus dem Speicher gelesen und vom Server übertragen, während die Anforderungswarteschlange gelöscht wird. Dadurch ist es möglich, einen bestimmten Client-Server-Betrieb ohne direkte Verbindung zum Netzwerk bereitzustellen.

    Die Überprüfung der Netzwerkkonnektivität erfolgt mithilfe des AFNetworkReachabilityManager oder der Reachability-Klassen von Apple ( developer.apple.com/library/ios/samplecode/Reachability/Introduction/Intro.html ) in Verbindung mit dem Beobachtermuster. Sein Gerät ist zu primitiv, um es im Artikel zu beschreiben.
    Однако, не все запросы должны быть отправлены в очередь. Некоторые из них могут не быть актуальными к моменту появления сети. Решить какие из команд дожны быть записаны в кеш очереди, а каки быть актуальны толко в момент вызова можно как на уровне слоя кеширования, так и на уровне слоя API.

    В первом случае, в листинг 9, вместо вызова метода сохранения в очередь, необходимо вставить виртуальный метод, и унаследовать от класса ApiLayer унаследовать классы LocalCacheLayerWithQueue и LocalCacheLayerWithoutQueue. После чего в заданном виртуальном методе класса LocalCacheLayerWithQueue сделать вызов [HttpQueue request: endpoint: method:]

    Во втором случае немного изменится вызов запроса из класса ApiLayer
    Листинг 10
    - (void)trackNotification:(NSDictionary *)data success:(void(^)(id response))success fail:(void(^)(id response))fail {
        [self request:data andEndpoint:TRACKNOTIFICATION_ENDPOINT andMethod:POST success:success fail:fail queueAvailable:YES];
    }
    



    Listing 9 bezieht sich genau auf diesen Fall, wenn (queueAvailable) angegeben ist.

    Ein anderes Problem ist das Problem der Bildzwischenspeicherung. Im Allgemeinen ist die Frage nicht kompliziert und daher mit einer unendlichen Anzahl von Implementierungen verbunden. Die SDWebImage-Bibliothek führt dies beispielsweise sehr erfolgreich durch: ( github.com/rs/SDWebImage ).

    In der Zwischenzeit gibt es einige Dinge, die sie nicht kann. Beispielsweise kann der Bild-Cache nicht nach festgelegten Kriterien (Anzahl der Bilder, Erstellungsdatum usw.) geleert werden, um bestimmte Fehler zu protokollieren oder zu korrigieren, d. H. Der Entwickler muss noch seine eigenen Fahrräder für das Caching erfinden.

    Ich gebe ein Beispiel für das asynchrone Herunterladen eines Bildes aus dem Netzwerk und das Korrigieren eines MIME-Fehlers (beispielsweise gibt Amazon häufig den falschen MIME-Typ an, wodurch der Webserver das Bild nicht als Binärdatei mit einem Bild, sondern als Datenstrom sendet).
    Listing 11
    #define LOCAL_CACHES_IMAGES_FILEPATH @"%@/Library/Caches/picture%ld.jpg"
    - (void)loadImage:(NSString*)link
              success:(void(^)(UIImage *image))success
              fail:(void(^)(NSError *error))fail
    {
        UIImage *image = [ImagesCache imageFromCache:link.hash];
        if(image == nil)
        {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                __block NSData *data;
                __block UIImage *remoteImage;
                __block NSData *dataImage;
                __block NSString *imgFilePath = [NSString stringWithFormat:LOCAL_CACHES_IMAGES_FILEPATH, NSHomeDirectory(), (unsigned long)link.hash];
                data = [NSData dataWithContentsOfURL: [NSURL URLWithString:link]]; // Reading DATA
                if(data.length > 0)
                {
                    remoteImage = [[UIImage alloc] initWithData: data]; // TRANSFORM DATA TO IMAGE
                    if(remoteImage!=nil)
                    {
                        dataImage = [NSData dataWithData:UIImageJPEGRepresentation(remoteImage, 1.0)]; // TRANSFORM IMAGE TO JPEG DATA
                        if(dataImage!=nil && dataImage.length > 0)
                            [dataImage writeToFile:imgFilePath atomically:YES]; // Writing JPEG file
                    }
                    else // try to fix BINARY image type (first method)
                    {
                        [dataImage writeToFile:imgFilePath atomically:YES];
                        remoteImage = [UIImage imageWithContentsOfFile:imgFilePath];
                    }
                }
                else // try to fix BINARY image type (second method)
                {
                    NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:link]];
                    NSURLResponse *response  = nil;
                    NSError *error           = nil;
                    data = [NSURLConnection sendSynchronousRequest:urlRequest returningResponse:&response  error:&error];
                    if (error == nil)
                    {
                        remoteImage = [[UIImage alloc] initWithData: data]; // TRANSFORM DATA TO IMAGE
                        if(remoteImage!=nil)
                        {
                            dataImage = [NSData dataWithData:UIImageJPEGRepresentation(remoteImage, 1.0)]; // TRANSFORM IMAGE TO JPEG DATA
                            if(dataImage!=nil && dataImage.length > 0)
                                [dataImage writeToFile:imgFilePath atomically:YES]; // Writing JPEG file
                        }
                        NSLog(@"USED SECONDARY METHOD FOR LOAD OF IMAGE");
                    }
                    else
                        NSLog(@"DATA WASN'T LOAD %@\nLINK %@", error, link);
                }
                dispatch_async(dispatch_get_main_queue(), ^{
                    if(remoteImage!=nil && success!=nil)
                    {
                        success(remoteImage);
                        [ImagesCache update:link.hash];
                    }
                    else
                    {
                        if(data.length == 0)
                            NSLog(@"%@", @"\n============================\nDETECTED ERRROR OF DOWNLOAD IMAGE\nFILE CAN'T LOAD\nUSED PLACEHOLDER\n============================\n");
                        else
                            NSLog(@"%@", @"\n============================\nDETECTED ERRROR OF DOWNLOAD IMAGE\nUSED PLACEHOLDER\n============================\n");
                        NSLog(@"LINK %@", link);
                        UIImage *placeholder = [LoadImage userPlaceholder];
                        if (success)
                            success(placeholder);
    //                    if(fail!=nil)
    //                        fail([NSError errorWithDomain:[NSString stringWithFormat:@"%@ not accessible", link] code:-1 userInfo:nil]);
                   }
                });
            });
        }
        else
        {
            success(image);
        }
    }
    


    Die Methode scheint sehr redundant zu sein, lässt sich aber leicht an die spezifischen Bedürfnisse des Entwicklers anpassen. Zu den wichtigen Punkten gehört, dass der Hash der Bild-URL als Schlüssel für das Caching verwendet wird. Es ist fast unmöglich, dass bei diesem Ansatz eine Kollision im Dateisystem des Geräts auftritt.
    Jedes Mal, wenn eine Datei aus dem Cache gelesen wird, wird das Zugriffsdatum geändert. Dateien, die längere Zeit nicht erneut gelesen wurden, können auch beim Start der Anwendung sicher gelöscht werden.

    Wenn es darum geht, eine Datei aus einem Anwendungspaket zu lesen, vergessen Entwickler eine Nuance: iOS SDK bietet uns Methoden wie [UIImage imageNamed:] und [UIImage imageWithContentsOfFile:]. Die Verwendung des ersten ist einfacher, hat jedoch erhebliche Auswirkungen auf die Speicherauslastung. Die heruntergeladene Datei verbleibt im Speicher des Geräts, bis die Anwendung abgeschlossen ist. Wenn dies eine Datei mit einem großen Volumen ist, kann dies ein Problem sein. Es wird empfohlen, die zweite Methode so oft wie möglich anzuwenden. Darüber hinaus ist es sinnvoll, die Lademethode geringfügig zu verbessern:
    Listing 12
    + (UIImage *)fromBundlePng:(NSString *)name
    {
        return [[LoadImage sharedInstance] fromBundlePng:name];
    }
    - (UIImage *)fromBundle:(NSString *)name
    {
        return [self downloadFromBundle:name.stringByDeletingPathExtension ext:name.pathExtension];
    }
    - (UIImage *)downloadFromBundle:(NSString *)name ext:(NSString *)ext
    {
        NSString *filePath = [[NSBundle mainBundle] pathForResource:name ofType:ext];
        if(filePath == nil)
        {
            NSString *filename = [NSString stringWithFormat:@"%@@2x", name];
            filePath = [[NSBundle mainBundle] pathForResource:filename ofType:ext];
        }
        return [UIImage imageWithContentsOfFile:filePath];
    }
    


    Jetzt müssen Sie sich nicht mehr fragen, in welcher Auflösung die Datei vorliegt.

    Workflow-Schicht:
    Alle implementierten Algorithmen, die nicht zu den Kernel-Schichten gehören und keine GUI darstellen, sollten in Klassen bestimmter Workflow-Sequenzen eingeordnet werden. Jeder dieser Prozesse ist in seinem eigenen Stil gestaltet und stellt eine Verbindung zum Hauptteil der Anwendung her, indem Verknüpfungen zu einer Instanz der entsprechenden Klasse in der GUI hinzugefügt werden. In den allermeisten Fällen sind alle diese Prozesse nicht visuell. Es gibt jedoch einige Ausnahmen, wenn beispielsweise eine lange Sequenz vordefinierter Animationsframes mit festgelegten Anzeigealgorithmen implementiert werden muss
    Der aufrufende Code muss über minimale Kenntnisse dieser Funktionalität verfügen. Alle Durchflusseinstellungen müssen gekapselt sein. Als Beispiel nennt Google einen Code für eine Benachrichtigung vom Analyseserver und schlägt vor, diesen an der Stelle einzuschließen, an der das Ereignis auftritt.

    Listing 13
               // Analytics
                [Analytics passedEvent:ANALYTICS_EVENT_PEOPLE_SELECT
                              ForCategory:ANALYTICS_CATEGORY_PEOPLE
                           WithProperties:nil];
    


    Wenn Sie neben diesem Code einen anderen Server benachrichtigen müssen, müssen Sie natürlich denselben Code mit Ihren Einstellungen hinzufügen. Dieser Ansatz ist nicht gerechtfertigt und inakzeptabel. Stattdessen müssen Sie eine Klasse mit einer Klassenmethode erstellen, um Analyseserver mit der angegebenen Funktionalität aufzurufen.

    Es gibt durchaus entwickelte Arbeitsprozesse, deren Funktionslogik vom inneren Zustand abhängt. Solche Prozesse sollten unter Verwendung der Muster "Strategie" oder "Zustandsmaschine" implementiert werden. In der Regel wird zusammen mit dem "Strategie" -Muster das "Mediator" -Muster verwendet, das die Anziehungskraft auf einen bestimmten Algorithmus vermittelt.
    Einer der am häufigsten verwendeten Prozesse - der Benutzerautorisierungsprozess - ist ein offensichtlicher Kandidat für die Neuformulierung mithilfe des Musters der „Zustandsmaschine“. Gleichzeitig sollte genau in diesem Ablauf die Verantwortung für die "automatische" Benutzerautorisierung liegen und nicht rekursiv von abstrakten Ebenen (Network Layer oder Validation Items) aufgerufen werden.

    Jeder Aufruf der Kernel-Layer wird von der Übertragung eines Callback-Objekts begleitet. Dadurch sollte die Steuerung an die Anwendung zurückgegeben werden, wenn der Befehl erfolgreich ausgeführt wird oder Fehler auftreten. In keinem Fall darf der implizite Aufruf der Kernel-Layer durch Objekte der Arbeitssequenz erlaubt sein.
    Außerdem sollte in keinem Fall erlaubt sein, dass universelle visuelle Kontrollen vom Zustand der Arbeitsabläufe abhängen. Wenn dies unbedingt erforderlich ist, sollten diese Kontrollen nacheinander privatisiert werden. Der Zugriff auf den Status von Steuerelementen kann über die Eigenschaften der Steuerelemente selbst, die Vererbung und Neudefinition von Methoden sowie in extremen Fällen über die Implementierung von Delegierungsmethoden und das Erstellen von Kategorien erfolgen. Der Schlüssel zu dieser kunstvollen Botschaft ist, dass Kategorien böse sind, die vermieden werden sollten. Das heißt, ich schlage nicht vor, Kategorien aufzugeben, aber ceteris paribus, Code ohne sie ist einfacher zu lesen und zweifellos vorhersehbarer.

    Lokale Speicherung:
    Der Wunsch der Entwickler liegt im Trend der neuen Technologien, manchmal stößt er auf gesunden Menschenverstand, und letzterer verliert oft. Einer der Modetrends war die Verwendung von lokalem Speicher, der auf CoreData basiert. Einige Entwickler bestanden darauf, dass es in so vielen Projekten wie möglich verwendet werden sollte, obwohl sogar Apple selbst erkannte, dass es bestimmte Schwierigkeiten gab.
    Es gibt eine Vielzahl von Möglichkeiten, um temporäre Daten in einem permanenten Speichergerät zu speichern. Die Verwendung von CoreData ist gerechtfertigt, wenn wir eine große Menge selten aktualisierter Daten speichern müssen. Wenn die Anwendung jedoch mehrere hundert Datensätze enthält und keiner dieser Datensätze in einem Array ständig aktualisiert wird, ist die Verwendung von CoreData für diese Zwecke unangemessen teuer. Es stellt sich daher heraus, dass das Gerät die meiste Zeit mit Ressourcen verbringt, um vom Netzwerk empfangene Daten mit den Daten zu synchronisieren, die sich bereits auf dem Gerät befinden, obwohl das gesamte Datenarray während der nächsten Sitzung aktualisiert wird.

    Verwenden von CoreData ( habrahabr.ru/post/191334) Außerdem müssen bestimmte Verfahren, Algorithmen und Architekturentscheidungen eingehalten werden, was die Auswahl einer Entwicklungsstrategie einschränkt und die Debugging-Mechanismen unserer Anwendung erheblich verkompliziert.

    In der Regel soll die Verwendung von persistentem Speicher aufgrund der Verwendung von Informationen, die bereits vom Netzwerk empfangen wurden, zu einer erheblichen Reduzierung des Netzwerkverkehrs führen. In einigen Fällen geschieht dies jedoch nicht, da die Quelle dieser Informationen der Server ist, der Entscheidungen über die Relevanz dieser Informationen trifft.

    Lokaler Speicher basierend auf dem Dateisystem

    Wenn Sie NSDictionary als Format für die empfangenen Daten verwenden, können Sie eine Reihe von Architekturproblemen automatisch lösen:
    1. Daten in Arrays können genau in der Reihenfolge dargestellt werden, in der sie vom Server empfangen wurden.
    2. Die Daten entsprechen eindeutig der verwendeten Anforderung an den Server bis zu den übertragenen Parametern in der POST-Anforderung (d. h. es war einfach, zwischen Objekten zu unterscheiden, die von einem bestimmten Befehl empfangen wurden, und Objekten, die von demselben Befehl empfangen wurden, wobei jedoch andere Daten als POST-Parameter übertragen wurden )
    3. Die atomare Natur des Schreibens eines Datenobjekts in einen dauerhaften Speicher.
    4. Momentan und atomar gelesene Daten aus persistentem Speicher.
    5. Vollständige ACID-Transaktionskonformität: en.wikipedia.org/wiki/ACID
    6. Keine Notwendigkeit, Daten zu normalisieren.
    7. Unabhängigkeit in der Dateninterpretation.
    8. Alle Daten sind immer aktuell.
    9. Der Support-Code ist minimal (1 Zeile).


    Der iOS SDK Reader / Writer macht NSDictionary zu einem idealen Format zum Speichern relativ kleiner, kurzlebiger Daten, da Single-Pass-Algorithmen verwendet werden.

    Es ist nicht erforderlich, zusätzliche Logik zum Lesen serialisierter gespeicherter Daten zu verwenden. Daten können mit demselben Befehl zurückgegeben werden, der Daten aus dem Netzwerk liest.

    Die negative Seite dieses Ansatzes ist, dass er die Leistung des Geräts stark beeinträchtigt. Eine Untersuchung des Problems zeigt jedoch, dass die Menge solcher Daten 5 KB nicht überschreitet. Die Daten werden sofort in einem einzelnen Block in den Speicher geladen und auch unmittelbar danach aus dem Speicher entfernt wie sie zum Beispiel nicht mehr benötigt werden, wenn der ViewController nicht mehr existiert. Gleichzeitig werden beim Lesen von Daten in Blöcken (zeilenweise) aus der SQL-Datenbank eine große Anzahl von Objekten (auf einer Ebene, die außerhalb der Kontrolle der Anwendung liegt) generiert, deren Gesamtmenge das angegebene Volumen überschreitet, und es entsteht eine zusätzliche Prozessorauslastung. Die Verwendung eines zentralen Repositorys ist gerechtfertigt, wenn Daten während vieler Sitzungen der Anwendung über einen längeren Zeitraum gespeichert werden müssen. Gleichzeitig werden Daten aus dem Netzwerk teilweise heruntergeladen.

    Lokaler Speicher basierend auf CoreData.

    CoreData bietet nicht die Möglichkeit, serialisierte Daten zu verwenden. Alle Daten müssen objektrelational transformiert werden, bevor sie von der lokalen Speicherschicht verwendet werden. Nach dem Empfang von Daten aus dem API-Profilbefehl werden Daten an die Kategoriemethode copyDataFromRemoteJSON übertragen, in der Daten aus dem Wörterbuch extrahiert und dann im entsprechenden verwalteten Objekt (einem Nachkommen der NSManagedObject-Klasse) gespeichert werden.
    Hier ist ein Beispiel, wie dies geschieht:

    Listing 14
        [[Client client] profile:@{} success:^(id response) {
            [[Member getCurrentMember] copyDataFromRemoteJSON:[response valueForKey:@"data"]];
        } fail:^(id response) {
        }];
    


    Ein noch besserer Ansatz wäre, wenn der Rückruf von der API validierte serialisierte Daten zurückgeben könnte, die außerdem in ein verwaltetes Objekt gepackt sind.

    Der allgemeine Algorithmus zum Arbeiten mit Daten lautet wie folgt:
    1. Dem Benutzer werden Daten angezeigt, die unmittelbar nach dem Starten der Anwendung im System vorhanden sind.
    2. Es wird angefordert, dieselben Daten von einem Remoteserver zu empfangen, da der Server seine Relevanz bestätigen muss. Diese Anfrage bestätigt die Autorisierung der Anwendung.
    3. Wenn die Daten vom Server empfangen werden, war die Autorisierung erfolgreich und die restlichen Daten werden zyklisch heruntergeladen.
    4. Wenn der Server die Autorisierung nicht bestätigt (das Token ist abgelaufen), werden alle Daten auf dem lokalen System gelöscht. Die Benutzeroberfläche wird aktualisiert.
    5. Die empfangenen Daten werden mit dem Inhalt des lokalen Speichers synchronisiert. (Das heißt, jedes Objekt wird teilweise vom lokalen Speicher subtrahiert, es wird geprüft, ob es einen Bezeichner für ein solches Objekt gibt. Wenn ein solcher Bezeichner bereits existiert, werden die Daten ignoriert / aktualisiert. Wenn dies nicht der Fall ist, werden die Daten hinzugefügt.)
    6. Nachdem der Aufzeichnungsprozess vollständig implementiert wurde, wird die Benutzeroberfläche aktualisiert.


    Vorteile dieses Ansatzes: Es wird
    vermutet, dass ein verzögertes Laden mit NSFetchController die Anzeige von Daten aus der Datenbank erheblich beschleunigen kann, wenn ihre Anzahl mehrere tausend Datensätze beträgt. Durch Hinzufügen von Daten zur Datenbank wird auch die Menge der über das Netzwerk übertragenen Informationen verringert. Daten werden hinzugefügt. Diejenigen, die angezeigt werden, werden dem Benutzer angezeigt. Der Benutzer kann nicht benötigte Daten löschen. Daten werden zu den bereits vorhandenen Objekten als Elemente ihres Arrays hinzugefügt.

    Die Nachteile dieses Ansatzes:
    1. Zu den Vorteilen des Ansatzes sollten zunächst alle oben genannten Vorteile gehören (Ansatz basierend auf dem Dateisystem):
    2. Последовательность отображения данных на экране не гарантируется, поскольку, данные извлекаются из SQLite базы данных, а там они лежат в «натуральном» порядке. Для создания последовательного отображения требуется вводить атоинкрементный номер, или какой-либо другой механизм, которые не предоставляется, ни CoreData ни SQLite.
    3. Данные никак не связаны с сетевыми запросами, что сильно осложняет их отладку.
    4. Сохранение данных в локальном хранилище происходит атомарно для всего контекста. Но, между вызовами записи данные могут быть потеряны, или перезатерты. Кроме того, процедура сохранения в базе может быть не вызвана.
    5. Большие объемы данных извлекаются из Database с существенно большей скоростью чем чтение плоского файла, однако, для сравнительно небольших файлов, скорость все равно будет выше.
    6. ACID не применим к SQLite в реализации с CoreData. Одновременная запись разных контекстов из разных потоков легко приводит к крешам приложения. Частично проблема решается путем использования библиотеки MagicRecords.
    7. Для нормализации данных необходимо применять специальные процедуры. Если некоторые поля заполняются по определенному условию, а объем данных возрастает, то либо данные необходимо дробить на большое количество объектов, либо извлекать из них абстрактные сущности, либо применять специальные процедуры для удаления устаревающих данных.
    8. Данные в CoreData всегда реляционны. Поэтому этому вопрос независимости рассматриваться может только в том случае, если схема CoreData не содержит связей между элементами.
    9. Da die Relevanz der Daten vom Server und nicht von der Anwendung bestimmt wird, müssen noch Daten gelöscht werden, die nicht vom Netzwerk empfangen wurden. Daher wirkt sich die Verwendung von CoreData nicht auf den Netzwerkverkehr in diesem Schema aus.
    10. Die Menge an Code ist um ein Vielfaches höher als die, die zum Verwalten des dateisystembasierten Speichers erforderlich ist. Durch die Verwendung von CoreData werden der Benutzeroberfläche bestimmte Einschränkungen auferlegt.


    Zweitens müssen die Nachteile des Ansatzes auch die Tatsache umfassen, dass:
    1. CoreData erfordert eine bestimmte Disziplin, um aus verschiedenen Anwendungsthreads heraus arbeiten und den aktuellen Kontext auswählen zu können.
    2. Die Datensynchronisation kann die Leistung des Geräts so beeinträchtigen, dass das Problem der Verwendung von 4S-Geräten sehr relevant ist.
    3. Das Debuggen von Anwendungen ist sehr kompliziert. Einige Operationen sind nicht offensichtlich. Um nach fehlerhaftem Verhalten zu suchen, müssen Sie die MagicalRecords-Bibliothek (https://github.com/magicalpanda/MagicalRecord) durchsuchen oder Ihre Klassen und Kategorien hinzufügen.

    Bevor Sie eine Wahl zwischen CoreData, dem lokalen Dateisystem oder einem anderen Speicher treffen, sollten Sie selbst wissen, wofür Ihr lokaler Speicher verwendet wird. Wenn Sie Daten zwischen Sitzungen speichern und akkumulieren möchten, ist CoreData ein idealer Mechanismus für eine solche Implementierung. Wenn es sich jedoch um temporäre Daten handelt, sollten Sie Optionen zum Speichern von Daten in Form von Flatfiles oder hierarchischen Speichern wie NoSQL-Datenbanken oder XML in Betracht ziehen.

    Bei Verwendung der MagicalRecords-Bibliothek tritt eine Situation auf, in der die Tabellenansicht Teil des UITableViewControllers sein muss, damit die Anwendung ordnungsgemäß funktioniert. Andernfalls wird es schwierig, den NSFetchController zu verwenden, der dem Laden von CoreData-Daten zugrunde liegt. Daher besteht eine Abhängigkeit bei der Verwendung der Benutzeroberfläche vom lokalen Speicher. Das heißt, die Implementierung von CoreData schränkt die Entwicklung der Benutzeroberfläche ein.

    Eine alternative Sichtweise

    Trotz der geäußerten Einwände kann die Verwendung von CoreData die Produktivität mit zunehmendem Datenvolumen potenziell steigern, wenn Sie die folgenden Alternativen verwenden:

    Alternative 1
    Normalisieren Sie die Server-API-Daten. Der Server sollte kein vollständiges hierarchisches Objekt mit vielen verschachtelten Entitäten zurückgeben, sondern viele kleine Objekte, die einfach zur Datenbank hinzugefügt werden können.
    In diesem Fall:
    Kleine Teile neuer Daten werden geladen, wodurch der Netzwerkverkehr verringert wird.
    Ermöglicht einer Anwendung, eine Anfrage mit Objekt-IDs an den Server zu senden, damit der Server eine Liste der zu löschenden Elemente zurückgibt.
    Es ist nicht erforderlich, die empfangenen Daten für jeden heruntergeladenen Datensatz zu synchronisieren.

    Alternative 2
    Die Aufgabe kann nur mit der Client-Anwendung gelöst werden: Erstellen Sie in CoreData eine Tabelle, in die das JSON-Objekt in seiner reinen Form unmittelbar nach dem Zugriff auf das Netzwerk geschrieben werden soll. Geben Sie dort zusätzlich das Aufnahmedatum, die Benutzer-ID, den Anforderungs-Hash und den Daten-Hash ein.
    Dies ermöglicht:
    1. Serialisieren Sie Daten im Handumdrehen in binäre Objekte, die auf dem JSON-Schema basieren.
    2. Um das Funktionieren dieses Mechanismus auf der Ebene der Kernschicht sicherzustellen, d. H. Für den Entwickler transparent.
    3. Wechseln Sie sofort zwischen Benutzerkontexten.
    4. Löschen Sie irrelevante Einträge, wenn sie veraltet sind, basierend auf den vom Server im Antwortheader angegebenen Informationen.
    5. Trotz der Verwendung von SQLite müssen die Serverdaten nicht normalisiert werden.
    6. Reduzieren Sie die Menge des verwendeten Codes erheblich.


    Fazit: Der
    Artikel hat sich als ziemlich lang herausgestellt, und ich bezweifle, dass die meisten Leser ihn bis zum Ende beherrschen werden. Aus diesem Grund habe ich beschlossen, den Teil, der mit der GUI zusammenhängt, von hier aus zu streichen. Zum einen ging es um die Erstellung der Benutzeroberfläche über UITabbar, zum anderen fand in einer der Skype-Gruppen eine sehr interessante Diskussion über die Verwendung bekannter MVC- und MVVM-Muster statt. Es macht keinen Sinn, die Prinzipien des Aufbaus einer Schnittstelle zu formulieren, ohne die vorhandenen Praktiken und Ansätze, die Entwickler zum Stillstand bringen, akribisch darzulegen. Dies ist jedoch das Thema eines weiteren großen mehrseitigen Artikels. Hier habe ich versucht, nur Fragen im Zusammenhang mit der Funktionsweise des Anwendungskerns zu berücksichtigen.
    Wenn die Leser ausreichend Interesse an diesem Thema zeigen, werde ich in naher Zukunft versuchen, die Quellklassen für die Verwendung als Anwendungsvorlage auszulegen.

    Jetzt auch beliebt: