Dasar Penggunaan Interface di Pascal

Zamrony P. Juhara

Apa itu interface?

Interface berarti antar muka. Ia adalah jembatan antara sistem dan user yang menggunakan sistem itu. Interface dibuat untuk mengabstraksi sistem. Pada kendaraan, kemudi, pedal gas, pedal rem adalah interface. Pengemudi cukup tahu kegunaan masing-masing. Kemudi untuk berbelok, pedal gas dan rem untuk menambah dan mengurangi kecepatan. Detail implementasi bagaimana agar kendaraan itu bisa berbelok tidak penting bagi pengemudi. Kendaraan itu bisa berbelok karena roda depannya (seperti pada mobil umumnya) atau karena roda belakang (misal fork lift) tidak penting, yang penting bagi pengemudi, putar kemudi ke kiri berarti kendaraan belok kiri atau sebaliknya.

Interface adalah kontrak kesepakatan dan aturan antara pembuat sistem dan pengguna sistem. USB adalah interface. Ia mengatur kesepakatan antara pembuat piranti dengan penggunanya. Sepanjang piranti menggunakan mekanisme USB, komputer yang memanfaatkan piranti tersebut dapat terhubung dengannya tidak peduli bahwa piranti tersebut adalah keyboard atau pengeras suara.

Dalam pemrograman, istilah interface mirip dengan contoh di atas. Ia adalah kontrak kesepakatan antara sistem dan pengguna sistem.

Interface di Pascal

Di pemrograman Pascal, kata tercadang interface punya dua kegunaan:

  1. Penanda bagian deklarasi pada sebuah unit yang dapat diakses dari program atau unit lain (Listing 1).
  2. Deklarasi tipe interface (Listing 2).
Listing 1.

unit namaunit;
interface
   //deklarasi sesuatu
implementation
   //bagian implementasi kode
initialization
   //bagian inisialisasi
end.
Listing 2.

unit namaunit;
interface
type
   IDrawable = interface
       procedure draw();
   end;
   IPaintable = interface (IDrawable)
       procedure paint();
   end;
implementation
end.

Di sini kita hanya fokus pada penggunaan interface untuk deklarasi tipe.

Interface vs kelas abstrak

Kelas abstrak adalah kelas yang memiliki satu atau lebih metode yang dideklarasi dengan kata tercadang abstract (Listing 3).

Interface seperti halnya kelas dapat diturunkan dari interface lain. Pada Listing 2, interface IPaintable akan memiliki metode paint() dan draw(). Anda bebas memberi nama interface tetapi di Pascal, konvensi penamaannya biasanya diawali dengan huruf kapital I untuk interface.

Kelas abstrak dan interface memiliki persamaan yaitu bahwa mereka memiliki satu atau lebih deklarasi metode yang implementasinya tidak tersedia. Untuk kelas abstrak, metode tersebut harus diimplementasi oleh kelas turunannya. Untuk interface, metode tersebut harus diimplementasi oleh kelas yang dideklarasi memanfaatkan interface tersebut.

Listing 3.

unit biological;

{$mode objfpc}{$H+}

interface

uses
   core;

type

    TBiologicalObject = class(TCoreObject)
    public
        procedure breathe(); virtual; abstract;
    end;

implementation

end.

Cara menggunakan kelas abstrak vs interface

Jika kita punya deklarasi kelas seperti Listing 4.

Listing 4.

type
    TMyAbstractClass = class(TObject)
       procedure saySomething(); virtual; abstract;
    end;
    TMyDerivedClass = class(TMyAbstractClass)
       procedure saySomething(); override;
    end;

Kode di Listing 5 berikut adalah cara kita menggunakan kelas abstrak tersebut.

Listing 5.

var myClass : TMyAbstractClass;

myclass := TMyDerivedClass.Create();
myclass.saySomething();
myclass.Free();

Untuk interface.

Listing 6.

type
    IMyInterface = interface
       procedure saySomething();
    end;
    TMyInterfaceClass = class(TInterfacedObject, IMyInterface)
       procedure saySomething();
    end;

Kode di Listing 7 berikut adalah cara kita menggunakan interface tersebut.

Listing 7.

var myInterface : IMyInterface;

myInterface := TMyInterfaceClass.Create();
myInterface.saySomething();

Listing 5 dan Listing 6 tidak jauh berbeda, lalu mengapa harus menggunakan interface?

Issue dengan single inheritance

Jika kita punya struktur objek seperti Gambar 1 berikut:

Gambar 1. Diagram struktur objek awal
Gambar 1. Diagram struktur objek awal.

Kita ingin mengubah kelas THuman dan TPainterRobot di atas dengan menambah kemampuan untuk menggambar (metode draw()) dan menambah sebuah kelas baru yang bertanggung jawab atas proses penggambaran bernama TCanvasObject sehingga menjadi seperti struktur objek pada Gambar 2.

Gambar 2. Diagram struktur objek yang baru
Gambar 2. Diagram struktur objek yang baru.

TCanvasObject tergantung pada kelas THuman dan TPainterRobot untuk menyediakan fungsionalitas menggambar.

Solusi 1

Kita tentu dapat menyelesaikan dengan kode seperti Listing 8.

Listing 8.

type
   TCanvasObject = class(TObject)
   private
      drawableObject : TCoreObject;
   public
      constructor Create(drawableObj : TCoreObject);
      procedure drawCanvas();
   end;

dengan implementasinya seperti berikut:

Listing 9.

constructor TCanvasObject.Create(drawableObj : TCoreObject);
begin
  drawableObject := drawableObj;
end;

procedure TCanvasObject.drawCanvas();
begin
  if (drawObject is THuman) then
  begin
    THuman(drawableObject).draw();
  end else
  if (drawObject is TPainterRobot) then
  begin
    TPainterRobot(drawableObject).draw();
  end;
end;

Untuk menggunakan TCanvasObject, bisa dengan cara seperti Listing 10.

Listing 10.

var canvasObject : TCanvasObject;
    drawableObject : TCoreObject;

drawableObject := THuman.Create();
canvasObject := TCanvasObject.Create(drawableObject);
canvasObject.drawCanvas();

Kekurangan kode Listing 9, kelas TCanvasObject harus tahu deklarasi kelas THuman dan TPainterRobot. Bila kemudian hari, kita ingin menambahkan kelas lain yang mampu menggambar, daftar if .. then .. else pada Listing 9 semakin panjang. Kode TCanvasObject semakin terikat ketat dengan kelas lain.

Karena deklarasi THuman dan TPainterRobot dapat diakses dalam TCanvasObject, metode lain dalam kedua kelas tersebut (breathe(), doAutomateTask(), doMechanicalTask()) dapat diakses dalam TCanvasObject meskipun sebenarnya metode tersebut tidak relevan. Kita hanya dapat bergantung pada kedisiplinan programmernya untuk tidak memanggil kode kode yang tidak relevan.

Solusi 2

Cara kedua adalah dengan mendeklarasikan metode abstrak bernama draw() di TCoreObject. Dengan cara ini Listing 9 dapat disederhanakan menjadi seperti Listing 11. Kelas TCanvasObject tidak perlu tahu deklarasi THuman dan TPainterRobot.

Listing 11.

procedure TCanvasObject.drawCanvas();
begin
  drawableObject.draw();
end;

Tapi ini berakibat semua kelas yang diturunkan dari TCoreObject dianggap memiliki kemampuan menggambar, padahal kemampuan ini hanya relevan bagi kelas THuman dan TPainterRobot.

Solusi 3

Solusi ideal adalah dengan mengenalkan interface baru misal bernama IDrawable yang deklarasinya seperti Listing 12.

Listing 12.

unit drawable;

{$mode objfpc}{$H+}

interface

uses
   Classes, SysUtils;

type
   IDrawable = interface
      procedure draw();
   end;

implementation

end.

Kelas TCoreObject diubah dengan menurunkannya dari TInterfacedObject seperti pada Listing 13. Deklarasi kelas THuman dan TPainterRobot diubah menjadi Listing 14.

Listing 13.

TCoreObject = class(TInterfacedObject)
end;
Listing 14

THuman = class(TBiologicalObject, IDrawable)
public
    procedure draw();
    procedure breathe(); override;
end;

TPainterRobot = class(TRobot, IDrawable)
public
    procedure draw();
    procedure doMechanicalTask();override;
    procedure doAutomateTask();override;
end;

Sedangkan deklarasi kelas TCanvasObject menjadi Listing 15.

Listing 15.

TCanvasObject = class(TObject)
private
    drawableObject : IDrawable;
public
    constructor Create(drawableObj : IDrawable);
    procedure drawCanvas();
    destructor Destroy(); override;
end;


constructor TCanvasObject.Create(drawableObj : IDrawable);
begin
  drawableObject := drawableObj;
end;

destructor TCanvasObject.Destroy();
begin
  drawableObject := nil;
  inherited Destroy();
end;

procedure TCanvasObject.drawCanvas();
begin
  drawableObject.draw();
end;

Sedangkan deklarasi drawableObject Listing10 sedikit diubah menjadi seperti Listing 16.

Listing 16

var canvasObject : TCanvasObject;
    drawableObject : IDrawable;

drawableObject := THuman.Create();
canvasObject := TCanvasObject.Create(drawableObject);
canvasObject.drawCanvas();

Loose Coupling

Interface membantu menulis kode yang ikatan dengan kode lain seminimal mungkin. Dengan keterikatan yang minimal, kita dapat mengubah kode lebih mudah.

Pada contoh Listing 15, karena kelas TCanvasObject hanya tergantung pada interface IDrawable, ia hanya hanya dapat mengakses metode draw(). Metode lain milik kelas THuman atau TPainterRobot tidak dapat diakses dari dalam TCanvasObject. Kita dapat dengan mudah mengganti implementasi lain sepanjang instance objek mengimplementasi interface IDrawable. Contoh bila kita punya deklarasi kelas seperti Listing 17.

Listing 17.

TStrangeObject = class(TInterfacedObject, IDrawable)
public
   procedure draw();
end;

Kita dapat mengganti baris


drawableObject := THuman.Create();

menjadi


drawableObject := TStrangeObject.Create();

pada Listing 16 dan kode kita tetap bekerja normal. Perhatikan bahwa TStrangeObject sama sekali tidak ada kaitannya dengan kelas TCoreObject.

Satu kelas, banyak interface

Sebuah kelas hanya dapat diturunkan dari satu kelas orang tua. Tetapi satu kelas dapat mengimplementasi satu atau lebih interface.

Jika misal ada kebutuhan kelas baru misal bernama TRepairCenter dan kelas baru ini membutuhkan kelas THuman dan kelas TIndustrialRobot harus memiliki kemampuan untuk memperbaiki sesuatu. Solusinya, kita buat deklarasi interface baru misal bernama ICanRepair.

Listing 18

ICanRepair = interface
   ['{EF4D5E9D-252D-4DEC-8351-0616B8E7B2C0}']
   procedure repair();
end;

Deklarasi THuman pada Listing 14, kita ubah dengan menambah interface ICanRepair seperti berikut:

Listing 19

THuman = class(TBiologicalObject, IDrawable, ICanRepair)
public
   procedure draw();
   proecdure repair();
   procedure breathe(); override;
end;

TIndustrialRobot = class(TRobot, ICanRepair)
public
    procedure repair();
    procedure doMechanicalTask();override;
    procedure doAutomateTask();override;
end;

kelas TRepairCenter hanya mengakses ICanRepair mirip cara kerja TCanvasObject pada contoh Listing 15.

Reference counting di interface

Di Delphi, deklarasi interface yang tidak ditentukan parent interface selalu diturunkan dari IInterface yang tidak lain adalah nama alias untuk interface IUnknown. Interface ini memiliki tiga metode _AddRef(), _Release() dan QueryInterface(). Ini karena awal dikenalkannya interface adalah untuk mendukung Component Object Model (COM) milik Windows.

Jika interface IDrawable diturunkan dari IUnknown, kenapa kita tidak pernah membuat implementasi QueryInterface(), _AddRef() ataupun _Release()? Ini karena semuanya sudah diimplementasi oleh kelas TInterfacedObject (Listing 13). Metode _AddRef() dan _Release() ditandai dengan awalan _ untuk menandakan ini adalah metode yang seharusnya tidak perlu dipanggil oleh programmer.

Ketika kita melakukan assignment ke variabel bertipe interface seperti pada contoh kode Listing 16, kompiler akan menyisipkan kode pemanggilan _AddRef() dan ketika variabel tersebut sudah tidak digunakan (out of scope) maka kompiler akan menyisipkan pemanggilan _Release(). Melakukan assignment nilai nil ke variabel bertipe interface juga akan memicu penyisipan kode _Release().

Implementasi _AddRef() pada TInterfacedObject adalah menambah jumlah referensi dan implementasi _Release() menguranginya nilai referensi ini. Bila jumlah referensi bernilai 0, maka metode Free() akan dipanggil. Ini alasan mengapa pada Listing 7 sama sekali tidak ada pemanggilan Free() disana.

Yang harus kita perhatikan adalah, kita harus konsisten menggunakan interface dan tidak mencampur aduk pemanggilan kelas agar proses reference counting ini berjalan benar yakni _AddRef() dan _Release() saling berpasangan. Jika tidak, ia akan menyebabkan memory leak atau access violation karena instance objek di hapus dari memori sebelum waktunya.

Jika kita tidak menginginkan automatic reference counting ini, kita bisa membuat implementasi IUnknown sendiri. Jika kita tempuh cara ini, manajemen memori interface sepenuhnya ditangan programmer.

Di Free Pascal, deklarasi interface yang tidak menentukan parent interface secara defaultnya sama seperti Delphi. Bedanya bisa diubah. Khusus untuk Free Pascal, dikenalkan direktif $INTERFACES yang nilainya bisa diset COM untuk perilaku yang sama seperti Delphi atau CORBA untuk tidak menggunakan IUnknown sebagai root parent. Bila tidak ditentukan, defaultnya adalah COM.

Bila menggunakan direktif {$INTERFACES CORBA}, ini berarti tidak ada lagi penyisipan kode _AddRef() dan _Release() dan manajemen memori sepenuhnya tanggung jawab programmer. Jika Anda menggunakan CORBA, interface lebih ramping lagi karena tidak ada metode _AddRef(), _Release() dan QueryInterface(), mirip dengan interface pada bahasa lain seperti Java.

GUID

Jika Anda perhatikan kode deklarasi interface di Pascal, selalu menggunakan string pengenal unik dengan susunan mirip seperti berikut.


['{86B33CF6-8319-49BA-9E58-D5750D89F38C}']

String tersebut biasa disebut Globally Unique Identifier (GUID). GUID tidak wajib. Anda dapat menggunakan interface tanpa mendefinisikan GUID. GUID digunakan untuk membedakan satu interface dengan interface lain. GUID ini diperlukan terutama bila Anda bekerja dengan Component Object Model (COM) yang ada di Windows untuk membedakan satu interface dengan interface lain. Metode QueryInterface() akan memanfaatkan GUID yang kita definisikan di diklarasi interface, demikian pula ketika menggunakan operator is dan as.

Jadi amannya, buat selalu GUID. Dari IDE Lazarus, tekan tombol Ctrl + Shift + G untuk menghasilkan GUID.

Source Code Demo

Jika tertarik dengan sample code terkait ini clone repositorynya dari http://bit.ly/2jr7DAF

Penutup

Interface membantu programmer menghasilkan kode yang memiliki ikatan minimal dengan kode lain (loose-coupled code). Kode semacam ini mudah untuk diubah dan dikembangkan atau diintegrasikan dengan kode lain. Dengan memanfaatkan interface, detil implementasi dapat diabstraksi semaksimal mungkin.