Transcription
Hey, my name is Forest. Welcome back! In today's video, you're going to learn about seven different software design patterns. Many of which you already use, whether you realize it or not. Because, simply put, they're solutions to common programming problems that appear over and over and over and over, regardless of what language or platform you're using. It's not complicated. It's just a matter of knowing them so you can understand when to use them and to actually use them because they are common practice for a reason, because they're good.
Back in 1994, these four developers, known as the Gang of Four, wrote this book. They documented, cataloged, and formalized 23 commonly used design patterns. So, I highly recommend reading that book, reading all the way through it, because all of those are heavily applicable today. We're not going to go over all 23, but I must note that all 23 do fall into three buckets.
Creational patterns are all about object creation. So, instead of creating objects directly, these patterns give you more flexibility on how objects come into existence. Structural patterns, it's a second category, deal with how objects relate to each other. Think of them as blueprints for building something, building larger structures from individual pieces, like Legos. So, creational patterns: object creation. Structural patterns: the structure of how they relate to each other.
And the third category, behavioral patterns, which handle communication between objects, how they interact and distribute responsibilities. Which includes the greatest pattern of all time, the Strategy pattern. Just, just always use a Strategy pattern. If you take anything, nothing from this video, take that.
But let's dive deeper into these three creational patterns, starting with the Singleton pattern. Everyone hates it until they need it. Think of the Singleton pattern as your app's logging system. When errors happen across your app, which we all know will happen since you're the one coding it, you want one central logger handling everything, using consistent formatting and writing to the same file or service. You don't want, you don't want different loggers interfering with each other. For example, this is where you have multiple loggers and creating chaos.
Logger one, new logger. Logger two, new logger. Which is another logger writing to the same file? The Singleton pattern, you have a single logger that everyone uses, and you get the instance from said logger.
Logger one, logger two, one logger, get instance of that logger. Stored in logger. You'll want to use the Singleton pattern when you absolutely need a single instance that's accessible globally, like a database connection pool or the logger. But it does have some trade-offs. The good is that you get guaranteed single instance and global access. The bad is that, well, it's a bit of a nightmare to test, and that's because you can't easily mock it. You can mock it, but you can't easily mock it. And if you want to call that a skill issue, by all means, try it yourself, then get back to me and tell me how it went. But also, in multi-threaded environments, you need special handling to prevent creating multiple instances.
But if you want me to really scare you, I'm going to, I'm going to use this analogy. A Singleton is basically just a global, a glorified global variable. Yeah, think about that for a second. Makes me nervous just thinking about it. But I'm not trying to scare you. It's not all bad or anything like that. It's like using a hammer to solve your problems. Sure, it works great on nails, but not everything is a nail. That's, that's the thing. Just use it when you genuinely need a single instance guarantee, not when you just want a global state.
Next up is the Builder pattern. Never tried to create something that has like 15 different optional parameters? Just me? No. All right. Anyway, you can think of the Builder pattern as, well, think about creating a complex API request. You've got headers, query params, body data, data, data, timeout settings, retry logic. Trying to do all of that in a single constructor will look something like this. As you can see, new HTTP requests, and we're just throwing in API example.com, post, and all of this based on our HTTP request class, like this.
However, the beauty of the Builder pattern is that you can chain methods in any order. You can skip the optional ones, and your code actually reads like English. Check this out. So here's our class, request builder, which starts off similar to our HTTP request, URL string, method string, headers, record string, string. However, here we're assigning it an initial value. In the below, we have all of our methods, as you can see, and our constructor results in a beautiful builder pattern as such. And what's great about this is that if you want to add any new options later, you just have to add a method to the class. No need to go through and update every single place you're creating an object.
You want to use a Builder pattern every time you're staring at a constructor that has more parameters than you have fingers, or when you need to construct an object step by step, like cooking. You don't just throw all the ingredients in at once, you follow a recipe step by step. The downside is you end up writing more code upfront. That is, but trust me, your future self and all of your teammates will thank you when they can actually understand what in the world's going on and can create objects without, you know, playing the game, "Oh, I wonder what this parameter does."
Next is the Factory pattern. You know how in object-oriented programming, like my beloved Java, we're always creating objects with "new" this and "new" that, as you've seen here? Well, the Factory pattern says, "Hold up, let me handle that for you." Think about creating different types of users in your app. You got admins, you got regular users, you got moderators. Here's what that could look like without a factory. Yeah, not the cleanest thing under the sun.
If type admin, user, create new admin. Or type moderator, user, new moderator. Or else, user, new regular user. But if we were to follow the factory pattern, boom, that's it. That's it. We use user factory, create an admin, ID one, name John, as you can see up here. And then the type is thrown into a switch statement. So all of that messy logic, the object creation logic, is hidden in the factory class, and all we have to do to create a new user is this.
It's just like when you're driving down the road and you look at a factory. Oh, that's a, that's a car plant. You know that cars pop out of there, but you don't see all of the intricacies and complexity and this and that of what's going on inside the factory because you don't need to know any of that, or at least you don't need to focus on that every single time you see a car come out of there. So just like with the code, you have, you have the macro idea of what's being popped out instead of worrying about all of the logic every single time you need to create a new user.
And then for us, if we want to change how an admin is created, just update the factory. Need to add logging every time a moderator is created, update the factory. Want to start pooling and reusing user objects, update the factory. You want to use factories every single time you see yourself using that "new" keyword all over your codebase. The downside of a factory is that you're adding another layer of abstraction. But is that really a bad thing? I don't think so. The real downside, in my opinion, is that they're annoyingly coupled. H, they are very dependent on the factory class. But the flexibility you get in return, overall, it's worth the trade-off for sure, and your code becomes way more maintainable because all of the creation logic is in the same place, in that factory class.
It manages the complexity, just like TwinGate manages the complexity of dealing with VPNs for accessing development environments and internal tools. And just to be clear, TwinGate, the sponsor of this portion of today's video, is not a VPN. It's a modern replacement, if you will. Here's the difference. Instead of exposing your services to the public internet or dealing with slow VPN connections, TwinGate creates secure, direct connections to exactly what you need when you're working remotely as a developer. That could be your staging environment, internal tools, cloud resources, whatever. You get instant access with incredible performance, while everything stays completely private. You can manage everything as code with their Terraform provider. It has a Kubernetes operator for service deployment, and the setup is remarkably simple. In short, TwinGate allows developers to securely access work resources remotely via zero trust network access with the speed and performance you need for development work. This means that you can easily control exactly who has access to what, which is perfect for development teams. Want to learn more and try it out? Well, click the link in the description and see how TwinGate can transform your development workflow.
Okay, now on to the second category. That was the creational category. This is the structural. Structural patterns, starting with Facade. The Facade pattern is exactly what it sounds like, putting up a, a pretty front to hide everything behind it. And you may be saying, "Well, isn't this kind of similar to what we just discussed?" The factory hides all of the stuff inside the factory. Remember, creational patterns are for object creation. Structural patterns are how objects relate to each other. And the Facade pattern, it is kind of odd how it's like it's a pattern. I mean, it makes sense. It's fully detailed. They, uh, it is a design pattern with specific implementation patterns and use cases, but really, it, it's really just good programming technique, I guess. All of these are good programming technique, but this one is really hard to miss.
Anyway, uh, let me move on to my analogies. You guys know how much I love my analogies. So, if you're watching this video, you like how I explain things, trying to put them into layman's terms, because that always helps me out. Make sure you subscribe, because that's how I explain everything. If you don't like it, you're not going to like the rest of my videos. But you know how when you order something online and you put in your card data and you click "buy now," and then, well, I mean, magically, everything else happens? Well, behind that simple button, a payment is being processed, there are inventory checks, there are shipping calculations, fraud detection. But you, as the customer, you don't really care. I mean, you care about fraud detection, things of that nature, but you don't really care about everything that's going on behind the facade, right? Behind the "buy now" button. That's, that's the idea here. It's, it's kind of just a really fancy word for encapsulation. Yeah, I kind of like that. It's a fancy word for encapsulation.
Let's look at some code. So without the facade, oh Lord help us, this is what it's like. So new payment processor, stored over here. New inventory system, stored over here. So on and so forth, everything that we just discussed. And then we run through, if fraud checker, if inventory system, if payment processor. Bless your heart if you got to maintain all this. And that is without a facade, and you'd have to write this every single time.
However, with the facade, you create a new order facade. And you may be wondering, well, what happened to all of these aspects in the facade? Well, all of that code is within our order facade class. Payment processor, payment processor, as you can see here. Then we have our constructor, and then we have place order, which has all of our if statements here to check through everything. And this is all the code you need to write because we're using a facade. Where again, if you weren't, you'd have to write this every single time.
Use a Facade pattern when you just have a mess of complex subsystems that you need to simplify. So for programming, it's exactly like that. We get the outward appearance of the interface or class that we're interacting with, and it hides all of that information, all the complexity within it, so we don't have to worry about it every single time. The only real downside of a facade, or at least the downside that I would like to point out, is when it could become a God object. I'm what's called a celestial sweetheart, a celestial, like a God, small G, son, knowing too much and doing too much. But compared to spreading the complexity everywhere, I'll take the trade-off.
And you're probably using facades all the time without realizing it. Think of an HTTP client. You just call fetch API users, and you get your data, right? You don't care about TCP connections or retry logic or header parsing or any of that low-level stuff. And I mean, this is actually built into programming languages, like an ArrayList in Java. It's hiding all sorts of array resizing complexity behind a clean interface, ArrayList. That's the beauty of facades. They're everywhere in good code.
Next is the Adapter pattern, which is a very easy one, simple one to understand, because we deal with adapters in real life all the time. Have you ever had a laptop that you wanted to connect to a TV that it didn't have, like, any, like, what the 2016 MacBook Pro? It didn't have the proper ports, so you used an adapter to go from a USB port to an HDMI port. The naming of these design patterns is not supposed to be complex. It is very simple, very, um, intentional, very specific. It, it's an adapter.
And you want to use the Adapter pattern when you're integrating third-party libraries or APIs that don't quite match what your code expects. So, let's say the third-party weather API gives us information in Celsius in kilometers, and I've created this weather API interface. However, our app is expecting freedom units of Fahrenheit and miles. Without an adapter, we'll do something like this.
Create implementation of third-party API with class third-party weather API that implements weather API, which is the getting our Celsius and getting our kilometers per hour. And then, in order for it to work in our app, we would have to come down here, create the new third-party weather API, stored in weather API, and then do the conversion from Celsius to Fahrenheit, conversion from kilometers to miles. And well, that's what you'd have to do every single time.
However, with the adapter, we implement the weather app, which is what has temp F and speed miles per hour. We'd create class weather adapter that implements weather app, which has our freedom units in constructor, private weather API, weather API. And then in our constructor, we say, "Hey, weather API interface, we need you," because we're going to get the temp C from that weather API, we're going to convert it to Fahrenheit, and it'll be all done in this method. And same idea with kilometers to miles. And all of this is being done within the class. So when we want to get that information from our API, we can do something like this. All of the logic is hidden right here, and then we're using that information. Oh, is it above 75? Oh, it's hot. Oh, is the wind speed above 10 mph? Oh, it's windy. That's it.
So instead of changing your code everywhere and putting that logic every single time you need to use Celsius and kilometers from the weather API, or if it was a library, trying to modify the entire library, good luck with that. All you have to do is wrap it in an adapter. Sounds like I was almost saying rapid dash wrap it in an adapter, rapid in an adapter. Say that five times fast.
The only thing is, it can get real tedious trying to make an adapter for everything. Best case scenario is that your library and API match up with what your app needs, but that's again, that's not always the case. And an adapter is always going to be better than having all of this logic spread throughout your entire codebase. And what's interesting about the adapter pattern, I don't know if this is just my mind trying to connect things, but since it's all about making different implementations work together through a common interface, it's kind of like the application of the Strategy pattern, which is the first behavioral pattern we'll be discussing. And just to clarify again, the structural pattern is how objects interact with each other. Behavioral patterns is exactly how they behave, how they communicate with one another.
So let's talk about the Strategy pattern. Think about how you get to work. Some days you may drive to work, other days you may ride your bike to work, other days you may ride the bus. I, I live in the middle of nowhere where we don't have public transportation, nor am I close enough to ride my bike anywhere, so I just, I do drive everywhere. But this is how I imagine city folk do it. Oh, let me change it up today. Let's take the subway. Today, let's take the train. Let me drive. Let me bike. Let me walk. I don't know. Regardless, what I'm trying to get at is, uh, you have a common goal. You need to get from point A to point B. You are trying to get to work. You just have different strategies of going about it. One is driving, one is walking, one is taking the bus. That's exactly what this pattern is all about.
So without the Strategy pattern, you have the if-else nightmare. You have a class of commuter, that's you. You're the commuter, and you are going to work. And if you're transport car, then you know you got to start car, check gas, navigate traffic, parking garage. All of the logic is here. Else if you're taking the bus, this logic. Taking the bike, that logic. And then this keeps growing with each transport type.
However, with the Strategy pattern, you just have an interface of transport strategy. And then you implement transport strategy in the specific class for said strategy. So car strategy, you put in the car specific logic. Bus strategy, same idea. Bike strategy, same idea. So that way, once you come down here and actually create a better commuter class, within the class, you can set the strategy and then just have one single if statement that utilizes whatever strategy you set up here and runs this logic for for all of them. And again, instead of having if else if else if and then keep on going for walking and subway and this and that.
So the usage would be just like this. With the better commuter, you would store that in commuter. And then you could set your strategy to car strategy and then just call go to work, and it'll do all of that logic with thing go to work. And then if you need to switch the strategy, let's do bike strategy instead. That's how we're going to set the strategy and go to work with bike strategy. And that's where again, each individual strategy has their specific logic within it. It is just so much cleaner. This is how you need to be writing code. Don't, don't, don't if else if else if else if else every single time.
Because the beauty of the Strategy pattern is that it, it allows you to define a, a family of algorithms. You put each into its own class and then make them completely interchangeable. Or, in other words, it's just a fancy way of saying programming at an interface level, letting you swap implementations without you needing to change any of your code. You just make the thing into an interface and you do all the logic below the interface.
When should you use it? All the time. If you got different ways of doing the same thing, then the Strategy pattern is going to be your best friend. It follows the open-closed principle perfectly. You can add new strategies without touching any existing code. And again, that is just creating a new class that implements the transport strategy is whatever strategy we have, car, bus, bike, walk strategy, and implement the transport strategy, have your logic within that class, the, the walk strategy class, you're good.
The downside, downside is that you end up with a bunch of classes, but that's a whole lot better than if-else if-else if-else and having the logic here and then having 20 different small classes all with their own L statements. Like I was just saying, yeah, I'm going to take the Strategy pattern every day of the week. And I know, I know this is going to hurt some feelings because I know a lot of y'all are just if-else-ing, if-else-ing everything. You know who you are. Don't you know who you are?
All right, the last but not least, the Observer pattern. And I'm going to use the, uh, YouTube example because you're watching this on YouTube, and you're the observer to my video. But let's focus on like notifications. When I upload a video, and you're subscribed, actually, when you click the bell, make sure you click the bell, make sure you're subscribed, all that. When you click the bell, I upload a video, you get notified. That's it. The Observer pattern in code, we'll go over it. Lets objects subscribe to events that happen to other objects. Objects subscribe to events that happen to other objects.
So here's the code without the Observer. You have a class called user account. This is the class to create you, the user. And right down here, you create the user account array, and you are put in an array with all of the other subscribers of this video channel, right? And then when I upload a video, what do I want to do? I want to loop through users and notify you somehow. That 500, 600,000 of y'all, I don't think so.
So instead, we use the Observer so that subscribers get notified automatically. Same idea, but we create an interface called subscriber. And then we're going to create a class, I just called it better video channel, just so I can have two video channel classes in here. And it is the same idea. We have the subscriber array, and then you are added when you subscribe or removed when you unsubscribe. And then when I upload a video, we would notify with the title of the video to the subscribers down here, as you can see. This is our private notify method, and we pass in the title so that it notifies all of my subscribers, each of my subscribers, I should say, with that information. Does that make sense? I think it's a good example. It's just a little, as long as you know how to read code, you know exactly that it makes sense. Otherwise, you're going to have to loop through millions and millions of people for many other channels every single time you upload a video, which is not very, uh, performant.
So this in code, like you see how it works in the code here, but this could be like, uh, servers looking for system errors or monitoring for them. So anytime there's a system error, boom, it gets notified. Or a component listening for, uh, state changes. So remember, object subscribing to events that happen to another object.
Now, there is one gotcha, though. If you, uh, well, if you go overboard on events, you could enter into what is known as event callback hell, where one event triggers another event, which triggers another event, and then all of a sudden, you're wondering why your app is sending you a notification from three weeks ago. But use responsibly. The Observer pattern is, is an amazing way to handle the, uh, "if this happens, do that."
And there you have it. Like any good tool, it's knowing when to use each one. So study them, use them, practice to get them ingrained in your head. For more programming concepts and developer stuff explained with too many analogies, make sure you subscribe to the channel. I'll see you in the next one.