Einfacher Aufbau und seriöse Informatik schließen sich nicht aus

Snap!: Bau’ deine eigenen Blöcke!
Kommentare

Scratch genießt unter Informatikern nicht gerade die beste Reputation: Die für Kinder vorgesehene Sprache ist zur Realisierung von komplexen Programmen ungeeignet. Mit der auf Scratch aufbauenden Programmiersprache Snap! will ein Forscherteam der UC Berkeley beweisen, dass sich einfacher Aufbau und seriöse Informatik nicht ausschließen.

Zur Motivation: Snap! entstand als Basissprache für einen Kurs über hohe Systemtechnik, der von bösen Zungen als Informatik für Nichtinformatiker zusammengefasst werden kann. Die Nutzung „echter“ Programmiersprachen wurde in diesem Bereich als zu schwierig empfunden, während Scratch andererseits zur Realisierung komplexer Aufgaben (Stichwort: Algorithmen) nur leidlich geeignet ist.

Snap! herbei

Für erste Schritte mit der blockbasierten Programmiersprache sind keine Downloads oder Installationen erforderlich – öffnen Sie den URL http://snap.berkeley.edu/snapsource/snap.html in einem aktuellen Browser, um sich an der Entwicklungsumgebung zu erfreuen. Snap! ist beim Browser nicht sonderlich anspruchsvoll – Tabelle 1 zeigt die Mindestanforderungen.

Chrome (Referenzbrowser) 43
Safari 8.4
Firefox 38
Edge 12
Opera 32
Internet Explorer 11 (teilweise)

Tabelle 1: Mindestanforderungen an die Entwicklungsumgebung für Snap!

Snap! arbeitet mit der in Abbildung 1 gezeigten IDE. Auf der linken Seite des Bildschirms findet sich die Toolbar, in der die Blöcke in acht Untergruppen aufgeteilt sind. Der Bereich in der Mitte dient zur eigentlichen Realisierung des Skripts, während der rechte Teil des Bildschirms für die Anzeige der Programmausgabe verantwortlich zeichnet.

Abb. 1: Die Snap!-IDE lebt komplett im Browser

Abb. 1: Die Snap!-IDE lebt komplett im Browser

Als erstes kleines Beispiel wollen wir eine Applikation realisieren, die den auf der rechten Seite des Bildschirms befindlichen Sprite langsam nach vorne bewegt. Ziehen Sie die in Abbildung 2 gezeigten Elemente Schritt für Schritt aus der Toolbar und assemblieren Sie sie nach dem von Microsoft Visio und Co. bekannten Verfahren. Das Durchsuchen der Kategorien wird dabei durch die Farben erleichtert: Jeder Block bekommt die Farbe „seiner“ Kategorie. Klicken Sie zu guter Letzt auf die in der oberen rechten Kante des Bildschirms eingeblendete Fahne, um das Programm zur Ausführung zu bringen.

Abb. 2: Mit diesem Programm bekommt der Sprite Beine

Abb. 2: Mit diesem Programm bekommt der Sprite Beine

Parameter und mehr

Jedes Skript beginnt mit einem Hat-Symbol: Es handelt sich um eine Art Einsprungspunkt, der den Einstieg in die Programmausführung beschreibt. Neben unserem hier verwendeten Autostarter gibt es auch fortgeschrittene Versionen, die nur bei bestimmten Ereignissen anspringen.

Das darunter stehende Move-Objekt ist ein Command-Block. Es handelt sich dabei um ein Objekt, das einen beliebigen Befehl repräsentiert – in unserem Fall die Bewegung um zehn Schritte. Die runden weißen Bereiche sind Parameter, deren Werte sich durch Anklicken verändern lassen. Je nach Typ des Parameterfelds können Sie sie entweder frei eingeben oder aus einer Liste wählen: Der Move-Block nimmt beliebige numerische Werte entgegen. Leider bewegt sich der Pfeil im Moment nach jedem Anklicken der Fahne nur ein einziges Mal. Zur Realisierung kontinuierlicher Bewegung brauchen wir eine Iteration – sie lässt sich, wie in Abbildung 3 gezeigt, durch einen Kontrollblock realisieren.

Abb. 3: „forever“ realisiert eine Endlosschleife

Abb. 3: „forever“ realisiert eine Endlosschleife

Wer die vorliegende Variante des Programms ausführt, stellt fest, dass der Pfeil den sichtbaren Bereich verlässt und auch nach dem Anhalten der Ausführung nicht an den Ursprungszustand zurückkehrt. Snap! ist – dies ist ein klares Zugeständnis an die didaktische Ausrichtung der Sprache – Sprite-basiert: Es handelt sich dabei um Objekte, die eine grafische Darstellung, Positionsinformationen und eines oder mehrere Skripte enthalten. In Snap! gibt es keinen Urzustand – Sprites behalten ihren Zustand auch beim Anhalten der Ausführung. Dies ist für uns im Moment kein Problem. Klicken Sie den Pfeil in der auf der unteren rechten Bildschirmkante befindlichen Sprite-Liste rechts an, und wählen Sie im daraufhin erscheinenden Kontextmenü die Option Show. Ein Klick auf das Pause-Symbol sorgt sodann dafür, dass der Sprite stehenbleibt.

C- oder gar kammförmige Blöcke dienen zur Realisierung konditionaler Programmausführung: Neben Schleifen gibt es auch eine If-else-Struktur. Wir wollen diese im nächsten Schritt zur Realisierung einer Bewegungsänderung einspannen, was zum in Abbildung 4 gezeigten Programm führt.

Abb. 4: Mit „if-else“ lässt sich konditionale Bewegung realisieren

Abb. 4: Mit „if-else“ lässt sich konditionale Bewegung realisieren

Rhomboide Blöcke realisieren Prädikate: Es handelt sich dabei um ein Objekt, das einen Entscheidungswert zurückliefert und als normale Zuweisung an Variablen nur wenig sinnvoll ist. Ovale Blöcke laufen unter der Bezeichnung „Reporter“: Sie liefern den von ihnen überwachten Wert zurück. Interessanterweise sind mathematische Operationen ebenfalls Reporter, die sich von ihren gewöhnlichen Kollegen nur in der Anzahl der Parameter unterscheiden. Ein weiterer netter Trick ist das Anklicken eines alleinstehenden Reporters – Snap! belohnt den Aufwand mit einem Flyout, das den aktuellen Inhalt des Ausdrucks anzeigt.

Die vorliegende Version des Programms bewegt den Sprite langsam nach vorne, um ihn beim Erreichen eines Grenzwerts spontan an den Anfangspunkt zurückzuwerfen. Das ist fürs Erste genug – komplexere Interaktionen setzen die Nutzung von Variablen voraus, die wir erst später betrachten werden.

Sprite Nr. 2 herbei

Eventorientierte Programmierung gilt – nicht nur – aus koppelungstheoretischer Sicht als Ideallösung: Wer ein komplexes System einmal auf diese Art und Weise entkoppelt hat, möchte die Methode nicht missen. In Snap! lassen sich Skripte über Ereignisse miteinander verbinden.

Schon aus didaktischen Gründen wollen wir unser Programm an dieser Stelle um einen zweiten Sprite ergänzen. Er soll mit dem Pfeil durch das Absetzen von Ereignissen kommunizieren und nebenbei in Sprechblasen Informationen über seinen „Geisteszustand“ präsentieren. Dazu klicken Sie auf das Pfeilsymbol über der Sprite-Liste. Snap! generiert daraufhin einen neuen Pfeil-Sprite, der an einem beliebigen Ort am Bildschirm in einer beliebigen Farbe erscheint. Passen Sie seine Position bei Bedarf per Drag-and-Drop an, um ihn abseits der Bewegung des Haupt-Sprites zu platzieren. Ab diesem Zeitpunkt zeigt der Sprite-Editor nur noch die Skripte an, die zum gerade selektierten Sprite gehören – wundern Sie sich also nicht, wenn das Bewegungsskript plötzlich unsichtbar wird.

Fügen Sie im Skriptbereich des neu angelegten Pfeils sodann das in Abbildung 5 gezeigte Skript ein – es könnte theoretisch auch im Haupt-Sprite stehen, was didaktisch allerdings weniger interessant wäre.

Abb. 5: Die Ereignisse harren ihrer Absendung

Abb. 5: Die Ereignisse harren ihrer Absendung

An dieser Stelle findet sich keine Raketenwissenschaft: Unser Programm emittiert regelmäßig ein Event namens Toggle. Zudem wird zu diesem Zeitpunkt auch eine Meldung ausgegeben, die in Form einer Sprechblase am Bildschirm erscheint. Die Methode say() nimmt einen zweiten Parameter entgegen, der die Dauer der Darstellung beschreibt – Sprechblasen verschwinden in Snap! automatisch vom Bildschirm, sobald sie ihre Haltbarkeitsdauer überlebt haben.

Snap! verwaltet Nachrichten über ihre Namen: Es ist nicht möglich, zwei oder mehr Signale mit demselben Namen zu haben. Das Anlegen eines neuen Signals erfolgt über die New…Option im beim Anklicken des Parameterfelds aufscheinenden Kontextmenü.

Variablen herbei

Abb. 6: „moveDir“ ist einsatzbereit

Abb. 6: „moveDir“ ist einsatzbereit

Damit können wir wieder in den Code des ersten Sprites zurückkehren. Variablen werden in einer eigenen Rubrik der Toolbar verwaltet – neue Wertspeicher lassen sich durch Anklicken des grauen Buttons MAKE A VARIABLE anlegen. Snap! fragt in diesem Fall nach Variablenname und Gültigkeitsraum: Neben einer Sprite-lokalen Variable können Sie auch ein globales Feld anlegen. Datentypen sind Snap! im Großen und Ganzen fremd: Neben einem numerischen, einem textuellen und einem boole‘schen Typ gibt es noch einige Verweistypen, die aber nur bei der Erstellung eigener Blöcke von Relevanz sind. Wie dem auch sei – die neu angelegte Variable erscheint sodann wie in Abbildung 6 gezeigt unter dem Button.

Zudem sind nun mehrere Skripte erforderlich – jede Programmroutine beginnt mit einem eigenen Hat-Block, der die Anwurfbedingung beschreibt. Abbildung 7 zeigt, wie das Programm als Ganzes aussieht.

Abb. 7: Das Entgegennehmen von Signalen setzt mehrere Subskripte voraus

Abb. 7: Das Entgegennehmen von Signalen setzt mehrere Subskripte voraus

Zudem sind nun mehrere Skripte erforderlich – jede Programmroutine beginnt mit einem eigenen Hat-Block, der die Anwurfbedingung beschreibt. Abbildung 7 zeigt, wie das Programm als Ganzes aussieht.

Im eigentlichen Bewegungsskript sind keine wesentlichen Änderungen zu bemerken: Der zurückgelieferte Wert dient nun zur Festlegung der Bewegungsrichtung. Das Togglen der Variable moveDir wirkt auch nur auf den ersten Blick komplex – man gewöhnt sich in der Praxis überraschend schnell an die Art der grafischen Darstellung.

An dieser Stelle bietet sich ein Vergleich zwischen Snap! und klassischen Programmiersprachen an: Das von Gegnern grafikorientierter Systeme gern vorgebrachte Argument der Verschwendung von Bildschirmplatz trifft in der Praxis nicht (oder nur wenig) zu. Es ist unwahrscheinlich, dass eine klassische Implementierung eines Signals in die in Abbildung 7 verwendete Bildschirmfläche passt.

Parallelisiere mich

Auch wenn unser Programm funktioniert, ist das Verhalten des zweiten Sprites unbefriedigend. Der say-Befehl blockiert die Programmausführung, weshalb die Zykluszeit statt vier sechs Sekunden beträgt. Als einfachste Lösung für dieses Problem bietet sich die Nutzung des ohnehin emittierten Signals an: Da Snap! Signale normalerweise asynchron verarbeitet, muss die say-Routine einfach wie in Abbildung 8 gezeigt in ihren eigenen Hat wandern.

Abb. 8: Aber jetzt asynchron!

Abb. 8: Aber jetzt asynchron!

Alternativ dazu gibt es den say-Befehl, der die Nachricht anzeigt und sofort retourniert. Bei seiner Nutzung stellt sich allerdings die Frage, wie man die Blase wieder entfernt – ist die Delta-Meldung permanent am Bildschirm, so hat man als Entwickler auch nichts davon.

Wirklich befriedigend ist derweil keine der beiden Methoden: Zur vollständigen Lösung des Problems müssen wir auf einen der Sprungbefehle von Snap! zurückgreifen. Die Sprache stellt drei verschiedene Kommandos zur Verfügung: run führt die in ihm enthaltenen Blöcke synchron aus, während launch eine asynchrone Abarbeitung anweist. Mit call kann man eine Art Funktion realisieren – im Moment reicht uns die Nutzung von launch aus, die das Eliminieren der Signalaufrufbeziehung ermöglicht.

Ein eigener Block

Ältere Dokumente bezeichnen Snap! als BYOB (Build Your Own Blocks) – im Enterprise-Bereich tätige Entwickler schließen ob des Begriffs BYOD (Bring Your Own Device) darauf, dass sich die Sprache von Scratch unter anderem durch die Möglichkeit der Realisierung eigener Blöcke unterscheidet.

Das Erzeugen neuer Elemente beginnt mit dem grauen Button MAKE A BLOCK, der am unteren Ende der Variables-Rubrik auf seine Nutzer wartet. Nach dem Anklicken erscheint ein Assistent, der Sie nach der Art des zu erzeugenden Blocks fragt: Neben normalen Kommandoblöcken dürfen Sie auch Reporter und Prädikate generieren. Wie bei der Variable gilt auch hier, dass der Gültigkeitsbereich nach Belieben festgelegt werden kann.

Im nächsten Schritt erscheint ein Pop-up-Fenster mit einem Hat-Block. Dieser dient als Mutterelement: Das Anklicken des Plus-Zeichens erlaubt das Anlegen von Eingabefeldern, die später vom Aufrufer zu bevölkern sind. Achten Sie dabei darauf, dass der Dialog durch das Anklicken des Pfeils in den Vollfunktionsmodus versetzt werden muss.

Fügen Sie im nächsten Schritt eine neue numerische Variable namens HowOften ein. An dieser Stelle lauert eine kleine Falle: Parameter erscheinen nicht in der Variablenliste der Toolbox, die für die sonstige Erstellung des Blocks zuständig ist. Reporter lassen sich nur durch Anklicken des Mutterreporters im Hat erstellen – eine Zitterfunktion würde wie in Abbildung 9 gezeigt aussehen. Klicken Sie sodann auf OK, um den Dialog zu schließen – der Block erscheint je nach Kategorie entweder unter Variables oder in der ihm zugewiesenen Blockgruppe.

Abb. 9: Wer diesen Block ausführt, sieht einen zitternden Sprite

Abb. 9: Wer diesen Block ausführt, sieht einen zitternden Sprite

Snap! erlaubt das Exportieren von selbsterstellten Blöcken, die sodann als .xml-Dateien zur Verfügung stehen und sich in andere Projekte laden lassen. Der Exportassistent lässt sich durch Anklicken des Dateisymbols auf der Oberseite des Bildschirms aktivieren: Wählen Sie danach die Option „Export blocks“ aus. Nach dem Aussuchen der zu verpackenden Teile bietet der Browser eine .xml-Datei an, die für die spätere Nutzung auf der lokalen Workstation gespeichert werden kann.

An die Hardware

JavaScript kann aufgrund diverser Beschränkungen nicht direkt auf die Hardware des Hosts zugreifen. Zur Umgehung dieses Problems bietet sich die Nutzung eines HTTP-Servers an, der die benötigte Ressource über ein REST-API exponiert. Auch dafür wollen wir ein kleines Beispiel zusammenstellen, dessen nativer Teil aus einer halbwegs aktuellen Version von Qt besteht. Rein theoretisch könnten Sie hier natürlich auch auf eine andere Programmiersprache setzen – es ist im Interesse des Kodierungsaufwands empfehlenswert, auf das Vorhandensein eines HTTP-Parsers zu achten. Nach der Erstellung eines neuen Projektskeletts auf Basis der Vorlage Console Application muss die .pro-Datei um einen Verweis auf das Netzwerkmodul erweitert werden:

QT += core network
QT -= gui

Im nächsten Schritt folgt die Erzeugung des eigentlichen Servers. Wir greifen hier auf die von Stefan Frings entwickelte QtWebApp-Bibliothek zurück, die sich als Quasistandard im Bereich Webserver für Qt etabliert hat. Entpacken Sie das Archiv in das Projektstammverzeichnis und binden Sie es in die Kompilation des Hauptprojekts ein:

include(QtWebApp/QtWebApp/qtservice/qtservice.pri)
include(QtWebApp/QtWebApp/httpserver/httpserver.pri)
include(QtWebApp/QtWebApp/logging/logging.pri)

Als nächster Akt sind Anpassungen in main.cpp notwendig. QtWebApp bezieht die diversen Konfigurationsparameter prinzipiell aus einer Qsettings-Instanz: Da der Zugriff auf niedere Ports Root-Rechte voraussetzen würde, lösen wir das Problem durch Setzen des Port-Attributs:

#include "httplistener.h"
#include "susrequesthandler.h"
int main(int argc, char *argv[])
{
  QCoreApplication a(argc, argv);
  QSettings* settings=new QSettings("test");
  settings->setValue("port", 8080);
  SUSRequestHandler* myHandler=new SUSRequestHandler();
  HttpListener * myListener=new HttpListener(settings, myHandler, &a);
  return a.exec();
}

Die für die eigentliche Kommunikation zuständige Intelligenz sitzt derweil in der Klasse SUSRequestHandler, die ein vom im Framework enthaltenen HttpRequestHandler abgeleitetes Q_Object sein muss. Der für uns relevante Teil des Objekts ist die service()-Methode, die folgendermaßen aussieht:

void SUSRequestHandler::service(HttpRequest& request, HttpResponse& response)
{
  response.write("SNAP:Das ist eine Meldung\nMit etwas Zusatztext\nDer uns nicht interessiert!");
}

Damit sind die Arbeiten an der Konsolenapplikation abgeschlossen – klicken Sie auf den Startbutton, um sie zu aktivieren und den Server für Zugriffe bereit zu machen. In praktischen Applikationen würde das request-Objekt ausgewertet werden, um weitere Informationen über die Art der eingegangenen Anfrage zu erhalten.

Auf Seiten von Snap! erfolgt der Zugriff über das in der Rubrik Sensing befindliche Http-Element. Es nimmt einen URL entgegen, der sodann bei jedem Aufruf des betreffenden Blocks abgefragt wird. Das Resultat ist ein String, der die kompletten angelieferten Daten samt Request Header enthält – zu seiner Unterteilung bietet sich die Nutzung eines Split-Blocks an.

Leider funktioniert das in der Praxis nicht: Wer den vorliegenden Server aus Snap! heraus anzusprechen sucht, wird fürs Erste mit einem leeren Resultat belohnt. Das Öffnen der Webkonsole des jeweiligen Browsers verrät sodann das in Abbildung 10 gezeigte Problem.

Abb. 10: Wie so oft: CORS sorgt für Ärger

Abb. 10: Wie so oft: CORS sorgt für Ärger

Der einfachste Weg zur Lösung des Problems besteht darin, QtWebApp zum Senden eines AccessControlAllowOrigin-Attributs zu animieren. Dazu ist folgende Änderung an service() erforderlich:

void SUSRequestHandler::service(HttpRequest& request, HttpResponse& response)
{
  response.setHeader("Content-Type", "text/html");
  response.setHeader("Access-Control-Allow-Origin", "*");
  . . .
}

Damit können wir wieder zur eigentlichen Realisierung des Codes zurückkehren. Für die Verarbeitung von Listen empfiehlt sich die Nutzung einer erweiterten Variante von Snap!, die einige zusätzliche Blöcke mitbringt. Mit ihnen präsentiert sich die Lösung unseres Problems wie in Abbildung 11 gezeigt – aus Platzgründen wurde der String-Vergleich hier nur vergleichsweise simpel implementiert.

Abb. 11: Der Zugriff auf den lokalen Server funktioniert problemlos

Abb. 11: Der Zugriff auf den lokalen Server funktioniert problemlos

An dieser Stelle findet sich keine Raketenwissenschaft: Nach dem Drücken des Flaggensymbols bevölkern wir eine lokale Variable mit den Ergebnissen des Http-Elements, die sodann gesplittet werden. Die entstehenden Fragmente müssen im nächsten Schritt nach den Wunschdaten durchforstet werden – eine Aufgabe, die in iterativen Sprachen zum täglich Brot jedes Entwicklers gehört.

Achtung, Cross-Site Scripting

Der Zugriff auf echte Webseiten ist noch undankbarer: Wenn die Zielseite snap.berkeley.edu nicht per CORS freigibt, blockieren die meisten Browser das Abrufen von Inhalten. Zur Umgehung des Problems empfehlen die Entwickler die Nutzung eines Proxy – die in der Dokumentation enthaltenen Beispiele nutzen corsproxy.com.

Diese Vorgehensweise wird vom Snap!-Entwicklerteam zur Bereitstellung diverser Hardwareressourcen benutzt: Auf der Projektwebsite gibt es zum Beispiel ein Programm, das den Zugriff auf einige Arduino-Varianten ermöglicht. Eine weitere interessante Anwendung ist das Realisieren schnell rechnender nativer Module: Für komplexe Berechnungen ist C++ nun mal besser geeignet.

Rechne wirklich schnell!

Die in Grundlagenlehrbüchern gern gezeigten Beispiele für Selektionen und Co. nutzen die Leistung des zugrunde liegenden Systems nur in den seltensten Fällen aus: Der Gutteil der Rechenleistung wird für die Aktualisierung der am Bildschirm befindlichen Informationen verwendet. Ein grafikorientiertes System wie Snap! leidet unter dieser Beschränkung naturgemäß besonders, muss doch permanent eine Vielzahl komplexer Elemente animiert werden.

Die Entwickler bieten mit dem Turbo Mode eine Alternativlösung an: Ist er aktiviert, so unterlässt die Runtime das Aktualisieren der Position der diversen anzuzeigenden Elemente. Er lässt sich entweder von Hand über das Optionsmenü oder direkt aus Programmen heraus aktivieren. Achten Sie darauf, dass das Aktivieren des Turbo Modes Sprite-Kollisionen und ähnliche primär grafische Prozesse außer Gefecht setzt: Wenn die Engine Ihres Programms darauf basiert, können Sie den Beschleuniger nicht einsetzen.

Intelligente Transpilierung

In der Anfangszeit der Informatik experimentierten Entwickler mit Methoden, um Flussdiagramme in ausführbaren Code zu verwandeln. Das Einbinden von Codefragmenten erwies sich dabei als zielführendste Methode: Das Programm wird in diesem Fall als Baum angenommen, der von innen nach außen in einen String reduziert wird.

Zur Nutzung dieses noch als experimentell bezeichneten Features ist ein Blick auf das Zahnrad auf der Oberseite des Bildschirms erforderlich: Wählen Sie im daraufhin erscheinenden Kontextmenü die Option „Codification Support“ aus. Ab diesem Zeitpunkt ist der Block Variables um vier Blöcke erweitert, die eine Zuweisung zwischen Block und Code-Snippet ermöglichen – im Internet finden sich Beispiele, die eine partielle Transpilierung in JavaScript und SmallTalk ermöglichen.

Mehr erfahren

Snap! hat im Moment keine sonderlich große Nutzerschaft: Im Fall technischer Probleme ist das ungünstig, weil die Suche nach Hilfe mitunter Zeit in Anspruch nimmt. Hier gibt es immerhin ein kleines Diskussionsforum; Fehler sollten im GitHub-Repository gemeldet werden. Das online bereitstehende Handbuch sollte in jedem Fall erste Anlaufstelle sein – es beschreibt die Sprache mehr oder weniger vollständig und wird von der bereitstehenden Highlight-Liste ergänzt. Zu einzelnen Blocks findet sich zudem im Kontextmenü Weiteres – wählen Sie einfach die Option „Help“ aus. Aufgrund der Nähe zu Scratch taugt auch das Scratch-Wiki als Referenz: Achten Sie allerdings darauf, dass sich nicht jedes Kommando in beiden Sprachen zu 100 Prozent analog verhält.

Fazit

Snap wird C++ und Co. in den nächsten fünf bis zehn Jahren mit Sicherheit nicht verdrängen. Dennoch ist der Mensch ein zutiefst grafisches Wesen; Code wird zudem im Laufe der Kompilation sowieso zu einem Baum. Es wäre – nicht nur aus wissenschaftlicher Sicht – hochinteressant, wenn die Snap!-Entwickler auf Sprite-Paradigma und Browserumgebung verzichten könnten: Wie bei der Ausrichtung der Weltkarte gilt auch in der Informatik, dass die Programmierung in „Sprache“ kein Naturgesetz darstellt.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -