You Don’t Need That Design Pattern — Transcript

Learn when to use design patterns in Python code and how to simplify with functions while applying abstraction where it truly matters.

Key Takeaways

  • Design patterns should be applied judiciously to avoid unnecessary complexity.
  • Simple functions and dictionaries can often replace class hierarchies effectively.
  • Abstractions are valuable when dealing with varying external dependencies like different LLM providers.
  • Dependency injection and protocols improve code flexibility and testability.
  • Start with simple code and refactor with patterns only when justified by complexity or coupling.

Summary

  • The video demonstrates a Python script that summarizes text using an LLM and initially uses design patterns like strategy and factory.
  • It explains how excessive use of design patterns can add unnecessary complexity when simpler solutions suffice.
  • The presenter refactors the code by replacing class hierarchies with simple functions and dictionaries to simplify prompt strategies.
  • This simplification makes the summarize document function shorter and easier to understand without losing functionality.
  • The video then discusses where design patterns are genuinely beneficial, such as abstracting LLM clients to handle different providers.
  • A protocol class is introduced to define an LLM client interface that supports generating text, token counting, and context limits.
  • Dependency injection is used to decouple the summarization logic from specific LLM implementations, improving flexibility.
  • The video highlights the adapter pattern as a way to integrate multiple LLM providers with different SDKs under a unified interface.
  • Testing is simplified by using fake LLM clients that implement the protocol, enabling isolated and reliable tests.
  • The key message is to avoid premature use of design patterns and instead start simple, adding abstractions only when necessary.

Full Transcript — Download SRT & Markdown

00:00
Speaker A
Here's a Python script that summarizes text using an LLM, and it also has a couple of design patterns. For example, there's a prompt strategy, which is an abstract base class. It has a build method, and then we have concise prompt strategy and a bullet prompt strategy. And then we also have a factory that is going to create these objects for us depending on the style of summarization that we want. So, that's a whole bunch of classes to basically build a string.
00:14
Speaker A
We don't always need design patterns. I'll show you how to simplify this, but then I'll also show where a design pattern in this case actually makes sense, and it's different from what we're seeing here. Now, writing great software
00:29
Speaker A
is not about memorizing design patterns. It's about understanding when abstractions actually help and when they make your code harder to work with.
00:40
Speaker A
That's exactly what I go deep into in Software Design Mastery. This is not just another course with patterns and principles. It's a system that guides you through the fundamentals of software design and teaches you how to make high-level decisions while AI helps you
00:48
Speaker A
write the actual code. If you want to be part of that, join the waiting list at iron.codes/mastery.
01:04
Speaker A
The link is in the video description. Now, the problem with this code is not that the strategy pattern is bad. It's that the code does not really need all this structure yet. There's just a little bit of variation in this code.
01:10
Speaker A
There's one prompt string that's selected by a style name, but in this particular solution, we already have like a bunch of abstract base classes, strategies, and factories, and all sorts of other things. It's fine if you
01:22
Speaker A
know beforehand that you're going to need like a million different prompt styles or whatever, but in this case, every abstraction has a cost. And in this case, the cost is just a bunch of added complexity. The way that all these
01:38
Speaker A
classes are used is that we have a couple of functions here, one to call an LLM, one to split text into chunks.
01:51
Speaker A
There is a summarize document function that uses all of these things or builds the factory and then creates a prompt strategy. And that's the main function where we have some document text and it summarizes the document.
01:59
Speaker A
When I run this, then this is what we get. In Python, functions are objects.
02:12
Speaker A
That means you can pass them around, you can store them in dictionaries, you can test them directly. So, for lots of cases when you write code and you need to add complication, functions are pretty powerful. For example, here what
02:17
Speaker A
you could do instead of this entire hierarchy of classes, we could also create two functions that each represent a different prompting strategy. So, we would have a concise prompt and we have a bullet prompt.
02:29
Speaker A
And then I'm also going to delete this prompt strategy class. And instead of the factory, we can use a dictionary.
02:54
Speaker A
And this dictionary is a mapping from string to one of these functions. So, let's call that a prompt builder function.
03:02
Speaker A
And let's also define an alias for that. So, this is a callable that takes a string and returns a string.
03:11
Speaker A
And then inside this dictionary, we have the concise prompt builder and the bullet prompt builder like so.
03:28
Speaker A
We also don't need this anymore, but we can now replace it with a function build prompt.
03:42
Speaker A
And this is going to get style and text. And it will also return text. And then what we can do is simply return—Let's actually call this prompts. That's better.
03:51
Speaker A
We're going to pass the style. And we're going to call that with the text. Now you could wrap this around a try-except block if you want and then turn the key error into a value error that would be a bit better error
04:06
Speaker A
reporting. But for now, this is fine. And then what to use that, the only thing we need to do is go to our summarize document function and we don't need this anymore.
04:19
Speaker A
But then here instead of the whole strategy process, we simply call build prompt and we pass the style and the chunk. And same here, this is going to be build prompt and we're going to pass the style and
04:29
Speaker A
the text. Like so. Let's run this again and you see we get exactly the same results. But now our code has become way simpler because we just have a couple of functions, no abstract base classes, etc. It's still designed well, it's just
04:46
Speaker A
a lot smaller. What's also nice about this approach is that actually now our summarize document function becomes shorter. We can see exactly what is happening here. We have a splitting into chunks, then we create the summaries, and then finally we create a full
05:02
Speaker A
document report. It means that because of our simpler design, it's easier to see what this code is actually doing. So that's all great and you might say, "Well, that's kind of obvious that you can remove this. You always do that,
05:18
Speaker A
right? You start with a bunch of classes and you replace them by functions. Yay, well done." But the interesting thing here is that there are other aspects of this code that actually do merit some design improvements and
05:30
Speaker A
that perhaps do merit using a class with some abstraction. For example, if you take a look at how the LLM is actually called, so that's currently happens with call LLM. So, that's fine and it returns this response which is not a big deal,
05:42
Speaker A
but the problem is of course that in real life call LLM is probably pretty complex, especially if you use different LLM providers. The SDK is going to be different, the response formats are going to be different, there might be
05:59
Speaker A
different token usage fields, the context limit might be different. And that's a problem for example because, well, basically summarize documents just chunks the text now by using a maximum number of tokens that's just a magic number. So, that means that at the
06:11
Speaker A
moment this code is not very flexible. So, in essence the summarize document function should know no details about how or about what LLM is being used and what the constraints are of that LLM. In order to support that, let's introduce
06:27
Speaker A
some abstraction and I'm going to use a protocol class for that. So, let's say we have a class LLM client which is a protocol and that is going to have a couple of helpful methods, properties optionally that deal with
06:42
Speaker A
what a particular LLM client needs to provide. So, that's going to be a generate method that's going to get a prompt.
07:02
Speaker A
And this is going to return the LLM response. So, that means that our clients, our implementation of the clients, they're going to be responsible for translating for a particular specific LLM into these LLM responses that then our text
07:13
Speaker A
summarization knows about. But since we now have a class, we can add other useful things here that are related to LLMs.
07:29
Speaker A
Like let's say the tokens. Or, we might want to know a max context window.
07:40
Speaker A
And of course, you can add more functionality here as well. So, the class is not just a simple wrapper around a method or something. It's really an abstraction of what an LLM actually provides and what the summarization workflow actually needs,
07:58
Speaker A
which is being able to generate text, counting tokens, knowing things like the context window limit, and getting back normalized response data. And once you have this class, we can provide a simple implementation.
08:13
Speaker A
So, let's say I create a fake LLM client. There's no inheritance relationship because of duck typing. And then we're going to have these three functions. And what we can do is basically move the bodies of these functions that we
08:27
Speaker A
already have right here. And this is then where we can also put our magic number.
08:43
Speaker A
The only thing we need to do now is fix the summarize document function. And because we're now dealing with a client object, we can use dependency injection.
09:01
Speaker A
And as you can see, I'm specifying the protocol here. So, that means that anything that implements the protocol, I can use together with my summarize document function. And now, instead of hard coding this data here, I can use the maximum context window.
09:14
Speaker A
And for example, cutting that in half just to be sure. Then here, instead of doing call—
09:29
Speaker A
And for example, cutting that in half just to be sure. Then here, instead of doing call LM, I'm doing LM.generate.
09:42
Speaker A
And same thing here. And finally, the only thing I need to do in my main function is create the client.
09:50
Speaker A
So, let's use our fake LM client and simply pass that as an argument to the summarize document function.
10:03
Speaker A
Like so. Let's run this code one more time. As you can see, the result is exactly the same. But now we introduced a useful abstraction, which is that all of the LM stuff, well, we're going to put that
10:14
Speaker A
behind an LM client interface that our summarization function then uses. Now, for specific types of LM providers, we can simply implement a class and we don't need to change anything in summarize document. What's interesting while I'm doing this refactor is that
10:30
Speaker A
now actually, secretly, we have introduced a design pattern, namely, the adapter. The adapter pattern basically means that you create a single interface and then you create implementations of that interface that adapt a specific service or tool to that
10:49
Speaker A
interface. And that's exactly what we're doing here. We have our LM client interface, but we can now create an adapter for each of the different LM providers. For example, let's say that next to this fake LM client, I can have another client called the
11:07
Speaker A
OpenAI client, which has different settings. Maybe you want to pass an API key here as well. The generate method has a different implementation. Uh the max context window is different from our fake LM client. And it's really easy to
11:20
Speaker A
change this now because in my main function, the only thing I have to do is I have to create my OpenAI client instead of the fake LM client. And then, when I run this again, you see now we
11:31
Speaker A
get an OpenAI summary of the text. Well, not real. I mean, I'm kind of faking everything here, but uh you get the idea. The most important thing to remember from this is the order in which this happens. I didn't start
11:44
Speaker A
with, "Hey, what pattern should I use?" or "I want to use the adapter pattern." I started with the analysis that in this particular case we have provider-specific SDK details and they're being used in functions that should not need to know about those
12:00
Speaker A
things. And then by refactoring the code and introducing a solution to that, the pattern appeared. And it's not a classic object-oriented implementation of the pattern, either.
12:12
Speaker A
I'm using protocol classes and structural typing. So, I'm using the natural features of Python to achieve this goal. But, we do end up with a design pattern, namely adapter. But, it appeared naturally. It We didn't think about this beforehand. Finally having a
12:28
Speaker A
good abstraction like this instead of just a bunch of random strategies and factories, it actually makes testing a lot easier. So, that's a good way to verify that what you're doing is actually going in the right direction.
12:41
Speaker A
For example, if you want to write a test for this, well, then you simply import that summarize document function, you create a fake client that you're just going to use for testing, and then you can create your test summarize document
12:55
Speaker A
function, and then do some asserts to verify that it's actually working correctly. And then we can run that, and it's very simple and straightforward to write test for this code. It means test writing is basically boring, which is good because
13:10
Speaker A
that means that AI can probably write most of those tests for you, so you don't have to do all that work. By the way, if you enjoy videos about writing simpler, better Python code, subscribe.
13:21
Speaker A
Give this video a like. That way I know I should make more content like this.
13:25
Speaker A
So, rule of thumb is to not start with patterns immediately. Look at where there's too much coupling or where a function tries to do too many different things and start simple. You can already start by moving a couple of things into
13:39
Speaker A
separate functions and then introduce abstractions if they really reduce coupling and they tie in with what you expect to change in the future.
13:49
Speaker A
And then design patterns will naturally appear. But, I'd like to hear from you. Do you think about design patterns at all when you write code? Does AI introduce design patterns and do they make sense in your experience?
14:02
Speaker A
Let me know in the comments. Now, YouTube thinks you might like this video next. Thanks for watching and see you next time.
Topics:design patternspythonsoftware designstrategy patternfactory patternabstractiondependency injectionLLMadapter patterncode simplification

Frequently Asked Questions

Why should I avoid using design patterns too early in my code?

Premature use of design patterns can add unnecessary complexity. It's better to start with simple code and introduce abstractions only when the code becomes too coupled or complex.

How can I simplify a strategy pattern implementation in Python?

You can replace the strategy pattern's class hierarchy with simple functions and a dictionary mapping strategy names to those functions, reducing complexity while maintaining flexibility.

What is the benefit of using a protocol class for LLM clients?

A protocol class defines a clear interface for LLM clients, enabling different implementations to be used interchangeably. This abstraction supports dependency injection, making the code more flexible and testable.

Get More with the Söz AI App

Transcribe recordings, audio files, and YouTube videos — with AI summaries, speaker detection, and unlimited transcriptions.

Or transcribe another YouTube video here →