Stop Building Ugly APIs: Use the Fluent Interface Pattern

Stop Building Ugly APIs: Use the Fluent Interface Pattern

ArjanCodes 36 799 просмотров 1 276 лайков

Machine-readable: Markdown · JSON API · Site index

Поделиться Telegram VK Бот
Транскрипт Скачать .md
Анализ с AI
Описание видео
💡 Learn how to design great software in 7 steps: https://arjan.codes/designguide. In this video, I take a small but realistic Python animation API and refactor it step by step into a fluent interface that reads like a story instead of a configuration file. You’ll see a clear “before” version with clunky lists and hard-to-read setup, and then watch how the same behavior becomes more expressive and maintainable by introducing method chaining and domain-specific fluent methods. Along the way, I explain what the fluent interface pattern actually is, how it differs from the builder pattern, where you already use it in Python without realizing it, and when you should not use it. 🔥 GitHub Repository: https://git.arjan.codes/2026/fluent. 🎓 ArjanCodes Courses: https://www.arjancodes.com/courses. 💬 Join my Discord server: https://discord.arjan.codes. ⌨️ Keyboard I’m using: https://amzn.to/49YM97v. 🔖 Chapters: 0:00 Intro 0:58 What Is a Fluent Interface? (Quick Definition) 1:43 Real-Life Fluent Interfaces in Python (You Already Use Them) 2:13 The Domain: A Tiny Animation Engine 6:17 Before Version: Non-Fluent Animation Definition 7:30 The Engine: How play_scene Works 8:17 Refactor Step 1: Add an add() Method 9:47 Refactor Step 2: Turn add() into a Fluent API 11:20 Refactor Step 3: Domain-Specific Fluent Methods 14:49 When to Use Fluent Interfaces 15:18 When NOT to Use Fluent Interfaces 16:34 Final Thoughts #arjancodes #softwaredesign #python

Оглавление (12 сегментов)

Intro

Let me show you two ways to describe the same animation. So, here's the first version. So, this animation consists of a number of steps. We rotate, we move, we scale, we fade, we move again, and we fade again. And then there's durations for each of these steps. Second version looks like this. So, it's the same behavior, but it has a completely different feel. And this second one is using a pattern called fluid interface. In this video, we're going to start with a small animation engine and make it cleaner step by step and refactor it into a fluent API that feels like you're telling a story in Python. Now, if you want to learn more about how to design pieces of from scratch, I have a free guide for you. You can get this at iron. gold/designguide. This contains the seven steps I take when I design new software. Hopefully, it helps you avoid some of the mistakes I made in the past. iron. gold/designguide, the link is also in the video description. Now, what is a

What Is a Fluent Interface? (Quick Definition)

fluid interface? A fluid interface is a style where methods return self, so you can chain them. For example, in Python that means you could type something like this. Et cetera, et cetera. Now, of course, it's not a valid code. I'm simply typing it here, but that's the idea. And the main goal for this type of interface is that it's more readable and that it's more like a flow. So, it's not really about performance or having a clever design or something like that. You typically use this interface to write code that describes a sequence of things to do in an order that they should happen. And even if you've never

Real-Life Fluent Interfaces in Python (You Already Use Them)

heard of the name fluid interface, this is probably a pattern that you've seen before. If you use Pandas, for example, it uses the fluent interface on data frames. Or in ORMs like SQLAlchemy, you use it to construct SQL queries. And finally, pipeline tools are also a really good candidate for fluid interfaces. And these APIs, they feel comfortable because they match how we think about transformations. And that's

The Domain: A Tiny Animation Engine

why I picked animation as a domain to show you how this pattern actually works. So, the way I set this up is that this is a simple example application that animates shapes on a Tkinter canvas. So, this is my main file. So, the first step is some basic Tk setup like creating a canvas. Uh then I create a graphics renderer. That's something that I built myself. I include in the code example. Then I define an animation for a square. I create a shape of that square where I pass the animation. So, animations are stored alongside shapes. I do the same thing for a triangle. So, it has some steps and durations. I then create the shape of the triangle. Uh then I have these two shapes. And then whenever I click a button or touch a key, it will start the animation by playing a particular scene. So, that gets the renderer and the shapes. When I run the script, it shows the canvas, and I can click on this. And then we have these two animated shapes that I just created. So, like I said, I already created a simple graphics engine and animation system for this. The graphic engine is very basic. It has a canvas, and it has a render method. And this render method allows us to render a shape. So, we get a shape ID, we get some points, and we get a color. So, graphics renderer doesn't know anything about specific shapes. It works on a pretty low level. And then it's if the shape is not there yet, it's going to store it, and otherwise, it's simply going to render it. So, it's a really simple setup. The animation system is slightly more complex. So, first, I have an animation step protocol. So, this is basically a definition of what an animation actually is. And it's very simple. It has an apply method that gets shape data. So, that's basically, if you look at the graphics library, that's basically a list of points. And points are tuple of two floats. And it gets a color, which is an alias for a string. And it gets some time value between zero and one that determines where we are in the animation. And it returns computed shape data and color. So, this is typically a pure function. Uh there's a helper uh function to do linear interpolation. And then we have some example animations like moving. So, we get dx and dy, and we apply that to the shape data. We have rotating. We have scaling, and we have fading, which is the only animation type that actually modifies the color. Then, I have a data class called animation that contains a list of these steps. And default, that's an empty list. I also have duration. So, the animation specifies how long each step is supposed to take. And they happen in sequence. And there's a start time, so you can let the animation start later. And then there's some helpful properties like computing the total duration of the animation and computing the end time, which is the start time plus the total duration. Finally, we have a shape data class. So, a shape is nothing more than something that has an ID, which is used by the graphics engine. We have a bunch of points, and we have a color. And then it can have an associated animation. It's optional. And then finally, there is a play scene function that gets a renderer, that gets a bunch of shapes. And then it looks for animations in those shapes. And it goes through a while loop to actually animate all of those shapes and render them. Now, maybe you want to do this slightly differently because now play scene is sort of both the animation engine and the game loop. So, you might want to split this up in a more complete example, but I've kept it relatively simple in this particular case. And then the way I use the main function I showed you before is that we create an animation object like so. I create shapes. And then in the end, I call play scene passing the renderer and the shapes. Now, the issue is that this is

Before Version: Non-Fluent Animation Definition

at the moment kind of clunky. It doesn't really feel that great to work with animation objects in that way. One thing we could try to do is maybe rewrite this slightly differently. For example, I could also do it in steps, so we don't define this whole animation object all at once. So, I could do something like this. Uh let's say the start time is zero. And then basically, for each of these steps, we can do something like this. Square animation. steps. add or append, actually. And then we can append a step like rotate 60. And then we can do the same thing for the durations. So, now we did this one, and then we can do the same thing for all of the other steps as well, which is probably even more verbose than what we had before. So, either of these options is not really great. It feels clunky. On top of that, you have to mentally keep steps and durations in sync. These always need to be the same length. So, it's really easy to make off-by-one mistakes. And reading this is kind of hard. Now, in the end, the way

The Engine: How play_scene Works

that we define the animation is independent from uh how uh play scene actually uses it to actually render the animation. Play scene only needs access to the data in shapes and animations. It doesn't care how it was defined, whether that's non-fluid or fluent. It just sees a list of steps and durations. And this for loop here iterates over these things in order to compute where we are in the animation. So, rendering this animated scene doesn't rely on how you define animations. And in general, that's something to consider when you write software. You can typically make a distinction between how things are defined and how they're being used. And if you can design it in such a way that these things are independent, then this gives you a lot of flexibility. Now, the

Refactor Step 1: Add an add() Method

first thing that I want to do is that I at least want to make it a bit easier to do stuff like this. So, what we can already do is extend the animation class so that we can append step and duration without having to touch these steps and durations list directly. In order to do that, let's create an add method in the animation class. So, this is going to get a step. And it's going to get duration. And then what I'll do simply is self. steps. append the step and self. durations. append the duration. So, now that we have this, at least this already becomes simpler because now I can simply write square animation. add and then we add a rotate 60 with a duration of 1 second like so. And I can remove these lines. And now I can basically rewrite these steps and durations so that each of these is just a single line. So, that's already a lot simpler than what we had before. And at least this way we don't manipulate steps and durations separately. And there's one place, namely the animation class, that is responsible for keeping this logic in sync.

Refactor Step 2: Turn add() into a Fluent API

Now, if we want to turn this into a fluent API, this is actually really simple now because instead of returning none, we need this to return the animation object. And then we can chain these method calls. So, what I'm going to do is let this return self. Like so. And then in the animation class, I will add a return method. And that's all there is to it. And what I can do now is in my main file, I can actually call add several times. So, I can do add rotate, but then I can call dot add, and then I can add a move. Like so. With the duration of one. And I can add the rest of the steps as well. And then you get something that looks like this. Just to be sure, let's run this and make sure it still does exactly the same thing. And fortunately, it does. So, what makes this work is the fluent interface method chaining. That's the key ingredient. However, this is still a bit low level. We're adding generic steps like uh rotate and move. And we need to know about all of these different objects. So, that also means in my imports, I need to import all of these things, which is kind of annoying. Or I can import animation, but then here I have to write animation. rotate, animation. move, etc. So, this is all still a bit low level. And uh it's still not very easy to read this. If you

Refactor Step 3: Domain-Specific Fluent Methods

really want to turn this into a fluent API, then you need to give it domain specific verbs. So, what you can do then is add methods to the animation class that add certain types of steps directly. For example, I can create a move method here. And move gets a dx. It gets a dy. And it gets a duration. And let's default put that to, let's say, half a second. And just like the add method, move is also a fluent interface method. So, this also returns self. So, then what this does is return self. add move dx dy and we pass the duration as well. So, move is simply a wrapper for adding a specific move operation. And I can add similar methods for all of the other operations that we have. So, rotating, scaling, fade to, etc. And now that we have this, I can go back into the main file. I can actually write this in a much simpler way. So, I can simply write square animation dot rotate 60 degrees and duration one. Dot move 200 one second. Dot scale 1. 3, etc. So, then ultimately, you end up with something like this for all of these animation moves. But then, what you can do instead is actually not do it here, but call it directly on the animation object. And then, if you put this between parentheses like so, and let's format this nicely, then you get something like this. And you can make this even shorter by removing the places where the duration is one second, because that's the default. Like so. And actually, start time zero is also already a default value. So, now we went from this very complicated animation setup to just a few simple lines of code by relying on the fluent interface pattern most importantly, but also being a bit smarter about the default value. So, it's almost like reading English, right? We have an animation, we rotate, then we move, then we scale, then we fade, etc. So, that's the fluid interface in short. And just as a final check, let's run this one more time. And indeed, this still works. And just to make sure that I'm not just making this up, let's take this square and move it, let's say, to 400 and scale it to three and see what happens then. And here, this is what happens. So, actually, this is pretty cool. We now have a little graphics and animation engine that you can use for games and whatnot. Now, like I said, it's kind of limited. I didn't really work on a proper game loop. Maybe I should do another video about that. I've been thinking about doing that for a while, actually. It could be fun, but I'm not sure how interested you are in stuff like that, because Python is not really the default language to do game development in. Let me know in the comments if you'd like me to do that video. Now, fluent interfaces. What I'm

When to Use Fluent Interfaces

showing you today, they're really a great fit when you have sequences of operations where the order matters, like in this animation engine that I showed you. If you want to have an API that reads more like a story, like a domain specific language. Things like animations, data pipelines, configuration flows, validation chains, query builders, these are all good examples of where the fluent interface is a really good pattern. But there's

When NOT to Use Fluent Interfaces

also situations where you shouldn't use this. Uh if you need like simple one-off operations, then well, just do a single function call and that's totally fine. Or maybe you have domain where things are not naturally sequential. Uh or maybe you want to have very strong guarantees about immutability at each step. The fluent interface that I showed you here basically modifies the animation at each step. So, that's something to be aware of. Or another case where a fluent interface doesn't really work that well is if you need to do things that are out of order, like first you want to apply step three, but you want to skip the step after that, etc. Also, if you have really long chains, then maybe a fluent interface is not a really good approach, either. Or you could create another layer on top of that generates sub chains. So, there's still some possibilities, but it's maybe not the best fit. In the end, fluent interfaces, they're a tool to increase readability. And once they start reducing readability of your code, then well, they're probably the wrong tool for you. And by the way, if you enjoy refactoring APIs like this and want more videos about design in Python, give this video a like and subscribe to the channel for more. It helps me a lot. Now

Final Thoughts

like I said, fluent interfaces, they're a great way to make your Python code more expressive and story-like. And they're already everywhere in the Python ecosystem. Once you understand the pattern, you see it all over the place. But I'd love to hear from you. Where could you apply a fluent interface in your own code? Or maybe you've already built this before. Let me know in the comments. And you might think, "Hey, doesn't this pattern actually look like the builder pattern you covered recently in another video? " Well, yes, it does, but actually, the intent is quite different. Builder is about constructing some complex object in a controlled manner. Whereas, the fluent interface is about making an API read more like a sequence of actions. So, in our case, the animation isn't just built, it represents a timeline of operations. So, in essence, builder and fluent interface, they're quite similar, but they're not exactly the same thing. If you want to learn more about the builder pattern, check out this video next. Thanks for watching. And see you next time.

Другие видео автора — ArjanCodes

Ctrl+V

Экстракт Знаний в Telegram

Экстракты и дистилляты из лучших YouTube-каналов — сразу после публикации.

Подписаться

Дайджест Экстрактов

Лучшие методички за неделю — каждый понедельник