As a team lead, I often find myself, during code reviews, telling the people on my team to "follow convention" when reviewing their PRs. This tends to evoke groans and epithets (many hushed, some audible). What are these "unwritten rules" that I am asking (nay, insisting? demanding?) that they follow?
In some sense, what this looks like for me personally, is that if someone is looking at a project written by some person and myself, they shouldn't be able to tell which parts I wrote and which parts they wrote. What we both write ought to blend in such that everything is seen as a cohesive whole rather than individual parts mixed together.
So this post, for me, is an attempt to break down what it means to "follow convention" for myself, my team, and for you, dear reader. Because following convention is more than simply running a linter and exercising proper code formatting. It's about following the leader, being a good team mate, and focusing on what matters by playing by the rules.
I'm using PHP & TypeScript on a daily basis, so these examples are centered around these languages. The principles should be universally applicable (I think). Let's go.
Convention, like an ogre, has layers
Here's the way I see it: convention is contextual. From personal experience, convention has (approximately) 3 layers from the most granular to the least:
- Local
- Framework
- Language
This list isn't authoritative. There might be 4 layers in some projects. In others, there may be only 2. This post is written considering Angular and Laravel, a makes a case for 3 layers.
The list above is from the outer-most layer to the inner-most, but in order to explain, let's look at them from the bottom up.
Language convention
Arguably this widest and foundational layer. Basically, this convention says:
use the language's features (core library and syntax) rather than some other way to accomplish the task at hand
TypeScript, for a long time, had the optional-chaining operator. It wasn't natively supported in JS until ES2020 (side note: I didn't know this was a thing until I wrote this.) If it wasn't in natively part of JS, and you are using TypeScript, then you should use TS's optional chaining (?.
) because it saves you time and is easy to read.
Remember, before it was in ES2020, optional-chaining was this in TypeScript:
console.log(maybe?.it?.exists?.here)
And this in Javascript:
var _a, _b;
// You could think of it in three ways:
//
// - A location to learn TypeScript where nothing can break
// - A place to experiment with TypeScript syntax, and share the URLs with others
// - A sandbox to experiment with different compiler features of TypeScript
console.log((_b = (_a = maybe === null || maybe === void 0 ? void 0 : maybe.it) === null || _a === void 0 ? void 0 : _a.exists) === null || _b === void 0 ? void 0 : _b.here);
PHP 8 introduced a number of str_*
functions. Some of these (like str_starts_with()
) were so needed that the framework themselves built their own and were eventually "pushed down" to the language. So you could still do this in PHP 8.0:
if (strpos($haystack, $needle) === 0) {
...
}
But you shouldn't. You should use:
if (str_starts_with($haystack, $needle)) {
...
}
Because that's what the language defined method for determining if a string starts with another string.
Framework convention
Borrowing from above, Framework convention says:
use the Framework's libraries (core library and syntax) rather than some other way and follow the framework's style and approach rather than your own to solve problems
Good frameworks are support structures and are designed to make you productive, safe, and able to build stuff that works. Like in the Language convention, Framework convention adds opinion to things. It also provides context. Some languages like PHP are "just backend web languages". But some languages like TypeScript or C# can be used anywhere.
So when you are in a Laravel application, don't write your own router. Use Laravel's. Don't use Symfony's DI. Use Laravel's.
But if you're working on a Drupal or a Symfony project, you'll want to use the routers, DI, and module/package systems that those frameworks provide.
Likewise, if a framework makes a preference for a particular decision, you should embrace that decision. For example, you may love Tailwind. I don't blame you. But if you're working on an Ionic project, you don't want to import and use Tailwind. You've made the decision to use Ionic. You'll want to use Ionic's utility and support classes for styling. Need more? Extend it!
Frameworks make certain decisions for you for a reason. You don't have to like it, but you should absolutely follow those conventions!
Local convention
In larger applications, it's not uncommon to work on modules or sub-projects within the larger project. Sometimes, those parts of the project which are older and potentially follow a slightly different convention. Think about stuff that was maybe written 4 or 5 years ago and hasn't been touched. In those cases:
use the local code (or module) context's libraries, and approaches rather than some other way and follow the local (or module) style and approach rather than your own
If you find a bug in a piece of OSS software, you'll want the PR to conform to the contribution standards and guidelines. The same goes for stuff you write in a large code base. If you tried to add a second Markdown parser to a project that already had one because you happened to have a preference for it, you'll get your PR (rightfully) rejected.
Maybe a module you're working on supports PHP 5.6 for some unholy reason. Your work will need to reflect that reality (though, maybe you should consider finding an alternative, if one exists?).
There are exceptions to this. If the thing you are writing is broken, or is blocking a language or framework upgrade, then do what you have to make a decision. You probably shouldn't rewrite an entire 3-year old controller to add a single feature to it. But if you're writing tests and finding bugs and you find yourself doing a bunch of that work already—talk to a team member. Maybe changing the local convention to match the project convention is warranted.
Convention hierarchy
That's the model. Bottom to top. In general, the underlying convention will win. This is true when you write new features. It's true when you upgrade or update functionality.
Can there be times when a higher convention overrides something below it? I imagine there can be. But there'd need to be a solid reason for it. And those exceptions should be treated as such. In an ideal situation, you'd not so much override a foundational convention as improve upon it.
Note that much of this is about how to write code, but there is a separate piece of all this dealing with things that are more architectural or stylistic... which I'll create in a follow-up to this piece. More on that later.
By way of contrived example, consider a situation where, in a PHP/Laravel application, you need to parse a CSV:
Within the project, you need to take some input and generate an array.
- The local piece of code doesn't seem to use a library.
- Laravel itself doesn't have a preferred way of handling CSVs.
- PHP has
str_getcsv
.
Minimally, this rules out trying to use some explode()
or preg_match()
to do this. You could use str_getcsv
. Or... you could go check out a library that solves this for you: like league/csv
.
Now, let's say you get tasked a few weeks later and you need to update a different Laravel project and you need to implement similar functionality. But let's say that this project uses laravel-excel
.
By convention, you'd want to learn and use that library, since it's what's there.
Exceptions
There are always exceptions. Doing refactors and going back and adding tests may constitute a time to do partial or one-off rewrites of some things.
Regardless of the scope and effort required, the goal should always be to aim for uniformity of convention. If the code base is so large that there isn't a standard uniformity, then maybe you find other problems to solve.
But if the business needs a new feature and—for sake of argument—that feature is already partially done in a partially-implemented JSON:API endpoint, perhaps the better solution is to argue, for the sake of convention, that the new functionality will require the use of the updated API and you can (finally) deprecate the old API.
Next steps
Convention is one part of this. While convention tends to mean preferring some ways of doing things over others, standards or "best practices" would cover architectural or more overarching topics like listing and CI. Practices tend to live at the Framework level and up. For example, code formatting tends to be framework-dependent. This is totally OK! That's why conceptually it seems to belong in a separate category.
Feedback is welcome!
Top comments (0)