Practicing YAGNI - Show me the code

Jason McCreary - Jan 29 '18 - - Dev Community

After my Practicing YAGNI post, many asked to see what practicing YAGNI looks like in code.

As a quick recap - YAGNI is a principle of eXtreme Programming. YAGNI is an acronym for You Aren't Gonna Need It.

I think Ron Jeffries, one of the co-founders of eXtreme Programming, summarizes practicing YAGNI well:

Implement things when you actually need them, never when you just foresee that you need them.

Why Practicing YAGNI is hard

Despite the fun acronym and straightforward summary, programmers often succumb to over-engineering, implementing new shiny tech, or adding additional features. Even if we overcome these urges, it's still unclear what practicing YAGNI means in code. After all, we're programmers and code speaks louder than words.

Rasmus Lerdof said to me once, "Show me the code". So I'm going to spend the rest of this article working through a series of stories. By practicing YAGNI, we'll see how the code evolves naturally, not inherently. These code samples are written in PHP to honor Mr. Lerdof.

The First Story

As marketing I want to get a list of all users so I can perform analytics.
Given I access a specific URL
Then I should get a list of all users in JSON format

The first story is the most challenging. This is when our urges are the greatest. Remember, YAGNI tells us to only implement what we actually need. Or, said another way, write the simplest thing possible.

Here is what that code might look like.

<?php
$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$query = 'SELECT * FROM users';
$result = $mysqli->query($query);
$users = $result->fetch_all(MYSQLI_ASSOC);

header('Content-Type: application/json');
echo json_encode(['users' => $users]);
Enter fullscreen mode Exit fullscreen mode

Admittedly this code is not elegant. It'll probably get you down voted on Reddit or StackOverflow. The Pragmatic Programmer discusses writing "Good Enough Software", and that's exactly what this is. At just 7 lines of code it's undeniably simple. Any junior programmer can readily work with the code. Most importantly, it completes the story. Nothing more. Nothing less (well, technically, we could have dumped the users table in JSON format and served a static file).

The Second Story

As legal I don't want the user's password to be in the response so we don't get sued.
Given I access the URL for the user list
Then I should get a list of all users in JSON format
And it should not include their password

So what's the simplest thing we can possibly do? Well we can adjust the SQL query to only select the appropriate fields.

<?php
$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$query = 'SELECT email, role FROM users';
$result = $mysqli->query($query);
$users = $result->fetch_all(MYSQLI_ASSOC);

header('Content-Type: application/json');
echo json_encode(['users' => $users]);
Enter fullscreen mode Exit fullscreen mode

The Third Story

As marketing I want to filter the user list by email address so I can group user data more easily.
Given I access the URL for the user list
And I set an "email" parameter
Then I should get a list of all users in JSON format whose email address contains the "email" parameter value

Again, let's adjust the SQL query based on the email parameter.

<?php
$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$query = 'SELECT email, role FROM users';
if (!empty($_GET['email'])) {
    $query .= 'WHERE email LIKE \'%' . $_GET['email'] . '%\'';
}

$result = $mysqli->query($query);
$users = $result->fetch_all(MYSQLI_ASSOC);

header('Content-Type: application/json');
echo json_encode(['users' => $users]);
Enter fullscreen mode Exit fullscreen mode

Now, while this completes the story, there are a few things we should not continue to ignore.

  • Increased complexity. The if statement introduces a new logical branch of the code.
  • Rate of change. Each of the recent stories has required changing the SQL statement.
  • Security risk. The added code is vulnerable to SQL injections.

These reasons should guide us to refactor our code. A common misconception is YAGNI and refactoring oppose one another. I find their relationship to be more symbiotic. YAGNI keeps my code simple, which makes it easy to refactor. If my code is easy to refactor, then I may quickly rework the code at any time. This gives me confidence to continue writing simple code and perpetuate the cycle.

So, let's refactor to extract the complexity. Being more familiar with the code at this point, a Repository object is an obvious candidate. In addition, we'll also ensure the Repository mitigates our risk of SQL injection.

<?php
require 'vendor/autoload.php';

$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$userRepository = new UserRepository($mysqli);
$users = $userRepository->getUsers($_GET['email']);

header('Content-Type: application/json');
echo json_encode(['users' => $users]);
Enter fullscreen mode Exit fullscreen mode

The Fourth Story

As marketing I want to see a description for "role" so I can better understand the data.
Given I access the URL for the user list
And the user's role is 1 or 2
Then I should see "Member" or "VIP" (respectively) for "role"

There are multiple implementations available, two quickly come to mind. We can implement this in the SQL statement or we could implement this with code. While updating the SQL statement is arguably the simplest, it dramatically increases its complexity. For this reason, I chose to implement this with code.

<?php
require 'vendor/autoload.php';

$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$userRepository = new UserRepository($mysqli);
$raw_users = $userRepository->getUsers($_GET['email']);

$users = [];
foreach ($raw_users as $user) {
    $users = [
        'email' => $user['email'],
        'role' => $user['role'] == 2 ? 'VIP' : 'Member'
    ];
}

header('Content-Type: application/json');
echo json_encode(['users' => $users]);
Enter fullscreen mode Exit fullscreen mode

This code works, however just as before it introduces new code branches and business logic. It also doubles our line count. So, I'll use some of the time I saved writing this simple solution to see if there's a more straightforward option. Indeed there is, PHP has a JsonSerializable interface we can implement to return a value to json_encode().

I can introduce a simple User object which implements JsonSerializable and move the new logic there. I'll update UserRepository to return an array of User objects. The final code is the same as when I started the story, proving a successful refactor.

<?php
require 'vendor/autoload.php';

$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$userRepository = new UserRepository($mysqli);
$users = $userRepository->getUsers($_GET['email']);

header('Content-Type: application/json');
echo json_encode(['users' => $users]);
Enter fullscreen mode Exit fullscreen mode

The Fifth Story

As Finance I need to get the user list in XML format so we can import it into our archaic systems.
Given I access the URL for the user list
And I set my "Accept" header to "application/xml"
Then I should get a list of all users in XML format

You're getting the hang of this now, so let's just add some if statements to vary the response.

<?php
require 'vendor/autoload.php';

$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$userRepository = new UserRepository($mysqli);
$users = $userRepository->getUsers($_GET['email']);

if (strpos($_SERVER['HTTP_ACCEPT'], 'application/xml') !== false) {
    header('Content-Type: application/xml');
    // output the XML...
} else {
    header('Content-Type: application/json');
    echo json_encode(['users' => $users]);
}
Enter fullscreen mode Exit fullscreen mode

We could continue down this path, but we would reintroduce code branches. If you were test driving this code with TDD (another key XP principle) you might have noticed this while adding contexts to test the different code paths. So let's use this opportunity to introduce an object.

<?php
require 'vendor/autoload.php';

$mysqli = new mysqli('localhost', 'dbuser', 'dbpass', 'dbname');

$userRepository = new UserRepository($mysqli);
$users = $userRepository->getUsers($_GET['email']);

ContentNegotiator::sendResponse($user);
Enter fullscreen mode Exit fullscreen mode

In this case a simple content negotiator we pass the user data to. It might even be static as it has no state and simply outputs a response.

Retro

Let's take a look at what practicing YAGNI afforded us:

  • Speed. We completed 5 stories quickly and easily as YAGNI kept us focused.
  • Simplicity. We actually decreased the size of the initial code. We introduced new objects as they were needed, improving their cohesion.
  • Stress-free. Finally, we achieved all this without stress. We didn't get "stuck" worrying about the architecture or scope.

Hopefully you find the same benefits from practicing YAGNI as I have.


Want more? Follow @gonedark on Twitter to get weekly coding tips, resourceful retweets, and other randomness.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player