Yhtenäinen arkkitehtuuri - yksinkertaisempi tapa rakentaa kokonaisia ​​sovelluksia

Nykyaikaisissa täyden pinon sovelluksissa - kuten yhden sivun sovelluksissa tai mobiilisovelluksissa - on yleensä kuusi kerrosta

  • tietojen käyttö
  • taustamalli
  • API-palvelin
  • API-asiakas
  • käyttöliittymän malli
  • ja käyttöliittymä.

Suunnittelemalla tällä tavalla voit saavuttaa joitakin hyvin suunnitellun sovelluksen ominaisuuksia, kuten huolenaiheiden erottamisen tai löysän kytkennän.

Mutta tämä ei tule ilman haittoja. Se yleensä maksaa muiden tärkeiden ominaisuuksien, kuten yksinkertaisuuden, koheesion ja ketteryyden, hinnalla.

Näyttää siltä, ​​että meillä ei ole kaikkea. Meidän on tehtävä kompromisseja.

Ongelmana on, että kehittäjät yleensä rakentavat jokaisen kerroksen täysin erilaiseksi maailmaksi yksin.

Vaikka otat tasot käyttöön samalla kielellä, ne eivät voi kommunikoida keskenään kovin helposti.

Tarvitset paljon liimakoodia yhdistääksesi ne kaikki, ja verkkotunnusmalli kopioidaan pinon yli. Tämän seurauksena kehityksen ketteryytesi kärsii dramaattisesti.

Esimerkiksi yksinkertaisen kentän lisääminen malliin edellyttää usein pinon kaikkien kerrosten muokkaamista. Tämä voi tuntua hieman naurettavalta.

Olen ajatellut paljon tästä ongelmasta viime aikoina. Ja uskon löytäneen tien ulos.

Tässä on temppu: varmasti, sovelluksen kerrokset on erotettava "fyysisesti". Mutta niitä ei tarvitse erottaa "loogisesti".

Yhtenäinen arkkitehtuuri

Perinteinen vs. yhtenäinen arkkitehtuuri

Kohdekohtaisessa ohjelmoinnissa, kun käytämme perintöä, saamme joitain luokkia, jotka voidaan nähdä kahdella tavalla: fyysinen ja looginen. Mitä tarkoitan tällä?

Kuvitellaan, että meillä on luokka, Bjoka perii luokan A. Sitten, Aja Bvoidaan nähdä kahtena fyysisenä luokkana. Mutta loogisesti niitä ei eroteta toisistaan, ja Bne voidaan nähdä loogisena luokkana, joka säveltää ominaisuudet Aomilla ominaisuuksillaan.

Esimerkiksi, kun kutsumme menetelmää luokassa, meidän ei tarvitse huolehtia siitä, onko menetelmä toteutettu tässä luokassa tai vanhempien luokassa. Soittajan näkökulmasta on vain yksi luokka, josta on huolta. Vanhempi ja lapsi yhdistetään yhdeksi loogiseksi luokaksi.

Entä saman lähestymistavan soveltaminen sovelluksen tasoihin? Eikö olisikaan hienoa, jos esimerkiksi käyttöliittymä voisi jotenkin periä taustasta?

Tällöin käyttöliittymä ja taustajärjestelmä yhdistetään yhdeksi loogiseksi kerrokseksi. Ja se poistaisi kaikki viestintä- ja jakamisongelmat. Itse asiassa taustajärjestelmän luokat, määritteet ja menetelmät olisivat suoraan käytettävissä käyttöliittymästä.

Emme tietenkään halua altistaa koko taustaa käyttöliittymälle. Mutta sama koskee luokan perintöä, ja on olemassa tyylikäs ratkaisu, jota kutsutaan "yksityisomaisuudeksi". Vastaavasti taustakuva voisi valikoivasti paljastaa joitain määritteitä ja menetelmiä.

Mahdollisuus tarttua sovelluksen kaikkiin kerroksiin yhdestä yhtenäisestä maailmasta ei ole pieni asia. Se muuttaa pelin kokonaan. Se on kuin siirtyminen 3D-maailmasta 2D-maailmaan. Kaikki on paljon helpompaa.

Perintö ei ole pahaa. Kyllä, sitä voidaan käyttää väärin, ja joillakin kielillä se voi olla melko jäykkä. Mutta oikein käytettynä se on korvaamaton mekanismi työkalupakissamme.

Meillä on kuitenkin ongelma. Sikäli kuin tiedän, mikään kieli ei salli luokkien perimistä useissa suoritusympäristöissä. Mutta me olemme ohjelmoijia, eikö vain? Voimme rakentaa kaiken mitä haluamme, ja voimme laajentaa kieltä uusien ominaisuuksien tarjoamiseksi.

Mutta ennen kuin pääsemme siihen, hajotetaan pino, jotta voimme nähdä, kuinka kukin kerros mahtuu yhtenäiseen arkkitehtuuriin.

Tietojen käyttö

Suurimmalle osalle sovelluksista tietokanta voidaan tiivistää jonkinlaisella ORM: llä. Joten kehittäjän näkökulmasta ei ole mitään tiedonsiirtokerrosta, josta pitäisi huolehtia.

Kunnianhimoisempia sovelluksia varten joudumme ehkä optimoimaan tietokantamalleja ja pyyntöjä. Mutta emme halua sekoittaa taustamallia näillä huolenaiheilla, ja tässä voi olla sopiva lisäkerros.

Rakennamme tiedonsiirtokerroksen optimointikysymysten toteuttamiseksi, ja tämä tapahtuu yleensä kehitysvaiheen loppupuolella, jos se tapahtuu.

Joka tapauksessa, jos tarvitsemme tällaisen kerroksen, voimme rakentaa sen myöhemmin. Monikerroksisen perinnön avulla voimme lisätä tiedonsiirtokerroksen taustamallitason päälle melkein ilman muutoksia olemassa olevaan koodiin.

Taustamalli

Tyypillisesti backend-mallikerros hoitaa seuraavat vastuut:

  • Verkkotunnusmallin muotoilu.
  • Liiketoimintalogiikan toteuttaminen.
  • Lupamekanismien käsittely.

Suurimmalle osalle backend-ohjelmia on hienoa toteuttaa ne kaikki yhdessä kerroksessa. Mutta jos haluamme käsitellä joitain huolenaiheita erikseen, esimerkiksi haluamme erottaa valtuutukset liiketoimintalogiikasta, voimme toteuttaa ne kahdessa kerroksessa, jotka perivät toisiltaan.

API-kerrokset

Käyttöliittymän ja taustajärjestelmän yhdistämiseksi rakennamme yleensä web-sovellusliittymän (REST, GraphQL jne.), Mikä vaikeuttaa kaikkea.

Web-sovellusliittymä on toteutettava molemmilta puolilta: API-asiakas käyttöliittymässä ja API-palvelin taustalla. Se on kaksi ylimääräistä tasoa, joista on syytä huolehtia, ja se johtaa yleensä koko verkkotunnusmallin kopiointiin.

Verkkosovellusliittymä ei ole muuta kuin liimakoodi, ja se on tuska perässä rakentaa. Joten, jos voimme välttää sen, se on valtava parannus.

Onneksi voimme hyödyntää uudelleen kerrosten välistä perintöä. Yhdistetyssä arkkitehtuurissa ei ole web-sovellusliittymää, jota rakentaa. Ainoa mitä meidän on tehtävä, on periä käyttöliittymämalli taustamallista, ja olemme valmiit.

Verkkosovellusliittymän rakentamiseen on kuitenkin edelleen hyviä käyttötapoja. Silloin meidän on paljastettava taustajärjestelmä joillekin kolmannen osapuolen kehittäjille tai kun meidän on integroitava joihinkin vanhoihin järjestelmiin.

Mutta olkaamme rehellisiä, useimmissa sovelluksissa ei ole tällaista vaatimusta. Ja kun he tekevät, se on helppo käsitellä jälkikäteen. Voimme yksinkertaisesti ottaa web-sovellusliittymän käyttöön uudessa tasossa, joka periytyy backend-mallikerroksesta.

Lisätietoja tästä aiheesta löytyy tästä artikkelista.

Käyttöliittymän malli

Since the backend is the source of truth, it should implement all the business logic, and the frontend should not implement any. So, the frontend model is simply inherited from the backend model, with almost no additions.

User Interface

We usually implement the frontend model and the UI in two separate layers. But as I showed in this article, it is not mandatory.

When the frontend model is made of classes, it is possible to encapsulate the views as simple methods. Don't worry if you don't see what I mean right now, it will become clearer in the example later on.

Since the frontend model is basically empty (see above), it is fine to implement the UI directly into it, so there is no user interface layer per se.

Implementing the UI in a separate layer is still needed when we want to support multiple platforms (e.g., a web app and a mobile app). But, since it is just a matter of inheriting a layer, that can come later in the development roadmap.

Putting Everything Together

The unified architecture allowed us to unify six physical layers into one single logical layer:

  • In a minimal implementation, data access is encapsulated into the backend model, and the same goes for UI that is encapsulated into the frontend model.
  • The frontend model inherits from the backend model.
  • The API layers are not required anymore.

Again, here's what the resulting implementation looks like:

Perinteinen vs. yhtenäinen arkkitehtuuri

That's pretty spectacular, don't you think?

Liaison

To implement a unified architecture, all we need is cross-layer inheritance, and I started building Liaison to achieve exactly that.

You can see Liaison as a framework if you wish, but I prefer to describe it as a language extension because all its features lie at the lowest possible level — the programming language level.

So, Liaison does not lock you into a predefined framework, and a whole universe can be created on top of it. You can read more on this topic in this article.

Behind the scene, Liaison relies on an RPC mechanism. So, superficially, it can be seen as something like CORBA, Java RMI, or .NET CWF.

But Liaison is radically different:

  • It is not a distributed object system. Indeed, a Liaison backend is stateless, so there are no shared objects across layers.
  • It is implemented at the language-level (see above).
  • Its design is straightforward and it exposes a minimal API.
  • It doesn't involve any boilerplate code, generated code, configuration files, or artifacts.
  • It uses a simple but powerful serialization protocol (Deepr) that enables unique features, such as chained invocation, automatic batching, or partial execution.

Liaison starts its journey in JavaScript, but the problem it tackles is universal, and it could be ported to any object-oriented language without too much trouble.

Hello Counter

Let's illustrate how Liaison works by implementing the classic "Counter" example as a single-page application.

First, we need some shared code between the frontend and the backend:

// shared.js import {Model, field} from '@liaison/liaison'; export class Counter extends Model { // The shared class defines a field to keep track of the counter's value @field('number') value = 0; } 

Then, let's build the backend to implement the business logic:

// backend.js import {Layer, expose} from '@liaison/liaison'; import {Counter as BaseCounter} from './shared'; class Counter extends BaseCounter { // We expose the `value` field to the frontend @expose({get: true, set: true}) value; // And we expose the `increment()` method as well @expose({call: true}) increment() { this.value++; } } // We register the backend class into an exported layer export const backendLayer = new Layer({Counter}); 

Finally, let's build the frontend:

// frontend.js import {Layer} from '@liaison/liaison'; import {Counter as BaseCounter} from './shared'; import {backendLayer} from './backend'; class Counter extends BaseCounter { // For now, the frontend class is just inheriting the shared class } // We register the frontend class into a layer that inherits from the backend layer const frontendLayer = new Layer({Counter}, {parent: backendLayer}); // Lastly, we can instantiate a counter const counter = new frontendLayer.Counter(); // And play with it await counter.increment(); console.log(counter.value); // => 1 

What's going on? By invoking counter.increment(), we got the counter's value incremented. Notice that the increment() method is neither implemented in the frontend class nor in the shared class. It only exists in the backend.

So, how is it possible that we could call it from the frontend? This is because the frontend class is registered in a layer that inherits from the backend layer. So, when a method is missing in the frontend class, and a method with the same name is exposed in the backend class, it is automatically invoked.

From the frontend point of view, the operation is transparent. It doesn't need to know that a method is invoked remotely. It just works.

The current state of an instance (i.e., counter's attributes) is automatically transported back and forth. When a method is executed in the backend, the attributes that have been modified in the frontend are sent. And inversely, when some attributes change in the backend, they are reflected in the frontend.

Note that in this simple example, the backend is not exactly remote. Both the frontend and the backend run in the same JavaScript runtime. To make the backend truly remote, we can easily expose it through HTTP. See an example here.

How about passing/returning values to/from a remotely invoked method? It is possible to pass/return anything that is serializable, including class instances. As long as a class is registered with the same name in both the frontend and the backend, its instances can be automatically transported.

How about overriding a method across the frontend and the backend? It is no different than with regular JavaScript – we can use super. For example, we can override the increment() method to run additional code in the context of the frontend:

// frontend.js class Counter extends BaseCounter { async increment() { await super.increment(); // Backend's `increment()` method is invoked console.log(this.value); // Additional code is executed in the frontend } } 

Now, let's build a user interface with React and the encapsulated approach shown earlier:

// frontend.js import React from 'react'; import {view} from '@liaison/react-integration'; class Counter extends BaseCounter { // We use the `@view()` decorator to observe the model and re-render the view when needed @view() View() { return ( {this.value}  this.increment()}>+ ); } } 

Finally, to display the counter, all we need is:

Voilà! We built a single-page application with two unified layers and an encapsulated UI.

Proof of Concept

To experiment with the unified architecture, I built a RealWorld example app with Liaison.

I might be biased, but the outcome looks pretty amazing to me: simple implementation, high code cohesion, 100% DRY, and no glue code.

In terms of the amount of code, my implementation is significantly lighter than any other one I have examined. Check out the results here.

Certainly, the RealWorld example is a small application, but since it covers the most important concepts that are common to all applications, I'm confident that a unified architecture can scale up to more ambitious applications.

Conclusion

Separation of concerns, loose coupling, simplicity, cohesion, and agility.

It seems we get it all, finally.

If you are an experienced developer, I guess you feel a bit skeptical at this point, and this is totally fine. It is hard to leave behind years of established practices.

If object-oriented programming is not your cup of tea, you will not want to use Liaison, and this is totally fine as well.

Mutta jos olet OOP: ssa, pidä mielessäsi pieni ikkuna auki, ja seuraavalla kerralla, kun sinun on rakennettava koko pinon sovellus, yritä nähdä, kuinka se sopisi yhtenäiseen arkkitehtuuriin.

Yhteyshenkilö on vielä alkuvaiheessa, mutta työskentelen aktiivisesti sen parissa, ja odotan julkaisevani ensimmäisen beetaversion vuoden 2020 alussa.

Jos olet kiinnostunut, merkitse arkisto tähdellä ja pysy ajan tasalla seuraamalla blogia tai tilaamalla uutiskirje.

Keskustele tästä artikkelista Changelog News -sivustolla .