# Friday, September 30, 2011

I enjoy continually learning from Robert Martin (Uncle Bob). Sometimes I disagree with him but usually only until I’ve thoroughly processed what he is trying to teach me. In his latest missive on 8thlight.com, Uncle Bob writes a stunningly cogent brief called Screaming Architecture in which he makes a critical point: software architecture is all about the use cases of a system and NOT the framework.

“Architectures are not (or should not) be about frameworks. Architectures should not be supplied by frameworks. Frameworks are tools to be used, not architectures to be conformed to. If your architecture is based on frameworks, then it cannot be based on your use cases.”

Focus on the use cases and perhaps even group them into logical hierarchies with tidy “40,000 foot level” epochs that describe the thing you are building. Is it a customer relationship management system? The first thing we should notice in an architecture, whether document or code, is that this is a CRM system. Presentation, delivery, storage and communication frameworks are all secondary, just plumbing, not the architecture.

“A good software architecture allows decisions about frameworks, databases, web-servers, and other environmental issues and tools, to be deferred and delayed.”

I’m not completely at this point in my journey toward architectural perfection but I recognize the need to strive for this idea of producing architectures that are designed to solve the problems of the users first rather than designed fit within the strictures of a framework while addressing the use cases after the fact.

I recommend you read the full article. Take Uncle Bob’s advice, as I intend to do, and focus more strongly on architecture for your use cases on your next project. Use frameworks carefully and work to keep your business code independent of your frameworks and thus imminently more testable. And don’t forget your tests.

posted on Friday, September 30, 2011 12:46:00 AM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Thursday, September 29, 2011

I’ve recently been interested in the subject of respect and the question of whether respect is given or earned. My conclusion is that respect is given most often when it is earned and certainly most easily when that is so. In contemplating and researching this subject, I ran across a post by Bret Simmons that I liked very much called 10 Ways to Earn Respect as a Leader in the Workplace.

I recommend you read the full article but I’ll share a few of my favorites here along with some personal thoughts about each one.

Get to know your co-workers and their families.
This is the author’s first point and I think most important one. It is very hard to give respect to someone who has no empathy and does not appear to be genuinely interested in you and your personal circumstances. A little bit of real care can earn a lot of respect.

Communicate clearly and regularly.
If one does not hear from or speak with his manager on a regular basis, it is impossible for the first point to occur. And if a person is left in the dark with respect to what is happening within the organization, an atmosphere of fear and a feeling of neglect can result in poor attitudes and even overt disrespect.

Make generous use of self-deprecating humor.
When I work with someone who refrains from making deprecating remarks about others but has the self confidence to take a jab at himself from time to time in order to put others at ease, I’m more likely to feel comfortable working with that person and will certainly have more respect for him.

Admit when you screw up.
Nothing can destroy the respect you have earned more rapidly than dodging responsibility for your own mistakes. Nothing can more irreversibly damage your own reputation more powerfully than when you throw a subordinate or coworker under the bus rather than owning up and facing the music. At the same time, the opposite is true. If I have a boss who takes responsibility for mistakes made and asks the team for their support as she takes corrective action, I will have just that much more respect for that person.

Stand behind your staff during times of difficulty.
When mistakes are made by subordinates or coworkers, stand by them. Don’t abandon them in an attempt to save yourself. Chances are you will all survive but you will have lost their respect forever. The author writes, “If you can’t stand behind one of your team members, then you don’t belong in management and you’re certainly not a leader.”

posted on Thursday, September 29, 2011 5:37:14 AM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Tuesday, September 27, 2011

In this sixth installment of C# Basics, I want to share a brief snippet of an extension method I’ve found useful that will introduce you to QDD as well. Quick and Dirty Design (QDD) is my name for having multiple tiny console application projects lying around in which I test little code snippets before putting them into something more serious. This is only necessary in projects in which TDD has not been used and no testing framework or tests are available for whatever reason.

But back to extension methods. From MSDN we learn:

“Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type. For client code written in C# and Visual Basic, there is no apparent difference between calling an extension method and the methods that are actually defined in a type.”

As the MSDN article points out, the most common extension methods you may run into are the LINQ standard query operations. But don’t let that stop you from providing yourself with some very nice little extension methods like this one:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace IOTest
{
  class Program
  {
    static void Main(string[] args)
    {
      string phone1 = @"8015551212";
      string phone2 = @"(8a0d1d)d d5d5d5d-d1d2d1d2";

      if (phone1.StripNonNumeric() == phone2.StripNonNumeric())
        Console.WriteLine("true");
      else
        Console.WriteLine("false");

      Console.ReadLine();
    }
  }

  static class Ext
  {
    private static Regex nonNum = new Regex(@"[^0-9]");

    public static string StripNonNumeric(this string item)
    {
      return nonNum.Replace(item, string.Empty);
    }
  }
}
posted on Tuesday, September 27, 2011 7:17:51 AM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Sunday, September 25, 2011

I remember listening to this wise counsel three years ago from Elder Joseph B. Wirthlin Of the Quorum of the Twelve Apostles. Today my mother reminded me of it and I enjoyed reading it again. If you have ever faced tough times or felt like you were on the losing end of a game, in sport, in work, in life, I hope this article will pick you up.

I would like to highlight one of the points Elder Wirthlin makes, perhaps my favorite:

Learn to laugh – When things go wrong, we can choose to be angry or sad or depressed, but if we choose to laugh, we can get through the present difficulty and more easily find a solution.

“The next time you’re tempted to groan, you might try to laugh instead. It will extend your life and make the lives of all those around you more enjoyable.”

When the stresses of work or a commute or a family crisis threaten to bring you down, laugh. It truly is the best medicine!

posted on Sunday, September 25, 2011 6:50:16 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Thursday, September 22, 2011

I often need to encrypt a string and then decrypt it. Sometimes its to move some value from one server to another without the benefit of SSL. So for the fifth installment of C# Basics, I’ll share the a generic version of a little encryption utility I’ve used many times and in many places.

Most Important: If you decide to use this code, be sure to change the key and vector text to something only you know. You might even want to use a hardware security module.

I like AES (formerly known as Rijndael, pronounced “rain-doll”) but you can pick your own algorithm. The code below will work with most of the .NET symmetric encryption algorithms.

using System;
using System.Security.Cryptography;
using System.Text;

namespace MyCrypt
{
  public static class Tokenizer
  {
    //create your own unique key and vector strings
    //maybe even lock them up and require a cert to get them out
    private const string keyText = @"The quick brown fox jumps";
    private const string vectorText = @"over the lazy dog.";
    private static byte[] key = null;
    private static byte[] vector = null;

    static Tokenizer()
    {
      key = GetMD5Hash(keyText);
      vector = GetMD5Hash(vectorText);
    }

    public static string Encrypt(string val)
    {
      if (string.IsNullOrWhiteSpace(val)) return null;
      RijndaelManaged rjm = new RijndaelManaged();
      rjm.KeySize = 128;
      rjm.BlockSize = 128;
      rjm.Key = key;
      rjm.IV = vector;
      byte[] input = Encoding.UTF8.GetBytes(val);
      byte[] output = rjm.CreateEncryptor()
        .TransformFinalBlock(input, 0, input.Length);
      string data = Convert.ToBase64String(output);
      return data;
    }

    public static string Decrypt(string val)
    {
      if (string.IsNullOrWhiteSpace(val)) return null;
      try
      {
        RijndaelManaged rjm = new RijndaelManaged();
        rjm.KeySize = 128;
        rjm.BlockSize = 128;
        rjm.Key = key;
        rjm.IV = vector;
        byte[] input = Convert.FromBase64String(val);
        byte[] output = rjm.CreateDecryptor()
          .TransformFinalBlock(input, 0, input.Length);
        string data = Encoding.UTF8.GetString(output);
        return data;
      }
      catch
      {
        return null;
      }
    }

    static byte[] GetMD5Hash(string data)
    {
      MD5 md5 = MD5CryptoServiceProvider.Create();
      return md5.ComputeHash(Encoding.UTF8.GetBytes(data));
    }
  }
}
posted on Thursday, September 22, 2011 9:01:42 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Tuesday, September 20, 2011

I spend perhaps six hours a day, five days a week, fifty weeks a year hitting the keys. At my slow average typing speed of 30 words per minute (I can type faster but I’m not constantly hitting the keys, so I’m guessing here) and assuming I spend thirty years of my life doing this, my total keystrokes (assuming an average of 6 keystrokes per word) will be:

60 minutes * 6 hours * 5 days * 50 weeks * 30 years * 30 words * 6 keystrokes = 16,200,000 keystrokes

Percentage of my work output firing off an ill conceived email providing advice to someone who does not care or want the advice and will certainly not waste time reading the missive: 0.01%

Today I wasted 0.01% of my available productivity.

I’ve already consumed nearly 50% of my available keystrokes, so I’m posting this to remind myself to conserve my energy and use what remains of my personal utility more wisely.

Important note to self. Do not waste time writing perfectly good advice and sending it to someone who could not care less about your opinion. Instead, read a good book. Write a new blog post. Refactor some old code. Or just watch the leaves in the trees dance in the wind. This would be a more valuable use of that time. Time that cannot ever be recaptured.

posted on Tuesday, September 20, 2011 10:18:58 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [1]
# Monday, September 19, 2011

In this fourth installment of C# Basics, let’s take a look at how you handle multiple choice questions in your code. When you need to decide on a course of action based on the multiple possible values a variable may have, you have three essential choices.

  1. if | else if … | else
  2. switch
  3. Dictionary<K,T>

The code below shows you an example of each:

public class DownloadViewModel
{
  public Byte[] Contents { get; set; }
  public string FileName { get; set; }

  public string ContentType1
  {
    get
    {
      string ext = Path.GetExtension(FileName).ToLower();
      if (ext == ".pdf")
        return "application/pdf";
      else if (ext == ".txt")
        return "application/txt";
      else  
        return "application/octet-stream";
    }
  }

  public string ContentType2
  {
    get
    {
      switch (Path.GetExtension(FileName).ToLower())
      {
        case ".pdf":
          return "application/pdf";
        case ".txt":
          return "application/txt";
        default:
          return "application/octet-stream";
      }
    }
  }

  public string ContentType3
  {
    get
    {
      if (map.Count == 0) LoadMap();
      string val = string.Empty;
      if (!map.TryGetValue(Path.GetExtension(FileName).ToLower(), out val))
      {
        val = "application/octet-stream";
      }
      return val;
    }
  }

  private void LoadMap()
  {
    map.Add(".pdf", "application/pdf");
    map.Add(".txt", "application/txt");
    map.Add("*.*", "application/octet-stream");
  }

  private Dictionary<string, string> map = new Dictionary<string, string>();
}

So how do you pick which one to use? For me, the choice is easy. If I have only a few possible values that are not likely to change, I’ll use the if|else if|else construct. If I have a fair number (more than 3 and less than 16) and these values are not likely to change, I’ll use the switch statement. But if the values are likely to change or be data driven or if the number of values is greater than 15, I prefer to use a Dictionary.

Another highly valuable use of the Dictionary is when the values will kick off some action that may be lengthy or complicated. In that case, I prefer a Dictionary<T, ActionForT>. I can then retrieve the ActionForT and call the Execute method. Like this:

public class DownloadModel
{
  public Byte[] Contents { get; set; }
  public string FileName { get; set; }

  public string ContentType
  {
    get
    {
      string ext = Path.GetExtension(FileName).ToLower();
      if (ext == ".pdf")
        return "application/pdf";
      else if (ext == ".txt")
        return "application/txt";
      else  
        return "application/octet-stream";
    }
  }

  public void Save()
  {
    if (map.Count == 0) LoadMap();
    Action action = null;
    if (map.TryGetValue(Path.GetExtension(FileName).ToLower(), out action))
    {
      action(); //execute the action stored in our map
    }
    else
    {
      map["*.*"](); //execute the default action
    }
  }

  private void SavePdf()
  {
    throw new NotImplementedException();
  }

  private void SaveTxt()
  {
    throw new NotImplementedException();
  }

  private void SaveOther()
  {
    throw new NotImplementedException();
  }

  private void LoadMap()
  {
    map.Add(".pdf", new Action(SavePdf));
    map.Add(".txt", new Action(SaveTxt));
    map.Add("*.*", new Action(SaveOther));
  }

  private Dictionary<string, Action> map = new Dictionary<string, Action>();
}
posted on Monday, September 19, 2011 6:12:46 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Sunday, September 18, 2011

I like many aspects of my job but one stands out above all others: helping other developers. Nothing gives me as much satisfaction as hooking up the mental chains and pulling a fellow developer out of the code mud in which she or he is stuck.

I grew up on a small farm where there was never any lack for work to do, much of which was tedious and boring. But it was always exciting to get a call from a neighbor asking us to come pull them out of the mud or jump start a car or rescue an animal from some odd predicament. It never took much time, but the simple gratitude of the person helped was the best compensation I’d ever known as a child.

Now my tractor is my brain and my software tools and the chains are made of thousands of hours of experience forging one experience into another and storing these up, mostly for my own work, but also for those enjoyable moments when I take a call from a colleague who is just stuck and needs a little guidance.

My tractor and chain is not better than anyone else’s really. But sometimes I’m in the right place at the right time to help another developer and this aspect of my job is my favorite. Spinning up a GotoMeeting session and helping another developer find and solve a problem may sound dull and boring to some, but it gives me a boost.

Sometimes my colleagues are reluctant to call. (I work remotely.) Perhaps this is because they don’t want to waste my time or because they feel the need to solve the problem on their own. That’s okay. It’s good to solve problems on your own but I do appreciate the opportunity to help when I can.

And that is one of the main reasons I get up in the morning and make the 30 second commute to my basement office. Today, I might get to hook up the chains to the tractor and pull a friend or a neighbor out of the mud.

Hook up your chains to your tractor and become a software development mentor.

posted on Sunday, September 18, 2011 8:46:46 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [1]
# Saturday, September 17, 2011

In this third installment of C# Basics, let's take a look at exception handling. It is one the easiest things to do poorly in an application. In C# you can capture an error and do something with it as easily as this:

try
{
  //do something that may raise an exception
}
catch (Exception e)
{
  //do something with e--log it, modify a return value, etc.
  //if you want the caller of your code to have a crack at the same exception
  throw; //raise the exception again with throw; (almost NEVER throw e;)
}

But do try your best to NEVER catch the base Exception class. Be more specific, especially where you have to do something specific with a particular type of exception and allows others to rise up the call stack. Here’s just one example of being a bit more specific:

try
{
  //call a service that reads data from SQL Server
}
catch (SqlException se)
{
  if (se.Message.Contains("Timeout"))
  {
    //send a timeout log message to the DBA service
  }
  throw; //raise it up the stack to be handled by the caller
}

Of course, you can and ought to handle different exceptions differently where required. Here’s a brief example:

try
{
  //call a service that reads data from SQL Server
  //call another service that writes to a file
}
catch (SqlException se)
{
  if (se.Message.Contains("Timeout"))
  {
    //send a timeout log message to the DBA service
  }
  throw; //raise it up the stack to be handled by the caller
}
catch (IOException iox)
{
  //special handling or logging of IO exceptions
  Console.WriteLine(iox.Message);
}
catch (Exception e)
{
  //log a general exception and rethrow
  throw;
}

And don’t forget, you can create your own specialized exception classes. In fact, with Visual Studio you can use the code snippet for Exception. Just put your cursor in a C# file, outside of a class declaration, and start typing Exception and when you see the Intellisense prompt just hit TAB TAB. And you will have the following:

[Serializable]
public class MyException : Exception
{
	public MyException() { }
	public MyException(string message) : base(message) { }
	public MyException(string message, Exception inner) : base(message, inner) { }
	protected MyException(
	 System.Runtime.Serialization.SerializationInfo info,
	 System.Runtime.Serialization.StreamingContext context)
		: base(info, context) { }
}

Flesh out your own custom exception and raise and catch it just like a pro.

public void ShowExample(int count)
{
  if (count < 0) throw new MyException("Count cannot be less than zero.");
  // . . .
}

public void CallExample()
{
  try
  {
    ShowExample(-4);
  }
  catch (MyException me)
  {
    Console.WriteLine(me.Message);
  }
}

Of course, in the above example, you would more likely want to just throw a new ArgumentException. There is a very long list of commonly used exception classes in the .NET base class libraries. Spend some time on MSDN and get familiar with them. Here’s a brief but incomplete list of common exception classes you may want to catch or throw and then catch.

  • System.ArgumentException  
  • System.ArgumentNullException  
  • System.ArgumentOutOfRangeException  
  • System.ArithmeticException  
  • System.DivideByZeroException  
  • System.FormatException  
  • System.IndexOutOfRangeException  
  • System.InvalidOperationException  
  • System.IO.IOException  
  • System.IO.DirectoryNotFoundException  
  • System.IO.EndOfStreamException  
  • System.IO.FileLoadException  
  • System.IO.FileNotFoundException  
  • System.IO.PathTooLongException  
  • System.NotImplementedException  
  • System.NotSupportedException  
  • System.NullReferenceException  
  • System.OutOfMemoryException  
  • System.Security.SecurityException  
  • System.Security.VerificationException  
  • System.StackOverflowException  
  • System.Threading.SynchronizationLockException  
  • System.Threading.ThreadAbortException  
  • System.Threading.ThreadStateException  
  • System.TypeInitializationException  
  • System.UnauthorizedAccessException
posted on Saturday, September 17, 2011 8:01:06 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [1]
# Friday, September 16, 2011

Someone once said, “By small and simple things are great things brought to pass.” The context was certainly not enterprise software development but I have come to believe that these words are profound in this context as well.

Every enterprise, medium and large, is challenged with complexity. Their systems and businesses are complex. Their politics and personnel and departments are complex. Their business models and markets and product lines are complex.

And their software is complex. But why?

Because we make it complex. We accept the notion that one application must accomplish all the wants and wishes of the management for whom we work, either because we are afraid to say, “This is a bad idea,” or because we are arrogant enough to think we’re up to the challenge. So we create large teams and build complex, confusing and unmanageable software that eventually fulfills those wants and wishes, sometimes years later—that is, if the project is not cancelled because the same management team loses patience and/or budget to see it through.

Some companies have made a very large business from selling such very large and complex software. They sell it for a very large “enterprise class” price to a business who wants all those nice shiny features but doesn’t want to take the risk of building it for themselves. And then they buy the very large enterprise software system and find that they must spend the next six to twenty months configuring and customizing the software or changing their own business processes and rules to fit the strictures of the software they have purchased.

The most successful enterprise projects I’ve worked on have all shared certain qualities. These projects have produced workable, usable and nearly trouble-free service all because they shared these qualities. The are:

  • A small team consisting of:
    • A senior lead developer or architect
    • Two mid-senior level developers
    • A project manager (part time)
    • And a strong product owner (part time)
  • A short but intense requirements gathering and design period with earnest customer involvement.
  • Regular interaction with product owner with a demo of the software every week.
  • Strong communication and coordination by the project manager, assuring the team and the customer understands all aspects of project status and progress.

The challenge in the enterprise is to find the will and means to take the more successful approach of “small and simple.”

You may say, “Well, that’s all fine and good in some perfect world, but we have real, hard and complex problems to solve with our software.” And that may be true. Software, no matter how good, cannot remove the complexity from the enterprise. But there is, in my opinion, no reason that software should add to it.

The real genius of any enterprise software architect, if she has any genius at all, is to find a way to break down complex business processes and systems into singular application parts that can be built by small, agile teams like I’ve described, and where necessary, to carefully define and control the service and API boundaries between them.

Must a single web application provide every user with every function and feature they might need? No. But we often try for that nirvana and we often fail or take so long to deliver that delivery does not feel like success.

Must a single database contain all the data that the enterprise will need to perform a certain business function or fulfill a specific book of business? No. But we often work toward that goal, ending up with a database that is so complex and so flawed that we cannot hope to maintain or understand it without continuous and never ending vigilance.

When your project complexity grows, your team grows, your communication challenges grow and your requirements become unwieldy and unknowable. When you take on the complex all in one with an army, you cannot sufficiently focus your energy and your strength and collectively you will accomplish less and less with each addition to your team until your project is awash is cost and time overruns, the entire team collectively marching to delivery or cancellation. And in the end either outcome, delivery or cancellation, feels the same.

By small and simple things, great enterprise software can be brought to pass. Sooner rather than later. Delivering solid, workable, maintainable, supportable solutions. Put one small thing together with another, win the war on complexity and provide real value to your management team. Do this again and again and you’ll have a team that will never want another job because they will have become all too familiar with real success.

posted on Friday, September 16, 2011 5:14:15 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [1]
# Thursday, September 15, 2011

I’ve had a series of ongoing discussions with a friend who is also a senior software architect. In recent months he’s become quite dissatisfied with his manager specifically and employer generally. He has put his resume out on the typical job search sites, Dice, Monster, TheLadders, LinkedIn and others, and has been sorely disappointed with the result. Technical recruiters seem to be, with rare exceptions, a sorry lot of would-be telemarketers rejected for lack of integrity by the local call-center-for-hire.

On a daily basis this friend of mine receives emails and calls with a hot job listing that do not generally match his skills or experience like the one that he forwarded to me today, not because he might really be a match but in hopes of sparking a conversation that goes something like this:

  • Recruiter: “I know you may not be a good fit for this job, but do you know anyone who is?”
  • Candidate: “No. I do not know anyone with this skill set and experience level interested in a 6 month contract.”
  • Recruiter: “Okay. I’d like to keep in touch with you and I will definitely call you if something comes through that is a better match for what you’re looking for.”
  • Candidate: “Okay, sure. Thanks for the call.”

My friend reports that he has at least three of those conversations per week and at least two or three such emails as the one below per day. I’ve changed the company names and the place to avoid the chance of embarrassing any of them, especially since one or two of them is a household brand name.

Hello,

I am Recruiter with XYZConsulting (formerly ABCConsulting), a dedicated QRS contractor sourcing service, managed by TenCharms. We recruit solely for QRS Contract Positions. Should you accept a contract position, it will be through our preferred partner TenCharms, an independent vendor chosen to manage this program for QRS.

I saw your resume on a job board. I am currently recruiting .net Consultant for a 12 months contract project with QRS – Chicago, IL.

Email me a copy of WORD formatted resume, along with these details:

  • Best time & number to speak to you:
  • Location:
  • Visa Status:
  • How soon you can start:
  • Work mode, W2, C2C (if self-incorporated):
  • Expectations on Compensation:

Position’s name: .net Consultant

Assignment period: 12 months
Location: Chicago, IL

Required skills
Primary Skills: asp.net, vb.net, Oracle / SQL Server, SQL. Secondary Skills: C#.net, MS Access / VBA

As this is an urgent requirement, kindly respond to it at earliest possible.

Best Regards,
Bad Excuse (name changed to protect the guilty)

Recruiter | XYZConsulting (formerly ABCConsulting)
A TenCharms Solution
Dedicated QRS Contractor Sourcing Services

My friend has shown me many other emails that are far worse than this one. Many of them are poorly written, lacking proper grammar and teeming with spelling errors, yet they claim to represent well known companies. I wonder if these companies for whom these recruiters work ever bother to review the practices and general quality of the communications of their agents. Somehow I doubt it.

In some of the emails my friend has forwarded to me, it becomes clear that the recruiting company has employed some shoddy keyword matching algorithm to select from a list of candidates who have put their resumes out into the swamp of job sites (see above mentioned sites). In some of these I have been able to identify one or two specific keywords that match my friend’s resume. I should know. I helped him write it.

Sometimes the keyword match is even close, such as, “architect” or “jQuery.” But since my friend is a .NET architect and the job is for a J2EE architect who also knows jQuery and PHP and Oracle while my friend does not mention Oracle or PHP on his resume at all, it is clear that the recruiter’s matching algorithm, whether it is their own or provided by one of the job sites, is severely lacking in even the most basic intelligence.

Either that or the recruiter is far more interested in spamming the candidate with a job posting that is clearly not a fit in hopes that the candidate will do the job of the recruiter for him or her or it.

To all such recruiters, please for the love of Pete, what’s wrong with you? Stop wasting my friend’s time!

posted on Thursday, September 15, 2011 7:30:16 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Wednesday, September 14, 2011

For the second installment of C# Basics, let’s take a look at the conditional operator, sometimes known as the ternary operator. How many times have you written code like this:

if (a == b)
{
  x = y;
}
else
{
  x = z;
}

Probably never, at least not quite this simplistic or with such ancient style variable names, or one would hope anyway. But the example serves its purpose. Now use the conditional operator and convert that code to this:

x = (a == b) ? y : z;

You can read more about the conditional operator on MSDN here.

posted on Wednesday, September 14, 2011 8:20:09 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Tuesday, September 13, 2011

I have worked in several development teams that have a Scrum-like development process where the team has adapted Scrum to their own team dynamics and organizational needs. I was intrigued by a recent MSDN article called Visual Studio ALM Rangers—Reflections on Virtual Teams that formalizes one of these this Scrum-but-not-Scrum processes and calls it Ruck. Here is the authors’ loose definition:

“To ensure that we don’t confuse anyone in terms of the methodologies or annoy process zealots, we provisionally chose to call our customized process ‘Ruck,’ which, in the rugby world, means ‘loose scrum.’”

The article authors share their experience in Microsoft’s ALM Rangers team and “provide guidance for working with teams in less-than-ideal distributed and virtual environments.” I found this introduction compelling and I wanted to try to restate their approach here with a few of my own thoughts mixed in.

The ALM Rangers’ Ruck process is very specific to their needs and team: a highly distributed team and a high degree of variability in team member availability where team members have multiple professional obligations that take higher priority than their work on the team. This is a very challenging situation for a team trying to be Scrum-like and may be closer to many popular open source projects with multiple contributors from around the world, with one very important exception: a fixed ship date.

I’m not advocating that any specific team adopt the ALM Rangers’ Ruck, but the choices and approach taken by this team could be very valuable to other teams faced with creating their own Ruck because they cannot, for whatever reason, get close enough to Scrum to really call their process Scrum.

One of the most important points I found the authors making had to do with adhering to the agile principle of self-organizing teams. They found that their “belief in letting developers choose work from areas in which they have interest” required that they allow teams to self-organize around these areas of interest rather than specific geographies or time zones.

One of the most difficult challenges facing enterprise software development projects is team size and how to break up larger projects into smaller chunks that can be addressed by smaller, more agile teams. The authors observe:

“We’ve learned that smaller projects and smaller team sizes enable better execution, proper cadence and deeper team member commitment. Also, it seems it’s more difficult for someone to abandon a small team than a larger team.”

Another interesting area the authors cover is that of project artifacts used to manage and break down the project into actionable tasks. The particular names of these seem less important than the fact that they are well defined and communicated and used consistently within their Ruck. In creating your own Ruck process, do not skip this important element.

Roles are also well defined. Knowing what position you play on the team at any one given moment can eliminate the need for over-the-shoulder management because each team member knows what their responsibility is. The task of the manager becomes one of coach rather than hall monitor, communicating these roles and responsibilities to team members rather than asking the question, “so what are you working on today?”

One more critical area to define is meetings. Don’s skimp on structuring meetings to achieve what you need to achieve without wasting too much time. As my brother tells me, “It takes a really good meeting to beat no meeting at all.”

And finally, I want to share one more brief excerpt from the authors. Their top recommendations:

“There are many recommendations, skills and practices that will help you improve the virtual team environment, the collaboration and effectiveness of each team member, and the quality of the deliverables. For us, the seven top recommendations include:

  1. Clearly define and foster a team identity and vision.
  2. Clearly define the process, such as the Ruck process.
  3. Clearly define the communication guidelines and protocols.
  4. Clearly define milestones, share status and celebrate the deliverables.
  5. Implement an ALM infrastructure that allows the team to collaborate effectively, such as TFS and associated technology.
  6. Promote visibility and complete transparency on a regular cadence.
  7. Choose your teams wisely, asking members to volunteer and promoting passionate and get-it-done individuals.”
posted on Tuesday, September 13, 2011 5:26:08 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Monday, September 12, 2011

This is the first in what I hope will be a long series of quick posts covering the basics of programming in C#. For this first installment, I’ll review the proper use and improper use of a class or struct constructor.

What is a Constructor
In a C# class, a constructor is a special code block for a class or struct called when an instance of that class or struct is created. If a constructor is not defined in code, the compiler will create a default constructor without parameters. Constructors can be overridden and can call a base class constructor.

Proper Use of a Constructor
Use a constructor to set or initialize class or struct fields. If this initialization code is lengthy, such as in the construction of a Windows Form class, move the initialization code to a partial class file and call it something like InitializeComponents. Here are a few examples:

public partial class Apple : Fruit
{
  //same as a default constructor if not constructor defined
  public Apple()
  {
  }
  
  //initialize a local class member
  public Apple(string name)
  {
    _name = name;
  }
  
  //initialize and call base class constructor
  public Apple(string name, double weight) : base(weight)
  {
    _name = name;
  }

  //constructor with conditional param to control construction behavior
  public Apple(bool initializeAll)
  {
    if (initializeAll) InitializeComponents();
  }

  //shown here but often hidden away in another file of defining the partial class
  void InitializeComponents()
  {
    //do some lengthy but simple initialization of class members
  }  
  
  private string _name;
}

Improper Use of a Constructor
Do not use a constructor for error prone or complex code. Do not use a constructor to execute tasks, business logic. I’m not going to include code examples of bad constructors but here are a few anti-patterns I’ve seen in my travels:

  • The constructor makes a call to an external service or database – BAD
  • The constructor creates child objects and calls methods on those objects to perform some business function – BAD BAD
  • The constructor asks for user input or reads from a file – BAD BAD BAD

Alternatives to a Constructor
If you require lengthy, long running or potentially error prone code to produce an instance object of a class or struct, use either a factory class or a static create method. Here’s how:

public class BookService : IBookService
{
  //a private default construtor prevents use of it outside of the class itself
  private BookService()
  {
  }
  
  public static BookService Create()
  {
    BookService instance = new BookService(); //calls private constructor
	
	//put lenghty or error prone code here that sets up the service
	
	return instance;
  }  
}

//use a factory for even more complex object creation
public static class BookServiceFactory
{
  public static IBookService Create(BookOptions options)
  {
    IBookService instance;
	
	//based on the BookOptions provide create the proper
	//concrete instance of an IBookService interface
	
	return instance;
  } 
}

Of course this is not a comprehensive treatment. It’s a blog post. If you want more, buy a book. If you have topics you would like to see addressed in future installments, please let me know.

posted on Monday, September 12, 2011 9:35:17 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Sunday, September 11, 2011

I normally use this blog to write of technology but today is a justifiable exception. Today I am reminded of and enjoyed reading again the words of a Prophet of God, Gordon B. Hinckley, spoken in a conference just a few short weeks after the fateful day ten years ago. I invite you to read his entire address, but here are a few excerpts:

You are acutely aware of the events of September 11, less than a month ago. Out of that vicious and ugly attack we are plunged into a state of war. It is the first war of the 21st century... But this was not an attack on the United States alone. It was an attack on men and nations of goodwill everywhere.
...
Religion offers no shield for wickedness, for evil, for those kinds of things. The God in whom I believe does not foster this kind of action. He is a God of mercy. He is a God of love. He is a God of peace and reassurance, and I look to Him in times such as this as a comfort and a source of strength.
...
Now, all of us know that war, contention, hatred, suffering of the worst kind are not new. The conflict we see today is but another expression of the conflict that began with the War in Heaven. I quote from the book of Revelation:

“And there was war in heaven: Michael and his angels fought against the dragon; and the dragon fought and his angels,

“And prevailed not; neither was their place found any more in heaven.

“And the great dragon was cast out, that old serpent, called the Devil, and Satan, which deceiveth the whole world: he was cast out into the earth, and his angels were cast out with him." KJV, Rev. 12:7-9

From the day of Cain to the present, the adversary has been the great mastermind of the terrible conflicts that have brought so much suffering. Treachery and terrorism began with him. And they will continue until the Son of God returns to rule and reign with peace and righteousness among the sons and daughters of God.
...
Are these perilous times? They are. But there is no need to fear. We can have peace in our hearts and peace in our homes. We can be an influence for good in this world, every one of us.

May the God of heaven, the Almighty, bless us, help us, as we walk our various ways in the uncertain days that lie ahead. May we look to Him with unfailing faith. May we worthily place our reliance on His Beloved Son who is our great Redeemer, whether it be in life or in death, is my prayer in His holy name, even the name of Jesus Christ, amen.

Update: Please read commentary posted by Thomas S. Monson, a living Prophet of God, in the Washington Post.

posted on Sunday, September 11, 2011 12:20:45 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Saturday, September 10, 2011

In my last post, I covered getting the Berkeley DB SQL API running under the covers of the System.Data.SQLite library for .NET. Another weekend has come and I’ve had some time to run some tests. The whitepaper Oracle Berkeley DB SQL API vs. SQLite API indicates that the Berkeley engine will have relatively the same read speed as SQLite but better write speed due to it’s page locking vs. file locking.

Berkeley Read Bug
Originally, my code would write a certain number of rows in a given transaction on one or more threads and then read all rows on one or more threads, but I had to give up on my read code because the Berkeley implementation of System.Data.SQLite threw a "malloc() failed out of memory" exception whenever I ran the read test with more than one thread. You can read the code below and decide for yourself whether I made a mistake or not, but the test ran fine with the SQLite assembly from the sqlite.org site.

The Tests
There were four tests, each inserting similar but different data into a single table with a column representing each of the main data types that might be used in a typical application. Two single threaded tests, the first inserting 6,000 rows in a single transaction and the second 30,000 rows. The third and fourth tests were multithreaded: 6 threads inserting 1,000 rows each and then 10 threads inserting 500 rows each. 

Here’s the SQL for the table:

CREATE TABLE [TT] 
(
  [src] TEXT, 
  [td] DATETIME, 
  [tn] NUMERIC, 
  [ti] INTEGER, 
  [tr] REAL, 
  [tb] BLOB
);

The Results
The results do not show a vast difference in insert speed for Berkeley as I might have expected. The tests were run on an AMD 6 core machine with 8GB of RAM. I’m writing to an SSD drive which may or may not affect results as well.

sqlitevbdb

You may choose to interpret the results differently, but I don’t see enough difference to make me abandon the more stable SQLite 3 engine at this time.

The Code
I invite you to review the code. If I’ve missed something, please let me know. It’s pretty straightforward. With each test, I wipe out the database environment entirely, starting with a fresh database file. I use the System.Threading.Tasks library.

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("SQLite Tests");
    var tester = new Tester();
    tester.DoTest("1 thread with 6000 rows", 1, 6000);
    tester.DoTest("6 threads with 1000 rows each", 6, 1000);

    tester.DoTest("1 thread with 30000 rows", 1, 30000);
    tester.DoTest("10 threads with 500 rows each", 10, 500);

    Console.WriteLine("tests complete");
    Console.ReadLine();
  }
}

class Tester
{
  public void DoTest(string testName, int threadCount, int rowCount)
  {
    DataMan.Create();
    Console.WriteLine("");
    Console.WriteLine(testName + " started:");

    List<Task> insertTasks = new List<Task>();
    DateTime beginInsert = DateTime.Now;
    for (int i = 0; i < threadCount; i++)
    {
      insertTasks.Add(Task.Factory.StartNew(() => InsertTest(i, rowCount), TaskCreationOptions.None));
    }

    Task.WaitAll(insertTasks.ToArray());
    DateTime endInsert = DateTime.Now;
    TimeSpan tsInsert = endInsert - beginInsert;

    //List<Task> readTasks = new List<Task>();
    //DateTime beginRead = DateTime.Now;
    //for (int i = 0; i < threadCount; i++)
    //{
    //   readTasks.Add(Task.Factory.StartNew(() => ReadTest(i), TaskCreationOptions.None));
    //}

    //Task.WaitAll(readTasks.ToArray());
    //DateTime endRead = DateTime.Now;
    //TimeSpan tsRead = endRead - beginRead;

    double rowsPerSecondInsert = (threadCount * rowCount) / tsInsert.TotalSeconds;
    //double rowsPerSecondRead = (threadCount * rowCount) / tsRead.TotalSeconds;
    Console.WriteLine("{0} threads each insert {1} rows in {0} ms", threadCount, rowCount, tsInsert.TotalMilliseconds);
    //Console.WriteLine("{0} threads each read all rows in {1} ms", threadCount, tsRead.TotalMilliseconds);
    Console.WriteLine("{0} rows inserted per second", rowsPerSecondInsert);
    //Console.WriteLine("{0} rows read per second", rowsPerSecondRead);
  }

  void InsertTest(int threadId, int rowCount)
  {
    DateTime begin = DateTime.Now;
    DataMan.InsertRows(rowCount);
    DateTime end = DateTime.Now;
    TimeSpan ts = end - begin;
    //Console.WriteLine("{0} lines inserted in {1} ms on thread {2}", rowCount, ts.TotalMilliseconds, threadId);
  }

  void ReadTest(int threadId)
  {
    DateTime begin = DateTime.Now;
    int count = DataMan.ReadAllRows();
    DateTime end = DateTime.Now;
    TimeSpan ts = end - begin;
    //Console.WriteLine("{0} rows read in {1} ms on thread {2}", count, ts.TotalMilliseconds, threadId);
  }
}

internal static class DataMan
{
  private const string DbFileName = @"C:\temp\bdbvsqlite.db";
  private const string DbJournalDir = @"C:\temp\bdbvsqlite.db-journal";

  public static void Create()
  {
    if (File.Exists(DbFileName)) File.Delete(DbFileName);
    if (Directory.Exists(DbJournalDir)) Directory.Delete(DbJournalDir, true);

    //Console.WriteLine(SQLiteConnection.SQLiteVersion);
    using (SQLiteConnection conn = new SQLiteConnection())
    {
      conn.ConnectionString = "Data Source=" + DbFileName + ";";
      conn.Open();
      using (SQLiteCommand cmd = conn.CreateCommand())
      {
        cmd.CommandTimeout = 180;
        cmd.CommandText = "DROP TABLE IF EXISTS [TT]; CREATE TABLE [TT] ([src] TEXT, [td] DATETIME, [tn] NUMERIC, [ti] INTEGER, [tr] REAL, [tb] BLOB);";
        cmd.CommandType = System.Data.CommandType.Text;
        int result = cmd.ExecuteNonQuery();
      }
      conn.Close();
    }
  }

  public static void InsertRows(int rowCount)
  {
    SQLiteParameter srParam = new SQLiteParameter();
    SQLiteParameter tdParam = new SQLiteParameter();
    SQLiteParameter tnParam = new SQLiteParameter();
    SQLiteParameter tiParam = new SQLiteParameter();
    SQLiteParameter trParam = new SQLiteParameter();
    SQLiteParameter tbParam = new SQLiteParameter();
    Random rnd = new Random(DateTime.Now.Millisecond);

    using (SQLiteConnection conn = new SQLiteConnection())
    {
      conn.ConnectionString = "Data Source=" + DbFileName + ";";
      conn.Open();
      using (SQLiteTransaction trans = conn.BeginTransaction())
      using (SQLiteCommand cmd = conn.CreateCommand())
      {
        cmd.CommandTimeout = 180;
        cmd.CommandType = CommandType.Text;
        cmd.CommandText = "INSERT INTO [TT] ([src],[td],[tn],[ti],[tr],[tb]) VALUES (?,?,?,?,?,?);";
        cmd.Parameters.Add(srParam);
        cmd.Parameters.Add(tdParam);
        cmd.Parameters.Add(tnParam);
        cmd.Parameters.Add(tiParam);
        cmd.Parameters.Add(trParam);
        cmd.Parameters.Add(tbParam);

        int count = 0;
        while (count < rowCount)
        {
          var data = MakeRow(rnd);
          srParam.Value = data.src;
          tdParam.Value = data.td;
          tnParam.Value = data.tn;
          tiParam.Value = data.ti;
          trParam.Value = data.tr;
          tbParam.Value = data.tb;
          int result = cmd.ExecuteNonQuery();
          count++;
        }
        trans.Commit();
      }
      conn.Close();
    }
  }

  public static int ReadAllRows()
  {
    List<DRow> list = new List<DRow>();
    using (SQLiteConnection conn = new SQLiteConnection())
    {
      conn.ConnectionString = "Data Source=" + DbFileName + ";";
      conn.Open();
      using (SQLiteCommand cmd = conn.CreateCommand())
      {
        cmd.CommandTimeout = 180;
        cmd.CommandText = "SELECT [rowid],[src],[td],[tn],[ti],[tr],[tb] FROM [TT];";
        using (SQLiteDataReader reader = cmd.ExecuteReader())
        {
          while (reader.Read())
          {
            var row = new DRow
            {
              rowid = reader.GetInt64(0),
              src = reader.GetString(1),
              td = reader.GetDateTime(2),
              tn = reader.GetDecimal(3),
              ti = reader.GetInt64(4),
              tr = reader.GetDouble(5)
              //tb = (byte[])reader.GetValue(6)
            };
            list.Add(row);
          }
          reader.Close();
        }
      }
      conn.Close();
    }
    return list.Count;
  }

  private static DRow MakeRow(Random rnd)
  {
    int start = rnd.Next(0, 250);
    byte[] b = new byte[512];
    rnd.NextBytes(b);
    return new DRow
    {
      src = spear.Substring(start, 250),
      td = DateTime.Now,
      tn = rnd.Next(2999499,987654321),
      ti = rnd.Next(3939329,982537553),
      tr = rnd.NextDouble(),
      tb = b
    };
  }

  private const string spear = @"FROM fairest creatures we desire increase,That thereby beauty's rose might never die,But as the riper should by time decease,His tender heir might bear his memory:But thou, contracted to thine own bright eyes,Feed'st thy light'st flame with self-substantial fuel,Making a famine where abundance lies,Thyself thy foe, to thy sweet self too cruel.Thou that art now the world's fresh ornamentAnd only herald to the gaudy spring,Within thine own bud buriest thy contentAnd, tender churl, makest waste in niggarding.Pity the world, or else this glutton be,To eat the world's due, by the grave and thee.";
}

internal class DRow
{
  public long rowid { get; set; }
  public string src { get; set; }
  public DateTime td { get; set; }
  public decimal tn { get; set; }
  public long ti { get; set; }
  public double tr { get; set; }
  public byte[] tb { get; set; }
}
posted on Saturday, September 10, 2011 10:51:47 AM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Sunday, September 04, 2011

My last post covered getting Berkeley DB up and running with .NET. Now it’s time to take it one step further and build the open source System.Data.SQLite ADO.NET library, replacing the SQLite 3 engine with Oracle’s version of the SQLite.Interop C library that gets embedded into the .NET System.Data.SQLite assembly.

In other words, when you complete the steps below, you’ll have a System.Data.SQLite library that can, supposedly, drop into your .NET projects that currently use the ADO.NET library found on the sqlite.org site. The real difference is that instead of SQLite with single threaded writes, or file locking for writes rather, you will be using the latest Berkeley DB storage engine which supports page level locking to allow more writers, assuming the writers are writing to different pages.

For more information on Oracle’s implementation of the SQLite API which they call the Berkeley DB SQL API and to learn where they differ, you should read the whitepaper: Oracle Berkeley DB SQL API vs. SQLite API.

  1. Download the Berkeley DB dbsql-adodotnet-5.2.28.zip source.
  2. Unlock the zip file (Right-click and select Properties and click the Unlock button.)
  3. Extract the contents to an empty directory of your choice.
  4. Open the SQLite.NET.2010.sln solution file.
  5. Choose Release and then build.
  6. Optionally show all files in the System.Data.SQLite.2010 project and open the Assembly.cs file and change the AssemblyProduct string to something like “System.Data.SQLite-BDB” so that it will show up in file Properties Details tab. This is the only way you will know that this assembly has the Berkeley DB interop engine built into rather than the SQLite engine.

If you’re curious, do a diff between the SQLite.Interop project files in the Oracle version and the original sqlite.org version. Clearly very different animals. Now the only thing that is left is to write up a nice little test in C# to compare the two libraries. The subject of a future post.

UPDATE #1 (9/5/2011): Tests today show that this “new” System.SQLite.Data library DOES NOT create a Berkeley DB database. At least as far as I can tell. So while there is a libdb_sql50.dll in the main Berkeley DB build that implements the sqlite3.dll API, there is no way that I can find so far to use that library from C#. More experimentation to come.

UPDATE #2 (9/5/2011): Modify the UnsafeNativeMethods.cs with line 34 as

private const string SQLITE_DLL = “libdb_sql52.dll”;

and then change the solution’s projects conditional compilation symbols to “SQLITE_STANDARD” and then build. This prevents the SQLite interop embedded DLL from being used. Now when you run your test app, you’ll need to copy to the bin directory of your test app the new built DLLs (see previous post):

libdb_sql52.dll
libdb_stl52.dll
libdb52.dll

I’ll post more on my tests later. So far, I’ve got the basics working but when I push the limits, I get some nasty crashes, so I’m skeptical of this scheme to use the SQLite API over the Berkeley DB engine.

posted on Sunday, September 04, 2011 9:35:55 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Saturday, September 03, 2011

Over the years, I’ve looked into using Berkeley DB for my own C# applications because it has a solid reputation as a very fast, reliable key/value database. Every time I’ve walked away disappointed with the .NET API wrapper—until now. My interest was renewed a few days ago when I noticed an item on StackOverflow comparing SQLite performance to Berkeley DB.  Since acquiring Berkeley DB, Oracle has been busy making it better, including adding better support for the .NET development community.

WARNING: read and understand the license terms and conditions for Berkeley DB before you choose to use it.

I began this most recent review by downloading the MSI Windows installer. DO NOT DO THIS! When I tried to compile the VS2010 solutions (see below), I got all kinds of errors. Next I tried downloading the 45MB zip file. This worked like a charm, except the .NET examples projects had a broken reference which was easily fixed. Follow these steps and you’ll be up and running your .NET app using Berkeley DB in no time.

  1. Download db-5.2.28.zip file.
  2. Right click the zip file and select Properties and click the "Unlock" button. This will unlock and make usable all the files in the zip file for your local machine. DO NOT SKIP this step.
  3. Extract the contents of the zip file to a directory, e.g. C:\Code so that you will have a C:\Code\db-5.2.28 folder.
  4. In that db-5.2.28 folder, you will find a build_windows folder. This will be your home for the next steps. Be sure to follow them in order.
  5. Open Berkeley_DB_vs2010.sln
    1. build debug win32 and x64
    2. build release win32 and x64
  6. Open Berkeley_DB_examples_vs2010.sln
    1. build debug win32 and x64
    2. build release win32 and x64
  7. Open BDB_dotNet_vs2010.sln (allow default conversion)
    1. build debug win32 and x64
    2. build release win32 and x64
  8. Open BDB_dotNet_examples_vs2010.sln
    1. In each project, delete the missing reference to "db_dotnet" and add reference to ..\AnyCPU\Release\libdb_dotnet52.dll
    2. build debug win32 and x64
    3. build release win32 and x64

That’s it. Now you can run the example projects and you have a .NET library and x86 (Win32) and x64 binary engines for that library to use. Enjoy!

posted on Saturday, September 03, 2011 1:51:21 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]
# Thursday, September 01, 2011

As I sit here nursing a head plagued with a migraine, I read with great interest Eric Sink’s latest in my RSS reader about his experiences learning Scrum, a paper submitted for the proceedings of Agile 2011. Here’s my favorite part:

“I have come to think of our daily standup as being similar to a security guard at a bank. Most security guards stand around for their entire career without ever firing their weapon. It's probably a boring job. But the consistent presence of that security guard probably prevents some big problems from ever happening. Our daily standup is the same way.  Nothing exciting ever really happens. But we can confidently assume that many big problems have been avoided because we regularly take the time to get synced up.

“The culture of Scrum teams seems to be built on working together in shared spaces. In contrast, our company has always placed a high value on each person having a private office.

“We are aware that there are tradeoffs here. A private office gives each person a quiet place to work, but it also creates the opportunity for people to get isolated. So even as we provide private offices, we create ways to drag people out of them, including soda in the kitchen, lunch together on Wednesdays, a pool table, and a video game room.”

For this I would consider relocation to Illinois. And I always tell recruiters I’m not interested in relocating.

posted on Thursday, September 01, 2011 1:48:15 PM (Mountain Daylight Time, UTC-06:00)  #    Comments [0]