Arsitektur lebih dulu

Perancangan arsitektur lebih dahulu: task, state terlindungi, dan kontrak digambar di muka.

Daftar isi

Arsitektur proyek interlocking digambar lebih dahulu sebelum perilaku lengkap diisi. Topik ini menetapkan komponen proyek, yaitu package domain yang memuat kontrak, objek terlindungi untuk state bersama, dan task pengendali. Ketiganya kemudian dikunci dalam kerangka berkas proyek yang dikompilasi nyata. Setiap keluaran pada topik ini merupakan keluaran asli kerangka, dan perilaku penuh proyek mengisi kerangka tersebut tanpa mengubah kontrak yang sudah digambar.

Komponen arsitektur#

Kerangka proyek terbagi menjadi tiga komponen dengan tanggung jawab yang tidak tumpang tindih. Package Domain_Interlock menyimpan tipe domain, kontrak transisi, dan jadwal durasi. Package State_Interlock memuat satu objek terlindungi bernama Keadaan yang menyimpan fase berjalan. Prosedur Utama berisi task Pengendali yang memutar siklus melalui kedua package tersebut.

Pembagian ini mengikuti satu prinsip, yaitu kontrak harus terbaca sebelum badan. Spesifikasi Domain_Interlock memuat seluruh aturan domain dalam bentuk yang dapat diperiksa, sedangkan badannya hanya menerjemahkan aturan itu menjadi kasus sederhana. Task Pengendali tidak pernah mengakses variabel fase secara langsung karena satu-satunya jalur menuju state adalah operasi terlindungi. Diagram berikut merangkum ketiga komponen beserta aliran panggilannya.

Task Pengendaliutama.adbObjek terlindungiKeadaan (state fase)Domain_Interlockkontrak dan jadwalAmbil, MajuMajumembaca durasi fase
Arsitektur kerangka interlocking

Aliran data pada kerangka ini berurutan dan tunggal. Task Pengendali mengambil fase dari Keadaan, membaca durasinya dari jadwal domain, menunggu selama durasi tersebut, lalu meminta pemajuan fase kepada Keadaan. Tidak ada task kedua pada kerangka ini, sehingga keluaran yang tercetak pasti berurutan dan sama persis pada setiap eksekusi.

Kontrak domain digambar di muka#

Spesifikasi package domain adalah tempat seluruh kontrak proyek ditetapkan. Tipe Fase memuat empat nilai fase, sedangkan tipe Detik dengan subtipe Durasi_Hijau dan Durasi_Kuning mengunci rentang durasi setiap jenis fase. Fungsi Sah menetapkan tabel transisi. Melalui aspek Post, fungsi ini menjanjikan bahwa hasil kembaliannya sama dengan ekspresi tabel itu, sedangkan prosedur Maju menjanjikan bahwa setiap hasil pemajuan sah. Predikat dinamis pada Baris_Jadwal menolak pasangan fase dan durasi yang tidak sah, dan konstanta Jadwal menuliskan jadwal penuh pada spesifikasi yang terbaca tanpa membuka badan.

ada domain_interlock.ads
package Domain_Interlock is

   -- Empat fase siklus. UTS menunjuk pasangan arah utara-selatan dan
   -- BRT pasangan arah barat-timur.
   type Fase is (Hijau_UTS, Kuning_UTS, Hijau_BRT, Kuning_BRT);

   -- Satuan waktu domain dalam detik.
   type Detik is range 0 .. 3_600;

   subtype Durasi_Hijau  is Detik range 10 .. 60;
   subtype Durasi_Kuning is Detik range 2 .. 5;

   -- Tabel transisi: hanya empat perpindahan yang sah.
   function Sah (Dari, Ke : Fase) return Boolean with
     Post => Sah'Result =
       ((Dari = Hijau_UTS  and then Ke = Kuning_UTS)
        or else (Dari = Kuning_UTS and then Ke = Hijau_BRT)
        or else (Dari = Hijau_BRT  and then Ke = Kuning_BRT)
        or else (Dari = Kuning_BRT and then Ke = Hijau_UTS));

   -- Memajukan fase sesuai tabel transisi.
   procedure Maju (F : in out Fase) with
     Post => Sah (F'Old, F);

   -- Satu baris jadwal memasangkan fase dengan durasinya. Predikat
   -- dinamis menolak kombinasi yang tidak sah, misalnya hijau tiga
   -- detik.
   type Baris_Jadwal is record
      Fase_Kini : Fase;
      Lama      : Detik;
   end record with
     Dynamic_Predicate =>
       (if Baris_Jadwal.Fase_Kini in Hijau_UTS | Hijau_BRT
        then Baris_Jadwal.Lama in Durasi_Hijau
        else Baris_Jadwal.Lama in Durasi_Kuning);

   type Jadwal_Siklus is array (Fase) of Baris_Jadwal;

   -- Jadwal yang digambar di muka dan terbaca tanpa membuka badan.
   Jadwal : constant Jadwal_Siklus :=
     (Hijau_UTS  => Baris_Jadwal'(Fase_Kini => Hijau_UTS,  Lama => 12),
      Kuning_UTS => Baris_Jadwal'(Fase_Kini => Kuning_UTS, Lama => 3),
      Hijau_BRT  => Baris_Jadwal'(Fase_Kini => Hijau_BRT,  Lama => 12),
      Kuning_BRT => Baris_Jadwal'(Fase_Kini => Kuning_BRT, Lama => 3));

end Domain_Interlock;

Aspek Post pada Sah menyatakan nilai kembalian sama dengan ekspresi tabel transisi, sehingga badan yang menyimpang dari tabel tertangkap saat run time dengan flag -gnata. Aspek Post pada Maju menegaskan bahwa fase baru selalu sah menurut tabel yang sama. Predikat pada Baris_Jadwal dievaluasi pada setiap inisialisasi objek bertipe itu, termasuk saat konstanta Jadwal disusun, sehingga program dengan jadwal yang salah ditolak sebelum task pertama berjalan.

ada domain_interlock.adb
package body Domain_Interlock is

   function Sah (Dari, Ke : Fase) return Boolean is
   begin
      return (Dari = Hijau_UTS  and then Ke = Kuning_UTS)
        or else (Dari = Kuning_UTS and then Ke = Hijau_BRT)
        or else (Dari = Hijau_BRT  and then Ke = Kuning_BRT)
        or else (Dari = Kuning_BRT and then Ke = Hijau_UTS);
   end Sah;

   procedure Maju (F : in out Fase) is
   begin
      case F is
         when Hijau_UTS  => F := Kuning_UTS;
         when Kuning_UTS => F := Hijau_BRT;
         when Hijau_BRT  => F := Kuning_BRT;
         when Kuning_BRT => F := Hijau_UTS;
      end case;
   end Maju;

end Domain_Interlock;

Badan package pendek karena seluruh kebijakan sudah ditetapkan pada spesifikasi. Prosedur Maju hanya menerjemahkan tabel transisi menjadi pernyataan case dengan empat cabang, sedangkan fungsi Sah mengulang ekspresi tabel yang dinyatakan pada aspek Post-nya. Kompilator menolak case yang kehilangan satu nilai enumerasi, sehingga pertumbuhan tipe Fase memaksa pemutakhiran badan ini secara eksplisit.

State terlindungi untuk fase bersama#

State bersama proyek ini hanya satu, yaitu fase berjalan, dan state itu disimpan di bagian private objek terlindungi Keadaan. Dua operasi terlindungi yang diekspor adalah Ambil untuk membaca fase dan Maju untuk memajukannya. Tidak ada entry dengan barrier pada kerangka ini karena siklus dengan durasi tetap tidak menunggu sinyal dari luar, dan barrier disediakan ketika permintaan dari luar siklus ditambahkan.

ada state_interlock.ads
with Domain_Interlock;

package State_Interlock is

   -- State bersama seluruh task, yaitu fase berjalan. Seluruh akses
   -- melalui operasi terlindungi, sehingga tidak ada task yang
   -- menyentuh variabel fase secara langsung.
   protected Keadaan is
      procedure Ambil (F : out Domain_Interlock.Fase);
      procedure Maju;
   private
      Fase_Berjalan : Domain_Interlock.Fase := Domain_Interlock.Hijau_UTS;
   end Keadaan;

end State_Interlock;
ada state_interlock.adb
package body State_Interlock is

   protected body Keadaan is

      procedure Ambil (F : out Domain_Interlock.Fase) is
      begin
         F := Fase_Berjalan;
      end Ambil;

      procedure Maju is
      begin
         Domain_Interlock.Maju (Fase_Berjalan);
      end Maju;

   end Keadaan;

end State_Interlock;

Operasi Maju mendelegasikan pemajuan kepada Domain_Interlock.Maju, sehingga aspek Post tabel transisi berlaku pada setiap pemajuan tanpa penulisan ulang. Kedua operasi pendek dan tidak memblokir, sesuai batasan aksi terlindungi. Penugasan fase hanya terjadi di dalam badan objek terlindungi, sehingga tidak ada task yang membaca fase di tengah penugasannya.

Pemisahan antara state dan task ini juga menyiapkan pengujian. Seluruh perilaku fase dapat diuji dengan memanggil operasi terlindungi dari program uji tanpa menyertakan task mana pun, sebab state berdiri sendiri sebagai unit. Apabila fase disimpan pada variabel biasa, pengujian semacam itu tidak mungkin dilakukan dengan aman.

Task pengendali dan berkas proyek#

Task Pengendali dinyatakan di dalam prosedur Utama dan berjalan selama delapan fase, yaitu dua siklus penuh. Setiap putaran mengambil fase dari Keadaan, membaca durasi dari Jadwal, mencetak satu baris keluaran, menunggu, lalu meminta pemajuan fase. Durasi yang ditunggu dibagi 100 agar kerangka selesai cepat, dan pembagian itu dihilangkan pada perilaku penuh proyek.

ada utama.adb
with Ada.Text_IO;
with Domain_Interlock;
with State_Interlock;

-- Kerangka utama proyek interlocking. Task Pengendali memutar siklus
-- melalui state terlindungi dan mengambil durasi dari jadwal domain.
procedure Utama is

   Jumlah_Fase : constant := 8;   -- dua siklus penuh

   task Pengendali;

   task body Pengendali is
      F    : Domain_Interlock.Fase;
      Lama : Domain_Interlock.Detik;
   begin
      for Urut in 1 .. Jumlah_Fase loop
         State_Interlock.Keadaan.Ambil (F);
         Lama := Domain_Interlock.Jadwal (F).Lama;
         Ada.Text_IO.Put_Line
           ("fase" & Integer'Image (Urut) & ": "
              & Domain_Interlock.Fase'Image (F)
              & " selama" & Domain_Interlock.Detik'Image (Lama) & " detik");
         delay Duration (Lama) / 100.0;   -- dibagi 100 agar kerangka cepat
         State_Interlock.Keadaan.Maju;
      end loop;
      Ada.Text_IO.Put_Line ("kerangka menyelesaikan dua siklus penuh");
   end Pengendali;

begin
   null;
end Utama;

Berkas proyek GPRbuild mengunci susunan kerangka. Source_Dirs menunjuk direktori tempat berkas proyek berada, objek dipisahkan ke folder obj, dan Exec_Dir menempatkan hasil build di direktori proyek. Flag -gnata dipasang pada package Builder sehingga pemeriksaan kontrak aktif untuk seluruh unit. Dengan susunan itu, pembaca berkas proyek mengetahui titik masuk dan pilihan pemeriksaan tanpa membuka satu pun berkas sumber.

gpr interlock.gpr
project Interlock is

   -- Kerangka proyek: satu direktori sumber, objek terpisah, dan
   -- pemeriksaan kontrak aktif melalui flag -gnata.
   for Source_Dirs use (".");
   for Object_Dir use "obj";
   for Exec_Dir   use ".";
   for Main use ("utama.adb");

   package Builder is
      for Default_Switches ("ada") use ("-gnata");
   end Builder;

end Interlock;

Susunan berkas kerangka terdiri atas enam berkas berikut.

text
interlock/
├── interlock.gpr
├── domain_interlock.ads
├── domain_interlock.adb
├── state_interlock.ads
├── state_interlock.adb
└── utama.adb
bash
# build kerangka proyek melalui berkas proyek
gprbuild -q -P interlock.gpr
# jalankan kerangka utama
./utama
text Keluaran
fase 1: HIJAU_UTS selama 12 detik
fase 2: KUNING_UTS selama 3 detik
fase 3: HIJAU_BRT selama 12 detik
fase 4: KUNING_BRT selama 3 detik
fase 5: HIJAU_UTS selama 12 detik
fase 6: KUNING_UTS selama 3 detik
fase 7: HIJAU_BRT selama 12 detik
fase 8: KUNING_BRT selama 3 detik
kerangka menyelesaikan dua siklus penuh

Keluaran tercetak berurutan dari fase 1 sampai fase 8 dan berakhir dengan pesan penyelesaian karena satu-satunya task yang mencetak adalah Pengendali. Setiap pemajuan melewati operasi terlindungi dan setiap durasi berasal dari jadwal yang diperiksa predikatnya, sehingga tidak ada satu angka pun yang ditulis dua kali. Kerangka ini menjadi tempat perilaku penuh proyek diisi.

Kontrak yang menolak pelanggaran#

Kontrak yang ditetapkan lebih dahulu berlaku sejak baris pertama program. Probe berikut, yaitu program kecil yang sengaja memicu pelanggaran kontrak, menyusun satu Baris_Jadwal dengan durasi hijau tiga detik, yakni kombinasi yang ditolak predikatnya. Kompilasi menggunakan flag -gnata agar pemeriksaan predikat benar-benar berjalan, dan program diharapkan berhenti sebelum baris apa pun tercetak. Predikat jadwal dipilih sebagai contoh karena penolakannya paling mudah dibaca, sedangkan aspek Post tabel transisi diperiksa dengan pola yang sama.

ada langgar_predikat.adb
with Ada.Text_IO;
with Domain_Interlock;

-- Probe pelanggaran kontrak: durasi hijau tiga detik melanggar
-- predikat Baris_Jadwal karena tiga detik di luar Durasi_Hijau.
procedure Langgar_Predikat is
   Baris : Domain_Interlock.Baris_Jadwal :=
             Domain_Interlock.Baris_Jadwal'(Fase_Kini => Domain_Interlock.Hijau_UTS,
                                            Lama      => 3);
begin
   Ada.Text_IO.Put_Line ("baris jadwal diterima");
end Langgar_Predikat;
bash
# kompilasi probe pelanggaran predikat
gnatmake -q -gnata langgar_predikat.adb
# jalankan probe, exception memang diharapkan muncul
./langgar_predikat
text Keluaran
raised ADA.ASSERTIONS.ASSERTION_ERROR : Dynamic_Predicate failed at langgar_predikat.adb:8

Pesan exception menyebut Dynamic_Predicate failed beserta berkas dan baris penyusunan objek yang melanggar, sedangkan teks baris jadwal diterima tidak pernah tercetak. Penolakan terjadi pada batas penyusunan data, bukan jauh di dalam logika, dan itulah kegunaan kontrak yang ditetapkan lebih dahulu. Jadwal yang tidak sah tidak pernah sempat dilihat oleh task mana pun. Pola yang sama berlaku ketika konstanta Jadwal disunting di kemudian hari, karena jadwal baru diperiksa predikatnya pada saat program dijalankan pertama kali. Pesan yang menunjuk berkas dan baris juga memangkas pencarian penyebab karena lokasi penolakan disebut langsung.

Kesalahan umum#

Empat kekeliruan berikut sering muncul ketika arsitektur digambar lebih dahulu, dan semuanya berkaitan dengan tempat keputusan ditulis.

  • Menulis badan sebelum spesifikasi selesai. Badan yang ditulis lebih dulu menempatkan keputusan domain di tempat yang tidak diperiksa kontrak. Perbaikannya, selesaikan spesifikasi beserta aspek dan predikatnya, lalu tulis badan yang mematuhinya.
  • Menyimpan fase pada variabel global biasa. Task yang membaca variabel di tengah perubahan menghasilkan keputusan yang salah tanpa pesan error apa pun. Perbaikannya, pindahkan state ke objek terlindungi dan akses hanya melalui operasinya.
  • Menempatkan delay di dalam aksi terlindungi. Aksi yang memblokir menahan seluruh task lain yang menunggu objek yang sama. Perbaikannya, lakukan penundaan di dalam task dan biarkan aksi terlindungi hanya membaca serta menulis state.
  • Menghitung durasi di dalam task. Angka durasi yang tersebar menyalin jadwal dan cepat kedaluwarsa. Perbaikannya, ambil durasi dari Jadwal yang diperiksa predikatnya sebagaimana dilakukan Pengendali.

Ringkasan#

  • Arsitektur kerangka terbagi menjadi package domain berisi kontrak, objek terlindungi untuk state bersama, dan task pengendali yang memutar siklus.
  • Kontrak ditetapkan pada spesifikasi berupa tabel transisi dengan aspek Post, rentang durasi bertipe, predikat jadwal, dan jadwal berupa konstanta.
  • Kerangka dikompilasi nyata melalui berkas proyek GPRbuild dengan flag -gnata, dan pelanggaran predikat tertangkap dengan pesan exception yang menunjuk baris penyusunan objek yang melanggar.

Referensi#