Site-ul dvs. web câștigă aderență și creșteți rapid. Ruby / Rails este limbajul dvs. de programare la alegere. Echipa dvs. este mai mare și ați renunțat la „modele grase, controlere slabe” ca stil de design pentru aplicațiile dvs. Rails. Cu toate acestea, încă nu doriți să renunțați la utilizarea Rails.
Nici o problemă. Astăzi, vom discuta despre cum să utilizați cele mai bune practici ale POO pentru a vă face codul mai curat, mai izolat și mai decuplat.
Să începem prin a analiza modul în care ar trebui să decideți dacă aplicația dvs. este un candidat bun pentru refactorizare.
Iată o listă de valori și întrebări pe care mi le pun de obicei pentru a determina dacă codul meu are nevoie sau nu de refactorizare.
Încercați să utilizați așa ceva pentru a afla câte linii de cod sursă Ruby aveți:
find app -iname '*.rb' -type f -exec cat {} ;| wc -l
Această comandă va căuta în toate fișierele cu extensia .rb (fișiere ruby) din folderul / app și va imprima numărul de linii. Vă rugăm să rețineți că acest număr este doar aproximativ, deoarece liniile de comentarii vor fi incluse în aceste totaluri.
O altă opțiune mai precisă și mai informativă este folosirea sarcinii rake Rails stats
care afișează un rezumat rapid al liniilor de cod, numărul de clase, numărul de metode, raportul dintre metode și clase și raportul liniilor de cod pe metodă:
bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
Să începem cu un exemplu real.
Presupunem că vrem să scriem o aplicație care urmărește timpul pentru joggers. În pagina principală, utilizatorul poate vedea orele pe care le-a introdus.
Fiecare intrare are o dată, distanță, durată și informații relevante suplimentare despre „stare” (de exemplu, vreme, tip de teren etc.) și o viteză medie care poate fi calculată atunci când este necesar.
Avem nevoie de o pagină de raport care să afișeze viteza și distanța medie pe săptămână.
Dacă viteza medie pentru intrare este mai mare decât viteza medie generală, vom notifica utilizatorul cu un SMS (pentru acest exemplu vom folosi Nexmo API RESTful pentru a trimite SMS).
Pagina de pornire vă va permite să selectați distanța, data și timpul petrecut la jogging pentru a crea o intrare similară cu aceasta:
Avem și un statistics
pagina care este practic un raport săptămânal care include viteza medie și distanța parcursă pe săptămână.
Structura app
directorul arată ceva de genul:
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
Nu voi discuta despre User
model, deoarece nu este nimic special, deoarece îl folosim cu Motto pentru a implementa autentificarea.
În ceea ce privește Entry
model, conține logica de afaceri pentru aplicația noastră.
Fiecare Entry
aparține unui User
.
Validăm prezența distance
, time_period
, date_time
și status
atributele pentru fiecare intrare.
De fiecare dată când creăm o intrare, comparăm viteza medie a utilizatorului cu media tuturor celorlalți utilizatori din sistem și notificăm utilizatorul prin SMS folosind Nexmo (nu vom discuta despre modul în care este utilizată biblioteca Nexmo, deși am vrut să demonstrez un caz în care folosim o bibliotecă externă).
Observați că | | + _ | modelul conține mai mult decât logica de afaceri. De asemenea, gestionează unele validări și apeluri de apel.
Entry
are principalul CRUD acțiuni (fără actualizare). entries_controller.rb
primește intrările pentru utilizatorul curent și comandă înregistrările după data creării, în timp ce EntriesController#index
creează o intrare nouă. Nu este nevoie să discutați despre evident și responsabilitățile EntriesController#create
:
În timp ce EntriesController#destroy
este responsabil pentru calcularea raportului săptămânal, statistics_controller.rb
primește intrările pentru utilizatorul conectat și le grupează în funcție de săptămână, folosind StatisticsController#index
metoda conținută în clasa Enumerable din Rails. Apoi încearcă să decoreze rezultatele folosind câteva metode private.
Nu discutăm prea mult punctele de vedere aici, deoarece codul sursă se explică de la sine.
Mai jos este vizualizarea pentru listarea intrărilor pentru utilizatorul conectat (#group_by
). Acesta este șablonul care va fi utilizat pentru a afișa rezultatele acțiunii indexului (metoda) în controlerul de intrări:
Rețineți că folosim parțiale index.html.erb
, pentru a extrage codul partajat într-un șablon parțial render @entries
astfel încât să ne putem păstra codul USCAT și reutilizabile:
Același lucru este valabil și pentru _entry.html.erb
parțial. În loc să folosim același cod cu acțiuni (noi și editate), creăm un formular parțial reutilizabil:
În ceea ce privește vizualizarea săptămânală a raportului, _form
afișează câteva statistici și raportează performanța săptămânală a utilizatorului prin gruparea unor intrări:
Și, în cele din urmă, ajutorul pentru intrări, statistics/index.html.erb
, include doi asistenți entries_helper.rb
și readable_time_period
care ar trebui să facă atributele mai lizibile uman:
Nimic extraordinar până acum.
Cei mai mulți dintre voi vor susține că refacerea acestui lucru este împotriva SĂRUT principiu și va face sistemul mai complicat.
Deci, această aplicație are nevoie într-adevăr de refactorizare?
Absolut nu , dar o vom lua în considerare doar în scop demonstrativ.
La urma urmei, dacă verificați secțiunea anterioară și caracteristicile care indică faptul că o aplicație are nevoie de refactorizare, devine evident că aplicația din exemplul nostru nu este un candidat valid pentru refactorizare.
Deci, să începem prin a explica Rails MVC structura tiparului.
De obicei, începe prin browserul care face o cerere, cum ar fi readable_speed
.
Serverul web primește cererea și folosește https://www.toptal.com/jogging/show/1
pentru a afla care routes
a folosi.
Operatorii efectuează activitatea de analiză a cererilor utilizatorilor, trimiteri de date, cookie-uri, sesiuni etc., și apoi solicită controller
pentru a obține datele.
model
sunt clase Ruby care vorbesc cu baza de date, stochează și validează date, efectuează logica de afaceri și, în caz contrar, fac greutăți mari. Vizualizările sunt ceea ce vede utilizatorul: HTML, CSS, XML, Javascript, JSON.
Dacă dorim să arătăm secvența unui ciclu de viață al cererii Rails, ar arăta cam așa:
Ceea ce vreau să realizez este să adaug mai multă abstracție folosind obiecte vechi de rubin vechi (PORO) și să fac ca modelul să fie de genul următor pentru models
acțiuni:
Și ceva de genul următor pentru create/update
acțiuni:
Adăugând abstracții PORO, vom asigura o separare completă între responsabilități ȘOAPĂ , ceva la care Rails nu este foarte bun.
Pentru a obține noul design, voi folosi liniile directoare enumerate mai jos, dar vă rugăm să rețineți că acestea nu sunt reguli pe care trebuie să le respectați la T. Gândiți-vă la ele ca la linii directoare flexibile care facilitează refactorizarea.
Înainte de a începe, vreau să discut încă un lucru. Când începeți refactorizarea, de obicei ajungeți să vă întrebați: „Este o refacere foarte bună?”
Dacă simți că faci mai multă separare sau izolare între responsabilități (chiar dacă asta înseamnă adăugarea mai multor coduri și fișiere noi), atunci acesta este de obicei un lucru bun. La urma urmei, decuplarea unei aplicații este o practică foarte bună și ne face mai ușor să facem teste unitare adecvate.
Nu voi discuta lucruri, cum ar fi mutarea logicii de la controlere la modele, deoarece presupun că faceți deja acest lucru și vă simțiți confortabil folosind Rails (de obicei modelul Skinny Controller și modelul FAT).
Pentru a menține acest articol strâns, nu voi discuta testarea aici, dar asta nu înseamnă că nu ar trebui să testați.
Dimpotrivă, ar trebui începe întotdeauna cu un test pentru a vă asigura că lucrurile sunt ok înainte de a merge mai departe. Acesta este un trebuie sa, mai ales la refactorizare.
Apoi putem implementa modificări și ne asigurăm că toate testele trec pentru părțile relevante ale codului.
În primul rând, ce este un obiect valoric?
Martin Fowler explică:
Obiectul valoare este un obiect mic, cum ar fi un obiect cu bani sau interval de date. Proprietatea lor cheie este că urmează semantica valorică mai degrabă decât semantica de referință.
Uneori, puteți întâlni o situație în care un concept își merită propria abstractizare și a cărui egalitate nu se bazează pe valoare, ci pe identitate. Exemple ar include Ruby’s Date, URI și Pathname. Extragerea către un obiect valoric (sau model de domeniu) este o mare comoditate.
De ce sa te deranjezi?
Unul dintre cele mai mari avantaje ale unui obiect Value este expresivitatea pe care o ajută în realizarea codului dvs. Codul dvs. va tinde să fie mult mai clar sau cel puțin poate fi dacă aveți bune practici de denumire. Deoarece Obiectul Value este o abstracție, acesta conduce la un cod mai curat și la mai puține erori.
Un alt mare câștig este imuabilitate . Imuabilitatea obiectelor este foarte importantă. Când stocăm anumite seturi de date, care ar putea fi utilizate într-un obiect valoric, de obicei nu vreau ca aceste date să fie manipulate.
Când este util acest lucru?
Nu există un răspuns unic. Faceți ceea ce este mai bine pentru dvs. și ceea ce are sens în orice situație dată.
Trecând dincolo de asta, totuși, există câteva linii directoare pe care le folosesc pentru a mă ajuta să iau această decizie.
Dacă vă gândiți la un grup de metode este legat, cu obiectele Value acestea sunt mai expresive. Această expresivitate înseamnă că un obiect Value ar trebui să reprezinte un set distinct de date, pe care dezvoltatorul dvs. mediu le poate deduce pur și simplu uitându-se la numele obiectului.
Cum se face asta?
Obiectele de valoare trebuie să respecte câteva reguli de bază:
În exemplul nostru, voi crea un list/show
obiect de valoare pentru abstract EntryStatus
și Entry#status_weather
atribute propriei clase, care arată cam așa:
Notă: Acesta este doar un obiect de rubin vechi simplu (PORO) care nu moștenește de la Entry#status_landform
. Am definit metode de citire pentru atributele noastre și le atribuim la inițializare. De asemenea, am folosit un mixin comparabil pentru a compara obiecte folosind metoda ().
Putem modifica ActiveRecord::Base
model pentru a utiliza obiectul valoric pe care l-am creat:
De asemenea, putem modifica Entry
metoda de a utiliza noul obiect valoare în consecință:
Deci, ce este un obiect de serviciu?
Sarcina unui obiect de serviciu este de a păstra codul pentru un anumit bit de logică de afaceri. spre deosebire de „Model gras” stil, în care un număr mic de obiecte conțin multe, multe metode pentru toate logica necesară, folosind obiecte de serviciu rezultă multe clase, fiecare dintre ele având un singur scop.
De ce? Care sunt beneficiile?
Pe de altă parte, nimic nu este perfect. Un dezavantaj al obiectelor de serviciu este că pot fi un exces pentru o acțiune foarte simplă. În astfel de cazuri, este posibil să sfârșiți prin a vă complica, mai degrabă decât a simplifica, codul.
Când ar trebui să extrageți obiecte de serviciu?
Nici aici nu există o regulă dură și rapidă.
În mod normal, obiectele de serviciu sunt mai bune pentru sistemele mijlocii și mari; cei cu o cantitate decentă de logică dincolo de operațiile CRUD standard.
Deci, ori de câte ori credeți că un fragment de cod ar putea să nu aparțină directorului în care urmați să-l adăugați, este probabil o idee bună să vă reconsiderați și să vedeți dacă ar trebui să meargă la un obiect de serviciu.
Iată câțiva indicatori ai timpului de utilizare a obiectelor de serviciu:
Cum ar trebui să proiectați obiectele de service?
Proiectarea clasei pentru un obiect de serviciu este relativ simplă, deoarece nu aveți nevoie de pietre prețioase speciale, nu trebuie să învățați un DSL nou și vă puteți baza mai mult sau mai puțin pe abilitățile de proiectare software pe care le dețineți deja.
De obicei folosesc următoarele linii directoare și convenții pentru a proiecta obiectul de serviciu:
EntryController#create
director. Vă încurajez să utilizați subdirectoarele pentru domenii care conțin logică de afaceri. De exemplu, fișierul app/services
va defini app/services/report/generate_weekly.rb
în timp ce Report::GenerateWeekly
va defini app/services/report/publish_monthly.rb
.Report::PublishMonthly
, ApproveTransaction
, SendTestNewsletter
.Dacă te uiți la ImportUsersFromCsv
, vei observa un grup de metode (StatisticsController#index
, weeks_to_date_from
, weeks_to_date_to
etc.) cuplate la controler. Nu este chiar bine. Luați în considerare ramificațiile dacă doriți să generați raportul săptămânal în afara avg_distance
.
În cazul nostru, să creăm statistics_controller
și extrageți logica raportului din Report::GenerateWeekly
:
Deci StatisticsController
acum pare mai curat:
Prin aplicarea modelului de obiect Service, grupăm codul în jurul unei acțiuni specifice, complexe și promovăm crearea unor metode mai mici și mai clare.
Teme pentru acasă: ia în considerare utilizarea Obiect valoric pentru StatisticsController#index
în loc de WeeklyReport
.
Ce este un obiect Query?
Un obiect Query este un PORO care reprezintă o interogare în baza de date. Poate fi reutilizat în diferite locuri ale aplicației, ascunzând în același timp logica interogării. De asemenea, oferă o unitate izolată bună de testat.
Ar trebui să extrageți interogări complexe SQL / NoSQL în propria clasă.
Fiecare obiect de interogare este responsabil pentru returnarea unui set de rezultate pe baza criteriilor / regulilor de afaceri.
În acest exemplu, nu avem nicio interogare complexă, deci utilizarea obiectului Query nu va fi eficientă. Cu toate acestea, în scop demonstrativ, să extragem interogarea în Struct
și creați Report::GenerateWeekly#call
:
Și în generate_entries_query.rb
, să înlocuim:
Report::GenerateWeekly#call
cu:
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
Modelul obiectului de interogare vă ajută să vă păstrați logica modelului strict legată de comportamentul unei clase, păstrând în același timp controlorii dvs. subțiri. Deoarece nu sunt altceva decât clase vechi Ruby simple, obiectele de interogare nu trebuie să moștenească de la def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
și nu ar trebui să fie responsabile pentru nimic mai mult decât executarea interogărilor.
Acum, să extragem logica creării unei noi intrări într-un nou obiect de serviciu. Să folosim convenția și să creăm ActiveRecord::Base
:
Și acum CreateEntry
este după cum urmează:
EntriesController#create
Acum, aici lucrurile încep să devină mai interesante.
Amintiți-vă în ghidurile noastre, am convenit că dorim ca modelele să conțină asociații și constante, dar nimic altceva (fără validări și fără apeluri). Deci, să începem prin eliminarea apelurilor de apel și să folosim în schimb un obiect Form.
Un obiect Form este un obiect Ruby Old Plain (PORO). Acesta preia controlerul / obiectul de serviciu oriunde trebuie să vorbească cu baza de date.
De ce să folosiți obiecte Form?
Când doriți să refactorați aplicația, este întotdeauna o idee bună să țineți cont de principiul responsabilității unice (SRP).
SRP vă ajută să luați decizii de proiectare mai bune în ceea ce privește responsabilitatea unei clase.
Modelul tabelului bazei de date (un model ActiveRecord în contextul Rails), de exemplu, reprezintă o singură înregistrare a bazei de date în cod, deci nu există niciun motiv pentru care să fie preocupat de ceea ce face utilizatorul dumneavoastră.
Aici intervin obiectele Form.
Un obiect Form este responsabil pentru reprezentarea unui formular în cererea dvs. Deci, fiecare câmp de intrare poate fi tratat ca un atribut în clasă. Poate valida faptul că aceste atribute îndeplinesc unele reguli de validare și poate transmite datele „curate” către locul unde trebuie să meargă (de exemplu, modelele bazei de date sau, probabil, producătorul de interogări de căutare).
Când ar trebui să utilizați un obiect Form?
Acest lucru vă permite să puneți întreaga logică a formularului (convenții de denumire, validări etc.) într-un singur loc.
Cum creați un obiect Form?
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
(în Rails 3, trebuie să includeți în schimb denumirea, conversia și validările)Vă rugăm să rețineți că puteți utiliza reforma bijuterie, dar rămânând cu PORO vom crea ActiveModel::Model
care arată astfel:
Și vom modifica entry_form.rb
pentru a începe să utilizați obiectul Form CreateEntry
:
EntryForm
Notă: Unii dintre voi ar spune că nu este nevoie să accesați obiectul Form din obiectul Serviciu și că putem apela obiectul Form direct de la controler, ceea ce este un argument valid. Cu toate acestea, aș prefera să am un flux clar și de aceea apelez întotdeauna obiectul Form din obiectul Service.
După cum am convenit mai devreme, nu vrem ca modelele noastre să conțină validări și apeluri de apel. Am extras validările folosind obiecte Form. Dar folosim încă câteva apeluri de apel ( class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
în after_create
model Entry
).
De ce vrem să eliminăm apelurile de apel din modele?
Dezvoltatori de șine de obicei începeți să observați durerea de apel invers în timpul testării. Dacă nu vă testați modelele ActiveRecord, veți începe să observați durerea mai târziu pe măsură ce aplicația dvs. crește și pe măsură ce este necesară mai multă logică pentru a apela sau a evita apelul invers.
compare_speed_and_notify_user
apelurile de apel sunt utilizate în principal în legătură cu salvarea sau persistarea obiectului.
Odată ce obiectul este salvat, scopul (adică responsabilitatea) obiectului a fost îndeplinit. Deci, dacă mai vedem apeluri de apel invocate după ce obiectul a fost salvat, ceea ce probabil vedem este apeluri de apel care ajung în afara zonei de responsabilitate a obiectului și atunci avem probleme.
În cazul nostru, trimitem un SMS utilizatorului după ce salvăm o intrare, care nu este într-adevăr legată de domeniul de intrare.
O modalitate simplă de a rezolva problema este prin mutarea apelului invers către obiectul de serviciu aferent. La urma urmei, trimiterea unui SMS pentru utilizatorul final este legată de after_*
Service Object și nu la modelul de intrare în sine.
Procedând astfel, nu mai trebuie să ne îndepărtăm de CreateEntry
metodă în testele noastre. Am făcut o chestiune simplă crearea unei intrări fără a fi necesară trimiterea unui SMS și urmărim un design orientat spre obiecte, asigurându-ne că clasele noastre au o singură responsabilitate (SRP).
Deci, acum compare_speed_and_notify_user
arata ceva de genul:
În timp ce putem folosi cu ușurință Draper colecție de modele de vizualizare și decoratori, mă voi ține de PORO de dragul acestui articol, așa cum am făcut până acum.
Ceea ce am nevoie este o clasă care va numi metode pe obiectul decorat.
Pot folosi CreateEntry
pentru a implementa asta, dar voi folosi biblioteca standard a lui Ruby method_missing
.
Următorul cod arată cum să utilizați SimpleDelegator
pentru a implementa decoratorul nostru de bază:
SimpleDelegator
Deci, de ce % app/decorators/base_decorator.rb require 'delegate' class BaseDecorator
Această metodă acționează ca un proxy pentru contextul de vizualizare. În mod implicit, contextul de vizualizare este o instanță a unei clase de vizualizare, clasa de vizualizare implicită fiind _h
. Puteți accesa asistenți pentru vizualizare după cum urmează:
ActionView::Base
Pentru a-l face mai convenabil, adăugăm un _h.content_tag :div, 'my-div', class: 'my-class'
metoda de a decorate
:
ApplicationHelper
Acum, putem muta module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= '#{object.class}Decorator'.constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
ajutoare pentru decoratori:
EntriesHelper
Și putem folosi # app/decorators/entry_decorator.rb class EntryDecorator
readable_time_period
ca astfel:
readable_speed
# app/views/entries/_entry.html.erb - +
Am ajuns cu mai multe fișiere, dar nu este neapărat un lucru rău (și amintiți-vă că, de la început, am recunoscut că acest exemplu a fost doar în scop demonstrativ și a fost nu neapărat un bun caz de utilizare pentru refactorizare):
- +
Chiar dacă ne-am concentrat pe Rails în această postare de blog, RoR nu este o dependență a obiectelor de servicii descrise și a altor PORO-uri. Puteți utiliza această abordare cu orice aplicație de cadru web, mobil sau consolă.
Prin utilizarea MVC ca arhitectură a aplicațiilor web, totul rămâne cuplat și vă face să mergeți mai lent, deoarece majoritatea modificărilor au un impact asupra altor părți ale aplicației. De asemenea, vă obligă să vă gândiți unde să puneți o logică de afaceri - ar trebui să intre în model, controler sau vedere?
Prin utilizarea PORO-urilor simple, am mutat logica de afaceri la modele sau servicii care nu moștenesc de la app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
, ceea ce este deja un mare câștig, fără a menționa că avem un cod mai curat, care acceptă SRP și teste unitare mai rapide .
Arhitectura curată își propune să plaseze cazurile de utilizare în centrul / partea de sus a structurii dvs., astfel încât să puteți vedea cu ușurință ce face aplicația dvs. De asemenea, facilitează adoptarea modificărilor, deoarece este mult mai modulară și izolată.
Sper că am demonstrat cum folosind obiecte Plain Old Ruby și mai multe abstracții decuplează preocupările, simplifică testarea și ajută la producerea unui cod curat, care poate fi întreținut.
Legate de: Care sunt beneficiile Ruby on Rails? După două decenii de programare, folosesc șine