Teaching TDD: The importance of expectations

by Rob Galanakis on 8/02/2014

I read this interesting article from Justin Searls about the failures of teaching TDD, and his proposed improvements. Uncle Bob wrote up an excellent response on how Justin’s grievances are valid but his solutions misguided.

Justin’s article included an excellent image which he calls the “WTF now, guys?” learning curve. I think it sums up the problems with teaching TDD perfectly, and its existence is beyond dispute. I’ll call the gap in learning curve the WTF gap.

WTF now, guys?  learning curve

Uncle Bob, in his post linked above, touches on a very important topic:

Learning to refactor is a hill that everyone has to climb for themselves. We, instructors, can do little more than make sure the walking sticks are in your backpack. We can’t make you take them out and use them.

Certainly, giving a student the tools to do TDD is essential; but it is equally essential to stress the inadequacies of any tool an instructor can provide. A walking stick is not enough. A good instructor must lay down a set of expectations about TDD for students, managers, and organizations so they can deal with the frustration that arises once the WTF gap is hit.

First expectation: TDD describes a set of related and complicated skills. TDD involves learning the skills of writing code, test design, emergent design, refactoring, mocking, new tools, testing libraries, and more. You cannot teach all of these in a week, or even on their own. Instructors must introduce them via TDD, slowly. Until the student has proficiency with all of these topics, they cannot get over the WTF gap. This fundamentally changes how TDD must be practiced; students need a mentor to pair with them on work assignments when new TDD skills are introduced. If an organization or team is starting TDD, they need to have a mentor, or budget considerable time for learning.

Second expectation: Your early TDD projects will suck. Just like you look back in horror on your projects from when you first learned programming, or learned a new language, your first projects you use TDD on will have lots of problems. Tests that are slow, brittle, too high-level, too granular, code that has bugs and is difficult to change. This is normal. The expectation must be set that the first couple libraries or projects you use TDD for are going to be bad; the goal is to learn from it. The WTF gap is real; people must expect it and persevere past it.

Third expectation: If your organization isn’t supportive, you will fail. If you are one lonely person using TDD on a team of people who are not, you will fail. If you are one lonely team trying to use TDD in part of a larger codebase where others are not, you will fail. If your organization sets ridiculous deadlines and does not allow you to learn the TDD skillset over time, being slower initially, you will fail. If you want to do TDD and are not enabled, go somewhere you will be, budget time for changing the culture, or live with endless frustration and broken dreams. You need help to bridge the WTF gap.

Fourth expectation: TDD for legacy code is considerably more difficult. Learning environments are pristine; our codebases are not. There is a different set of strategies for working with legacy code (see Michael Feathers’ book) that require a pretty advanced TDD skillset. Beginners should stay away; use TDD for new things and do not worry about refactoring legacy code at first. At some point, pulling apart and writing tests for tangled systems will get easier, and may even become a hobby. The WTF gap is big enough; don’t make it more difficult than it need be by involving legacy code.

My feeling on teaching TDD is that no matter how you teach it, whether with some of Justin’s flawed ideas or Bob’s proven ones, you need to set proper expectations for students, managers, and teams.

No Comments

What a powerful thing metaprogramming is!

by Rob Galanakis on 5/02/2014

While editing a chapter of my book, I was introducing the concept of metaprogramming using Python’s type function. It occurred to me that I had already introduced metaprogramming several chapters earlier when introducing decorators.

Defining a function within another function is as important to my programming as bread is to French cuisine. I began thinking of all those cultures without wheat; all those languages without, or with newly added support for, metaprogramming. I have never done serious development in a language without anonymous functions, closures, and reflection.

It was exciting to think of a coming generation of programmers who are in my shoes (I started programming relatively late in life), who would be inherently comfortable with passing functions. It was exciting to realize where languages are going, keeping static typing but removing the explicit part. It was exciting to think of how flexible, expressive, and powerful languages have become.

It also allowed me to think of less flexible languages and what they’ve been able to achieve. I am lucky to be programming now, but surely each programmer before me felt the same about those before them. More will feel the same after and about me. Really my luck is to be part of what is still such a new and remarkable part of the human endeavor.

All of this feeling from type and decorators. What a powerful thing metaprogramming is!


Agile Game Development is hard

by Rob Galanakis on 2/02/2014

I’ve spent the last few weeks trying to write a blog post about why Agile software development is inherently more difficult for games than other software. I searched for some fundamental reason, such as games being works of art, or being entertainment, or being more difficult to test, or anything about their very nature that makes game development different from other types of software development.

I couldn’t find one. Instead, I came up with reasons that are purely circumstantial, rooted in business models and development environments. Nonetheless, it is the situation we are in; the good news is, we can change it.

4+ Reasons Agile Game Dev is Tricky

Number one: the insane business model based on packaged games. Develop a game for years, market the hell out of it, ship it, profit, repeat. Crunching hard is probably in there, as is going bankrupt. Each year fewer and fewer games garner a larger share of the sales, and budgets are often reaching into the hundreds of millions of dollars to continue this model. This is pure insanity, so development methodologies of greater sanity, like those based on Agile principles, simply cannot thrive. Often they struggle to even take hold. Don’t underestimate the depth of this problem. We have a generation of executives and marketers (and developers) who know only this model, and trying to explain to them how you need to be flexible and iterative with releases and develop with tests can feel like a losing battle.

Number two: We’ve equated Scrum with Agile. Agile embodies a set of principles, but we’ve equated those principles with a (limited) set of tools: the Scrum project management methodology (you can substitute Lean and Six Sigma in the previous example; this phenomenon is not unique to games). If you’re ever tried to impose Scrum on an art team, you can see how much of a disaster it is. Rather than take Agile or Lean principles and ask “what is a good way to work that values these principles?”, we just institute some form of Scrum. I’ve seen many people dismiss Agile because Scrum failed, which is a shame. And like Scrum, I’ve also seen forms of soulless Kanban implemented (soulless because it doesn’t support the principles of Kanban, like limiting work and progress, managing flow, and understanding constraints).

Number three: Game development was late to the Agile party. Software has had about 15 years to figure out how to apply Agile to business and consumer applications and websites. While “flaccid Scrum” now seems common in games, that’s relatively recent; combined with multi-year development cycles in these so-called “Agile” shops, there hasn’t been much of the learning and reflection that underpins Agile. On top of this, Agile is in a period of maturity right now and is being appropriated by project management, so it is difficult to innovate in the methodology space to come up with an alternative to something like eXtreme Programming that works in game development.

Number four is pretty interesting: Game sequels are not iterations. It is very common to build up mountains of debt to ship a game, and then throw away and rewrite those mountains for the sequel. This worked okay because sequels were usually much more disruptive than innovative so there were more opportunities for rewrites. In contrast, consider that the MS Office UI stayed basically the same from 1993 to 2006. Now as games are entering a loosely defined “software as a service” model, our development priorities must change. We need to be able to iterate month-by-month on the same codebase and pull things forward. This is a new set of skills we need to develop.

There are a number of smaller items that are less important but still should be pointed out:

  • Game development hasn’t embraced open source and is on Windows. Many developers and executives I’ve met have a distrust of OSS (CCP’s use and support of Python and other OSS is a source of pride for me and others here) and the majority of game development is on Windows. The Agile movement has strong roots in OSS and Linux, so aside from the cultural differences between the two communities (which should not be underestimated), there was just a lack of engagement between game developers on Windows and Agile evangelists on Linux.
  • Game development reinvent wheels. The availability of lots of excellent open source middleware has given non-game developers a leg up on focusing on their core product. If you had to develop your product and webserver, you’d incur not just the cost of developing both but of splitting focus. Game development has historically done a poor job of using middleware and has often reinvented the wheel; this has probably historically been due to the desire for maximum performance and ridiculous deadlines and business models. With more hardware to spare, I suspect this will change and we’ll see things like HTTP used between client/server instead of custom RPC stacks.

Reasons Agile Game Dev is not Tricky

Finally, there are a number of arguments I have thought over and rejected, including:

  • Games are art and art cannot be iterated on like other software.
  • Games require too much ‘infrastructure’ to make anything playable.
  • Games want users to spend time, not save time.
  • Games are impossible, or at least significantly more difficult, to test.
  • Fat clients are difficult to distribute.
  • Frequent releases are confusing for games, which are traditionally content-heavy.

Call to Action

There are solutions to all of these problems, but it requires getting to the core of Agile’s principles, and even more importantly, the Lean principles those are based on. What game development needs is a new set of practices and tools, better suited to our technological problems, that fulfill the same principles and can be mixed and matched with existing Agile practices and methodologies. Some ideas or topics for discussion in future posts.


A story of simplification and abstraction (stackless timeouts)

by Rob Galanakis on 11/01/2014

Someone was asking me the other day how to implement a timeout in a thread. His initial implementation used two background threads: one to do the work (making requests to a web service and updating a counter), and the other in a loop polling the counter and sleeping. If the first thread stopped updating the counter, the second should report some sort of error.

I helped him simplify the design in a couple ways. First I had him use stackless instead of threads and taught him how threading and microthreads work. Based on that, I suggested that instead of a counter and loop/sleep, there is a parent tasklet that kicks off a child tasklet which does the actual work.* The parent tasklet recvs on a channel with a timeout, and the child tasklet sends on the channel to act like a heartbeat. If the parent recv times out, it means the child tasklet hasn’t reported in and the user can be alerted. This simplified the code considerably.

I then asked a colleague (Kristján Valur) how to do a timeout with stackless, and he told me about the stacklesslib.util.timeout context manager. Doh! It ended up being as simple as:

    for item in items:
        with stacklesslib.util.timeout(200):
except stackless.util.TimeoutError:

It’s pretty amazing what sort of power you’re able to wield with a good language and framework. It’s so important to have the right abstractions, but you need to know how to use it. Even with documentation, nothing beats a little help from your friends.

*Instead of a channel, we probably could have used an Event.


Why CCP is still using Python 2

by Rob Galanakis on 9/01/2014

We at CCP are maybe the heaviest users of Python in videogames, though I have no data to back that up. (I’ll also use this opportunity to say this is a personal blog post, I am in no official capacity here) We use it in the client and server of both EVE Online (PC) and DUST 514 (PS3), and nearly all of our internal infrastructure uses it. What’s stopping us from upgrading at least some portions to Python 3?

Everything. Even if Python 2 weren’t good enough (it is), even if a hundred compelling features were added to Python 3 (there aren’t), even if Stackless was available for Python 3 (it isn’t), we still probably wouldn’t switch. Because literally everything conspires against enterprise employees who want to upgrade any significant codebase. Let’s go over some of those things.

We have our own localization solution inside EVE and the unicode/str bugs have been worked out. Oh, the solution is a nightmare, and our string handling is often a mess, but that just means changing it would be even more difficult. So there’s no real external product need, and internal products and tools usually aren’t localized. But wouldn’t it be great to change it and get rid of that technical debt and simplify things?

Sure it would be great to get rid of that tech debt! As it would for literally every area of our 11+ year old product. There is a limited amount of technical debt we can clean up, and none of it has to do with string handling or any Python 3 features. We just removed a custom importer we’ve wanted to remove for years, which paves the way for other technical debt cleanup. But we’re at least a year from another codebase-wide cleanup, of which there are many to do (let’s remove our dozen remaining builtins, please!). When low-level or non-value-adding work involves convincing people all the way up to the corporate/business level, there are very few people who can organize that sort of thing. I generally prefer that energy is spent on activities that general more value to our engineers.

We have relatively few automated tests. We’ve made some great progress on testing in the past couple years, especially for new or refactored/rewritten code, but there’s absolutely no way to uncover and fix Python 3 upgrade bugs easily. We have “extensive” manual regression tests, but it takes time to get builds to those testers and I imagine the regression test cycle would take months to get everything worked out. It would be a hard sell to QA and the turnaround time on bugs would mean the upgrade process would take months, not weeks.

The only place a Python 3 upgrade gets traction is with the core of the Python community here. And unfortunately we spend most of our capital by trying to improve our own systems, training our Python programmers, and even keeping other developers using Python.*

We can get backports of libraries developed for or in the stdlib of Python 3. If they weren’t provided in a sustainable way by others, we’d just copy them in and keep them locally.

We keep a critical eye on performance. While Python 3 seems fast enough for us now, that’s a pretty new development, and we absolutely will not regress on performance. So that could mean upgrading our codebase and still not be able to use Python 3, since we won’t fully know until we do it. Lovely!

Middleware, the scourge of OSS! We use Autodesk Maya, which uses 2.6 (soon 2.7) for our art pipeline. We would need to write a large chunk of our code to support Python 2 and 3 (an ongoing inconvenience), but also need infrastructure to test them in both (added work but not a huge deal, since we already do this in some cases for 2.6/2.7). But middleware is an easy excuse to stop an upgrade. Even upgrading middleware like Maya, which carries little risk, can take over a year.

Ultimately we’ve been too sloppy and monolithic** (like a large number of enterprise users, I’d imagine). Our products make money. Our developers are supposed to be working on those products and thus making money. We didn’t do a good job balancing technical and business needs, and besides our codebase is really old (EVE Online’s codebase is still an ancestor of the original codebase that EVE released with in 2001).

All of this means the “default” Python for me is Python 2. It is my “go to” language when I’m working on something where I have the option. I haven’t used Python 3 for any real work, so there’s still a threshold to cross. Familiarity and comfort. I don’t even have Python 3 installed at work. And most of the people I know are working in Python 2, so there’s a synergy of inertia. I have no problem with Python 3*** and plan to roll it out at work somewhere (it is a 2014 goal of mine), though I am intrigued with all the Python 2.8 (both vanilla and Stackless) discussions going on recently.

* There are some defections to other languages, and causing inconvenience to these customers on the fence is only more likely to push them into other languages. I have nothing against using other languages inhouse (in fact I wholly support it), I just want to make sure they’re used for the right reason. There’s value in being able to work in other people’s code without having to learn a new language and environment, and there’s value to doing as much as possible in high level languages.

** This has in many ways changed, with lots better automated testing and code quality overall, but legacy code is still the majority. We’re just producing less new legacy code.

*** I also understand that upgrading applications like EVE Online was not part of the Python 3 adoption strategy.


Multiple return values and errors

by Rob Galanakis on 31/12/2013

I was pointed at a rather simplistic article on arstechnica about why most programming languages only return a single value from a function, and thought about my “first class tuples” post from a few weeks ago. Python can effectively support multiple return values due to its built-in tuple unpacking, and doesn’t need to resort to heavyweight or unnecessarily verbose custom types or dictionaries (see tuples post for my rationale). I use “multiple return values” in this way often for the internals of a package where the API can be more fluid and implicit.

This wouldn’t be interesting but I was rereading some articles by the most excellent Laurence Tratt, including this one: A Proposal for Error Handling, which is about his experience writing very fault-tolerant software in C with error codes and comparing that to languages with exceptions. His proposal basically comes down to language support for (return value, error code), and turning error codes not explicitly handled by the caller into exceptions that should only rarely be caught. I know that sounds *just* how exception handling works, but there are subtle yet vital differences he provides in his article (which is worth a read, as I rarely see people who “get” both error-code programming and exception-based programming so thoroughly).

The talk of multiple return values and Laurence’s proposal reminded me of GoLang, and its support for (return value, error code) and panic/recover (like exceptions that should only rarely be caught). The design has always intrigued me, though I have never written a large enough Go program (or feel I understand Go idioms enough) to have a strong opinion. But given Go’s primary use for service programming, in light of Laurence’s article it seems like a very solid design.

But back to Python. I’m curious if anyone ever implemented a system where (return value, error code) was used by convention instead of exceptions as the primary error-handling mechanism? Because we have this built-in tuple unpacking, we can use (return value, error) without explicit Go-style language support, but I’m curious if anyone has done it and what their experience has been.


TDD via Tic-Tac-Toe

by Rob Galanakis on 28/12/2013

For the last few years, I’ve done a fair bit of teaching of TDD, usually 1-on-1 during pair programming sessions but also through workshops. I’ve tried out lots of different subject matter for teaching TDD, but my favorite has been Tic-Tac-Toe (or whatever your regional variation of it is). It has these benefits:

  1. It’s a game. People like programming a game. Learning through games is good. It also reinforces the idea of ‘testability.’ A number of times I had students combine the ‘playing’ of the game with the ‘logic’ of the game. Teaching them to split out the algorithm from the input/driver was a useful exercise.
  2. It’s not trivial. It is a problem with some real meat that can be solved in a number of different ways. It’s significant enough that a real design and architecture emerges, unlike TDD for a single function. A a bonus, the diverse answers make it fun to teach because I keep being led to new solutions by students.
  3. The rules are just right. Simple and clear. It has a clear ‘happy path’ and ‘unhappy path’.
  4. It’s bounded. You can know when it’s good enough, but there are also endless exceptions to check if you want. Which is a great way to teach people to know when something is complete enough.
  5. It has no dependencies. I prefer not to introduce people to TDD and mocking at the same time.

In the workshops I ran, we did a ‘mob programming’ of a Tic-Tac-Toe game, and then students paired up to develop an AI. While the AI was fun, it was developing the game that was probably a better exercise. And like I mentioned already, I’ve done lots of introductory TDD pairing sessions using it, recently with someone interviewing here which is when I think the Tic-Tac-Toe game really proved itself a successful subject matter. I highly suggest you give Tic-Tac-Toe a try if you’re looking for a way to demonstrate/teach TDD to someone.

If you’re interested, the code and slides I created for the workshops is here: https://github.com/rgalanakis/tddtraining I may make a blog post in the future with some more detail about how the workshops went, if there’s any interest.


Reflections on porting Planet

by Rob Galanakis on 22/12/2013

I use the Python Planet software to generate the RSS feed and static site  for planet.tech-artists.org, like is done for planet.python.org. I’ve been using it for several years and recently got the urge to do some open source work, so I “forked” the original Planet code (actually, the Bazaar repo is no longer so I just worked off the tar) and began to port it to use Jinja2 instead of the HTML templating engine derived from Perl. I am mostly done and just need to clean out a few more planet.tech-artists.org specific things before linking to the github repo here, but I did have some interesting reflections on porting code that is 7.5 years old!

  • All the dependencies were just copied in. This was before the popularity of pip. Was this a common way to do dependency management back then (how widely used was easy_install)?
  • In some cases it was difficult to know what was part of planet and what was copied in. I’ve replaced what I could with a requirements.txt. feedparser is still maintained, sanitize is kindly provided on PyPI, and the other modules were provided for way old compatibility, so now only htmltmpl is copied in.
  • The standard library is way better nowadays. This code ran on 2.1 or 2.2 (conflicting comments to which is the minimum). No socket.setdefaulttimeout, no logging module. I guess the backwards compatibility situation is even worse nowadays though, which code often having to support 2.5 to 3.4!
  • The code was still pretty good on every level. Structurally it is well thought out. Surprising to me was that stylistically it was still pretty good as well, aside from missing use of new language features (for example: context managers, ‘in’ keyword).
  • It did a lot of stuff, especially via configuration, that I can’t imagine anyone uses. In fact Planet is really an entire static site generator. Maybe people used it for that in 2006? Anyway, nowadays there are so many better options, Planet is only used for pages like Planet Python and Planet TechArt, so I took out unnecessary configuration which simplified things greatly. And of course I dropped the compatibility modules that are no longer needed, and the planet-cache.py utility which I never used.
  • It had “unit” tests! And they used the unittest module! But I couldn’t get them to work (and they’re now entirely broken). It even had a script to run all of them, though nowadays I guess everyone uses test runners, thank goodness.
  • Looking over the HTML templating engine code wasn’t the first time I’ve felt uncomfortable reading code, to later find out it was ported from Perl. Perl systems stand out like a sore thumb! I was very happy to get Jinja2 working. I actually don’t have complaints with htmltmpl.py/HTML::Template, but it isn’t what I want to be using in 2014.

I work every day in a Python codebase originally written in the early 2000′s, so I’m not stranger to legacy systems. But that is “enterprise” code which is different from open source packages, and I wasn’t writing Python in 2006, so it is interesting to get a glimpse into what the Python world was like 7.5 years ago. Kudos to the original authors for doing a great job.


Removing external hiring as a tool (Part 3 of 3)

by Rob Galanakis on 17/12/2013

In this post I hope to explain how hiring externally as a tool for fixing problems ultimately leads to a weaker organization.

When I began writing this post, I was having a hard time. Whereas the post talking about what a bad idea firing is was easy, the situation is considerably better for hiring. For starters, there are more organizations that do a good job. Very rigorous hiring practices, even during growth. It’s also easier to talk about how a company hires people than how it fires people, as its generally a positive experience (which is why this article probably seems a lot weaker than the previous). Of course sometimes you need to hire specialists for very specific areas (I’m talking something like ‘hardware emulation’ or ‘low-level rendering engines’, not ‘databases’ or ‘UI programming’), where it is prohibitive to train or grow people in a year. And sometimes there are amazing individuals you just need to have (take Ward Cunningham being hired into New Relic or John Carmack becoming CTO of Occulus as examples). Then there are organizations in periods of rapid growth. While rapid growth is always risky, it’s often necessary to bring enough force to bear in innovative companies. I’d much rather see stable growth but it’s not always an option. So I needed a clear demonstration of the problems.

Then, like manna from an ironic heaven, I saw this article about Abercrombie’s CEO. After investor calls for his resignation, Abercrombie renewed his contract and stated their plan to hire in three executives to manage each of their brands. The idea is that one of them will replace the current CEO, and the CEO has done a bad job managing things, and of course did not groom internal candidates, so it seems they are due to come from the outside.

I don’t understand how anyone expects this to end well. Each Brand President will come in, make changes (including, probably, layoffs!) that benefit the short term (because their goal is to be CEO), and then: 1) The one that becomes CEO will stay a few years. The average tenure of a CEO in America is about 4.5 years. 2) The two that do not will likely leave, meaning new executives will be hired in and there will be more instability.

This is the sort of hiring- not just at the CEO level but all leadership levels down to team lead- that I think can be misguided. Why?

Mostly, because it disguises a problem. Most organizations buy into the idea that internal candidates should be preferred to external ones (another Lean principle!), yet still need to look outside for senior talent and managers. I would compare the situation and solution to DevOps: if deployments are an issue, the worst thing to do is isolate their handling to a small group and deploy less frequently. The DevOps movement has shown us the power of the mantra of “if it hurts, do it more often.”

I believe the inability of an organization to groom internal candidates indicates severe management problems, and because the feedback cycle is so slow for personnel changes, trying to defer it and “fix it for the next time” will never actually fix the issues. Internal hiring will force an organization to confront its issues, which can include:

  • Stifling managers that do not or cannot groom their reports for seniority and leadership.
  • A dysfunctional project that people do not want to work on and would be under-staffed if people were allowed to transfer.
  • Projects that depend on a couple of people, making them unable to transfer.
  • A general lack of learning and growth, perhaps because everyone is 100% allocated, with no slack time.
  • Work that is not challenging or evolving, causing the same experience over and over.
  • Valuing efficiency and specialties of individuals over utility and value.

All of these issues (and more) cause issues with internal hiring, but also are bad for the organization overall. Wouldn’t it be great if you could both fill a key role and address issues?

Is the risk too great of promoting a bad candidate? I don’t know: is the risk too great of hiring in an unknown quantity into a leadership position? Is the risk too great of having a candidate who wants a job change but your organization can’t give it to him or her?

If you are looking to do anything but shrink, you should always have ‘junior’ positions open and take the cream of the crop. This is especially true if you are outside a major tech hub.

There’s also another type of problematic hiring: adding resources to failing projects (whether outside or inside hires). We all know Brookes’ Law, that “adding people to a late project makes it later” but I can’t count how many times people do it anyway. If there are problems with a project, adding people is the worst way to address issues. “We need more resources” is a tantalizingly simple explanation for why something isn’t getting done, but I’ve never seen it be the actual reason. It is, like hiring leadership, a great way to disguise and distract from the real problems. This topic requires a separate post, though.

I also want to point out a perversion of ‘internal hiring’: creating an excess of managers and handing out seniority titles as candy. What I’m advocating here is when you need a manager, look internally, not to turn someone into a manager because they want it. Likewise, I’m not saying you should give someone a more senior title because otherwise you’d open up a senior developer spot, I’m suggesting you give them the responsibilities (say, team leadership) and see how they handle it.

It is much easier to hide the lack of internal hiring in technology companies because it is growing so quickly (there’s a need for external hires, and people can get jobs elsewhere if they become frustrated). But ultimately I see a dependence on external hires on the other side of the ‘firing as a tool’ coin. I don’t think you can do one without the other. They are inseparable from not just a cultural level but a practical one as well. It is about investing in your employees over looking for easy answers.

No Comments

“Delighting customers” is Lean’s secret handshake

by Rob Galanakis on 13/12/2013

Whenever I see the words “delighting customers” (which is, let’s face it, an awkward phrase) in a non-Lean context like a job description, I can feel the author winking at me. It tells me “we try to be Lean and if you get our drift you probably want to join us.” It instantly gives said company plenty of extra points.

I just wonder how long until “delighting customers” becomes a played out catchphrase (if it’s not already)?

No Comments

Switch to our mobile site