TweetFollow Us on Twitter

OOP World
Volume Number:7
Issue Number:9
Column Tag:Developer's Forum

OOP in a non-OOP World

By Steve Sheets, Herndon, VA

[The following article was based on a presentation at the 1990 MacWorld conference in Boston. The speech was given as part of a panel discussion on Object Oriented Programming.]

OOP in a non-OOP World.

“Object Oriented Programming.” These words congure up images of new programming langauges like Smalltalk and C++. Visions of programmers everywhere, throwing out their old compilers and sample code, and starting to program exclusively in an Object Oriented Programming Language are called to mind. Jokes about those who can only program in C or Pascal (“how many Pascal Programmer does it take...”) begin to appear.

Time will tell if the above situations are more spoof than prediction. However, the thinking that inspired them has a very specific fallacy that I would like to address. First, let it be known that I am one of the world’s biggest fans of Object Oriented Programming. I honestly believe that object oriented programming is likely to become the next step in the evolution of programming languages. However, I do not agree that this next step in programming style will be the exclusive domain of the new Object Oriented Programming Langauges. This article intends to show how to make use of object oriented techniques while programming in a non-object-oriented environment. It is possible to use the ideas contained in Object Oriented Programming Languages without using the traditional object oriented constructs like objects, classes, inheritances, or methods. To understand how to go about using the techniques, but not the items, a little history lesson is in order. Even though this lesson is on a slightly different subject, it does have a point.

Back in the long ago, when Computers were first created and introduced to the swarms of eager programmers, the majority of the programming languages were line oriented. Languages like Basic and early versions of Fortran and Cobol usually paralleled Assembly language, where almost every line of code did a specific task and every line was followed by another line of code.

The flow of the program was usually from one line to another. The GOTO or Conditional GOTO statements (or their equivalents) were used to control the flow by branching, but in general, the flow was linear. This technique could cause problems. For one thing, a programmer had to understand practically every single line of the code before he could work with any piece of it. Otherwise, he would never know for sure if the portion of the code he was modifying had an effect on some other line of code. If a person were to try to work with code he had not created, he would likely have to spend a great deal of time studying it before he could work with it.

A second, nastier problem with the linear flow arrangement became apparent as programs became more complex. A simple piece of code might be modified again and again, with different branches at many points in the code. Soon programmers started getting Spaghetti Code, where the code was so complex, and the flow so strange, that no one, perhaps not even the original programmer, could understand how the code worked.

To solve this problem, more Procedural oriented languages were introduced. Languages like Pascal, C, and later expanded versions of line oriented languages were given syntax and commands that allowed a programmer to divide the code into more manageable procedures. People stated talking about the joys of this new Procedural Programming style.

Now a interesting thing started happening. Programmers who used the procedural languages, and who learned the joy of procedural programming, would sometimes still have to program using the older languages. However, they had gotten used to the advantages of the newer languages. So they added some of those new ideas to their programs, even though they were using the old languages. They wrote the programs so that they had separate subroutines and functions. Even Basic could be written in this procedural style, once the programmer understood the concept.

Notice that just because a program is written in a good procedural base programming language, it does not necessarily follow that the program is good code. Some of the worst examples of spaghetti code have been written in the so called higher level languages. These languages just made it easier to learn a new idea, like procedural programming. The languages helped to teach programmers good programming habits. Once the style was learned, it was not necessary to use the new language or features in order to implement the technique.

That’s the end of the history lesson. Now, to come back to the present; nearly everyone has been exclaiming the joys of these new Object Oriented Programming Languages. Notice any possible parallel to my history lesson? Just like those who years ago promoted Procedural Programming, they are right. There are many benefits to using object oriented programming. This new type of programming will allow you to code better. But you do not have to use Smalltalk or C++ or Object Pascal in order to implement the benefits of object oriented programming.

Many programmers would probably like to use an object oriented approach, or at least they would be interested in learning about it. However, these people may be unable, for perfectly valid reasons, to start using an Object Oriented Programming Language today. Many may have to work in a language specified by their company, or one that is specified by their job contract. Some may have to work on a computer that does not have a Object Oriented Programming Language. Or some might have to program with co-workers who have not yet “seen the light” of Object Oriented Programming. And many may have already invested a large amount of time and energy a piece of non-object-oriented code, resources they might not have to reinvest in order to convert over to an object oriented approach.

That last reason was the one that kept me from using an Object Oriented Programming Language on my project, America Online. There was already an Alpha version of the code written in Think Pascal, without Object extensions. Neither MacApp or any other Object Oriented Programming Language was an option for America Online when I came onto the project. While there is almost nothing left of the original Alpha code in the current version of America Online, there never was a time when we could take the time that would have been needed to convert the entire code to use Object Pascal extensions or MacApp. So instead, what we did was to look at various parts of America Online and decided what we could improve using what we had learned about America Online.

Since I’m going to refer frequently to my project in this article, perhaps I’d better give you a little information about it. For those who are not familiar with it, America Online is a telecommunication service which connects our host mainframe to personal computers using proprietary protocols and software. In my case, we are using Macintoshes connected over a serial modem line. Using a low-level protocol, packets of data are transferred, error free, from the host to the Mac or visa versa. When a data packet comes in, the first few bytes of data designate what task that packet is intended to preform. A group of tasks related to a specific online function is called an “Engine”. Engines include the Form Engine (to create the windows), the Chat engine (to handle online chat rooms), Async Engine (to handle other actions), File transfer engine (to handle uploads & downloads), the LogOn Engine (to handle the log on process), and the LogOff engine (which handles the log off process).

When a packet of information comes in from the Host, America Online has to decide which engine handles that packet. Originally, America Online used procedural styles. Each Engine had a Packet Handling routine that decided whether or not that packet was handled by that engine. If it did, the packet was handled and the function returned TRUE. If it did not, the function did nothing but return FALSE. Every engine accessed the global variable space of the program in order to store states.

The original code segment that parsed a packet looked something like this.

{1}

FUNCTION Form_Engine(thePacket:TDataType);   FORWARD;
FUNCTION Chat_Engine(thePacket:TDataType);   FORWARD;
FUNCTION File_Engine(thePacket:TDataType);   FORWARD;
FUNCTION LogOn_Engine(thePacket:TDataType);  FORWARD;
FUNCTION LogOff_Engine(thePacket:TDataType); FORWARD;

PROCEDURE ParsePacket(thePacket:TDataType);
BEGIN
 IF NOT Form_Engine(thePacket) THEN
 IF NOT Chat_Engine(thePacket) THEN
 IF NOT Async_Engine(thePacket) THEN
 IF NOT File_Engine(thePacket) THEN
 IF NOT LogOn_Engine(thePacket) THEN
 IF NOT LogOff_Engine(thePacket) THEN
 ;
END;

This piece of code just simply bothered me. It looks cumbersome and is difficult to manipulate. Each engine is always called, no matter whether or not the engine is on. There is no sense of which packet might effect what global data. Adding more engines to the code made the code segment even more awkward.

So I decided to apply a few of the techniques that I learned in Object Oriented Programming classes. I created some new data structures. These new structures included a new record type (TEngineRec), a pointer to this record (TEnginePtr) and a handle to the record (TEngineHdl). TEngineRec consisted of a TEngineHdl, a normal data Handle and a ProcPtr.

{2}

TYPE  TEngineRec = RECORD
 EngineData: Handle;
 EngineProc: ProcPtr;
 NextEngine: TEngineHdl;
 END;
 TEnginePtr = ^TEngineRec;
 TEngineHdl = ^TEnginePtr;

For those unfamiliar with ProcPtr, it is a pointer to an actual procedure or function in memory. Depending on your language or development system, it allows the procedure it indicates to be invoked. To do the actual invoking of this procedure, I defined an Inline call that places the correct parameters on the stack and then jumps to the procedure the pointer is aimed at and executes it.

In order to make this code work, I also created one global variable, GEngineList, a TEngineHdl type.

VARGEngineList: TEngineHdl;

Obviously the idea was to try to create a linked list of Engines. GEngineList points to the first Engine in the list, and each engine points to the next one. A set of routines to add and subtract Engines to the the list was also needed. I therefore declare the following call.

{3}

FUNCTION AddEngine(theProc:ProcPtr;theData:Handle):TEngineHdl;
FORWARD;

PROCEDURE RemoveEngine(theEngine:TEngineHdl);
FORWARD;

These routines are uses to add and remove an engine from our list of active engines. The AddEngine call would be invoked like this:

{4}

VARtheFormEngine:TEngineHdl;
 theFormData:Handle;

theFormData :=...{Code to initilize data}...

theEngine:=AddEngine(@Form_Engine,theFormData);

Notice that when an engine was added, the pointer was passed to the Engine’s Packet_Handling routine. That routine will be executed later, when a packet comes in.

Now, let us revisit the original piece of code. The original code will pass a packet to all engines until it finds one that wants the packet. The new code only passes packets to engines that have been installed.

{5}

PROCEDURE New_ParsePacket(thePacket:TDataType);
VARHandled:BOOLEAN;
 theList:TEngineHdl;
BEGIN
 Handled:=FALSE;
 theList :=GEngineList;
 WHILE (NOT Handled) and (theList<>NIL) DO BEGIN
   IF Call_To_Proc(thePacket,theList^^.theData, theList^^.theProc)
 THEN Handled:=TRUE
 ELSE theList:=theList^^.NextEngine
 END;
END;

Call_To_Proc is a glue routine that actually jumps to the Procedure pointed to at theList^^.theProc with thePacket and theList^^.theData as parameters.

Now, imagine what happens during a normal logon sequence. As a user first goes online, a LogOn Engine is created. Once the person is completely online, the LogOn Engine is removed and it’s handle and data handle are deallocated from memory. Then the Forms Engine and the Async engines are added to the Engine List. These Engines normally run for the life of an online session to handle most normal forms and asynchronous events.

At some point the user might decide to go to a Chat room. The Chat engine is added when, and only when, the user enters a Chat room. When he leaves the Chat room, the engine is removed. The same can happen to a File transfer engine.

Some of the advantages of this approach become apparent if you consider how we can use the new code to create new situations. Since the data for each engine is completely contained in that engines data handle, the protocol could be changed so that a user can be in more than one chat room at the same time. This would mean a chat room packet would have to have some information in it to determine whether it pertained to room “A” or room “B”, but that information would only require a couple of bytes of data and would be easy to include.

There is no reason why File transfer, or any other engine that could be conceived, could not be added and invoked any number of times, at any point in an online session. This gives us an amazingly powerful online asynchronous environment. What we are really seeing is an example of the Object Oriented concept of encapsulation of data and the concept of instance of an object.

More than one instance can easily be added to the code at any time. Until an instance is added, additional memory need not be allocated. If an engine is changed, or even if a new one is added, only routines related to the new engine need to be added. The parser code segment used to send a packet to the correct engine is never changed.

I have always considered Data Encapsulation and Instance of objects to be two of the three most important ideas associated with Object Oriented Programming. The third important idea is that of Inheritance. And this function of an Object Oriented Programming Language can be adopted to non-Object Oriented Programming Languages, too.

Actually we are already halfway there in our example. ProcPtrs can be manipulated so that certain data structures have pointers to some procedures, while other instances use other procedures. This simulates the idea that a method can be overridden or inherited. To see this more easily, lets consider another example that has ProcPtrs.

America Online has a very complete Forms engine that allows us to create almost any form using packets sent from the host. Each form packet has information describing windows and different fields on these windows. Different field types that can be on a window might include buttons, icons, pictures, non-editable text, editable text, lists, and so on. All the fields have certain tasks that they have to be able to perform. For example, each field must be able to be drawn, it must be able to handle a mousedown in it’s area, it must be able to be hilited, it must be able to handle a keystroke, it must be able to invert itself and it must have an action associated with it. Following this idea, a field data structure might look like this.

{6}

FieldRec = RECORD
  fArea : Rect;
  fActionNum : INTEGER;
  fText: Str255;
  fData : Handle;
  pMouseDown,pUpdate,pActivate,pHilite,pKeydown,pAction : ProcPtr;
 END;

FieldRec = ^ FieldPtr;
FieldHdl = ^FieldPtr;

Besides the data, a number of ProcPtrs are declared, one for each major task. Some of the tasks might be do nothing tasks. For instance, a non-editable text field would ignore keystrokes, or a picture would not need to have an action associated with it. Others are more specific for the given field type (the keystroke routine for an editable text field).

For example, when the window needs to be updated, the code might find the field list for that window. Then it would one by one cycle through each of the fields, invoking the draw command so that that portion of the window is drawn. That code would look something like the following example. Notice if the ProcPtr is nil, that means there is a do-nothing task, and it should be skipped.

{7}

PROCEDURE UpdateDisplay(theDisplay:DisplayHdl);
VAR theField:FieldHdl;
BEGIN
 theField:=theDisplay^^.FirstField;
 WHILE (theField<>NIL) DO BEGIN
   IF theField^^.pUpdate<>NIL
   THEN Invoke_Update(theField,theField^^.pUpdate);
   theField:=theField^^.NextField;
 END;
END;

If a mousedown occurs, a similar scan is done. In that case, each field is checked to see if the mousedown occurs in it. The Mousedown ProcPtr will only be invoked if the mousedown has occurred in the relevant field. Notice that no matter which type of field is used, these pieces of code will never change.

Somewhere in the code, there will be a segment of code that creates these fields; this procedure is called New_Field. Each kind of field will have its own declaration section in the procedure. For example, a picture might look like this.

{8}

FUNCTION New_Field(theKind:INTEGER;
 theBox:Rect;
 theID:INTEGER):FieldHdl;
VAR theField:FieldHdl;
BEGIN
 ...
 CASE Kind OF
 ...
 kPicture: BEGIN
   theField^^.fArea:=theBox;
   theField^^.fData:=GetPicture(theID);
   ...
   theField^^.pUpdate:=@Draw_Pic_Field;
   theField^^.pHilite:=@Invert_Area_Field;
   theField^^.pAction:=NIL;
 ....
 END;
 OTHERWISE
 New_Field:=theField;
END;

You can see that this kind field basically does nothing but draw itself. A more interesting kind of field might be a clickable Icon field.

{9}

 kClickIcon: BEGIN
   theField^^.fArea:=theBox;
   theField^^.fAction:=theAction;
   theField^^.fData:=GetIcon(theID);
   ...
   theField^^.pUpdate:=@Draw_Icon_Field;
   theField^^.pHilite:=@Invert_Icon_Mask_Field;
   theField^^.pAction:=Do_Action_Number;
   theField^^.pMouseDown:=@Track_Invert;
   ....
 END;

This type of field has it’s own draw and invert calls. It also uses the Track_Invert call in order to handle a mousedown. That call keeps track of whether or not the mouse is inside the field area. It invokes the Invert ProcPtr repeatedly to hilite or unhilite the field depending on if the mouse is in or out of the area. If the user releases the button while the mouse is over the field, that call invokes the Action ProcPtr. The action ProcPtr is the most generic one, the Do_Action_Number procedure which does an action defined in the ActionNumber field.

If an entirely new kind of field needs to be created, say for example a clickable Picture, some of the previously defined routines could be used (inherited) to define this kind of object. This code would be added to the new field.

{10}

 kClickPict: BEGIN
 theField^^.fArea:=theBox;
 theField^^.fAction:=theAction;
 theField^^.fData:=GetPicture(theID);
 ...
 theField^^.pUpdate:=@Draw_Pic_Field;
 theField^^.pHilite:=@Invert_Area_Field;
 theField^^.pAction:=Do_Action_Number;
 theField^^.pMouseDown:=@Track_Invert;
 ....
 END;

We have created a new type of field, with it’s own specific appearance and function, without adding any new code except for the creation routine. Even if we had to create a new field with a new special task, (such as a new way to handle drawing,) only a single new procedure would need to be written (in this case, Draw_Bitmap_Icon). We can use the old procedures/tasks for the rest of the new field.

Notice that this Clickable PICT field is related to a non-Clickable PICT field. These fields don’t quite function as a superclass and a subclass, but there is a relationship involved. This might not be true inheritance, but it sure does make it easier to create new fields when they are needed.

Inheritance, instancing of objects and data encapsulation are possibly the three most important ideas in object oriented programming, and they have all been simulated in a non-object-oriented procedural language. And I would like to add that this this is not merely sample code that is never used in a real-world application. These three ideas are used over and over again inside of America Online.

The code would have never been designed the way it was if I had not aware of the object oriented concepts and had not seen them used in a object oriented world. America Online is a better product because of the object oriented lessons learned. The code is more flexible, easier to work with and more powerful. Even if a programmer is never to use MacApp in a major project, or he is never to use an Object Oriented Programming Language in a job, the project or job he is working on, the code he creates may be better if he uses the object oriented concepts he learns.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

ffWorks 1.0.6 - Convert multimedia files...
ffWorks (was iFFmpeg), focused on simplicity, brings a fresh approach to the use of FFmpeg, allowing you to create ultra-high-quality movies without the need to write a single line of code on the... Read more
Adobe Acrobat 18.011.20035 - Powerful PD...
Acrobat DC is available only as a part of Adobe Creative Cloud, and can only be installed and/or updated through Adobe's Creative Cloud app. Adobe Acrobat DC with Adobe Document Cloud services is... Read more
Adobe Acrobat Reader 18.011.20035 - View...
Adobe Acrobat Reader allows users to view PDF documents. You may not know what a PDF file is, but you've probably come across one at some point. PDF files are used by companies and even the IRS to... Read more
EtreCheck 4.0.3 - For troubleshooting yo...
EtreCheck is an app that displays the important details of your system configuration and allow you to copy that information to the Clipboard. It is meant to be used with Apple Support Communities to... Read more
Yummy FTP Pro 2.0 - $29.99
Yummy FTP Pro is an advanced Mac file transfer app which provides a full-featured professional toolkit combined with blazing speeds and impeccable reliability, so whether you want to transfer a few... Read more
Opera 51.0.2830.34 - High-performance We...
Opera is a fast and secure browser trusted by millions of users. With the intuitive interface, Speed Dial and visual bookmarks for organizing favorite sites, news feature with fresh, relevant content... Read more
Apple iOS 11.2.6 - The latest version of...
iOS 11 sets a new standard for what is already the world’s most advanced mobile operating system. It makes iPhone better than before. It makes iPad more capable than ever. And now it opens up both to... Read more
Google Chrome 64.0.3282.167 - Modern and...
Google Chrome is a Web browser by Google, created to be a modern platform for Web pages and applications. It utilizes very fast loading of Web pages and has a V8 engine, which is a custom built... Read more
Adobe Photoshop CC 2018 19.1.1 - Profess...
Photoshop CC 2018 is available as part of Adobe Creative Cloud for as little as $19.99/month (or $9.99/month if you're a previous Photoshop customer). Adobe Photoshop CC 2018, the industry standard... Read more
Apple iOS 11.2.6 - The latest version of...
iOS 11 sets a new standard for what is already the world’s most advanced mobile operating system. It makes iPhone better than before. It makes iPad more capable than ever. And now it opens up both to... Read more

Latest Forum Discussions

See All

Check out these 5 games that are a lot l...
So you're in love with Minecraft, but you're looking for something else to play as well? You've come to the right place then, because this list is all about games that are a bit like Minecraft. Some of them, more than others. [Read more] | Read more »
Our top 5 characters from casual RPG Cre...
Creature Quest definitely lives up to its name with a host of collectible creatures based on fantasy tales and world mythologies. To celebrate Creature Quest’s first birthday, we’re going to lay out what we think are the five best characters in the... | Read more »
Around the Empire: What have you missed...
Did you know that Steel Media has a whole swathe of other sites dedicated to all aspects of mobile gaming? Sure you'll get the very best iPhone news, reviews, and opinions right here at 148Apps, but we don't want you missing out on a single piece... | Read more »
All the best games on sale for iPhone an...
Oh hi there, and welcome to our round-up of the best games that are currently on sale for iPhone and iPad. You thought I didn't see you there, did you, skulking behind the bushes? Trust me though, the bushes aren't where the best deals are. The... | Read more »
The Battle of Polytopia Guide - How to H...
A new update just released for The Battle of Polytopia (formerly Super Tribes), which introduces online multiplayer. For all the fans of Midjiwan’s lite take on Civilization, this is certainly welcome news, but playing online isn’t as easy and... | Read more »
Here are the very best mobile games to p...
It's Valentine's Day! Did you get loads of cards and chocolates and other tacky, simple expressions of human affection? Did you send out tat because you find it almost impossible to express emotion unless there's a section dedicated to it at your... | Read more »
Florence (Games)
Florence 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: Florence is an interactive storybook from the award-winning lead designer of Monument Valley about the heart-racing highs and... | Read more »
Purrfect Date (Games)
Purrfect Date 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: Cats are a lil’ like marmite. Either you absolutely head-over-heels love’ em, or… nahhh, who are we kidding?! Everyone... | Read more »
More monsters to collect and evolve in C...
A laid-back mix of RPG and TCG, Creature Quest is all about building your deck, evolving your creatures and winning in battle. It’s the creation of VC Mobile, set up by Might and Magic producer Jon Van Caneghem. There are elements of that classic... | Read more »
Check out this awesome hands-on with the...
Well, PlayerUnknown's Battlegrounds has come out on mobile. This isn't a clone, this isn't a riff on the battleroyale mechanics of the game, it's the official mobile port by Tencent. But there's a little bit of a hitch. [Read more] | Read more »

Price Scanner via MacPrices.net

Save up to $350 with these Apple Certified Re...
Apple has a full line of Certified Refurbished iMacs available for up to $350 off original MSRP. Apple’s one-year warranty is standard, and shipping is free. The following models are available: – 27... Read more
B&H offers $200 discount on Silver 15″ Ma...
B&H Photo has Silver 15″ Apple MacBook Pros on sale for $200 off MSRP. Shipping is free, and B&H charges sales tax for NY & NJ residents only: – 15″ 2.8GHz Touch Bar MacBook Pro Silver (... Read more
12″ Apple iPad Pro Sale of the Year! Models u...
B&H Photo has 12″ #iPad Pros on sale for up to $150 off MSRP. Shipping is free, and B&H charges sales tax in NY & NJ only: – 12″ 64GB WiFi iPad Pro: $719 $80 off MSRP – 12″ 256GB WiFi... Read more
Deals on 32GB 9″ iPads: Up to $50 off MSRP, s...
B&H Photo has 2017 9.7″ 32GB iPads on sale for $299 including free shipping plus NY & NJ sales tax only. Their price is $30 off MSRP, and it’s currently the lowest price available for these... Read more
15″ 2.2GHz Retina MacBook Pro available for o...
Apple has Certified Refurbished 15″ 2.2GHz Retina MacBook Pros available for $1699. That’s $300 off MSRP for this model, and it’s the lowest price available for a 15″ MacBook Pro currently offered by... Read more
13″ 3.1GHz/256GB Silver Touch Bar MacBook Pro...
Amazon has the Silver 13″ 3.1GHz/256GB Touch Bar MacBook Pro (MPXX2LL/A) on sale for $1649.99 including free shipping. Their price is $150 off MSRP, and it’s the lowest price available for a new 13″... Read more
Saturday Sale: Amazon offers 13″ 1.8GHz/256GB...
Amazon has the 13″ 1.8GHz/256B Apple MacBook Air on sale today for $250 off MSRP including free shipping: – 13″ 1.8GHz/256GB MacBook Air (MQD42LL/A): $949.99, $250 off MSRP Their price is the lowest... Read more
Roundup of Apple Certified Refurbished 12″ Ma...
Apple has Certified Refurbished 2017 12″ Retina MacBooks available for $200-$240 off the cost of new models. Apple will include a standard one-year warranty with each MacBook, and shipping is free.... Read more
Apple offers Certified Refurbished 10″ and 12...
Apple is now offering Certified Refurbished 2017 10″ and 12″ iPad Pros for $100-$190 off MSRP, depending on the model. An Apple one-year warranty is included with each model, and shipping is free: –... Read more
Apple Canada offers Certified Refurbished Mac...
 Canadian shoppers can save up to $560 on the purchase of a 2017 current-generation MacBook Pro, MacBook, or MacBook Air with Certified Refurbished models at Apple Canada. Apple’s refurbished prices... Read more

Jobs Board

*Apple* Store Leader - Retail District Manag...
Job Description:Job SummaryAs more and more people discover Apple , they visit our retail stores seeking ways to incorporate our products into their lives. It's your Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description:SalesSpecialist - Retail Customer Service and SalesTransform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description:SalesSpecialist - Retail Customer Service and SalesTransform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.