And the prize for ugliest API goes to…

In FireMonkey/XE2, most platform-specific functionality was filtered through a Platform singleton object. In this, the FMX.Platform unit defined an abstract TPlatform base class, with FMX.Platform.Win and FMX.Platform.Mac providing (private) concrete descendants of it (TPlatformWin and TPlatformMac respectively) that did the actual work. In FireMonkey/XE3, in contrast, the Platform singleton has been removed in favour of a TPlatformServices object that you query different interfaces from, each interface providing a different sort of ‘platform service’ – for example, there’s a clipboard service (IFMXClipboardService), menu service (IFMXMenuService), and so on.

In practice, TPlatformWin and TPlatformMac still exist, and at present, they also implement all the interfaces you can query for when it comes to their particular platform. Nonetheless, in principle, the new setup allows providing an alternative implementation for any given IFMXxxxService interface. For example, if you’ve had enough of the awful stock Mac menu bar code, you can write your own implementation of the IFMXMenuService interface, unregister the default implementation, then register your replacement:

type
  TMacMenuServiceFix = class(TInterfacedObject, IFMXMenuService)
  strict private
    FDefaultImpl: IFMXMenuService;
    procedure DestroyMenuItem(const AItem: IItemsContainer);
    //...
  public
    constructor Create;
  end;

constructor TMacMenuServiceFix.Create;
begin
  inherited Create;
  FDefaultImpl := IFMXMenuService(
    TPlatformServices.Current.GetPlatformService(IFMXMenuService));
  TPlatformServices.Current.RemovePlatformService(IFMXMenuService);
  TPlatformServices.Current.AddPlatformService(IFMXMenuService, Self);
end;

In this example I store a reference to the stock implementation in order to delegate to it for methods that I don’t wish to customise.

All in all I think this change a positive one. Nonetheless, the API is – how can I put this – rather ugly! Check out how I retrieve the default implementation of IFMXMenuService above:

  FDefaultImpl := IFMXMenuService(
    TPlatformServices.Current.GetPlatformService(IFMXMenuService));

Notice how I have to type the word ‘service’ *four* times, the phrase ‘PlatformServices’ and ‘IFMXMenuService’ twice each, and the word ‘current’ once yet completely pointlessly – for some reason the person who wrote TPlatformServices doesn’t seem to realise there are things called class vars and class methods. I wonder if he’s related to whoever designed TTimeZone for the XE release…?

That said, wouldn’t it be better if we could just write this instead:

  FDefaultImpl := TPlatformService.Get<IFMXMenuService>;

Similarly, in cases where we are open to the possibility there is no default implementation, you have to write code like the following, annoying cast included:

var
  MenuService: IFMXMenuService;
begin
  if TPlatformServices.Current.SupportsPlatformService(
    IFMXMenuService, IInterface(MenuService) then

Wouldn’t that be better like this though?

var
  MenuService: IFMXMenuService;
begin
  if TPlatformService.Available<IFMXMenuService>(MenuService) then

I think so, and whadya know, it took about two minutes to write:

  TPlatformService = record
    class function Available<IntfType: IInterface>(
      out Service: IntfType): Boolean; static;
    class function Get<IntfType: IInterface>: IntfType; static;
  end;

uses System.TypInfo;

class function TPlatformService.Available<IntfType>(
  out Service: IntfType): Boolean;
var
  Guid: TGUID;
begin
  Guid := PTypeInfo(TypeInfo(IntfType)).TypeData.Guid;
  Result := TPlatformServices.Current.SupportsPlatformService(
    Guid, IInterface(Service));
end;

class function TPlatformService.Get<IntfType>: IntfType;
var
  Guid: TGUID;
begin
  Guid := PTypeInfo(TypeInfo(IntfType)).TypeData.Guid;
  Result := IntfType(TPlatformServices.Current.GetPlatformService(
    Guid));
end;

[Edit: originally I had ‘TCurrentPlaformService’ rather than ‘TPlatformService’, but as Eric Grange rightly says in the comments, why have the word ‘current’ at all? There’s also an argument for dropping the ‘T’ prefix, but I’ve kept that in to be consistent with the wider D2009+ RTL.]

Advertisements

15 thoughts on “And the prize for ugliest API goes to…

  1. And you could further clarify it by using just “PlatformServices” for the “TCurrentPlatformServices”, since after all, it’s not a concrete type, but a singleton ersatz, so one might as well hide the plumbing!

    • Yes on removing the ‘current’ thing, not so much on the ‘T’ prefix – I’ve gradually reconciled myself to it, though I agree it doesn’t look wonderful.

      ‘a singleton ersatz’

      A record type with ‘class’ vars and methods is actually my preferred sort of ‘singleton’ – if the ‘object’ gets complicated enough to require something more, I’m inclined to think a singleton was a bad idea in the first place…

      • Oh, I agree a global var is best hidden in a class var, and since Delphi doesn’t support static classes, a record is the next best thing to host it.

        But when a record (or class) is never meant to be used as a type, but is just there as a form of namespace for “global” variables and methods, I personally tend to drop the ‘T’ these days, so it isn’t confused with a type.

        • On the ‘T’ thing, I did the same originally, Like you suggest, it looks odd because while what the routines are attached to is formally a type, it isn’t a ‘type’ in a more general sense of the word (e.g., ‘types vs tokens’ in analytic philosophy). OTOH, this was a latent ambiguity in Delphi naming conventions since the beginning – the difference now is just that class and static methods are being used outside the classic virtual constructor scenario more.

  2. The standard COM-like pattern would be to write:
    var
    MenuService:IFMXMenuService;
    begin
    if TPlatformServices.Supports(IFMXMenuService,MenuService) then
    menuService.DoStuff;

    Too bad nobody did it like that.

    W

    • It would appear whoever wrote it was trying to avoid exposing a method with an untyped pointer, because of course, it boils down to a Supports/QueryInterface call. I’m not quite sure why forcing ugly-looking casts are actually any ‘cleaner’ than a properly documented untyped parameter mind.

  3. The OpenTools Api is chock full of this nonsense as well. I think it would probably benefit from a similar treatment. Definitely a code model to bring to Embarcadero’s attention, if only to see what sort of excuses they come up with about why they won’t use it.

    Thankfully, we don’t need ’em for it.

  4. Also if IFMXMenuService and other services are/were based on the same ‘root’ interface, you could use that to constrain the generic (and hope that the IntelliSense in the IDE picks up the hint)

    • I’ll have to check, but is that actually possible using the current state of both generics and interface types? Off the top of my head, the elephant in the room is the reduction of Delphi interfaces to IUnknown, and subsequent reliance on explicit GUIDs for their identities.

    • it’s just a matter of syntax, once you get to write in C-like languages, after a few months, you’re going to love it! the only drawback that I see is the “if ( a = b )” which can mean assignment o.O

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s