Continuing to discuss the fundamental computer science concepts and ideas, we now delve deep into another major factor that helps great programmers to stand out: design patterns. Much like algorithms, they are an essential skill that can help you build great software.
Their popularity, however, often leads to misuse: it’s tempting to think that every programming problem can be solved by sticking some design pattern on it. To avoid this pitfall, you need to understand this topic well — this knowledge will help you make the right decision.
In this article, we will explore the most popular software design patterns like singleton, decorator, observer, and factory. We’ll also discuss why it’s important to know them well — and then we’ll provide you with some great learning resources.
Getting the definition right
A design pattern is a generalized and reusable approach to solving a particular software design problem. This definition may seem quite abstract, so we can analyze and expand on some of its parts:
- Generalized approach: This is only a “thought framework”, but not a concrete solution in the form of “Do X to achieve Result Y”
- Reusable: In theory, this approach should be applicable in both Problem A and in Problem B.
With this information in mind, we can generalize this definition to make it easier to understand:
Design pattern is code that is modeled in a generic way which allows the developer to solve a new problem similar to how they solved some previous problems.
Design patterns, therefore, are extremely useful because they provide frameworks (no pun intended) for writing quality software. Although it’s tempting to define “quality software” as “software that has 0 bugs and run flawlessly”, we can agree that such code doesn’t exist in the real world — so we can redefine it as “software that is optimized, but not overoptimized, to run well.”
Once the software moves past the point of
print(“Hello world!”) and gets more complex, it starts to carry the “fingerprints” of the developers, meaning that each developer utilizes their own conventions and practices. While this isn’t a bad thing per se, mixing different coding habits from different developers can lead to chaotic development workflow — imagine trying to convince your colleagues that your favorite color is objectively the best color there is and they should start using it as well. ¯_(ツ)_/¯
In this scenario, nobody really knows what’s going on anymore:
- Which naming scheme should we choose? Is this piece of code called “controller” or “handler” or “executor”?
- Which dependencies have we been using? Why are there so many of them? Why do they depend on each other? Which of them are obsolete?
- Why did we ever create this monstrosity of a class that spans several thousands of lines of code?
Naturally, proficiency in design patterns can help you in technical interviews: just like your data structures knowledge, they’ll be tested to assess your skillset. Curiously enough, some developers report that there’s a conundrum that manifests itself once the developer passes the interview and starts working: algorithm knowledge doesn’t really come in handy often, but that of design patterns is far more needed.
Controversies and skepticism
Design patterns, however, sometimes fall victim to the developer community’s outcries that they are “overused”. Why does that happen? At its core, each pattern is a tool designed to solve particular problems. A decorator, for instance, can excel at tackling Problem L, but it can be suboptimal in Problem M. Here are some of the issues that may arise:
Inefficiency. As some developers are tempted to utilize design patterns whenever possible, their code becomes unnecessarily duplicated. When deciding which pattern to choose, the programmer must first ask another question: “Does this problem call for a pattern at all?” Although design patterns generally make code more readable and clearer, overuse/misuse of them leads to the opposite result — software that is unnecessarily complex.
No formal standards to follow. Additionally, the way they’re organized and built can vary from language to language significantly. Their formal footing can be found in various computer science books — but rarely in any of the official docs. This is reflected in languages like Lisp and Dylan which, unlike C++, implemented features that render 16 out of 23 design patterns obsolete.
Therefore, we can draw an important conclusion: design patterns aren’t silver bullets. It’s tempting to generalize every problem, throw various patterns at it, and hope that one of them sticks — but this approach doesn’t really work. Here’s the good news: you do have a silver bullet in the form of your problem-solving ability!
So what are some of the design patterns we should know?
Now we can look at each of them more closely: their functionality, pros, and cons. As design patterns are an essential part of object-oriented programming, we can reiterate a few OOP concepts for better understanding:
- Class is a blueprint for some data structure that may feature a number of data elements and a number of methods (i.e. functions) inside of it.
- Object, therefore, is an instance of a class.
You can also check this article out for an even deeper dive in the OOP knowledge ocean.
This pattern enforces the ”One class — single instance” rule.
How it functions: A unique resource (hardware/service/store or any other object that can be represented in a unique way) is encapsulated and made available in the entire software.
How it can be used: The main benefit associated with Singletons is making an object that already performs some useful function upfront have only a single instance. A good example can be found in working with databases; the process would look like this: connect to the database → select the specific database to use → have all queries routed to the same object as multiple connections would be superfluous in this case.
Caveats and downsides: However, Singletons also have some disadvantages. Their ability to “isolate” classes and objects can cause problems when the project grows in size and introduces more and more models and controllers: in this environment, connecting to multiple databases becomes justified — but Singletons can get in the way.
When used carelessly, they can create two-way data flow. This can be a problem because the one-way data flow is more preferable: in this case, all data streams downward, but not upward. This comes in handy when separating the software’s layers and components: when a business logic-related bug happens, the developer can safely conclude that it probably stems from the controller.
Another problem has to do with unit testing: Singletons make objects and their related methods heavily connected to each other, so testing becomes problematic because of this relationship. In this case, the developer is forced to designate a distinct class to the Singleton.
Additionally, Singletons may introduce hidden dependencies: as they’re easy to utilize throughout the software, the developer is always tempted to overuse them. Coupled with the fact that their references aren’t always obvious, tracking where the given Singleton actually comes from becomes dubious.
This pattern helps to manage software’s structure, allowing to dynamically extend the given object’s functionality with additional features. In a sense, it’s similar to Lego: we assemble the object, then decorate it with Feature A, then Feature B, then Feature C…
How it functions: Its work process is divided between 5 actors: component, concrete component, base decorator, concrete decorator, and client.
- Components manage the interface shared by the wrappers and wrapped objects.
- Concrete components define the behavior of the wrapped objects.
- Base decorators ensure that each operation is delegated to the given wrapped object.
- Concrete decorators override base decorators’ methods.
- The Client has the ability to wrap components in multiple decoration layers.
How it can be used: Decorators are essential when the developer intends to add new functionality to an existing object (or update the functionality) during run-time.
Caveats and downsides: On the other hand, they can introduce a slew of complexity: for one, component instantiation becomes more troublesome because the component also has to be wrapped into various decorators. Additionally, it’s tricky to make Decorator A keep track of Decorator B — to do that, we need to examine a number of decorator layers, which is rather costly in terms of resources.
This design pattern is applied in one-to-many relationships between objects. When the object’s state is changed, other objects are notified about this change and updated accordingly.
How it functions: Three actor classes — Subject, Observer, and Client, are utilized:
- Subject servesas the keeper of data/business logic.
- Observer uses an updated interface which helps to receive signals from the Subject.
How it can be used: This pattern is optimal for software that requires the states of various objects to be synchronized, while the number of said objects is unknown/changes dynamically. This a typical scenario for graphical user interfaces and elements like buttons which would later be hooked to some custom code elements.
Caveats and downsides: Observers are designed to make the subject contain strong references to the observers, ensuring that they’re online. Also, the developer must utilize explicit and implicit detachment — these two factors can lead to memory leaks. To avoid this, the references should be changed from strong to weak.
This design pattern is optimal for creating objects.
How it functions: The pattern’s name — Factory — gives us a hint of how it works. Instead of requiring constructors, Factory offers a generic interface which can be used to create various objects (their type can be specified by the developer).
How it can be used: Factory’s ability to easily generate objects really comes in handy in these situations:
- A large number of relatively small objects/components that share the same properties.
- The setup of objects/components will involve a high level of complexity.
- Various objects need to be instantiated in various environments.
Caveats and downsides: Factory can handle high-level complexity well — but when the developer misuses this pattern, their software can suffer from unnecessary overcomplication. If creating objects via Factory’s generic interface isn’t required, it’s better to avoid this pattern altogether. Additionally, unit testing becomes more complex.
As always, Harvard University provides a huge amount of awesome learning resources. Curated by David J. Malan, CS164: Software Engineering offers a lecture on design patterns with a lot of insights and hands-on approach.
Finally, it comes to a point where you should put your knowledge to test. You can use learning platforms like CodeWars and HackerRank: they offer curated lists of design patterns exercises complete with solutions, explanations, and varying difficulty. Here’s the exercises collection on CodeWars (HackerRank, on the other hand, doesn’t group its exercises into collections, so you might want to search around the website).
The world of programming often seems chaotic — after all, we manipulate 1s and 0s to build something awesome and it often seems like magic. However, concepts like data structures, algorithms, and design patterns help us bring everything in order — they highlight just how well we’ve adapted to organize code and data in an efficient manner.
The post Design Patterns Overview: Helping You Write Better Software appeared first on Soshace.