Full transcript of “The 8 Golden Rules of Writing Clean Functions - Clean Code In Kotlin”. Click a timestamp to open the video at that moment. Need another video? Use the YouTube transcript generator.
0:00 Hey guys, and welcome back to a new video. In this video, I really have eight golden rules or eight evergreen principles that really make your Kotlin functions clean. And they are really evergreen, not not specifically constrained to Kotlin. You can also just as well stick to these principles in other languages. I will use Kotlin as an example here because my whole channel is about Kotlin. But if you care about clean code, then watch this video. First principle about writing clean functions is about the single responsibility principle. That comes from the set of five solid principles. It's all about the S, single responsibility, and it just says that functions should have a single responsibility. This also applies to classes, but here we focus on functions. And in other words, single responsibility means a single reason to change. So, if we take a look at this registration service bad here, for
0:45 example, and this register user function, it's not very difficult. You don't have to be a very good developer to tell that it's very hard to understand this function when just giving it a quick look without really diving further into it what really happens here. But you can't just stare at this function for 5 to 10 seconds and tell me exactly what registering a user in this case means. And that's mainly because it violates the single responsibility principle because it has multiple responsibilities. It has multiple reasons to change. One reason to change could be that some kind of validation actually is different. So, if we, for example, say that valid passwords from now have to contain at least 13 characters rather than 12, then that is a reason to change this register user function, even though what makes a valid password is not strictly bound to
1:33 registering a user. This could also be something that is needed in some kind of different context, for example, just changing the user's password. But still, it would be one thing, one reason to change this function. If we then scroll a little bit further here, you can also see that we have some kind of hashing algorithm. So, that we actually take the password, we send it through a one-way function, that is what we call a hash, and that's typically how passwords get stored in databases, so we don't have to store them in plain text. But it's again, it's our responsibility. Hashing itself has nothing to do with registering a user. It's maybe part of registering a user, but it's nothing that has to be in the register user function directly. And then of course, we have the actual user registration where we create a new user object and we make sure we save it in our database. And then finally, you can also see that
2:19 we also really send an email here via our email client to just welcome the user. So there's just way, way, way too many things happening in this function, which overall just increase the cognitive load. So cognitive load just means the amount of mind processing power you in the end need in order to understand what something does in the code. And it's not just about the cognitive load, but it's also that with this kind of design, your code gets much less flexible. Because what happens if you for example have to validate an email address somewhere else in the code, not just when registering a user, but maybe I don't know, when inviting someone to your event and you invite them by email, you enter the email and you also need to validate whether that is a valid email. So this kind of email validation code here would also be needed elsewhere in the project, but because it's here, you can't easily
3:05 reuse it. So this makes it tempting to actually duplicate that logic and then you have the same logic twice and changing it would also then require two changes. So it becomes a pain in the ass to maintain code like this. Much better is to stick to the single responsibility principle and have a register user function like this. It's super intuitive what this actually does. You can see, okay, checks whether the email is valid, checks whether the password is valid, then hashes the password, saves the user and then sends the welcome email. And all these parts that are of course still part of registering a user, they have been outsourced into their own functions. So the moment we have separate functions, we can of course also call them in different contexts. So it's no surprise that this is much, much more readable code. Clean code principle about functions number two, and that is
3:53 avoid side effects. A side effect in the programming world is something that reaches outside the function itself. So, here here we have a pricing service, which has a function apply discount. So, if we have some kind of order, we can apply some kind of coupon code to that order. It calculates the discount, then marks this coupon as redeemed, and then also tracks this coupon redeemed analytics event, for example. But, these two function calls here are what we would consider a side effect, because it reaches outside this apply discount function. It actually has real, possibly persistent effects on some kind of other place of the project. Like in this case, our database. It will fire an analytics event without that being clear, and that is the main problem. When applying a discount, and we actually just get back the the kind
4:39 of new total after having applied the discount, that alone does not imply at all that the discount will also be marked as redeemed, and that it will also track an analytics event. But, given the fact that these things still happen, makes this so dangerous. And this is especially something that beginners like to do, because they maybe have some kind of code here to stay at this example, where they have to apply discount, and they also have to mark it as redeemed. And then the intuitive thought is to simply put this mark as redeemed function here inside the apply discount function, because it will work in their particular use case. But, it will only work as long as that is the only use case of apply discount. If at some other point, you maybe just want to calculate a discount, and you say, "Hey, I want to just apply this discount to this order yet, but I don't I don't want to redeem it yet, or even send an analytics event." Then it's very
5:25 easy to still call this function, because well, it just applies a discount, that's what it communicates, but then it actually still redeems it. It still tracks the analytics event under the hood, without you noticing that from the call side when just reading the function name. So, these kinds of side effects, where you save something in a database, where you share some kind of data with an external service, like analytics, where you just change a global field, a global variable in some way, that is very dangerous. And of course, in some cases you have to do that, and these side effects would be desirable. But, I would only do that if it's actually clear that this function comes with that side effect, and that the side effect is kind of the main effect of that function in this case. So, the much better approach here in this case would be we have a pricing service, we say, "Okay, we we just have
6:12 a function to calculate the total amount." This is what would in the end just apply the coupon here to an order in case the coupon exists. And then we have an order service that really places this order. Because the moment we place an order, it's obvious that we will also want to redeem the coupon if there is a coupon. We also know that we want to save this order, and that we also need to calculate the total amount. But, we do not have any side effects anymore of a flow that is completely unrelated to really placing an order. Principle number three is dry. Do not repeat yourself. That's something we've touched a little bit, but in the end here, let's take a look at this receipt printer bar. We have a bunch of format functions to format an order line, an invoice footer, a refund message. And all of these functions rely on the same kind of number format instance.
6:58 They all format in the same identical way with a maximum fraction digits of two. And this is not ideal because this piece of code here is just repeated multiple times. And the dry principle says, "Do not repeat yourself. Do not have the same code twice in your project." Because if we improve this and we actually take this format price function and we put it in a function, so we just have this code once, and we can just call this format price function in all these three different cases, that will be much more maintainable code. Because obviously, on the one end you have a less code since you only need to define this code here once and not three times in this example. If you have changes, then they only need to be done at this single place and not at three places, so you also don't have to remember making these changes at all these different places. you only need to test this logic once. That's also a big
7:45 thing here. Because if you have some kind of automated test cases, then you don't want to write the same kinds of test cases for multiple functions that in the end contain the same kind of logic. Principle number four is minimize the number of parameters. If we take a look at this invoice service bad and the create invoice function, that is a pain to read. This is such a long function signature, so many parameters. And in the end, those parameters are needed in some way in order to create an invoice, of course, but stuffing them all as single function parameters really again increases the cognitive load. So again, how much your mind has to process when reading a function in order to really understand what's going on. And the higher this cognitive load, the higher the amount of thinking power you need to spend in
8:31 order to understand something, the faster your mind will also be drained, and that will of course increase the chance of you making some kind of mistakes in the future because your mind is simply exhausted. So if you pass parameters to a function, only pass very distinct parameters to a function that are not related in some way. If they are related, you would rather want to bundle them in a data class in Kotlin. So something like uh here, where we have this create invoice function. See how clean this now is? Where we just see, okay, we have a customer, and a customer obviously contains name, email, address. Uh we have an address, which bundles street, city, zip code, and country. We have money, which consists of amount and currency. We have an invoice that has a number, a customer, money, and so on.
9:16 And then, that way, oops, here, uh we just need to pass these three objects into this function to make it very clear what creating a valid invoice actually need. And then we can really go through it step by step at the call side where we call this function to create a customer, then we will see, ah, okay, this is what a customer needs. Ah, this is what an address needs, and so on. This is a much nicer way to actually pass data to a function than stuffing all these single primitive parameters into the function itself. So, this is obviously much, much cleaner code. Principle number five is fail fast. And that's an easy one to understand, but still something that many developers do wrong. If you have a function like importing transactions, and that function actually has different cases where it could fail, where it could throw an exception here, for
10:02 example, then throw these exceptions as early as you can. So, at the moment where you know throwing this this exception is inevitable, then that should be thrown. Because obviously otherwise all the prior code you would execute for absolutely no reason at all. You want to know when the function fails as soon as possible. So, if you have something like this, uh then make sure that you throw these exceptions directly at the begin- beginning of the function, or at least the moment you know you have to throw this exception. Principle number six is early returns. So, this is still something I see very, very often that people have functions like this, sign up. Obviously, signing up means we have to validate someone's email, we have to maybe check whether the account exists in the first place, we have to validate
10:48 the password here. Very simple ways, of course. And then we would create an account and return a successful result. And in all these other cases here, in case the password does not match, the password is too short, the email may be in use, the email may be invalid. But if we look at this, it it kind of looks ugly. Because there are so many indentations, so many conditions, you don't even clearly see which else belongs to which if. And that's exactly why you should stick to early returns. So, if there are certain criteria in your function, certain paths where you would return a value, so like an an error result here, a successful result, then also enter these paths as early as possible. So, what this means is we want to structure our code like this instead. We want to first of all check if the email is not valid, then we want to return it. If
11:35 there is a user, then we want to return that. If the password is too short, then we want to return that. Because this way we of course get rid of all these indentations, we completely get rid of the else cases, and it's much much easier to actually read and understand the function in which cases it would actually return what kind of error. Because if we did not return in any of these cases, then we know, okay, from this point on everything must be successful. Principle number seven is have the right level of detail that just fits to the class the function is part of. So, let's take a look at this checkout service bad. And obviously, yes, single responsibility is violated here. We instantly see this. There are way too many things happening. But this is not about single responsibility at this example. It's about the level of detail of this function not fitting to the purpose. So, this is a checkout
12:22 service. It's something where a user really places an order, where a payment maybe gets made. But if we take a look at the function code, it's very very low-level code. So, this is for example code that would validate a credit card number. Then here some kind of total price gets calculated, and then the order will actually be placed. And instead what you need to do is you need to adjust the level of detail to the current thing you're doing. So, to checking out in this case. And since checking out itself is a very very high-level flow, that's even something a non-technical user would understand what happens during a checkout. That means the function itself also needs to solely consist of high-level actions, or at least as slightly lower-level actions as checking out itself. So, what I mean is if we take a look at this place order function, this is exactly what I mean.
13:07 Placing an order should in the end just call other functions that are part of placing an order. So, validating the card for example, this is part of it. But you don't want to include the actual code that validates a card in the place order function itself. Calculating the total may be part of that. Submitting this to our API would of course be part of it. So, it's just one level of detail below placing an order and that is how the function itself should look like. And the further we actually dig down into functions that are part of these higher level flows and then a little bit of less higher level flows like validating a cart here, the further we dig down, the more low level the code also gets. The more, I don't know, variables you have like counter variables, the more if conditions, the more plain functions like filtering something and the more stuff looks just
13:55 like this here, where you don't actually call some real ready-made functions of other parts of the code, but you really just execute a raw logic. So, this loom checker here that actually validates the credit card number, that would actually be something a normal non-technical user would have no idea about. So, this should definitely not be part of such a high-level flow as checking out. And finally, principle number eight and that is especially important in Kotlin, when working with extension functions, create extension functions when it makes sense. Because the purpose of an extension function is to show that a certain parameter of this function is actually the owner of this function. So, something like this would actually not be that ideal. So, we have a function that actually just has the purpose to append a row to a CSV file.
14:41 We obviously attach the file and the actual values we want to attach, so the CSV row. But in this case, this file here is actually the owner because the owner's also where something gets changed because a row simply gets added to this file. And the intuitive well-designed approach here would be to make this an extension function of file instead of just passing this file as a parameter. So, something like this, file append CSV rows and then we really just pass our values and this would be a much more intuitive approach to call this function because we maybe work with a file and we want to do something with that file and that that file is the central object of calling this function, of achieving a result with. On the other hand, making this an extension function of a list of strings, for example, and
15:26 then saying, "Okay, we pass the file instead." That would obviously be ugly design because this list of strings is not the owner. This is not something where we apply a change to. That is the object we apply a change with. So, be careful when building extension functions. Don't use them blindly just because they exist in Kotlin and you've heard somewhere that it's very useful. Use them when the actual object or the the the class instance that is extended is really also the owner of that function and not in other cases. If you're actually an Android developer or Kotlin multiplatform developer, so some kind of mobile developer using Kotlin, and you're watching this, and you just say, "I want to write cleaner code. I want to build really, really stable apps." Then take a look at this video's description because we have the senior app development mentoring where we in the end help you to really technically get to a senior developer level as an
16:13 Android or Kotlin multiplatform developer, and then also really help you speed up that output, that senior-level output, with the help of AI and clear workflows. But, that comes after really having gained a technical foundation, being really confident about your skills, and also obviously being able to correct the agent when it's wrong. Take a look at that program. It really delivers insane results for our customers. And other than that, thanks so much for watching this video. I will see you back in the next one. Have an amazing rest of your week. Bye-bye. >> [music]