loading...
Cover image for Practicing YAGNI - Show me the code

Practicing YAGNI - Show me the code

gonedark profile image Jason McCreary ・6 min read

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]);

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]);

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]);

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]);

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]);

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]);

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]);
}

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);

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.

Discussion

pic
Editor guide
Collapse
dserodio profile image
Daniel Serodio

I'm familiar with the "As a $ROLE, I want $FEATURE so that $BUSINESS_BENEFIT" user story template, but I'd never seen it combined with "Given ... And ... Then". Do you have any useful links about this use story template? Thanks.

Collapse
leojpod profile image
leojpod

You might be interested in checking out the Cucumber project (cucumber.io/).
It has implementation in many languages (I personally use cucumber-js) and it lets you test your app based on these user stories.

in short: you keep a folder where all your stories (cucumber call that features) are described in these terms (Given ... When ... And ... Then ...) then another folder keeps all the implementations of these steps (e.g. Given that user goes to /home) and it runs tests based on that. It's pretty neat to use.

Collapse
dserodio profile image
Daniel Serodio

Nevermind, I found it myself :) "What's in a Story", by Dan North: dannorth.net/whats-in-a-story/

Collapse
gonedark profile image
Jason McCreary Author

Awesome. I didn't know the exact source either. Just always wrote them that way.

Collapse
leojpod profile image
leojpod

Hi Jason!
That was a nice read.

How do you approach YAGNI with framework? I can see how I would do it with Elm and to some extend I try to approach it that way. But what about the build system or the introduction of a framework? Do you start with it right away or wait until you feel the need for it?

Maybe it's a dump question and YAGNI isn't incompatible with a framework, you just start going with YAGNI once the framework installation is completed.

Collapse
gonedark profile image
Jason McCreary Author

YAGNI is really framework agnostic. One might argue you could call YAGNI on the use of a framework. However, I try not to call YAGNI on tooling - common tech among a team. That’s not to say their use should never be challenged, but YAGNI shouldn’t be the only motivation.

So, yes, YAGNI is something you practice regardless of tooling. In the case of a framework, you could use YAGNI as a motivation to stick closely with the conventions of the framework and not try to customize it too much.

Collapse
leojpod profile image
leojpod

YAGNI must work well with something like Sails.js then.
Thanks!

Collapse
doug7410 profile image
Doug Steinberg

I saw your talk this weekend at Sunshine PHP. I really loved the examples and the humor. You did an excellent job at getting the message across. Thanks for posting this. I'm sharing with all my coworkers.

Collapse
onkarjanwa profile image
Onkar Janwa

Hi Jason,
That is a nice post.

It looks like most of the mvc frameworks already follow YAGNI, implement logic in models, send response through helpers and put all series of actions in controller.

Collapse
gonedark profile image
Jason McCreary Author

Thanks.

What you describe is more design and architecture. YAGNI may have lead to these decisions. But where things go is not focus of YAGNI. YAGNI is about implementing things when you actually need them, never when you just foresee that you need them.

Collapse
webwizo profile image
Asif Iqbal

Quite interesting!
I'm gonna try to find more about YAGNI which I have heard first time.

Thanks mate.

Collapse
gonedark profile image
Jason McCreary Author

Thanks. I would recommend watching my talk to give you more of the motivation behind YAGNI.