We can categorize Design Patterns under 3 main headings:
- Architectural Patterns
Examples: MVC (Model-View-Controller), SOLID, MVVM…
- Design Patterns
It is a specific Architectural Pattern, such as Singleton. Using it alone in a project is not sufficient, and optimization with several different patterns is necessary.
- Anti-Patterns
A group of patterns that many programmers use to solve problems the easy way, even though they shouldn’t. An example is a “god object” that collects everything you might need for the game to run, probably called GameController. The problem with these classes is that the code size grows too large, making editing more challenging and debugging more difficult.
What Patterns Can Be Used in Unity?
Until 2021, it was not possible to use MVC or MVVM type architectures in Unity’s design. UI objects and all objects in the scene could be used as instances matched with relational structures called components.
In 2021, Unity developed its new archetype based on XML under the name UIElements, but it is still in beta and is a method developed only for UI. We still cannot access or manipulate GameObjects on the Scene without the MonoBehaviour infrastructure. The reason for this is that Unity runs under a single core and the main thread.
Considering the limitations mentioned above, we can notice that there are very few architectures we can apply specifically to Unity. Here, our constraint is that the game runs synchronously on the main thread. If we combine the methods we explained in the garbage and constraints sections, we need to choose an architecture that implements a multi-threaded infrastructure and maintains memory allocation processes most stably with parallel code writing. Let’s take a look at some (the most well-known 11) design patterns in order;
1. Singleton Principle
The Singleton design pattern is used to obtain a single instance of a class. The purpose is to provide a global access point to the created object. As long as the system runs, a second instance is not created, thus ensuring that the desired object is created only once. Singleton objects are created only once when they are first called, and subsequent requests are handled through this object.
- By controlling object creation, it prevents other objects from creating their own copies from Singleton objects. Thus, it ensures that all objects are accessed only through a Singleton object.
- It provides flexibility because it has the ability to control and modify the class creation process.
- Other objects add an extra load to the system because they check whether the Singleton object has been created each time a reference is requested.
- “new” cannot be used when creating a Singleton object. In some cases, developers cannot access the source code and cannot create an object directly from the class. This can cause complexity.
- It is not clear how the Singleton object will be deleted. In languages that provide memory management, the Singleton pattern can deallocate the object because it holds it as a private reference. In languages like C++, other classes can delete the object, but this leaves an unused reference.
- Memory allocated by Singleton is never automatically cleaned up and continues to occupy memory even if the object is not used. This is because static variables are ignored by the Garbage Collector.
2. Open-Closed Principle
In simple terms, a class or method should maintain existing properties, i.e., not change its behavior, and be able to acquire new properties.
Our classes/methods should be closed to change but open to extension of new behaviors.
This principle forms the basis of writing code in a sustainable and reusable structure. Robert C. Martin
Open: It allows new behaviors to be added to the class. When requirements change, a class should be able to add new or different behaviors to meet new requirements. Closed: A class’s basic properties should not be changeable.
We cannot anticipate every feature or development that may occur while developing software. Therefore, we should not develop code that we think may occur. (see: K.I.S.S.)
For new upcoming features, we should apply a flexible development model that does not change the existing code and does not disrupt the existing structure, which can easily adapt and keep up with future requirements.
3. K.I.S.S. Principle
Acronyms
- “Keep It Simple, Stupid”
- “Keep it Simple, Silly”
- “Keep It Short and Simple”
- “Keep It Simple and Straightforward”
- “Keep It Small and Simple”
KISS is a principle that suggests striving for simplicity. It recommends selecting the simplest and most straightforward solution when solving a problem. It should be so simple that at first glance, we should say, “Even a fool can do and understand this.”
KISS rejects the idea that complex solutions are “smarter” solutions. Many engineers/programmers think they do “smart” work by finding complex solutions and building complex structures. They believe that the harder it is for an outsider to understand, the more value they are adding.
However, it is difficult to simplify. In software processes, code/software architecture tends to grow and become more complex over time. The important thing is to provide the desired functionality at a minimal level of complexity.
Do not show off your intelligence through code complexity.
- Do not choose complex solutions over simple ones for small performance gains.
- Divide problems into sub-problems. Solve sub-problems with short, low-parameter methods.
- Do not think too much about exceptional cases and complicate the structure.
- Do not try to solve future problems today. (see: YAGNI)
- Do not be afraid to delete code. The best code is the shortest code.
- Do not be afraid to rearrange the code. Divide your code frequently and simplify it with the help of IDEs.
4. YAGNI Principle
Acronym
- “You Aren’t Gonna Need It”
YAGNI is one of the Extreme Programming (XP) principles. It argues that work/development should not go beyond what is needed, with the idea that “the most likely thing to be useful should be done” and the thought that it will be needed in the future.
One of the most common reasons why software developers develop and include features that are not currently needed or not on the work list is because they think that developing such features at that time will be more effective and cheaper due to the expectation that the feature will be needed in the future. However, this feature that is not included in the workflow (or defined as “nice to have”) is included in the “hypothetical feature” category from the moment it is written, and this feature may never be requested during the later stages of the project. As a result, analysis, development, and testing times are wasted, and unnecessary resource (time, motivation, etc.) consumption occurs.
It’s one of my mantras - focus and simplicity. Simple can be harder than complex: You have to work hard to get your thinking clean to make it simple. But it’s worth it in the end because once you get there, you can move mountains. Steve Jobs
5. Flyweight Principle
The Flyweight pattern is a design pattern that minimizes memory usage due to object creation. If memory consumption arises from handling too many objects together, we can use the Flyweight design pattern.
Flyweight reduces the creation of frequently used objects with the Flyweight Object Pool logic.
If we look at the structure of the Flyweight pattern, we have a class called FlyweightFactory. This class keeps the list of classes we will reproduce repeatedly, i.e., the classes that inherit from Flyweight class, and provides client access to this list through a method. When the client wants to create an object through this method, the method first checks if the object is in the object pool, and if it is, it returns it from this list. If it does not exist, it first adds it to this list and then returns it.
6. Interface Segregation Principle
This principle suggests creating multiple, more specialized interfaces instead of combining all responsibilities into a single interface.
Rather than having a single interface, we should carry out operations with multiple interfaces separated by their usage. Each different responsibility should have its own specific interface. This way, the person using the interface will only be concerned with what they need. If we have only one interface for multiple purposes, it means we are adding too many methods or properties, which violates the Interface Segregation principle.
Objects should never be forced to implement properties/methods etc. that they don’t need.
7. Observer Principle
Observer is a design pattern that acts like its name implies: it observes the changes made to an existing object’s state and notifies other objects of those changes. More specifically, when there is a change in the “y” property of object “x”, the Observer design pattern informs the “z”, “w”, “k”, etc. objects that are observing object “x” of the new state.
8. Object Pool Principle
Object pool is a design pattern that can be used in distributed systems or in situations where objects are difficult to manage by software developers. It belongs to the Creational (creation of objects) design patterns. Instead of continuously creating the desired objects, a pool is created initially and filled with objects.
9. Dependency Inversion Principle
Dependencies between classes should be minimized, especially high-level classes should not be dependent on low-level classes.
The dependency of a class, method, or property on other classes should be minimized. Changes made to a subclass should not affect the upper classes.
When there is a change in behavior in high-level classes, the low-level behavior must conform to the change. However, when there is a change in behavior in low-level classes, there should not be any disruption in the behavior of upper-level classes.
public interface Message
{
void SendMessage();
}
public class SMS: Message
{
public void SendMessage()
{
// sms send service
}
}
public class Email: Message
{
public void SendMessage()
{
// email send service
}
}
public class Notification
{
private List<Message> messages;
public void SendNotifications()
{
foreach (var message in messages)
{
message.SendMessage();
}
}
}
10. Single Responsibility Principle
The single responsibility principle states that our classes should have a well-defined single responsibility. A class (object) should only be modified for one purpose, which is the responsibility assigned to that class. In other words, a class should only have one thing to do.
If the class or function you are developing serves multiple purposes, it means you are in a development process that violates this rule. When you realize this, you need to break it down into pieces according to the goals.
When requirements change, there will be parts of the code that need to be changed. This means that some or all of the written classes (objects) need to be changed. The more responsibility a class takes on, the more it will have to change. This can cause many pieces of code to change, making implementation of changes more difficult.
When developing a class or function, we need to determine its responsibility or purpose, and design the class accordingly, so that we can make the desired improvements by updating and correcting as few things as possible. Reducing responsibility also means being more adaptable to change.
Test: A class with a single responsibility will have a much smaller number of test cases.
Less Dependency: A single responsibility in a single class provides less dependency.
Simpler and More Understandable Structures: Fewer responsibilities allow for simpler or smaller structures. Smaller structures are much more advantageous than monolithic ones and increase code understandability/readability.
11. Liskov Substitution Principle
In our code, we should be able to use derived (child) classes in place of their base (parent) classes without having to make any changes.
Derived classes or child classes should be able to use all of the properties and methods of their base class or parent class in the same way and be able to contain their own new properties.
Reply by EmailWhen objects/classes consisting of lower-level classes are replaced with objects of the upper class, they should exhibit the same behavior. Derived classes should be able to use all the properties of the derived class.