IPragmatic


 MII considered harmful ?

This page would be a little more critical, but i feel that it can describe my residual misunderstanding of interfaces ...

Programming with interfaces is a not hard task: syntax is very clean, pointers are "smart", there are many base classes to inherit from and runtime type  support is cool. 

What i find harder is "design"...  (the place where interfaces help more).

A problem here is that there are many alternative "styles" to choose:

 

 

 


Trying to model interfaces, i prefer avoid the "interface as contract" definition, that i find more appropriate in other environments like UML.

To me, an interface is a "view" on a "dark ball", that contains an hidden set of objects (again, aggregation ...)

 


But these "views" aren't "flat" but they support a dimension of (single) inheritance ...

where the "holes" (of different sizes) represent the contexts in which interfaces can be used ...

 


but this picture looks more correct,
after all, interfaces are single rooted ...

 

 


A strange thing is happening here

you cannot give the ball a name!

Aggregates are anonymous, and you cannot declare an interface type that "glues" together its parts.

but then, why stop at the ball  ? also holes (in the last picture) could be overlapped ...

I'm not able to draw it, so image ...

If the ball has a name, there must be a hole all around ... so we can peel the ball a throw away its frontier, then some hole can be overlapped in an union one (a triangle plus a square gives a seven-pointed star) with some bifurcation inside the ball ...

uh, I think I've confused "names" with "shapes" ...

 


Delphi (and COM) remains the only language in the club (with C++, Java, C#, Corba) that does not support multiple interface inheritance, and a question raises spontaneously at this point:

Is multiple interface inheritance (MII) a "sound" type system ?

I really don't know...

Surely, you may run into difficulties when there is some name clashing among methods ...

there are many "solutions" here, but how much satisfactory ?

 

(multiple) inheritance and method names (semantics) do not always mix well ...

 

//---------------------------------------------------------------------------

template < class Super >
class TOddPlayer : public Super
{
protected:
   virtual void BeforePlay() {};
   virtual void AfterPlay() {};

public:

   void Play()
   {
      BeforePlay();

      for (int i = 0; i < 4; i++)
         Super::Play();           // required!

      AfterPlay();
   };

};

template < class Super >
class TOddTimer : public Super
{
private:
   int Timer;
protected:
   virtual void OnStart(int Value)     {};
   virtual void OnTick(int Value)      {};
   virtual void OnStop(int Value)      {};

public:

   void StartTimer(int Value)
   {
      Timer = Value;
      OnStart(Timer);
   };

   void StopTimer()
   {
      OnStop(Timer);
      Timer = 0;
   };

   void TickTimer()
   {
      Timer++;
      OnTick(Timer);
   }

};


//---------------------------------------------------------------------------

class TViolinist
{
protected:
   virtual void Play()
   {
      FingerMove();
   };

   virtual void FingerMove()
   {
      cout << "Play IX Simphony ..." << endl;
   };
};


class TChessMaster
{
protected:
   virtual void Play()
   {
      ChessMove();
   };

   virtual void ChessMove()
   {
      char c = random(8) + 'A';
      int  r = random(8) + 1;
      cout << "Queen in " << c << r << ", Check!" << endl;
   };
};

//---------------------------------------------------------------------------

class TOddViolinist : public TOddTimer<TOddPlayer<TViolinist> >
{
protected:
   virtual void BeforePlay()
   {
      StartTimer(80); //allegro?
   };
   virtual void AfterPlay()
   {
      StopTimer();
   };
   virtual void OnStart(int Value)
   {
      cout << "set metronome to " << Value
         << " tick per minute" << endl;
   };
   virtual void OnTick(int Value)
   {
      cout << "Tock" << endl;
   };
   virtual void OnStop(int Value)
   {
      cout << "stop metronome" << endl;
   };

   virtual void FingerMove()
   {
      TViolinist::FingerMove();
      TickTimer();
   };
};


//---------------------------------------------------------------------------

class TOddChessMaster : public TOddTimer<TOddPlayer<TChessMaster> >
{
private:
   int ResidualTime;
protected:
   virtual void BeforePlay()
   {
      randomize();
      StartTimer(-ResidualTime);
   };
   virtual void AfterPlay()
   {
      StopTimer();
   };
   virtual void OnStart(int Value)
   {
      cout << "start chronometer, resudual time:"
         << ResidualTime << " sec"<< endl;
   };
   virtual void OnTick(int Value)
   {
      cout << ( Value % 2 ? "Tick" : "Tack" ) << endl;
   };
   virtual void OnStop(int Value)
   {
      ResidualTime = -Value;
      cout << "stop chronometer, resudual time:"
         << ResidualTime << " sec"<< endl;
   };

   virtual void ChessMove()
   {
      TChessMaster::ChessMove();
      TickTimer();
   };

public:
   TOddChessMaster() : ResidualTime(100) {};

};


//---------------------------------------------------------------------------

...

//---------------------------------------------------------------------------

void Test()
{
   cout << " TOddViolinist " << endl;

   TOddViolinist v;

   v.Play();

   cout << "TOddChessMaster" << endl;

   TOddChessMaster c;

   c.Play();


   cout << "Press a key..." << endl;

   getch();

}

//---------------------------------------------------------------------------

#pragma argsused
int main(int argc, char* argv[])
{
   Test();
   return 0;
}
//---------------------------------------------------------------------------


and its output

TOddViolinist
set metronome to 80 tick per minute
Play IX Simphony ...
Tock
Play IX Simphony ...
Tock
Play IX Simphony ...
Tock
Play IX Simphony ...
Tock
stop metronome
TOddChessMaster
start chronometer, resudual time:100 sec
Queen in H7, Check!
Tick
Queen in C6, Check!
Tack
Queen in G5, Check!
Tick
Queen in B7, Check!
Tack
stop chronometer, resudual time:96 sec

 

Delphi approach to multiple inheritance is simple: "prohibit and rewrite as SII"

This is different from a "no decision" rule, and it presents some advantages:

 

 

as a rewrite sample ...

 

type

   IInterfaceBase = interface(IUnknown)
   ['{1BF42676-382B-11D5-B563-00AA00ACFD08}']
      procedure BaseMethod;
   end;

   IInterfaceA1 = interface(IInterfaceBase)
   ['{1BF42671-382B-11D5-B563-00AA00ACFD08}']
      procedure AMethod;
   end;

   IInterfaceA2 = interface(IInterfaceBase)
   ['{1BF42672-382B-11D5-B563-00AA00ACFD08}']
      procedure AMethod;
   end;

   IInterfaceA3 = interface(IInterfaceBase)
   ['{14166251-51DE-11D5-B57C-00AA00ACFD08}']
      procedure AMethod;
   end;

   IInterfaceSingle = interface(IUnknown)
   ['{1BF42675-382B-11D5-B563-00AA00ACFD08}']
      function AsInterfaceA1: IInterfaceA1;
      function AsInterfaceA2: IInterfaceA2;
      function AsInterfaceA3: IInterfaceA3;
      property InterfaceA1: IInterfaceA1 read AsInterfaceA1;
      property InterfaceA2: IInterfaceA2 read AsInterfaceA2;
      property InterfaceA3: IInterfaceA3 read AsInterfaceA3;
   end;

{
// Illegal...

   IInterfaceSingle = interface(IInterfaceA1,IInterfaceA2,IInterfaceA3)
   end;
}

with a possible implementing class ...

 

type

   TMySingle = class(TInterfacedObject,
          IInterfaceSingle,
          IInterfaceA1,IInterfaceA2,IInterfaceA3,  //repeated inheritance
          IUnknown)
   protected
   {TMySingle.IUnknown}
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;

   protected
      procedure IInterfaceA1.BaseMethod = AlterBaseMethod;
      procedure IInterfaceA1.AMethod = A1Method;
      procedure IInterfaceA2.AMethod = A2Method;
      procedure AlterBaseMethod;
      procedure BaseMethod;
      procedure AMethod;
      procedure A1Method;
      procedure A2Method;
      function  AsInterfaceA1: IInterfaceA1;
      function  AsInterfaceA2: IInterfaceA2;
      function  AsInterfaceA3: IInterfaceA3;
   public
      destructor Destroy; override;
   end;

implementation of these methods is based on repeated interface inheritance in class declaration

 

function TMySingle.AsInterfaceA1: IInterfaceA1;
begin
   result := Self as IInterfaceA1;
end;

function TMySingle.AsInterfaceA2: IInterfaceA2;
begin
   result := Self as IInterfaceA2;
end;

function TMySingle.AsInterfaceA3: IInterfaceA3;
begin
   result := Self as IInterfaceA3;
end;

 

Visibroker for Delphi takes a different approach (keeping first line of inheritance and including remaining methods)...

 

type

   IInterfaceBase = interface(IUnknown)
   ['{1BF42676-382B-11D5-B563-00AA00ACFD08}']
      procedure BaseMethod;
   end;

   IInterfaceA1 = interface(IInterfaceBase)
   ['{1BF42671-382B-11D5-B563-00AA00ACFD08}']
      procedure AMethod;
   end;

   IInterfaceA2 = interface(IInterfaceBase)
   ['{1BF42672-382B-11D5-B563-00AA00ACFD08}']
      procedure AMethod;
   end;

   IInterfaceA3 = interface(IInterfaceBase)
   ['{14166251-51DE-11D5-B57C-00AA00ACFD08}']
      procedure AMethod;
   end;

   IInterfaceSingle = interface(IInterfaceA1)
   ['{1BF42675-382B-11D5-B563-00AA00ACFD08}']
      procedure IInterfaceA2BaseMethod;
      procedure IInterfaceA2AMethod;
      procedure IInterfaceA3BaseMethod;
      procedure IInterfaceA3AMethod;
   end;

 


another topic is "interface reflection" compared to object reflection

let's consider this code, adapted from  Jim Coplien book

  

unit SignCoplien;

interface

uses
   Math,SysUtils;

type

   TNumber = class(TObject)
   protected
      function Promote(ANumber: TNumber): TNumber; virtual;
      function OpPlus(ANumber: TNumber): TNumber; virtual;
   public
      procedure Dump; virtual;
      function IsA(ANumber: TNumber): Boolean;
      function Plus(ANumber: TNumber): TNumber;
      function SquareRoot: TNumber; virtual; abstract;
   end;

   TComplex = class(TNumber)
   private
      FRe: double;
      FIm: double;
    function GetArg: double;
    function GetModule: double;
   protected
      function OpPlus(ANumber: TNumber): TNumber; override;
      function Promote(ANumber: TNumber): TNumber; override;
      function GetIm: double; virtual;
   public
      procedure Dump; override;
      property Re : double read FRe write FRe;
      property Im : double read FIm write FIm;
      property Module : double read GetModule;
      property Arg : double read GetArg;
      constructor Create;                    overload; virtual;
      constructor Create(ARe,AIm: double);   overload; virtual;
      constructor CreateTrig(AMod,AArg: double);   overload; virtual;
      function SquareRoot: TNumber; override;
   end;

   TReal = class(TComplex)
   protected
      function GetIm: double; override;
      function OpPlus(ANumber: TNumber): TNumber; override;
      function Promote(ANumber: TNumber): TNumber; override;
   public
      procedure Dump; override;
      constructor Create(ARe: double); overload;
      constructor Create(ARe,AIm: double);   overload; override;
      property Im : double read GetIm;
      function SquareRoot: TNumber; override;
   end;


procedure Test;

implementation

uses
   SignUti;

{ TNumber }

procedure TNumber.Dump;
begin
   WriteLn('____________________________');
   WriteLn('Class:',ClassName);
end;

function TNumber.IsA(ANumber: TNumber): Boolean;
begin
   result := InheritsFrom(ANumber.ClassType);
end;

function TNumber.OpPlus(ANumber: TNumber): TNumber;
begin
   raise Exception.Create('Abstract ...');
end;

function TNumber.Plus(ANumber: TNumber): TNumber;
var
   TempNumber: TNumber;
begin
   if Self.IsA(ANumber) then
   begin
      TempNumber := ANumber.Promote(Self);
      result := ANumber.OpPlus(TempNumber);
   end
   else
   if ANumber.IsA(Self) then
   begin
      TempNumber := Self.Promote(ANumber);
      result := Self.OpPlus(TempNumber);
   end
   else
      raise Exception.Create('Incompatibile ...');
end;

function TNumber.Promote(ANumber: TNumber): TNumber;
begin
   raise Exception.Create('Abstract ...');
end;

{ TComplex }

procedure TComplex.Dump;
begin
   inherited;
   WriteLn('Re:',FRe);
   WriteLn('Im:',FIm);
end;

function TComplex.OpPlus(ANumber: TNumber): TNumber;
var
   Temp: TComplex;
begin
  Temp := ANumber as TComplex;
  Temp.FIm  := Temp.FIm + Self.FIm;
  Temp.FRe  := Temp.FRe + Self.FRe;
  result := Temp;
end;

function TComplex.Promote(ANumber: TNumber): TNumber;
var
   Temp: TComplex;
begin
   if (ANumber is TComplex) then
   begin
      Temp := TComplex.Create(
               TComplex(ANumber).Re,TComplex(ANumber).Im);
   end
   else
   if (ANumber is TReal) then
   begin
      Temp := TComplex.Create(TReal(ANumber).Re,0.0);
   end
   else
      raise Exception.Create('UnPromotable ...');
   result := Temp;
end;

constructor TComplex.Create;
begin
   inherited Create;
end;

constructor TComplex.Create(ARe,AIm: double);
begin
   inherited Create;
   FRe := ARe;
   FIm := AIm;
end;

function TComplex.GetIm: double;
begin
   result := FIm;
end;

function TComplex.SquareRoot: TNumber;
begin
   WriteLn('____________________________');
   WriteLn('==> Complex Sqrt ...');
   result := TComplex.CreateTrig(Sqrt(Module),Arg/2);
end;

function TComplex.GetArg: double;
begin
   if Abs(FRe) <= 0.0 then
      if FIm > 0.0 then
         result := Pi
      else
      if FIm < 0.0 then
         result := -Pi
      else
         result := 0.0
   else
      result := ArcTan2(FIm,FRe);
end;

function TComplex.GetModule: double;
begin
   result := Sqrt(Sqr(FRe)+Sqr(FRe));
end;

constructor TComplex.CreateTrig(AMod, AArg: double);
begin
   inherited Create;
   FRe := AMod * Cos(AArg);
   FIm := AMod * Sin(AArg);
end;

{ TReal }

procedure TReal.Dump;
begin
  inherited;
end;

function TReal.OpPlus(ANumber: TNumber): TNumber;
var
  Temp: TReal;
begin
  Temp := ANumber as TReal;
  Temp.FRe  := Temp.FRe + Self.FRe;
  result := Temp;
end;

function TReal.SquareRoot: TNumber;
begin
   WriteLn('____________________________');
   WriteLn('==> Real Sqrt ...');
   if Re < 0.0 then
   begin
      WriteLn('??? Immaginary!');
      result := TReal.Create(1/0-1/0);
   end
   else
      result := TReal.Create(Sqrt(Re));
end;

function TReal.Promote(ANumber: TNumber): TNumber;
var
   Temp: TReal;
begin
   if (ANumber is TReal) then
   begin
      Temp := TReal.Create(TReal(ANumber).Re);
   end
   else
      raise Exception.Create('UnPromotable ...');
   result := Temp;
end;

constructor TReal.Create(ARe: double);
begin
   inherited Create(ARe,0.0);
end;

constructor TReal.Create(ARe, AIm: double);
begin
   raise Exception.Create('Illegal ...');
end;

function TReal.GetIm: double;
begin
   raise Exception.Create('Illegal ...');
end;


///////////////////////////////////////////////////////////////////////////////

{ Test }

procedure TestCoplien;
var
   c1,c2: TComplex;
   r1,r2: TReal;
   x,y: TNumber;
begin

   r1 := TReal.Create(3.0);
   r2 := TReal.Create(-5.0);

   c1 := TComplex.Create(1.0,1.0);
   c2 := TComplex.Create(-5.0,0);

   x := r1.Plus(r1);             x.Dump;   x.Free;
   x := r1.Plus(r2);             x.Dump;
   y := x.SquareRoot;   x.Free;  y.Dump;   y.Free;

   x := c1.Plus(c1);             x.Dump;   x.Free;
   x := c1.Plus(c2);             x.Dump;
   y := x.SquareRoot;   x.Free;  y.Dump;   y.Free;

   x := r1.Plus(c2);             x.Dump;
   y := x.SquareRoot;   x.Free;  y.Dump;   y.Free;

   x := c1.Plus(r1);             x.Dump;
   y := x.SquareRoot;   x.Free;  y.Dump;   y.Free;

end;

procedure Test;
begin
   TestCoplien;
   SignUti.Pause;
end;

end.
 

It looks a bit odd a first sight, a TReal class is derived from a TComplex one, and plus operation works in this way:

so, what's wrong in consider a TReal as a specialization of a TComplex ?

nothing, but anyway something looks strange ...

The oddity here is that is natural to imagine a TComplex implementation based on a couple of TReal types

You can even let TComplex class derive from a TReal class and add a TReal field for imaginary part ...

but subtype relation ?

this should be in (higher) interfaces types, where IReal can be a subtype of IComplex (subtype of INumber).

we have a problem here, however

INumber.Plus method should require a INumber parameter, and (interface) inheritance let us pass IComplex and IReal values, but, if you look at TNumber.Plus implementation, you can find why it don't work.

Interfaces don't support

 


an "intriguing" idea ...

how to mix aggregation, composition and layering

a big idea related to delegation is "visual" development, when you program by dragging components from a palette to a component container (a DataModule for example).

D6 introduces visual support for interfaces, letting bind components together via interfaces the component supports (great!)

If we consider aggregation, adding components to a DataModule could be seen as adding parts to an aggregate ("implements" with property)

but if we write a simple component with the same set of interfaces, we can make the component act as a "proxy" of the DataModule (the latter can be the delegated object of a delegating component)

but this component could be placed in a higher layer container and so on ...

a ball that when "closed" looks like a component but if "opened" is a DataModule ...

 



Next  Up..