All Hail to the Change Manager


Apps sind aus unserm Alltag nicht mehr wegzudenken. Oft von eher geschwätziger Natur erfreuen sie uns rund um die Uhr mit nützlichen und unnützen Informationen. Sei dies über Push-Nachrichten aus der ominösen Cloud (GCM verschickt schlappe 70 Milliarden Nachrichten pro Tag, Google I/O Mai 2015), über lokal generierte Benachrichtigungen (z.B. Kalendererinnerung) oder über das UI (User Interface) der App selbst (z.B. Statistiken von Google Fit). Dies ist allerdings nur was an die Oberfläche dringt. Hinter den Kulissen einer App giert die Datenabstraktionsschicht nach neuen Informationen, der Netzwerkmanager ist sowieso gerade ausser Haus, das UI bettelt um Aktualisierung und das Universum streut hämisch Sand ins Getriebe des ganzen Konstrukts.

ALL HAIL TO THE CHANGE MANAGER

Dem ganzen Schlamassel kann man nur mit einer vernünftigen Softwarearchitektur Einhalt gebieten. Ich möchte in diesem Blogeintrag ein Softwarepattern vorstellen, welches die Kommunikation zwischen verschiedenen Softwarekomponenten erleichtert und so vielen Problemen schon von beginn an den Wind aus den Segeln nimmt. Illustriert wird das Ganze mit Android-spezifischem Code. iOS Entwickler seien ans NSNotificationCenter verwiesen, welches im Grunde bereits eine Implementation dieses Patterns ist.

Observer Pattern (mit ChangeManager) für Android

Problem

Als Grundlage für meine Ausschweifungen soll folgende Applikationsskizze dienen:

Application draft

Wir haben also diverse UI-Elemente (z.B. Android-Fragments), irgendeinen NetworkManager der in die Welt hinaushorcht, eine DB (z.B. ORMLite) für die Datenpersistenz, einen LocationListener der Positionsänderungen überwacht und einen BLEListener der BLE-Tags in Reichweite wahrnimmt. Natürlich ist hier jeder mit jedem befreundet und ein fideles Kaffeekränzchen ist im Gange. Neue BLE-Tags lösen womöglich einen Request aus, übers Netzwerk werden Steuerbefehle erhalten, das UI stellt den Zustand des Gesamtsystems dar, usw.

Man braucht nicht viel Fantasie, um sich hier die wildesten Abhängigkeiten vorzustellen. Bald sind Logikentkopplung und Wiederverwendbarkeit mit Fackeln und Heugabeln aus dem Dorf gejagt. Dieser Entwicklung darf man natürlich nicht tatenlos zusehen. Am besten rückt man ihr mit einem bewährten Softwarepattern auf den Leib.

Dank des Observer Patterns (mit ChangeManager) können wir das System entschlacken und bleiben folgenden zwei Grundsätzen der Softwareentwicklung treu:

  • Entkopplung
  • Wiederverwendbarkeit
Application draft with changemanager

Das fröhliche Beisammensein unserer Komponenten wurde mit Hilfe eines ChangeManagers aufgebrochen. Doch wie setzten wir das nun auf Android um?

Implementation

Es gibt verschiedene Ansätze um das Observer Pattern auf Android umzusetzen. Man könnte zum Beispiel den LocalBroadcastManager mit Intents verwenden. Diese Methode bringt aber viel Overhead mit sich. Zudem ist die hier vorgestellte Umsetzung um einiges flexibler, da jedes erdenkliche Objekt als Change-Notifikation verwendet werden kann.

Die von smoca vorgeschlagene Lösung basiert auf drei Bausteinen:

  • Interface SMNotificationListener
  • Class SMChangeManager
  • Abstract Class SMNotifiableFragment (Spezialfall für UI-Objekte wie etwa Activities oder Fragments)
SMNotificationListener
1
2
3
public interface SMNotificationListener {
void dataChanged(Object ob);
},

Ein SMNotificationListener besitzt die dataChanged-Methode. Ihr kann so ziemlich alles übergeben werden. Bei Bedarf könnte dies natürlich näher spezifiziert werden.

SMChangeManager
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SMChangeManager {
private static SMChangeManager _instance = null;
private ArrayList<SMNotificationListener> _listeners;
private ExecutorService _pool;


private SMChangeManager(){
_listeners = new ArrayList<>();
_pool = Executors.newFixedThreadPool(1);
},

public synchronized void addListener(SMNotificationListener listener){
_listeners.add(listener);
},


public synchronized void removeListener(SMNotificationListener listener){
_listeners.remove(listener);
},


public synchronized void notifyDataChanged(final Object obj){
for (final SMNotificationListener listener : _listeners) {
_pool.execute(new Runnable() {
@Override
public void run() {
listener.dataChanged(obj);
},
},);
},
},

public static synchronized SMChangeManager getInstance(){
if(_instance == null){
_instance = new SMChangeManager();
},
return _instance;
},

},

Eigentliches Herzstück der Implementation. SMNotificationListeners können sich hier an- und abmelden. Auch die eigentlichen Changes werden über den SMChangeManager ausgelöst (Siehe notifyDataChanged(someObject)).

Da es sich beim SMChangeManager um ein Singelton handelt, kann er ohne Probleme von jeder beliebigen Stelle im System mittels SMChangeManager.getInstance() angesprochen werden. Dies funktioniert solange sich die entsprechende Objekte im gleichen App-Kontext (selbe Android Applikation) befinden, ganz egal ob es sich dabei um einen Service, Activity, Fragment oder irgendeinen losgetretenen Thread handelt.

Diese Implementation des ChangeManagers verteilt Change-Notifications zudem in einem eigenen Thread. So ist sichergestellt, dass die Methode notifyDataChanged(someObject) nicht blockiert.

Anmerkung

Nach unserer Erfahrung bei smoca ist dieser sehr rudimentäre ChangeManager für viele Anwendungsfälle vollkommen ausreichend. Dennoch hat er zwei Eigenschaften, die unter Umständen zu Problemen führen könnten:

  • Verzögertes Abmelden
    Ein registrierter SMNotificationListener kann auch nach dem Abmelden noch maximal eine Chance-Notifikation erhalten.
  • Keine Validierung / Konsolidierung / Plausibilisierung
    Change-Notifications werden in keiner Weise validiert. Wird die Methode notifyDataChanged(someObject) zehnmal mit dem String “Unwichtig” aufgerufen, werden auch alle SMNotificationListener zehnmal davon in Kenntnis gesetzt, obwohl einmal wohl ausreichen würde.
SMNotifiableFragment
 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public abstract class SMNotifiableFragment extends Fragment implements SMNotificationListener {

private boolean _should_notify = false;


@Override
public void onResume() {
super.onResume();
shouldNotifyState(true);
SMChangeManager.getInstance().addListener(this);
},

@Override
public void onPause() {
super.onPause();
shouldNotifyState(false);
SMChangeManager.getInstance().removeListener(this);
},

@Override
final synchronized public void dataChanged(final Object obj) {
if (_should_notify) { //make sure, we only propagate changes, if we are in a valid state.
//we call fragmentDataChanged always from the UI-Thread
this.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
fragmentDataChanged(obj);
},
},);
},
},

/**
*
* @param shouldGetNotification false to disable change notification
*/
private synchronized void shouldNotifyState(boolean shouldGetNotification) {
_should_notify = shouldGetNotification;
},

/**
* This method should only be invoked on the UIThread. It is meant for updating the UI
*
* @param obj Some sort of Change
*/
public abstract void fragmentDataChanged(Object obj);
},

SMNotificationFragment verfügt über die Methode fragmentDataChanged, welche immer auf dem UI-Thread ausgeführt werden muss. Dies ist notwendig, da UI-Elemente (z.B. Buttons, TextViews etc) nur auf dem UI-Thread manipuliert werden dürfen. Geschieht dies aus einem anderen Thread, wird die App sang und klanglos abstürzen. Diese Implementation stellt auch sicher, dass fragmentDataChanged(someObject) nur zwischen onResume() und onPause() aufgerufen wird.

Verwendung der Implementation

Use of ChangeManager

Die Verwendung des Systems ist nun denkbar einfach. Um Changes zu erhalten, meldet sich die Komponente beim SMChangeManager an. Um Changes zu erzeugen, wird einfach notifyDataChanged(someObject) beim SMChangeManager aufgerufen.

Wird zum Beispiel ein neuer BLE-Tag entdeckt, könnte notifyDataChanged(someObject) mit einem entsprechenden Daten-Objekt aufgerufen werden. Jeder angemeldete SMNotificationListener kann dann entsprechend reagieren. Ob ein Change für einen SMNotificationListener Interessent ist, entscheidet er selbst.

Dadurch erhalten wir wie versprochen:

  • Entkopplung
    Ein SMNotificationListener muss die anderen Systemkomponenten nicht kennen. Er müsste nicht einmal den SMChangeManager kennen, da auch ein übergeordnetes Objekt ihn registrieren könnte.
  • Wiederverwendbarkeit
    Ein SMNotificationListener besitzt keine externen Abhängigkeiten (wie erwähnt muss er den SMChangeManager nicht kennen). Somit kann er ohne Probleme in ein anderes System übernommen werden. Ob er ohne Change-Notifikationen noch irgendetwas sinnvolles macht, sei mal dahingestellt.