Steve vs Matt — How two developers approach the same problem

Published on by

Steve vs Matt — How two developers approach the same problem image

It's very common to see two programmers who code the same feature differently. But it's much less common for those two programmers to see eye-to-eye and remain friendly. Thankfully, we have a chance to see how two friends approach the same coding challenge differently and each friend's reflection on the other's code and approach.

Recently Matt shared an opinion on Twitter: "Most interfaces in PHP code are completely useless." One respondent tagged Steve about his love for interfaces, and Steve & Matt decided to write an article about how we each code.

The context

As our topic, we chose Steve's most recent tutorial, Creating a Password Generator. Steve will show his code from the article and explain his reasoning, and then Matt will respond to Steve's coding style; then Matt will take the same spec and write it his way, explain his reasoning, and Steve will give his notes.

We hope this will be an excellent opportunity for conversation and learning!

The spec

In Steve's article, he created a Generator class with two methods: generate and generateSecure. They (optionally, in Laravel) pull from a list of nouns and adjectives., both defined in config. They both generate adjective-noun-adjective-noun type passwords, but generateSecure will replace all a's with 4's, e's with 3's, i's with 1's, and o's with 0's.

How Steve writes it

I think most people can agree that I love Interfaces, sometimes a little too much. One of my main reasons for this is because, like most things - defining a contract makes life easier. The time investment of an interface is nothing compared to running in circles because you need help figuring out what method you should be calling. But enough about that, as I agree that I tend to reach for them all too soon "at times" ...

As read in my other article I won't bore you all with the same walkthrough of the code again, but instead, summarise it into understandable chunks to allow an honest insight from my co-author Matt.

What I start with is an interface for the things I want to be able to list. In the tutorial, I limited this to only 'Adjectives' and 'Nouns'. However, I wanted to add this interface because it would allow me to extend this behavior in applications that provide a fun context. The interface looks like the following:

interface ListContract
{
public function random(): string;
 
public function secure(): string;
}

Now imagine, if you will, that this was being used in a Fishing application (random example I know). You could implement your own FishList or EquipmentList that allows you to generate a fun password like trout-rod-bass-lure, which, as I am sure you would agree, would be much more memorable for a fan of fishing! However, what was lost in the other tutorial, is that this is more aimed at one-time passcodes than production passwords. That, however, is not the point of this article.

Once we have our interface in place, we can build a list class that implements the List Contract itself - building out our first implementation. For this tutorial, I will slightly diverge from the original article and add some fun. We will create a LaravelList to use random words associated with Laravel.

final class LaravelList implements ListContract
{
use HasWords;
}

Using the other parts of the tutorial, I can add a section to my configuration like so:

return [
// nouns and adjectives generated already
'laravel' => [
'taylor',
'james',
'nuno',
'tim',
'jess',
'dries',
'vapor',
'sanctum',
'passport',
'eloquent',
]
];

Using the exact implementation I did in the original tutorial, I will show the service provider how I want to implement this.

final class PackageServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(
abstract: GeneratorContract::class,
concrete: fn (): GeneratorContract => new PasswordGenerator(
nouns: new NounList(
words: (array) config('password-generator.nouns'),
),
adjectives: new LaravelList(
words: (array) config('password-generator.laravel'),
),
),
);
}
 
// boot method would be below.
}

I am completely undecided on the generating of the passcodes/password through - I think there are some improvements to be made instead of my initial solution of:

trait HasWords
{
/**
* @param array $words
*/
public function __construct(
private readonly array $words,
) {
}
 
public function random(): string
{
return $this->words[array_rand($this->words)];
}
 
public function secure(): string
{
$word = $this->random();
 
$asArray = str_split($word);
 
$secureArray = array_map(
callback: fn (string $item): string => $this->convertToNumerical($item),
array: $asArray,
);
 
return implode('', $secureArray);
}
 
public function convertToNumerical(string $item): string
{
return match ($item) {
'a' => '4',
'e' => '3',
'i' => '1',
'o' => '0',
default => $item,
};
}
}

I am definitely sure about using a match statement (you'll see below that Matt used str_replace instead), as it is very extendable and understandable from a glance. Using this, I can directly understand what the output of each potential letter could be - and extending this is just a step of adding another case. Reducing the number of default returns as you grow means that you understand exactly what should come back from this method. Yes, this is something that you could consider as over-engineering slightly. However, as I implemented a contract that says all I have to do is implement random and secure it leaves me a lot of freedom to decide on my class. In particular, I could improve this - without it having a knock-on effect on the rest of the application or anything already integrated.

Finally, onto the generator class itself, it is something I designed so that I could use Laravel container to bind the instance - allowing you to override parts of it in userland should your use case differ from mine. This is precisely what the container is designed for. I can resolve the specific instance from the container or use a facade to interact with the implementation statically. I pass a variadic amount of 'parts' into the build method so that if you want to generate something longer or change the order, the class or contract itself doesn't care. All it cares about is that it will return a string.

final class PasswordGenerator implements GeneratorContract
{
public function __construct(
private readonly ListContract $nouns,
private readonly ListContract $adjectives,
) {
}
 
public function generate(): string
{
return $this->build(
$this->nouns->random(),
$this->adjectives->random(),
$this->nouns->random(),
$this->adjectives->random(),
);
}
 
public function generateSecure(): string
{
return $this->build(
$this->nouns->secure(),
$this->adjectives->secure(),
$this->nouns->secure(),
$this->adjectives->secure(),
);
}
 
private function build(string ...$parts): string
{
return implode('-', $parts);
}
}

Overall this solution allows for a lot of extension and customization regarding userland or quick adaptations should it be needed. It is erring on the side of over-engineering, though!

Matt's code review of Steve's code

I think you did a lot of the review yourself, my friend! 😆

Steve's caveats here bring a lot of my thoughts already: a lot of usages of interfaces and traits over-engineer for the possible future, but that ends up with us creating a more complicated system that's less capable of adapting if the future becomes something other than what we are imagining might happen.

I think the match statement instead of the str_replace you'll see me use in my example is the most enjoyable little bit of difference. str_replace is clearer upfront; more people are familiar with it, and it requires fewer lines of code and doesn't need an extra array_map. However, as Steve pointed out, the syntax cleanly shows the initial and replacement letters on the same lines, and that's definitely nice. Either way, it's fun to see all the different ways we can use match.

I also have one note about the NounList and LaravelList classes: what if, instead of type hinting an interface, we instead had a single class named WordList that took a list of words into its constructor? That way, we could still get the random() method abstracted away, but we're avoiding a tiny interface and a new class for every list of words we might want to use in the future.

How Matt writes it

Let's talk about the spec and where my brain goes with how I would build this, step by step.

The general API

The spec for this project is to create a class, Generator, with two methods: generate and generateSecure. That's our entire public API. I'll name it PasswordGenerator just to be a bit more clear.

In my brain, this is really one method and one decoration of that method. I imagine we'll run the output of generate() through some sort of securing method. So I imagine this being our general class structure:

class PasswordGenerator
{
public function generate(): string
{
// generate a password
}
 
public function generateSecure(): string
{
return $this->makeStringSecure($this->generate());
}
 
public function makeStringSecure(string $string): string
{
// replace some characters in the password with numbers
}
}

Making a string secure

Since makeStringSecure is the simpler of the two methods we haven't implemented, let's build it out. Effectively, we're replacing instances of a few vowels (a, e, i, and o) with numbers (4, 3, 1, 0) that look similar. str_replace to the rescue!

class PasswordGenerator
{
public function generate(): string
{
// generate a password
}
 
public function generateSecure(): string
{
return $this->makeStringSecure($this->generate());
}
 
public function makeStringSecure(string $string): string
{
return str_replace(
['a', 'e', 'i', 'o'],
['4', '3', '1', '0'],
$string
);
}
}

Once I finished writing this method, I realized I wanted to quickly check to make sure I wrote it right. "Wouldn't it be nice," I thought to myself, "to have a test to check this method instead of having to test it manually every time?" So, let's spin up a really quick test to prove that makeStringSecure method works the way we want.

class PasswordGeneratorTest extends TestCase
{
/** @test */
public function it_converts_some_vowels_to_numbers()
{
$generator = new PasswordGenerator();
 
$this->assertEquals(
'fly1ng-f1sh-sw1mm1ng-l1z4rd',
$generator->makeStringSecure('flying-fish-swimming-lizard')
);
}
}

With this test, I'm confident I handled the example Steve gave in his article. However, this particular string doesn't capture the letters "e" or "o", so I'm going to switch it up a little to make the test a bit more robust:

class PasswordGeneratorTest extends TestCase
{
/** @test */
public function it_converts_some_vowels_to_numbers()
{
$generator = new PasswordGenerator();
 
$this->assertEquals(
'fly1ng-g04t-3l0p1ng-l1z4rd',
$generator->makeStringSecure('flying-goat-eloping-lizard')
);
}
}

Generating the passwords

Finally, we need to build out the method for generate(). What's the spec?

Bringing in the word lists

Passwords will be created by joining words from two lists, which we want to pass into the constructor and (in Laravel apps) store in the config as password-generator.nouns and password-generator.adjectives.

So, first, let's provide the password generator with the list of nouns and adjectives and then build the generate() method. First, the lists:

class PasswordGenerator
{
public function __construct(
public readonly array $adjectives,
public readonly array $nouns
) {
}

We can pass the nouns and adjectives in every time we instantiate a generator:

$generator = new PasswordGenerator(
config('password-generator.adjectives'),
config('password-generator.nouns')
);

Or, if we're working with Laravel, we can bind it to the service provider:

class AppServiceProvider()
{
public function register(): void
{
$this->app->singleton(
PasswordGenerator::class,
fn () => new PasswordGenerator(
config('password-generator.adjectives'),
config('password-generator.nouns')
)
);
}
}

If we've bound it to the service provider, we can then pull an instance out of the Laravel container without having to explicitly pass it in our word lists. We can type-hint the class in any of a number of different places (route definitions, constructors of commands, etc.) or pull it ourselves:

$generator = app(PasswordGenerator::class);

Generating a password from the word lists

Now that we have access to the lists of nouns and adjectives as properties on our instance, let's build the generate() method to create a password.

The spec states that passwords should be adjective-noun-adjective-noun. That means we need to be able to get a random adjective and a random noun easily, and then join them.

class PasswordGenerator
{
public function generate(): string
{
// adjective-noun-adjective-noun
return implode('-', [
$this->adjectives[array_rand($this->adjectives)],
$this->nouns[array_rand($this->nouns)],
$this->adjectives[array_rand($this->adjectives)],
$this->nouns[array_rand($this->nouns)],
]);
}

If we knew we planned to use this tooling later, perhaps to generate passwords with a different structure, it may be worth creating a method for pulling a random adjective and one for pulling a random noun. We could also consider storing the "structure" of the password somewhere. But the spec doesn't call for that, so YAGNI. We build what we need when we need it.

Final output

Here's what our final product looks like:

class PasswordGenerator
{
public function __construct(
public readonly array $adjectives,
public readonly array $nouns
) {
}
 
public function generate(): string
{
// adjective-noun-adjective-noun
return implode('-', [
$this->adjectives[array_rand($this->adjectives)],
$this->nouns[array_rand($this->nouns)],
$this->adjectives[array_rand($this->adjectives)],
$this->nouns[array_rand($this->nouns)],
]);
}
 
public function generateSecure(): string
{
return $this->makeStringSecure($this->generate());
}
 
public function makeStringSecure(string $string): string
{
return str_replace(
['a', 'e', 'i', 'o'],
['4', '3', '1', '0'],
$string
);
}
}

And here's an updated test, modified to pass in blank word lists. We could write some more tests for how it pulls words from the lists, but for now, this is updated to not error.

class PasswordGeneratorTest extends TestCase
{
/** @test */
public function it_converts_some_vowels_to_numbers()
{
$generator = new PasswordGenerator(adjectives: [], nouns: []);
 
$this->assertEquals(
'fly1ng-g04t-3l0p1ng-l1z4rd',
$generator->makeStringSecure('flying-goat-eloping-lizard')
);
}
}

Why do I write this way

When I program, I aim to write flexible, comprehensible, changeable, and deletable code. We can't predict the future, but we can write code that is easy to change in the future, so I opt for simple, clear, concise code.

In my way of thinking, you can always add tools or structures later, but it's much harder to remove them later. I adhere very strongly to YAGNI ("you aren't gonna need it") ideals, and if you're curious to learn more about my thoughts here, check out my Laracon Online talk Abstracting Too Early.

I build interfaces when I see a concrete, tangible need for them. I build abstractions when I see a pain point that's best solved by that abstraction and no earlier.

Steve's code review

I absolutely love Matts's idea of the 'generateSecure' simply being a decorator - and this, for my implementation, would make a great focal point on a refactor.

String replace for me, while it is more straightforward - if I run any code styling could lose the purpose of the simplification. When they are aligned above each other, it makes sense and is easy to read. However, as this list grows, it takes more work to manage. As I said - code-style formatters could easily destroy this ease of reading, whereas this is not likely to happen on a match statement.

Passing in an array into the constructor for the password generator to control the adjectives and nouns offers flexibility that my solution definitely does not have. You have to work harder to achieve what I did, but for a relatively simple piece of functionality like this - it is possibly time spent on over-engineering where an array would have sufficed.

The benefits of this more straightforward approach become clear as you look at the generate method on the password generator. As the list grows, this will undoubtedly be simpler and easier to manage with less memory usage. It does lose a little in context should you extend, but that is a small price to pay for the extension.

Overall, Matt made evident improvements to my approach that aim at simplicity over extension - which is the desired approach for a feature like this.

Conclusion

Thanks so much for checking this out! We hope seeing two programmers approach the same problem with different priorities and mindsets , but respect for each other can lead to a super enjoyable conversation where we all learn!

Steve McDougall photo

Technical writer at Laravel News, Developer Advocate at Treblle. API specialist, veteran PHP/Laravel engineer. YouTube livestreamer.

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article