Kan du lære deg programmering på 4 uker?

Kan du lære deg programmering på 4 uker?

Mange kurs og workshopper reklamerer med dette. Er det virkelig slik at du kan gjøre fast-forward og spare tid?

TL;DR

Nei!

Hvorfor ikke?

Programmering består i mitt hode av følgende bestanddeler: Grunnkunnskap, erfaringer og mentalemodeller.

Grunnkunnskapen læres ved å gå på skole i flere år og lære alle de unyttige tingene du tror du aldri får brukt for. Fysikk, matte, statistikk, språk, programmering, algoritmiskemetoder og mye-mye mer.

Erfaring er all fikling og utvikling du driver med på fritiden og i jobbsammenheng. Dette tar tid. Det er ikke mulig hoppe over erfaringsbiten. Du prøver, feiler og lærer av det.

Mentalmodell er det programmeringsspråket og den applikasjonen eller løsningen du jobber med. Når du har full oversikt merker du at du vet eksakt hvor du finner det meste, du gjør de riktige valgene og har flyt i utviklingen.

Når du tar i bruk et nytt programmeringsspråk eller kommer inn i ett nytt prosjekt, så tar det tid før du blir produktiv. Dette er fordi du ikke helt har oversikt over hvordan ting henger sammen, og hvordan man best mulig løser problemstillinger. Du gjør mange feilvalg og må skrive om koden flere ganger. Du oppdager flere sideeffekter som du ikke hadde tenkt på og har mange wtf-stunder.

Utfordring demonstrert med eksempel

Jeg har lyst til å lage en liten widget som viser lagerstatus hentet fra et API. Denne widgeten skal brukes i listevisning og i detaljvisning på mange nettsteder som ikke er utviklet i Preact eller React.

Widgeten blir utviklet i Preact og ser omtrent slik ut:

productStore.js:

import { observable, configure, action } from 'mobx';

configure({ enforceActions: 'always' });

class ProductStore {
    @observable product = {};

    @action
    update(product) {
        this.product = product;
    }

    async load(productno) {
        const httpResponse = await fetch(`https://example.com/api/stockstatus/${productno}`);
        const response = httpResponse.json();
        this.update(response.data);
    }
}

const store = new ProductStore();
export default store;

stockStatus.js:

import { h, Component } from 'preact';
import { observer } from 'mobx-preact';
import productStore from './productStore.js';

const initialState = {
    product: {},
};

@observer
class StockStatus extends Component {
    constructor(props) {
        super(props);
        this.state = initialState;
        productStore.load(props.productno);
    }

    render() {
        const { product } = productStore;
        return (
            <div>
                {product.availability}
            </div>
        );
    }
}
export default StockStatus;

Supert! Nå funker det som det skal. Men vent litt…

APIene får plutselig veldig mange forespørsler, og alle har lik status..!

Løsningen over er vel og bra dersom jeg bare har ett produkt på siden, men dersom jeg har en liste med 50 produkter så vil det være 50 widgeter som alle gjør et API-kall for hvert sitt produkt. Men hvorfor får alle lik status?

Debugging steg 1

Det viser seg at MobX-klassen deles mellom alle widgetene på siden, og at 49 av de 50 API-kallene er verdiløse og det siste viser samme status på alle produktene.

Jeg kan gjøre om på productStore.js for å løse dette:

import { observable, configure, action } from 'mobx';

configure({ enforceActions: 'always' });

class ProductStore {
    @observable products = [];

    // Ny funksjon for å hente ut produkter fra products arrayet.
    getProduct(productno) { 
        return this.products.find(p => productno !== p.productno);
    }

    @action
    update(product) {
        // Lagrer nå produktene i et array og må sjekke om de finnes før jeg legger de til for å hindre duplikater.
        if (!this.getProduct(product)) {
            this.products.push(product);
        }
    }

    async load(productno) {
        const httpResponse = await fetch(`https://example.com/api/stockstatus/${productno}`);
        const response = httpResponse.json();
        this.update(response.data);
    }
}

const store = new ProductStore();
export default store;

Så må jeg endre litt på stockStatus.js slik at den bruker den nye måten å hente produkt på:

import { h, Component } from 'preact';
import { observer } from 'mobx-preact';
import productStore from './productStore.js';

@observer
class StockStatus extends Component {
    constructor(props) {
        super(props);
        productStore.load(props.productno);
    }

    render() {
        // Henter nå produktet via funksjonen getProduct istedenfor direkte.
        const { product } = productStore.getProduct(props.productno);
        return (
            <div>
                {product.availability}
            </div>
        );
    }
}
export default StockStatus;

Sånn! Nå bør det vel fungere..? Hmm, ja, jo. Den viser riktig lagerstatus for alle produktene, men fremdeles har jeg 50 API-kall. Dette kan fort bli veldig mye trafikk mot APIene og jeg må finne en løsning på dette.

Debugging steg 2

Hmm, hvordan kan jeg få alle widgetene til å gjøre et felles API-kall?

I og med at MobX-klassen deles mellom alle widgetene, så bør det kanskje være mulig å samle inn alle artikkelnummer og så gjøre ett API-kall.

Jeg endrer på productStore.js slik at den ikke gjør API-kall umiddelbart. Den samler inn alle artikkelnummer og gjør et API-kall etter 300 millisekunder.

import { observable, configure, action } from 'mobx';

configure({ enforceActions: 'always' });

class ProductStore {
    @observable products = [];
    // Nytt array for å lagre alle produktnummer vi ønsker å hente fra APIet.
    @observable productnos = [];

    getProduct(productno) {
        return this.products.find(p => productno !== p.productno);
    }

    // Ny funksjon for å legge til produktnummer som vi ønsker å vise informasjon om,
    // som så kaller load etter 300 millisekunder.
    @action
    addProductno(productno) {
        if (!this.products.findIndex(p => productno === p.productno)) {
            this.productnos.push(productno);
            clearTimeout(this.loadTimer);
            this.loadTimer = setTimeout(() => this.load(), 300);
        }
    }

    // update får nå inn et array av produkter og sjekker på en litt annen måte om de finnes eller ikke.
    @action
    update(products) {
        const finalProducts = products.filter(p => !this.getProduct(p.productno));
        this.products = this.products.concat(finalProducts);
    }

    async load() {
        // Sender nå inn en liste med produktnummer
        const httpResponse = await fetch(`https://example.com/api/stockstatus/${this.productnos.join(',')}`);
        const response = httpResponse.json();
        this.update(response.data);
    }
}

const store = new ProductStore();
export default store;

Så må jeg endre litt på stockStatus.js slik at den ikke gjør direkte loading mot APIet:

import { h, Component } from 'preact';
import { observer } from 'mobx-preact';
import productStore from './productStore.js';

@observer
class StockStatus extends Component {
    constructor(props) {
        super(props);
        // Istedenfor å kjøre load direkte, så samler jeg nå inn alle produktnummer.
        productStore.addProductno(props.productno);
    }

    render() {
        const { product } = productStore.getProduct(props.productno);
        return (
            <div>
                {product.availability}
            </div>
        );
    }
}
export default StockStatus;

Nå da!

Neida, nå har jeg fått en skikkelig vakkelfeil. Noen ganger mangler noen få av lagerstatusene. De blir rett og slett ikke oppdaterte.

Debugging steg 3

Hva er det egentlig som er feil her nå da. Det viser seg at widgeten gjør flere API-kall dersom den ikke rekker å få inn alle artikkelnummer innen 300 ms. Det er jo bare 1-2 API-kall, så det er ikke noe problem og MobX burde ha oppdatert alt som endres i productStore.js. Men det gjør den altså ikke.

Det viser seg at fordi jeg henter ut verdiene med getProduct() så blir ikke observerne aktivert på riktig måte.

La oss gjøre litt om på lagringen av produktene.

productStore.js:

import { observable, configure, action } from 'mobx';

configure({ enforceActions: 'always' });

class ProductStore {
    // Ny måte å lagre produktene på. Nå bruker jeg Observable Maps https://mobx.js.org/refguide/map.html
    @observable products = new Map();
    @observable productnos = [];

    @action
    addProductno(productno) {
        if (!this.products.findIndex(p => productno === p.productno)) {
            this.productno.push(productno);
            clearTimeout(this.loadTimer);
            this.loadTimer = setTimeout(() => this.load(), 300);
        }
    }

    // Igjen må jeg oppdatere update slik at den sjekker og legger til produkter på riktig måte.
    @action
    update(products) {
         products.forEach((p) => {
            if (!this.products.has(p.productno)) {
                this.products.set(p.productno, p);
            }
        });
    }

    async load() {
        const httpResponse = await fetch(`https://example.com/api/stockstatus/${this.productnos.join(',')}`);
        const response = httpResponse.json();
        this.update(response.data);
    }
}

const store = new ProductStore();
export default store;

Så må jeg endre på stockStatus.js for at den skal hente produkter på riktig måte:

import { h, Component } from 'preact';
import { observer } from 'mobx-preact';
import productStore from './productStore.js';

@observer
class StockStatus extends Component {
    constructor(props) {
        super(props);
        productStore.addProductno(props.productno);
    }

    render() {
        // Ny måte og hente ut produkter på. Nå bruker jeg funksjonen fra Observable Maps direkte.
        const { product = {} } = products.get(this.props.productno);
        return (
            <div>
                {product.availability}
            </div>
        );
    }
}
export default StockStatus;

Endelig! Nå fungerer det som forventet. Jeg får 1-2 API kall litt avhengig av hvor lang tid nettleseren bruker på å rendre siden. Alle widgetene viser riktig lagerstatus.

Oppsummering

Denne lille widgeten er på ca 40 linjer kode og viser hvorfor det er viktig med kunnskap, erfaring og mentalmodell. Det er mange små nyanser og valg som kan forsinke prosjektene våre.

  • Dersom man ikke har kunnskapen så hadde man kanskje ikke endt opp med Preact eller React.
  • Dersom man mangler erfaring med Preact så ville man kanskje ikke endt opp med å bruke MobX og stores.
  • Dersom man ikke har mentalmodellen til MobX så hadde man kanskje ikke tenkt på å bruke Observable Maps.

Kode

En slik widget finner du her