Gute, schlechte und hässliche Benutzeroberfläche in der mobilen Entwicklung | von Michal Klimczak Januar 2022

Kotlin und Swift haben vielleicht viel funktionale Magie in das Spiel eingeführt, aber da sie die natürlichen Erben von Java und Obj-Ca sind, haben sie starke OOP-Grundlagen. Vererbung ist immer noch ein grundlegendes Konzept, und Schnittstellen / Protokolle spielen eine wichtige Rolle im Toolkit eines mobilen Entwicklers.
Aber wie bei allem gibt es echte Gründe, sie zu verwenden, und es gibt falsche. Beim Code-Design bemühen wir uns, bewährte Verfahren zu befolgen, auch wenn wir uns nicht zu 100 % sicher sind, warum sie gut sind. Und obwohl es überhaupt keine schlechte Sache ist, kann es zu subtilem Missbrauch führen. Wenn wir uns zum Beispiel an die protokollorientierte Programmierung gewöhnen, verspüren wir möglicherweise das Bedürfnis, sie auf jedes einzelne Problem anzuwenden – es ist ein weit verbreitetes Vorurteil, Alles sieht aus wie ein Nagel, wenn man einen Hammer hält.
Sehen wir uns also die verschiedenen Gründe für die Verwendung von Schnittstellen und Protokollen an.
Polymorphismus
Polymorphismus ist genau der Grund, warum Schnittstellen geschaffen wurden. Die häufigsten Beispiele sind Modellklassen, z. Angenommen, Sie haben eine Chat-App und verarbeiten verschiedene Arten von Nachrichten.
Auf diese Weise können Sie die Liste handhaben Message
Gegenstände und z.B. sortiere sie nach ihren timestamp
aber immer noch in der Lage sein, zwischen Arten von Text- und Bildnachrichten zu unterscheiden. Ohne Schnittstelle müssten Sie auf hässliche Lösungen wie eine Mehrzweckklasse mit vielen Feldern zurückgreifen, die zurückgesetzt werden können.
Mehrfachvererbung
Weder Kotlin noch Swift erlauben Mehrfachklassenvererbung (als Lösung für Diamantproblem), aber seine Vorteile können durch Schnittstellen erreicht werden.
Auf Android, wenn Sie feststellen, dass Sie einigen eine Menge unabhängiger Verpflichtungen hinzufügen Base
Stunden, als BaseActivity
oder BaseViewModel
Möglicherweise möchten Sie ein Upgrade von einfacher Vererbung auf Komposition über eine in Erwägung ziehen Schnittstelle mit Standardimplementierung.
Unter iOS können Sie mithilfe der Protokollerweiterung etwas Ähnliches tun.
In diesen Beispielen können Sie die Kotlina-Erweiterungsfunktion (fun Activity.showToast(...)
) und Protokollerweiterungen zu UIView
direkt (extension UIView ...
), aber die Einführung einer Schnittstelle / eines Protokolls gibt Ihnen mehr Kontrolle – die Funktion ist auf Klassen beschränkt, die davon erben, sodass die Erweiterung nicht an die gesamte Codebasis weitergegeben wird.
Grenzen zwischen Schichten
In seiner Pure Architecture schlägt Uncle Bob eine Struktur vor, die aus bogenartigen Schichten besteht. Die eigentliche Grundlage dieses Paradigmas ist die Regel der Abhängigkeit – die äußeren Schichten können Wissen über die inneren Schichten haben, aber die inneren Schichten sollten nichts über die äußeren wissen.
Reine Architektur ist in der mobilen Entwicklung weit verbreitet, aber eine Sache, die oft missverstanden wird, ist, dass sich die Datenschicht außerhalb der Domänenschicht befindet. Die Geschäftslogik muss nicht wissen, wie Daten geliefert werden, sondern muss nur ihre Eingaben definieren, normalerweise über Repository
Schnittstelledie über eine Datenschicht implementiert werden kann.
Die Trennung der Geschäftslogik und ihre vollständige Unabhängigkeit von Ansichten und Daten kann große Vorteile haben. Das wird Ihnen jeder sagen, der einen Antrag mit White Labels mit unterschiedlichen Zielen unter Verwendung unterschiedlicher Datenquellen gestellt hat. Wir sollten jedoch die Kosten für übermäßiges Engineering stark berücksichtigen – in vielen mobilen Anwendungen gibt es wirklich nicht viel Geschäftslogik und wir könnten mit einem Haufen trauriger einzeiliger UseCase-Klassen enden, die Daten vom Repository nach ViewModel übertragen.
Dieser Punkt wird erweitert Das hässliche Abschnitt etwas mehr.
Doppeltest
Historisch gesehen waren Schnittstellen unvermeidlich, wenn wir Testduplikate wie haben wollten er spottet ich Fälschungen bei Unit-Tests. Es könnte jedoch argumentiert werden, dass dies eher ein Missbrauch dieser Sprachstruktur als eine legitime Verwendung war. In Java war dies einfach eine Einschränkung des Spott-Frameworks.
In Swift könnte ein protokollbasierter Scheintest so aussehen:
Dies ist jedoch nicht erforderlich. Wir können ein einfaches verwenden class
Pro Grinder
ich GrinderMock
Ich kann davon erben. Ein möglicher Nachteil ist, dass es nicht kompilieren würde, wenn Grinder
Er war struct
Dies ist der endgültige Typ, von dem nicht geerbt werden kann.
Ähnlich in Kotlin, in diesem Fall muss die Schnittstelle nicht verwendet werden. Obwohl alle Klassen standardmäßig endlich sind, mögen sich spöttische Frames verspotten oder Mockito kann mit dem endgültigen Typ leicht gehandhabt werden.
Rückrufe
Ein gängiges Muster in der frühen Android-Java-Welt war die Einführung einer Callback-Schnittstelle, die anonym wie folgt implementiert wurde:
Dies ist in Kotlin nicht mehr idiomatisch. Funktionen sind Typen in Kotlin, sodass Sie sie einfach als Variablen weitergeben können
class ListAdapter(val itemClickListener : () -> Unit) { ...
Oder gezielt einsetzen funktionale (SAM) Schnittstelle Typ, so:
fun interface OnClickListener { fun invoke() }
“Weil ich es leicht durch eine andere Implementierung ersetzen kann”
Okay, ich gebe zu, das ist wohl der Hauptgrund, warum ich diesen Artikel überhaupt schreibe. Es ist sehr üblich, diese Antwort zu erhalten, wenn man nach dem Grund für das Schreiben gefragt wird einzige Implementierungsschnittstelle. Manchmal wird es als „Reduzierung von Fusionen“ umformuliert, was sicherlich schlau klingt.
Verstehen Sie mich nicht falsch, die Reduzierung von Fusionen kann sehr sinnvoll sein, z. oben erwähnt Grenzen zwischen Schichten Das Beispiel ist legitim. Aber selbst der Rat von Onkel Bob sollte nicht als Evangelium verstanden werden, jede mögliche Lösung sollte im Kontext des Problems betrachtet werden, das wir zu lösen versuchen.
Daran müssen wir uns alle erinnernclass
es hat bereits eine eigene Schnittstelle – dies sind die öffentlichen Methoden, die es präsentiert, und ihre Ergebnisse. Der Schlüssel zum Entwerfen einer guten Schnittstelle ist das Denken in Bezug auf ihre Ein- und Ausgänge, die Verantwortung dieser Klasse. Ein zusätzlicher Hinweis: Unit-Tests sind ein großartiges Werkzeug, um diese Art des Denkens zu fördern.
Am Ende des Tages wollen wir nur einen Wartungscode schreiben. Wie im Zitat zusammengefasst Pragmatischer Programmierer:
Wenn Sie auf ein Problem stoßen, beurteilen Sie, wie lokalisiert die Reparatur ist. Ändern Sie nur ein Modul oder sind die Änderungen über das ganze System verstreut? Wenn Sie eine Änderung vornehmen, wird dadurch alles behoben oder treten auf mysteriöse Weise andere Probleme auf?
Schließen Sie die Schnittstelle von der Implementierung aus formal, mit nur zwei getrennten Typen, ist keine Wunderwaffe, um dies zu erreichen. Getrennt haben interface
oder protocol
es hat immer seinen Preis – zunehmende kognitive Komplexität, umständlichere Codenavigation usw. Wir sollten immer zwei Dinge bedenken:
- wenn es wirklich die Konnektivität reduziert – unsere Schnittstellen können immer noch schlecht gestaltet sein, selbst wenn sie von der Implementierung getrennt sind;
- wenn die Reduzierung der Fusion die Kosten wert ist.
Wenn Sie bis hierher gelesen haben und eine einfache Zusammenfassung dieses Artikels wünschen, holen Sie sich diese rote Fahne 🚩: Wenn Sie eine Schnittstelle mit nur einer Klasse entwerfen, die davon erbt, möchten Sie vielleicht innehalten und darüber nachdenken.