The Humble Function - Foundation to Functional Programming

When learning about functional programming, you won't go far before you run into the concept of a function. But we're not talking about some syntax or keywords in the language, but from a mathematical sense.

A function is a mapping between two sets such that for every element in the first set, it's mapped to a single element in the second set.

Since a set is a collection of elements, this is similar to a type where values are part of that type. With this understanding, we can modify our definition of a function to be:

A function is a mapping between two types such that for every value in the first type, it's mapped to a single value in the second type.

So What Does a Function Look Like?

Before diving into code, let's build up our understanding of functions more. When drawing out the function, we can model it like this.

Generic mapping from A to B

Generic mapping from A to B

The first type (A) is a circle where each possible value is listed, using ... to denote a pattern. The arrows map from each value of A to a value in B.

With this example, we know we have a function if the mapping satisfies the following rule:

Every element on the left maps to a single element on the right.

This rule seems easy enough to follow, but let's look at a mapping where this rule doesn't hold.

Functional Heartbreak

Let's say that we needed to write some code that could take a given month and return the number of days it has. Given this, here's what the mapping would look like.

Days in Month Mapping

Mapping from month name to days in month

To check if we have a function, we need to see if there's any element on the left with two or more arrows coming out.

In this case, February is breaking our rule because it could map to 28 or 29, depending on if it's a leap year. Since there isn't a parameter to denote if it's a leap year, our mapping isn't consistent and can't be a function.

One way to fix this would be to change our type on the left from MonthName to MonthName and year. Making this change gives us this new mapping.

Days in Month Mapping with Month Name and Year

Month and year mapping to days in month

Hip to Be Square

Let's look at a mapping that is a function, the square function.

Square mapping from number to number

Square mapping from number to number

Does every value on the left map to a single value on the right?

Yep, every value does. In fact, there are some values on the left that map to the same value on the right, which isn't a problem.

If we wanted to, we could restrict the type on the right from number to non-negative number, but there's no harm in having it be wider than needed.

Kinds of Functions

With this understanding of functions, let's talk about the two kinds of functions we can write and how they interact with each other.

Pure Functions

First, we have the pure function. These functions depend wholly on their inputs and they do not interact with outside state. For example, pure functions won't interact with databases, file systems, random generation, or time.

Pure functions are great because they're easy to test, composed with other functions, and don't modify state. Another advantage is that pure functions can be replaced with the result of their call (in other words, you could replace a call to square(3) with its result, 9 and the program is the same). This is known as referential transparency and is a great tool for troubleshooting an application.

The main downside to pure functions is that since they don't talk to other systems (including input/output), it's impossible to write a useful program with just pure functions.

Impure Functions

Impure functions, on the other hand, focus on interacting with outside state. These functions will call to the database or other systems, get the time, and generate random data as needed.

They allow us to write useful programs because they interact with input/output, however, the trade off is that they can be harder to test, they don't compose, and since they modify state, you may not be able to run them multiple times and get the same result.

One way to identify an impure function is by looking at its type signatures. For example, a function that takes inputs but returns void has to be modifying state or talking to another system, otherwise, why would you call it? Another signature is a function that takes no inputs, but it can return a value (like readLine() from nodejs), where did it get the value from? Just short of returning a constant value, it had to get it from somewhere.

Building an Application

Building an application requires both pure and impure functions, but how do we leverage the best of both worlds?

When I think about software, I think about how data flows through the system. For example, if someone is using a console application, they're inputting their commands to the terminal, which in turn converts them to commands to run, where the output is displayed on the screen.

As such, an application is made of three kinds of functions.

  • Boundary functions - These are responsible for getting input/output. They should have zero business rules (or the least possible as they are impure functions.
  • Business functions - These are the business specific rules that need to be ran on the given inputs. As these are typically the most important of an application, they are designed as pure functions
  • Workflow functions - This is the combination of boundary components and business components to build something useful. Since they're using impure functions, this will also be impure, but I often use this as my composition root for the application.

Effervescent Applications with Fizz Buzz

To demonstrate, let's build a version of FizzBuzz that uses this mindset.

For the problem, we have the following requirements.

  • If the number is divisible by 3, print "Fizz".
  • If the number is divisible by 5, print "Buzz".
  • If the number is divisible by both 3 and 5, print "FizzBuzz".
  • If the number isn't divisible by any of these, then print the number.

Given that we're building a console application, we will need to support getting input via the console and printing to the console.

Let's go ahead and build up our boundary functions.

// Impure function that allows us to get number from user
function getInput(): number {
  // using prompt-sync https://github.com/heapwolf/prompt-sync
  const prompt = require("prompt-sync")({ sigint: true });
  const response = prompt("What number to calculate FizzBuzz to?");
  if (!(+response) || (+response)<1) {
    console.log('Invalid response, please enter a positive number greater than 1');
    return getInput();
  }
  return +response;
}

// Function that wraps console.log for printing
function printOutput(content:string): void {
  console.log(content);
}

At this point, we have a way of getting a number via getInput and a way to print a string via printOutput. In printOutput, this is a tiny function with no business rules whatsoever. getInput, however, has some business rules about validation, but we'll see later on how to refactor this.

For now, let's leave these two and look into creating our business rule functions.

// Business rules for FizzBuzz
function calculateFizzBuzz(input:number): string {
  if (input % 3 == 0 && input % 5 == 0) {
    return "FizzBuzz";
  }
  if (input % 3 == 0) {
    return "Fizz";
  }
  if (input % 5 == 0) {
    return "Buzz";
  }
  return `${input}`;
}

// Helper function to create a range of numbers from [1..end]
function createRangeFromOneTo(end:number): number[] {
  if (number < 1) {
    return [];
  }
  return Array.from(Array[number].keys()).map((x)=>x+1);
}

With calculateFizzBuzz defined, we could write unit tests to ensure the correctness of the behavior. We could also create a mapping to double-check that we have a function.

Now, let's revisit our getInput function. We've got some business rules that deal with validation (e.g. the input must be a number and greater than 1). Given that this is a light business rule, we could leave it here; however, testing this becomes harder because we don't have a way to ensure that the validation works as expected.

To solve this problem, we could extract the validation logic to its own pure function and update getInput to use the new function.

function isInputValid(input:string): boolean {
  if (!(+input)) {
    return false;
  }
  return (+input) > 1;
}

function getInput(): number {
  // using prompt-sync https://github.com/heapwolf/prompt-sync
  const prompt = require("prompt-sync")({ sigint: true });
  const response = prompt("What number to calculate FizzBuzz to?");
  if (!isInputValid(response)) {
    console.log('Invalid response, please enter a positive number greater than 1');
    return getInput();
  }
  return +response;
}

Nice! With this in place, we can go ahead and implement our last function, the FizzBuzz workflow.

function runFizzBuzzWorkflow (): void {
  // Data coming in
  const maximumNumber = getInput();

  // Calculating results
  const results = createRangeFromOneTo(maximumNumber).map((x)=>calculateFizzBuzz(x));

  // Print Results
  results.forEach((x)=>printOutput(x));
}

// example invocation
runFizzBuzzWorkflow();

This is a straightforward implementation as we get the maximumNumber to calculate, create an array of numbers from 1 to maximumNumber, map each of those to their FizzBuzz representation, and then print them to the screen.

Let's go one step forward. In our example, we assumed that the input and output was coming from the console, but what if we needed to change to read and write to a file?

We could move the boundary functions to be parameters to runFizzBuzzWorkflow, let's take a look at what that would give us.

function runFizzBuzzWorkflow (readInput:()=>number, writeOutput:(string)=>void){
    // Data coming in
  const maximumNumber = readInput();

  // Calculating results
  const results = createRangeFromOneTo(maximumNumber).map((x)=>calculateFizzBuzz(x));

  // Print Results
  results.forEach((x)=>writeOutput(x));
}

// example invocations
runFizzBuzzWorkflow(getInput, printOutput); // using console read/write
runFizzBuzzWorkflow(()=>42, printOutput); // using hardcoded input with console log

With this change, we can now swap out how we can input or output by creating new functions with the right type signatures. This makes testing workflow components easy because you can stub in your own mocks (no need for mocking frameworks).

If you understand the power of switching out your boundaries, then you also understand other modern architectures like Ports and Adapters as they follow a similar principle.

Wrapping Up

In this post, we looked at what a function is, how it relates to types, and how to tell if a mapping is a function. From there, we covered the differences between pure and impure functions and how you need both to build any useful application. Finally, we wrapped up by implementing the FizzBuzz problem using this approach.

Five Tips to Improve Your Coaching Conversations

As a leader, you're responsible for coaching and growing your team, helping them be successful. To do this, you need to set the tone and example of the behaviors you want the team to have.

No matter how good of a team you have or how good of a leader you are, you will have to have a conversation about performance. Whether it's delivery, professional skills, or technology skills, you will have a moment where you need someone to change their behaviors.

Having these types of conversations can be scary, no matter how much experience in leadership you might have. However, when done correctly, these moments can greatly impact the other person, helping them grow tremendously.

On the other hand, poor coaching will do the absolute opposite. The other person can become confused or angry. They could even shut down and disengage altogether, making coaching them that much harder.

So what does good coaching look like? I can't guarantee that these steps will solve all your woes; however, I do guarantee that following these tips will increase the odds of the other person listening and at least consider your feedback. Remember, you want to do this coaching because some behavior has caught your attention, and you want to correct it. If the other person doesn't listen or want to engage, then you literally can't make this happen.

Step 1: Be Timely

The sooner you can have this conversation, the more effective it will be. Remember, the point of feedback is to let the other person know how they're doing and correct if needed. They can't do this if the behavior happened three weeks ago because there's no correlation at that point.

Imagine if you had a test suite that only told you about failing tests a week after the build started. There's no way you could make the right decisions, so why would we think that's the case for behavior?

If I've noticed a pattern and feel that it's time to coach, I will get that feedback to them that week, if not the next day.

Step 2: Be Specific

When giving this feedback, the behavior may be obvious to you but not even a thought for the other person. Because we can't control what the other person is thinking, we need to set the context for the feedback so they know what you're talking about.

Let's take my son, for example. He's particular about his food, so when he says that "dinner was awesome," that makes me feel great as I'm happy he enjoyed dinner. But I have no clue what he actually liked or why he thought that. Was it the food? The way it was served? The fact that we had a picnic? No idea, so I'd respond with, "What made it stand out to you?". When he mentions that he liked the pizza, I go "Ah! He enjoyed the food, nice!"

Providing this specific context is crucial for the other person because it lets them kow what caught your attention and drastically reduces the confusion in the conversation. For those who like more concrete details, sharing links to chats, emails, or other artifacts with the behavior can be helpful because you can use it as the foundation for the conversation.

Step 3: Explain Why

There's a reason that you're having this conversation. There's something that's important to you, and, in your opinion, it wasn't important to the other person. We've got to explain why it's important and why you're commenting on it.

I never want to remove someone's autonomy as I like to set the direction and let the team blaze a path, with me guiding to make sure we don't get lost in the wilderness. However, for someone to have autonomy, they need to understand the goals and the reasoning behind it.

If they don't have this knowledge, then it's that much harder for them to make the right decisions. Ensuring they know the why is a leader's responsibility.

Step 4: Seek To Understand

You're working with a team of professionals. A professional makes the right decisions based on their knowledge and experience. If the person is making mistakes, we need to understand why they made the choice that they did.

For example, let's say I'm coaching someone who's consistently missing meetings. I'm frustrated that they're unresponsive and that they don't care. The issue here is that it's okay for me to feel frustrated, but I can't make the judgment that they don't care. I don't know that, and it causes more problems than it solves. I won't vent my feelings to the other person because even though it'd make me feel better, it doesn't help the situation.

A better approach would be to understand why they're missing meetings. Is it something outside of work? Could it be that they don't know why they need to attend? What if they didn't receive an invite? In any of the above cases, there was a solid reason why they didn't attend, and I wouldn't have known that if I had not opened the conversation.

Don't assume malice or apathy when something happens. We are humans first, which means we're going to make mistakes.

Step 5: Working Together

The entire point of coaching is to help the person improve, and we also don't want to take away their autonomy. To make this happen, we need to work with the other person to come up with ideas that can help improve the situation. It's not any one person's responsibility, but it's your responsibility to brainstorm with them and help guide them down the correct path.

The key here is to have an open mind and really consider all ideas. One of my favorite leadership books, First, Break All The Rules, talks about how great leaders work with their people to have their strengths shine and to make their weaknesses a non-issue.

In the missing meeting example, I found out that the issue was that they didn't know why they needed to attend the meeting, so they didn't attend, in order to focus on their development work. Working together, I changed invites to include the reason for attending and encouraged them to chat with me so we could figure it out if they didn't know why they needed to be there.

Case Study - Bringing It Together

In this example, let's explore where we would need to do some coaching.

While reviewing a pull request from Bruce, you see a comment from Alvin, a member of your team, where they were particularly critical of the work. Reading through the pull request, you see Alvin has left more harsh comments about Bruce's work.

Talking with Bruce, they mention that they don't work well with Alvin as it seems like he's always critical of Bruce.

Based on this scenario, we know that Alvin has left some harsh words for Bruce, which makes them less likely to work together. If Alvin keeps this behavior up with other people, this will impact others wanting to work with him, reducing his effectiveness.

After collecting your thoughts, you reach out to Alvin to see if he's got a few minutes to chat about Bruce's pull request.

"Hey Alvin, I noticed you left some pretty harsh comments that in Bruce's pull request. For example, saying that 'this code is convoluted, rewrite it'. Even if that was the case, it's not clear why you think that. I'm more concerned with how the messaging came across because we work with others to accomplish our tasks, and that communication style can make people not want to work with us.

I don't believe you intend to alienate others, so can you walk me through your thought process here and why you thought this was the right approach?"

In this example, we've already hit four out of the five tips. Our feedback was timely and specific to the problem. We included why it caught our attention and started with an open-ended question for the conversation about the behavior.

In the follow-up conversation, Alvin mentions that he was having a rough day, particularly outside of work, and that he wasn't entirely focused on his tone. Given that this is the first time Alvin has done this, we want to focus on fixing the issue before it becomes a pattern.

"I understand that it can be hard to focus on your tone when you're having a rough time, however, we can't speak to others this way. I don't want this to become a pattern, so what are some things that we could do instead when we're not in the right mental head space for code reviews?"

At this point, we've acknowledged what was said and reaffirmed expectations. Using another open-ended question, we can start brainstorming things that we could do to help improve Alvin's tone. Since we're opening the conversation, Alvin is also giving feedback on what might work for him and what wouldn't work.

Wrapping Up

Giving critical feedback to someone is not the easiest thing to do, however, it can have the most impact for them. To help frame the conversation, our coaching should:

  • Be Timely
  • Be Specific
  • Explain the Why
  • Seeking to Understand
  • Be Collaborative

Building Relationships Through One-on-Ones

As a leader, one of your goals is to build a strong, high-performing team. To do this, you'll need to establish and develop relationships within the team and with each other. One approach I've found helpful to build and sustain these relations is through one-on-ones.

When most people think of one-on-ones, they typically think of some scheduled time, every so often, where they talk about work concerns or whatever is on the leader's mind. Some might find one-on-ones a waste of time and skip them.

Let's face it, we've all had bad one-on-ones where the conversation was forced and stiff. Or, it felt like the other person wasn't listening or cared about what was being talked about. If enough one-on-ones go down this route, it's no surprise that people don't want to have these conversations.

So, how do we improve the situation? In this post, I'm going to show you three things you can do to improve the one-on-ones you're having with the team by making sure that they're being heard, relationships are being built, and, just maybe, you might even get to know them better.

It's Not a Status Update

A common mistake is treating one-on-ones as status updates for work or projects. Even though it might be tempting to get an update (especially on important projects), remember this time is for the other person to talk with you about what's on their mind. They can't do this if you're asking for updates on work. If you're leveraging stand-ups or a task board, then you should be able to get updates from there. If this isn't sufficient, ask for updates outside this conversation. The one-on-one is where you give the other person your full attention.

Despite your best intent, this can be a tough habit to break. To help get out of this mindset, start time-boxing the updates to be a set period of time (e.g., ten minutes) with the goal of making this period shorter in subsequent one-on-ones.

Another technique I use is resetting, where I remind my teammate of the purpose and goal of the conversation. Doing this, it's a gentle nudge in the correct direction and reinforces that this time is for them, not for whatever is on my mind.

Don't Get Distracted

In the world of working remotely, it's easy to get distracted by chat notifications or emails. You're on the call, and you see the notification at the bottom of your screen, and before you realize it, you've read it and already thinking about a response. This may be great for you getting things done; however, for the other person, it's clear that you stopped paying attention. So what's important? Is it the notification or the other person?

pedestrians looking distracted in downtown
Credit to Matt Quinn via Unsplash

To help reinforce the focus, I put myself in Do Not Disturb mode, which will hide notifications from me until the meeting ends. If the issue is truly important, then someone would give me a call, in which case, I can excuse myself from the one-on-one to see what's up. In this rare case, I will reschedule our conversation as it's essential that we meet.

Allow Them to Set the Agenda

Another mistake I see leaders make is that they'll come to the one-on-one with a list of topics they want to discuss for the day. This can be particularly true if you're coaching this person and you want to provide concrete feedback. However, remember, the goal is to build and strengthen the relationship, and you can't do that if you're always setting the agenda for the conversations.

I find it amazing that you can learn quite a bit about the other person based on what they bring up. For example, if they're speaking about a conflict with another person, that lets me know that they're aware of relationships and how they're being perceived. On the other hand, if they're talking about a project and the concerns they have, then they're thinking outside of a task and are thinking at a higher level.

In one case, I was in a one-on-one with an engineer where they brought up their concern about supporting an API as they didn't know much about it. My first impression was that they didn't know how to read the code, but digging in, it turned out that they didn't see how the API fit into the bigger picture of the system. Learning this was a great fact check because I thought it was a technical issue, but in reality, it was a system question, which changed my perspective on them.

(Bonus) - Seeding a Conversation

I mentioned that the other person should own the agenda, but sometimes, they may not have much on their mind or a topic that sticks out to them. For these cases, I come prepared with a list of questions to help start the conversation.

pedestrians looking distracted in downtown
Credit to Markus Spiske via Unsplash

In Warren Berger's The Book of Beautiful Questions, he talks about the power of open-ended questions and how they can help people be more open. So instead of asking, "How's the project coming along?", which can be answered in a binary fashion, we could instead ask, "What's one thing about the project that stands out to you?", which could give us a richer response and allow you to learn more.

For those looking for questions, here's an excerpt of questions I've used to help seed a conversation:

  • What's one thing about the current project that turned out to be harder than you thought?
  • What was your biggest win last week, and why does it stand out?
  • What's something that happened in the past week that you want to learn more about?
  • I know you've been working more with this past week, talk to me about that experience
  • What's something that you're looking forward to?

Wrapping Up

To build great teams, you will need to foster relationships with the team and the individual members. Having one-on-ones can help with build these relationships, but only if you treat them like so. By allowing your teammate to set the agenda, giving them your full attention, and cutting out project updates, you can improve the quality of your conversations and get to know them better.

Building Relationships with Intent

As a leader, one of your superpowers is how you can connect your team with someone else. For example, if your team is struggling to work with an API and you know someone who's made recent changes or is a Subject Matter Expert, you can get your team unblocked and moving faster.

I've worked with leaders like this, and it's amazing how fast and helpful it is to get unstuck quickly. Don't get me wrong, sometimes it's the right strategy to "burn the time" to learn, but it's helpful to know someone who can get you unstuck if you need it.

With one leader, it seemed like they always knew a guy, no matter the topic, and I was amazed at how they did it. So like a lifelong learner, I asked, and they told me networking.

large group of people networking
Credit to ProductSchool via Unsplash

Ugh

If you're like me, you hear networking and think about dozens (if not hundreds) of people milling around, introducing themselves, and sharing business cards. Don't get me wrong, that approach can work for some people, but to me, that sounds exhausting. Instead of a hummingbird, going from flower to flower, I'm more like a bear. I just want to sit down and eat my jar of honey.

So what am I supposed to do? How am I supposed to network if I don't like large groups?

Given time, you can work on becoming more comfortable in large groups. However, why fight your natural tendencies and what you're good at?

For me, it's small groups and one-on-one conversations. As such, that's my approach to networking. Though it takes a bit longer, I find that I build stronger relationships with those people, and in turn, can be just as successful. I like to think about one-on-ones as lazy river conversations as I never know where it will take us.

man sitting in inner tube
Credit to Kiara Kulikova via Unsplash

One Cup of Coffee

At a previous company, we used Slack and, as such, had an integration called Random Coffee that would pair the members of a channel up to get together for the week. Such a simple idea, but so powerful when you now have a built-in excuse to chat with someone.

Coffee Chat
Credit to Priscilla Du Preez via Unsplash

After a couple of weeks of getting to know people, I started learning what others did, what interests them, and who to go to about specific issues. Combine that with asking, "How did you know that?" I found that I could quickly fill gaps in my knowledge.

But something else happened. Once I knew the person, I didn't see them as a name in the chat anymore, I saw them as their selves. I'd find myself saying, "Oh, it's Chris, and Chris is cool, so I'll help him out," instead of thinking, "Ugh, another thing to do." In a way, these conversations humanized those I worked with, and I found myself caring more about them.

Caring By Knowing

To me, this is the most important thing about relationship building and networking. Deep down, I want to care about those I work with because I want them to be successful. I can't help them be successful if I don't know them both as a colleague and as a person.

Having one-on-ones is how I know my people and how I continue building care. It might be as simple as knowing what types of things they like to work on or what they did over the weekend. However, having these conversations helps both of us open up, and I get to know them so much better. Once I know them, I can guide and direct them better, looking for opportunities I wouldn't have thought of before.

How Do I Start?

If you want to start this for your own company, you don't have to have Slack to make this happen. The important thing is getting buy-in from others and explaining the why behind the exercise.

Once you have buy-in, you can start low-tech by using an Excel sheet and randomizing the list of names. This isn't the most robust solution, but it's a start and you can iterate as you figure out the timing, the frequency, and the other steps that setting this up would look like.

Once you've got something in motion, you can always work on automating the process later. Don't let a perfect solution stop you from starting with a good solution.

What I've found successful is having either a weekly or fortnightly scheduled message in our main channel that assigns the groups. From there, participants are encouraged to share something they've learned about their counterparts during their conversation. To help make sure that people meet, scheduling a set time during the week for all the groups can be helpful as it removes another barrier (e.g., if you know that you'll have coffee at 10:30 am on Tuesdays, you learn to expect it).

If you have a group that is just starting, it might be helpful to provide some starting questions to help jump-start the conversation. A good list of questions can be found in one of my gists.

Wrapping Up

To be a successful leader, you must cultivate and grow relationships with those you work with. Not only does it help your team be successful, but it allows you to have a richer experience with your work and helps solidify that we're all working together.

Better Domain Modeling with Discriminated Unions

When I think about software, I like designing software so that doing the right things are easy and doing the wrong things are impossible (or at least very hard). This approach is typically called falling into the pit of success.

Having a well-defined domain model can prevent many mistakes from happening just because the code literally won't let it happen (either through a compilation error or other mechanisms).

I'm a proponent of functional programming as it allows us to model software in a better way that can reduce the number of errors we make.

Let's at one of my favorite techniques discriminated unions.

Motivation

In the GitHub API, there's an endpoint that allows you to get the events that have occurred for a pull request.

Let's take a look at the example response in the docs.

[
  {
    "id": 6430295168,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6430295168",
    "event": "locked",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-04-13T20:49:13Z",
    "lock_reason": null,
  },
  {
    "id": 6430296748,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6430296748",
    "event": "labeled",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-04-13T20:49:34Z",
    "label": {
      "name": "beta",
      "color": "99dd88"
    },
  },
  {
    "id": 6635165802,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6635165802",
    "event": "renamed",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-05-18T19:29:01Z",
    "rename": {
      "from": "Secret scanning: dry-runs for enterprise-level custom patterns (cloud)",
      "to": "Secret scanning: dry-runs for enterprise-level custom patterns"
    },
  }
]

Based on the name of the docs, it seems like we'd expect to get back an array of events, let's call this TimelineEvent[].

Let's go ahead and define the TimelineEvent type. One approach is to start copying the fields from the events in the array. By doing this, we would get the following.

type TimelineEvent = {
  id: number;
  url: string;
  event: string;
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  lock_reason?: string;
  label?: {
    name: string;
    color: string;
  };
  rename?: {
    from: string;
    to: string;
  };
};

The Problem

This definition will work, as it will cover all the data. However, the problem with this approach is that lock_reason, label, and rename had to be defined as nullable as they can sometimes be specified, but not always (for example, the lock_reason isn't specified for a label event).

Let's say that we wanted to write a function that printed data about TimelineEvent, we would have to write something like the following:

1
2
3
4
5
6
7
8
9
function printData(event: TimelineEvent) {
  if (event.event === "labeled") {
    console.log(event.label!.name); // note the ! here, to tell TypeScript that I know it'll have a value
  } else if (event.event == "locked") {
    console.log(event.lock_reason);
  } else {
    console.log(event.rename!.from); // note the ! here, to tell Typescript that I know it'll have a value
  }
}

The main problem is that the we have to remember that the labeled event has a label property, but not the locked property. It might not be a big deal right now, but given that the GitHub API has over 40 event types, the odds of forgetting which properties belong where can be challenging.

The pattern here is that we have a type TimelineEvent that can have different, separate shapes, and we need a type that can represent all the shapes.

The Solution

One of the cool things about Typescript is that there is a union operator (|), that allows you to define a type as one of the other types.

Let's refactor our TimelineEvent model to use the union operator.

First, we need to define the different events as their own types

type LockedEvent = {
  id: number;
  url: string;
  event: "locked"; // note the hardcoded value for event
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  lock_reason?: string;
};

type LabeledEvent = {
  id: number;
  url: string;
  event: "labeled"; // note the hardcoded value for event
  commit_id?: string;
  commit_url: string;
  created_at: string;
  label: {
    name: string;
    color: string;
  };
};

type RenamedEvent = {
  id: number;
  url: string;
  event: "renamed"; // note the hardcoded value for event
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  rename: {
    from: string;
    to: string;
  };
};

At this point, we have three types, one for each specific event. A LockedEvent has no knowledge of a label property and a RenamedEvent has no knowledge of a lock_reason property.

Next, we can update our definition of TimelineEvent to use the union operator as so.

type TimelineEvent = LockedEvent | LabeledEvent | RenamedEvent

This would be read as A TimelineEvent can either be a LockedEvent or a LabeledEvent or a RenamedEvent.

With this new definition, let's rewrite the printData function.

1
2
3
4
5
6
7
8
9
function printData(event: TimelineEvent) {
  if (event.event == "labeled") {
    console.log(event.label.name); // note that we no longer need !
  } else if (event.event == "locked") {
    console.log(event.lock_reason); 
  } else {
    console.log(event.rename.to); // note that we no longer need !
  }
}

Not only do we not have to use the ! operator to ignore type safety, but we also have better autocomplete (note that locked_reason and rename don't appear when working with a labeled event). Better autocomplete

Deeper Dive

At a general level, what we've modeled is a sum type and it's great for when you have a type that can take on a finite number of differing shapes.

Sum types are implemented as either tagged unions or untagged unions. Typescript has untagged unions, however, other languages like Haskell and F#, use tagged unions. Let's see what the same implementation in F# would have looked like.

// specific type definitions omitted since they're
// similar to typescript definition
// ....
type TimelineEvent = Locked of LockedEvent | Labeled of LabeledEvent | Renamed of RenamedEvent

let printData e = 
    match e with
    | Locked l -> printf "%s" l.lock_reason
    | Labeled l -> printf "%s" l.label.name
    | Renamed r -> printf "%s" r.rename.``to`` // the `` is needed here as to is a reserved word in F#

A tagged union is when each shape has a specific constructor. So in the F# version, the Locked is the tag for the LockedEvent, Labeled is the tag for the LabeledEvent, so on and so forth. In the Typescript example, we worked around it because the event property is on every TimelineEvent and is a different value.

If that wasn't true, then we would had to have added a field to TimelineEvent (typically called kind or tag) that would help us differentiate between the various shapes.

Wrapping Up

When defining domain models where the model can have different shapes, you can use a sum type to define the model.

Keeping Track - My Task Tracking Approach

When it comes to keeping track of things to do, I recall an ill-fated attempt at using a planner. My middle school introduced these planners for the students that you had to use to keep track of dates (and, weirdly enough, as a hall pass to go to the bathroom).

Looking back, the intent was to have the students be more organized, but that wasn't what I learned. I found it cumbersome and a pain to keep track of. Also, you had to pay to replace it if it was lost or stolen.

What I learned to do instead was to keep track of everything I needed to do in my memory, and if I forgot, well, I had to pay the penalty.

I recall seeing my peers in high school and college be much more organized, and they made it so simple. Just color code these things, add these other things to a book and highlight these things.

I didn't realize that my peers had developed a system for studying and keeping track of what they needed to do. Since I didn't know what it was called and felt awkward admitting I didn't know what it was, I would continue relying on my memory to get things done. However, this approach doesn't scale and is prone to having tasks drop from the list.

When I started working at my second professional job, I found my boss to be organized and meticulous, and he never let anything slip. I learned a ton from him about process improvement and was introduced to a Kanban board for the first time.

As an engineer, I would use a version of his approach for years, but when I got into leadership, I felt that I needed a better system. As an individual contributor, I could rely on the task board for what I needed to do, but that approach doesn't work for a leader because not all of your tasks are timely or fit in a neat Jira ticket.

Why a System?

Why do we need a system at all? Isn't memory good enough? The problem is that the human mind is fantastic at problem-solving but isn't great when it comes to recollection. In fact, multiple studies (like this one or this one) have shown that the more stressed you are, the worse your memory can become.

With this context, you need to have some system to get the tasks out of your head and stored elsewhere. Whether that's physical sticky notes in your office, a notebook that you use, or some other tooling, I don't particularly care, but you do need something.

My Approach

I'm loosely inspired by the Getting Things Done approach to task completion, which I've implemented as a Trello board. Having an online tool works for me because I can access it anywhere on my phone (no need to carry a notebook or other materials).

Another side effect of having an online tool is that at any point I have an idea or a task that I need to do, I can add it to my Trello board in two clicks. No more worries about remembering to add the task when I'm back home or in the office, which allows me to not stress about it.

Work Intake Process

On my Trello board (which you can copy a template from here), all tasks end up in the first column, called Inbox. The inbox is the landing spot for anything and everything. Throughout the day, I will process the list and move it to the appropriate column.

  • Is it a task that I can knock out in 5 minutes or less? Just do it!
  • Is it a task that will take more than 5 minutes? Then I move it into the To Do column
  • Is it a task that I might be interested in? Is it a bigger task that I need to think more about? Then that goes into the Some Day column
  • If the task is no longer needed, then it gets deleted.

Deciding What To Do Next

Once the inbox is emptied, I look at the items in the To Do column and pick the most important one. However, determining the most important one is not always the easy.

For this, I leverage the Eisenhower Matrix approach.

Named after Dwight D. Eisenhower, the idea is that we have two axes, one labeled Important and the other labeled Urgent. With these labels, tasks fall into four buckets:

  • Urgent and Important - (e.g., production broken, everything is on fire)
  • Urgent and Not Important - (e.g., last minute request, something that needs to be done, but not necessarily by you)
  • Not Urgent and Important - (e.g., strategic work, things that need to get done, but not necessarily this moment)
  • Not Urgent and Not Important - (e.g., time wasters, delete these tasks)
Eisenhower Matrix with four quadrants: Urgent & Important, Urgent & Not Important, Not Urgent & Important, and Not Urgent & Not Important
(2023, March 7). In Wikipedia. https://en.wikipedia.org/wiki/Time_management

Dealing with Roadblocks

In an ideal world, you could take an item and run it to completion, but things aren't always that easy. You might need help from another person or are waiting for someone to do their part.

When this happens, I'll move the item to the Waiting column and pick up a new task as I don't like to be stalled.

However, I keep an eye on the number of items in flight as I've found that if I have more than three items in flight, I struggle with making progress and spend my time context-switching between the items instead of completing work. It can be challenging if the tasks are wholly unrelated (development tasks, writing, and reviewing pull requests) as the cost of regaining the context feels higher than if the tasks are related (e.g., reviewing multiple pull requests for the same repository).

Getting Things Done

As items get completed, I add them to the Done column for the week. To help keep track of what I got done for the week, I typically call my Done column the week it spans (e.g., Apr 17-23, 2023). Once the week ends, I can refer back to the column, see where I spent my time, and reflect if I made the right choices for the week.

Finally, I'll archive the list, create a new column for next week and repeat.

Telling the Story: The Pitfall of a Single Data Point

Let's say that you're sitting down to read a new book, and you come across the following:

The King's Knave Inn was but a short distance from the Alverston train depot, just outside the town proper. (excerpt from The Infernal Machine by John Lutz)

After reading this, a friend interrupts your reading and asks your thoughts on the book so far. What would you say?

Most likely, you'd respond that you need to read more, and it's still too early to decide if the book is good or not.

To honestly answer this question, you would need to read more of the book (ideally all of it) to get a full picture of the story.

When measuring an engineer's performance and effectiveness, why don't we take the same approach?

My experience has been that leaders look for one or more metrics to quantify a person. At the face of it, I understand why, as it's hard to compare people if there aren't numbers.

However, the mistake I see leaders make is what they're trying to measure. For example, do you measure the number of pull requests? What about the number of stories completed in a sprint? How about the number of bugs shipped to production? Something else entirely?

The problem is that even if you use all of the above (please don't do this), you're still not seeing the whole picture, but only bits and pieces. This would be like reading five chapters at random from a book and then giving an opinion.

The other problem with using metrics is that the measurement will cease to be effective as people will start gaming the system (see Goodhart's Law).

For example, if we measure effectiveness by the number of completed pull requests, then what stops someone from creating hundreds of single-line pull requests that don't accomplish anything?

On the other side, what about the engineer who reduced scope and time because they knew how to simplify the approach or came up with a more straightforward solution? This insight won't show up as a pull request or a completed story; however, this should be rewarded just the same.

To really determine how effective someone is, we need to look at things holistically, which can be done by examining how well someone does in these three areas:

  • Understanding the problem (e.g., why are we doing this?)
  • Understanding the system (e.g., how are we doing this?)
  • Understanding the people (e.g., whom are we doing this with?)

By looking into these areas, you will see what your team is good at and where they could use coaching, helping you be more effective. You might also realize that your team is doing things that aren't so obvious.

You can't write a report to generate these metrics. To understand this, you have to understand your team and how they work together. This involves paying attention, taking notes, and being engaged. Passive leaders will struggle if they use this approach.

Understanding the Problem (The Why)

To be successful, we first have to understand the problem that's being solved. Without this base knowledge, it's impossible to build the right solution or even ask the right question to the problem at hand.

How comfortable are they within the problem domain? Do they know certain terminologies, our customers, the users, workflows, and expected behaviors?

Besides quizzing, there may not be an obvious way to measure this; however, here's my approach.

First, you can look at the questions that are being asked. Are they surface level or are they deep? You can see these questions through chats and meetings, comments on the stories or pull requests, and interactions with others.

Second, look at the solution they came up with. Did they design it with domain knowledge in mind? For example, are things named correctly? Did their solution take care of the main workflows? What about the edge cases?

Third, how are they handling support issues? Being on support is a quick way of learning a problem domain and system. As such, I'm looking at how much help they need and how they communicate with others.

By using this approach, you can get a good sense of how knowledgeable someone is in the problem domain without quizzing them.

Understanding the System (The How)

There's always a push to deliver more things, and in order to do that, we have to understand the current system, its limitations, what's easy vs. what's hard, and from these constraints, determine the correct path to take.

In addition, once the system is live, we need to support it. If we don't know the moving parts, what it interacts with, and how it's used, we're going to have a bad time.

Like understanding the problem, we can measure system knowledge without quizzing them. In particular, I've found pull request comments and code reviews to be insightful on someone's knowledge of the system.

For example, do they call out that there's already something in the system that does this new piece of functionality? Do they suggest taking a simpler approach with what we have? Do they propose a different solution altogether because the system has a limitation? All of these are indicators of someone's system knowledge.

Another way to gauge system knowledge is by looking at how the person handles support requests. If you can understand the problem, find the cause, and create a fix, then by definition, you have to have a solid understanding of the system.

Understanding the People (The Who)

When it comes to the third part of being effective, we have to measure how they work with those around them. Most people think engineering is a solitary line of work, and that can be true when it comes to the development phase.

However, in reality, engineers work with others to design, develop, and iterate on a solution, and this can only happen when working with others. As such, building these relationships are paramount to being successful.

If you want to go fast, go alone. If you want to go far, go together.

Measuring team cohesion can be difficult (it could be its own post), however, we start simply be getting peer feedback on the person. We can also look at the communication between them and others through their comments, messages, or meetings.

Another way to measure this is through your company's recognition system. Whether it's an email or some other tool your company uses, you need to keep tabs on these recognitions, as you can use them as a talking point during 1:1s and review time.

Wrapping Up

So, how do we measure how effective someone is? We know that a single data point isn't sufficient and that if we limit ourselves to metrics, we can get a skewed sense of the person. To know, we have to take a holistic approach.

To accomplish this goal, I recommend measuring the following areas:

  • Understanding the problem (e.g., why are we doing this?)
  • Understanding the system (e.g., how are we doing this?)
  • Understanding the people (e.g., whom are we doing this with?)

In each of these areas, we can get a sense by observing their interactions they have, the questions they ask, the approaches they take, and how likely people want to work with the individual.

Three Steps to Better Interviews

At some point in your career, you're going to be conducting interviews. Regardless of the role, you have the opportunity to shape the future of the company as your recommendation controls whether this person is going to be a colleague or not.

What a lot of people don't realize is that an interview is can be the first experience that someone has with your company. As such, you want this experience to be fantastic, even if they're not hired, as they could be a future customer of yours.

With interviewing to be so important (there are whole books about the subject), it's confounding to me when companies don't invest in training or resources to help grow their leaders into being better interviewers. Especially, when making a bad hire can cause so much damage and is expensive to resolve in the long run.

Over my career, I've seen my share of good and bad interviews and have some tips and tricks to improve your interviewing skills. In this post, I'm going to share three tips that help me have better conversations with candidates.

1. Build Better Conversations Using Scenarios

The first mistake I seen interviewers make is that they have a set of questions that they want to pepper the candidate with, in an effort to figure out if they're going to be a good fit or not.

An ideal interview should flow more like a conversation where the candidate is getting to know you and the company and where you are learning about the candidate. As such, a never-ending list of questions makes the candidate feel like they're being interrogated and it doesn't allow for a natural conversation. A great interview should feel like tennis, each player receiving and sending the ball to the other side.

For example, let's say that I want to know a candidates familiarity with REST APIs. I could ask questions like

What's the difference between GET and POST?

What's the difference between a 404 and 400 response code?

Even though I'll get answers, this is not much of a conversation, but more of a quiz. Instead, I ask the following

In this scenario, I'm a newer engineer sitting down to make some changes to one of your APIs and I seem to be running into some issues.

For example, when I invoke the endpoint via GET, I'm getting back a 404 (Not Found). Doing some digging, it seems like it's related to the resource not being there, but I'm not sure how to troubleshoot. What would you recommend?

With the above, the candidate has a clear problem (e.g. can't communicate with the API) and has plenty of space to talk about what they're thinking (incorrect route, API not running, etc..). As the candidate is talking things through, I'm getting more insight on what they know and how they think about things. For example, if they mention that a firewall could be blocking the request, I could dig into that a bit more and learn that they have knowledge in networking or cloud technologies.

Another advantage of this approach is that we can add more steps. For example, here's one of the scenarios I ask to measure understanding of REST APIs.

In this scenario, I'm a newer engineer sitting down to make some changes to one of your APIs and I seem to be running into some issues.

For example, when I invoke the endpoint via GET, I'm getting back a 404 (Not Found). Doing some digging, it seems like it's related to the resource not being there, but I'm not sure how to troubleshoot. What would you recommend?

I've fixed the typo, made another request, and I'm now getting a 401 (Unauthorized). Looking up the response code, this implies that I don't have permissions, but I'm stuck on next steps. What would you recommend for troubleshooting?

Oh right, Bearer Token, I remember reading that in the README, but I didn't understand at the time. After generating the token and making another request, I'm now getting a 400 (Bad Request). Looking up the status code, it seems like it's something related to the payload or route. How would you troubleshoot?

Finally! After fixing that issue, I was able to get a 200 (Ok) response back, thanks for the help!

By using the above scenario, I can learn quite a bit about what systems an engineer has worked with, what gaps they might have, and how they troubleshoot issues. This is a lot more effective than knowing if an engineer can tell the difference between GET and POST.

2. Build Better Conversations Using Open-Ended Questions

Another common mistake I see is asking closed-ended questions to gauge knowledge. Even though these are binary in nature (Yes/No) or have a specific answer (What's the capital of North Carolina?), they come off as interogative instead of conversational. In addition, these types of questions are informational and could easily be looked up, where as open-ended questions are opinion based and come from experience.

For example, if we were to ask:

What's the difference between an Observable and a Promise?

We would know if the candidate knows the difference or not and that's about it. Even though this knowledge is helpful, we could learn this (and more) by rephrasing it to be open-ended instead.

For example, if we were to ask:

When would you use an Observable over a Promise?

With this question, not only do we learn if the candidate can talk about Observable vs Promise, but we also know if they know which scenarios to use one over the other.

For more effective questions, we could turn this question into a scenario, by asking the following

Let's say that we're working on a web component that has to call an API to get some data. It looks like we could call the API and have the value returned be either an Observable or a Promise. What would you recommend and why?

In this scenario, we get to learn if the candidate knows the differences between Observable and Promise, can reason about why one approach would be better than another, and explain that to another engineer. No matter their choice, we could follow up by asking why they wouldn't pick the other one option.

3. Build Better Conversations By Asking For Examples

For the final mistake, I see interviewers ask some form of a leading question, where based on the phrasing of the question, the candidate would be pressured or coerced into answering a particular way.

For example

This position involves mentoring interns to be associate engineers. Is that something you're comfortable with?

This is a leading question because if the candidate were to say "No", then they would believe that they wouldn't get the job. So they would always answer yes, regardless of how they feel, which makes this question useless as it doesn't tell us anything about the candidate. Most leading questions tend to also be close-ended questions, so a double strike for this style of interviewing.

But Cameron! I need to know this information as this person would be responsible for coaching up our engineers! Cool, then let's tell the candidate, but let's also provide some context and allow them to tell us their experience.

For example, we could phrase the question this way

One of the responsibilities for the role is to help grow interns into associate engineers so we can grow terrific engineers internally. With this context, can you walk us through a time where you had to coach someone up? What was your approach? What would you do differently?

With this question, you've still mentioned the skill you're looking for, however, you've added context on the "why" behind the question and you've set the candidate up to talk about their experience, which in turn, gives you more context about the person.

Thinking With Properties: Examining Where

Note: This post is for C# Adevent Calendar 2020 organized by Matthew Groves. Check out some of the other psots that are happening over the month of December!

What Do We Mean By Properties?

When I think about software, I will generally think about the properties that the solution has to have Where properties are the characteristics that the code has. Sometimes, the property is obvious (for example, if you square a number, the result should always be positive). In this post, we’re going to look at LINQ’s Where method, examine some of the properties, and come up with a more efficient way of creating filters.

Examining LINQ's Where Method

For those not familiar with Where, it’s a method that promises that it will return a subset of a list Where each item fulfills some criteria (referred to as a predicate). The type signature for Where is

IEnumerable<T> => Func<T, bool> => IEnumerable<T>

At face value, this sounds pretty straightforward, but there are a few more properties that Where provides that aren’t obvious at first, but are beneficial

  • The results can’t be null (worse case, it’ll be an empty list because no item fulfilled the criteria)
  • The results can’t be larger than the original list
  • The results are in the same order that the original list was in (i.e. if we’re trying to find the even numbers in a list that is 1..10, then you’re guaranteed to get 2, 4, 6, 8, and 10. Not, 8, 2, 6, 10, 4)
  • The results will only contain elements that were in the original list (i.e. it can’t create elements out of thin air and it can’t copy elements in the original list)

Common LINQ Mistake with Where

That’s a ton of guarantees that you get from leveraging Where instead of doing your own filtering inside loops. With these properties in mind, let’s take a look at a common mistake that developers make when working with Where

1
2
3
4
5
6
7
8
9
// Generate numbers from -10 .. 100
var numbers = Enumerable.Range(-10, 111);

// Determines all positive numbers that are divisible by 6
var positiveDivisbleBySix = numbers
                            .Where(x=>x > 0) // iterates through the whole list (111 comparisons, returning 100 results)
                            .Where(x=>x % 2 == 0) // iterates through the new list (100 comparisons, returning 50 results)
                            .Where(x=>x % 3 == 0); // iterates through the new list (50 comparisons, returning 16 results)
// Overall metrics: 261 comparisons over 111 total elements)

By leveraging multiple Where statements, the list will be iterated once per statement. which may not be a big deal for small lists, but for larger lists, this will become a performance hit. In order to help cut down on the iterations, it’d be ideal to combine the multiple Where statements into a single one like so

1
2
3
4
// Generate numbers from -10 .. 100
var numbers = Enumerable.Range(-10, 111);

var positiveDivisibleBySix = numbers.Where(x => x > 0 && x % 2 == 0 && x % 3 == 0);

By combining the criteria in a single Where statement, we eliminate the multiple iteration problem, however, we introduce code that’s a bit harder to read and if we want to combine a non-fixed number of predicates, then this approach won’t work.

Since the goal is to take multiple predicates and combine them to a single predicate, my intuition is to leverage LINQ’s Aggregate method where we can take a List of items and reduce down to a single item.

Refactoring Multiple Where with Aggregate

In order to leverage Aggregate, we’ll first need to have a list of item to reduce down. Since all of the predicates are Func, we can easily create a List like so

1
2
3
4
Func<int, bool> isPositive = x => x > 0;
Func<int, bool> isEven = x => x % 2 == 0;
Func<int, bool> isDivisibleByThree = x => x % 3 == 0;
var predicates = new List<Func<int, bool>> {isPositive, isEven, isDivisibleByThree};

Now that we have a list of predicates, we can go ahead and start stubbing out the Aggregate call.

var combinedPredicate = predicates.Aggregate(...., ....);

In order to use Aggregate, we need to determine two pieces of information. First, what should the predicate be if there are no predicates to combine? Second, how do we we combine two predicates into a single predicate?

Defining the Base Case

When using Aggregate, the first thing that we need to think about is the base case, or in other words, what should the default value be if there are no elements to reduce down?

Given that the result needs to be a predicate, we know that the type should be Func<int, bool>, but how do we implement that? We’ve got one of two choices for the base case, we can either filter every item out (i.e. if no predicates are specified, then no items are kept) or we keep every item.

For our use case, we want to keep every item if there are no predicates, so our base case looks like the following

Func<int, bool> andIdentity = _ => true;

Defining How To Combine Predicates

Since we’re combining predicates, our combine function will need to have the following type

Func<int, bool> => Func<int, bool> => Func<int, bool>

1
2
3
4
Func<int, bool> combinedPredicateWithAnd(Func<int, bool> a, Func<int, bool> b)
{
  return x => ...;
}

With this in mind, we know that for an item to be valid, it has to match every predicate in the list which implies that we’ll be leveraging the && operator

1
2
3
4
Func<int, bool> combinedPredicateWithAnd(Func<int, bool> a, Func<int, bool> b)
{
  return x => ... && ...;
}

Now that we know to use &&, we can then use a and b to determine if the item is valid

1
2
3
4
Func<int, bool> combinedPredicateWithAnd(Func<int, bool> a, Func<int, bool> b)
{
  return x => a(x) && b(x);
}

Bringing It All Together

With the base case established and a way to combine predicates, here’s how we can solve the original problem.

// Define the predicates
Func<int, bool> isPositive = x => x > 0;
Func<int, bool> isEven = x => x % 2 == 0;
Func<int, bool> isDivisibleByThree = x => x % 3 == 0;
var predicates = new List<Func<int, bool>> {isPositive, isEven, isDivisibleByThree};

// Defining the Aggregate functions
Func<int, bool> andIdentity = _ => true;
Func<int, bool> combinedPredicateWithAnd(Func<int, bool> a, Func<int, bool> b)
{
  return x => a(x) && b(x);
}

// Combining the predicates
Func<int, bool> combinedAndPredicate = predicates.Aggregate(andIdentity, combinedPredicateWithAnd);

// The new solution
// Generate numbers from -10 .. 100
var numbers = Enumerable.Range(-10, 111);
var positiveDivisbleBySix = numbers.Where(combinedAndPredicate);

Going forward, if we need to add more predicates, all we need to do is to add it to the List and the rest of the application will work as expected

Wrapping Up

In this post, we explored LINQ’s Where method by examining its various properties. From there, we took a look at a common mistake developers make with Where and then showed how to resolve that issue by using Aggregate.

Shout out to Matthew Groves for letting me participate in C# Christmas (csadvent.christmas)

Mars Rover – Implementing Logger – Design

Welcome to the tenth installment of Learning Through Example – Mars Rover! At this point, we’ve wrapped up all the functionality for the Rover, so now it’s time to start implementing the logging requirements. Like always, we’ll first take at the requirements to ensure we’ve got a good idea of what’s needed. From there, we’ll talk about the different approaches for implementation we could take. Finally, we’ll decide on a rough approach for implementation

What Do We Need?

If we look back at the original requirements for logging, we find the following

In order to help troubleshoot failures with the emulation, every time a command is received, both the command received, the rover’s location, and the rover’s orientation should be logged.

Determining Intent From Vague Requirements

Yep, these requirements are clearly defined, but what’s the problem we’re trying to solve? Why do we care if this happens or not? After talking more with the Subject Matter Expert (SME), it seems like there are two main reasons for the logging requirement. First, they want a way to troubleshoot if something goes wrong in the emulation, and having this trace will be helpful in reproducing the error. Second, they want to have different runs be separated in the logging in the event they need to troubleshoot a specific run.

Going from Vague to Concrete Requirements

Given the above intent, it sounds like an easy approach we can take is to log all of this information to a file with a specific name. In real production code, you would likely use a logging solution instead of implementing your own, but this will provide a good opportunity to learn some of the System.IO namespace in .NET.

With a rough implementation in mind, we have another discussion with our SME, and come up with the following requirements:

  • When the application starts, the user will need to specify where to log the information to (known as the path)
  • If the path doesn’t exist, the user is informed of such and the application doesn’t continue
  • If the path isn’t accessible (maybe due to a permissions issue), then the user is informed of such and the application doesn’t continue
  • If the path is valid, then a text file will be created that contains the information generated by the emulator
  • Every time the Rover receives a command, its Location and Orientation should be logged

Software Design Guidelines

Now that we have requirements, how do we want to begin implementation? Do we want to add this logic to the Rover class? Or should we add a new component to handle logging?

Adding Logging to Rover

So one approach to this problem is to widen the responsibilities for the Rover, add a Log method and update the existing methods to use this new method. The advantage of this approach is that the logging is in one place so it’s easy to make changes. Another advantage is that since the Rover component already exists, we can go ahead and add some new tests pretty quick.

Unfortunately, this approach has two major downsides. First, by widening the responsibilities, we’ll need to update all existing tests to deal with logging (which may not be a trivial change to make). Second, since the Rover handles the logging, we’ve muddled our easy-to-test business rules with a cross-cutting concern and now the Rover would have a couple of different reasons to change. Bummer 🙁

Writing a New Component

The advantage of writing a new component is that we can focus on making a Logger component that just knows how to log any message to storage, regardless of what uses this component. By taking this approach, we can have a “dumb” component (almost not worth testing) and keep the things that we care about (i.e. the business rules) easier to test.

The main downside of this approach is that we have yet another component to manage. In our particular case, that’s alright, but as the codebase becomes larger, this is something to keep in mind.

Looking at Dependencies

With our rough approach in mind (creating a new component to handle logging), let’s take a look at what this new component is going to need to work as expected. Based on the requirements, the Logger will need to know where to log the information to (the path) and what to log (the message). These necessary pieces of information are known as dependencies because the Logger won’t work if these are not provided.

When it comes to injecting in a dependency, there are three main approaches, each providing benefits and drawbacks. Let’s take a closer look at these approaches.

Constructor Injection

First and foremost, constructor injection makes a lot of sense if the dependency doesn’t change throughout the lifetime of the object. Let’s say that we have an object that knows how to retrieve records from a database. As part of its work, it will need access to a connection string to connect to the database in question.

public class ItemRepository
{
  private readonly string _connectionString;

  // By adding the dependency here, there's no way you that
  // an ItemRepository could be created without specifying
  // a connection string.
  public ItemRepository(string connectionString)
  {
    _connectionString = connectionString;
  }
}

// Example usage
// If the connectionString isn't provided, then
// the code doesn't compile!
var itemRepository = new ItemRepository("Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True")

Generally speaking, an application doesn’t speak to multiple databases, so the odds of the connection string needing to change during the lifetime of this class is slim. Given that, it makes sense to inject this dependency at the constructor level.

One of the benefits of injecting dependencies at the constructor level is that you’re revealing your intent to other developers. In this case, you’re signaling others “Hey, if you’re going to use this class, you need to specify a connection string, otherwise this isn’t going to work!”

The main downside of this approach is that by adding more parameters to the constructor, you may end up with classes that have a ton of dependencies. However, if you find yourself in that situation, you might have another problem going on.

Method Injection

For this approach, method injection makes sense if the dependency can change during the lifetime of the object. Let’s say that we have a component that needs a way to send notifications and that the notification mechanism is based on user preferences. For example, if I’m making a purchase, then I want the order confirmation to be emailed to me whereas other customers might want their receipt texted to them.

public class PurchaseWorkflow
{
  // By adding the NotificationStrategy here,
  // there's no way that a developer can call 
  // the Complete method without specifying 
  // a way to notify the customer.
  public void Complete(Order order, INotificationStrategy strategy)
  {
    string receiptDetails = CreateReceipt(order);
    strategy.SendNotification(order.Customer, receiptDetails);
  }
}

// Example Usage
var order = new Order("Cameron", 22.50);
var workflow = new PurchaseWorkflow();
// If a NotificationStrategy isn't passed in, this code won't compile.
workflow.Complete(order, new TextNotificationStrategy()); 

If we were to use constructor injection, that means that we would need to instantiate a whole new object just because the notification mechanism needed to change which seems a bit excessive for our use case.

Otherwise, method injection has the same main advantage of constructor injection (i.e. revealing design intent) and disadvantage (can lead to a bunch of necessary parameters to pass in)

Property Injection

Unlike the other two approaches, property injection allows us to set the dependency as a property on the object and then start using the object. Unlike the other two approaches where you have to always specify the dependency, this approach allows you to set the dependency, use the class, then switch out the dependency without having to create a new instance (like constructor injection would force you to) and without having to specify it every time a method was called.

However, the main downside is that you as the developer need to remember to set this property otherwise the code will compile, but you’ll run into a runtime exception. It’s because of this limitation that I’ll avoid this injection technique and stick with either constructor or method level injections.

public class PurchaseWorkflow
{
  // By having the notification be set as a property, 
  // developer can set this dependency once and then
  // only change it when needed.
  // However, there's nothing forcing a developer to 
  // set this property.
  public INotificationStrategy NotificationStrategy {get; set;}

  public void Complete(Order order)
  {
    string receiptDetails = CreateReceipt(order);
    NotificationStrategy.SendNotification(order.Customer, receiptDetails);
  }
}

// The Mistake
var order = new Order("Cameron", 22.50);
var workflow = new PurchaseWorkflow();
workflow.Complete(order); // This will cause a NullReferenceException to be thrown

// Proper Usage
var order = new Order("Cameron", 22.50);
var workflow = new PurchaseWorkflow();
// As long as you remember to set this property, things should work!
workflow.NotificationStrategy = new TextNotificationStrategy();
workflow.Complete(order);

Designing the Logger

Now that we’ve looked at the various ways we can inject dependencies, we can start coming up with the rough design for Logger. Looking back at the requirements, we have two dependencies to worry about (the path and the message to log).

Given that the path is not going to change during a run of the emulator, we should leverage constructor injection for this dependency. For the message, since that will be based on the Rover's Location and Orientation which will change during the emulation, we should leverage method injection for this dependency.

With all of this in place, we now have a much clearer idea of how the Logger component is going to look!

Wrapping Up

In this post, we explored the requirements for logging and why the logging logic should be a separate component due it being a cross-cutting concern. From there, we explored at the three main ways to inject dependencies (constructor, method, and property). With this knowledge, we were able to make some smart decisions on how to inject where to log the information and what to log. In the next pose, we’ll build upon this design by creating the Logger component. From there, we were able to make decisions on how to inject the path of where to log and the message to log. In the next post, we’ll start implementing the logic for creating the Logger component.