Below are few Anti-Patterns that every developers should know.
Anti-patterns in software development refer to common practices or solutions that appear to be effective initially but ultimately lead to poor design, inefficiency, maintainability issues, or other negative consequences in the long run. They represent solutions that might seem plausible, but due to lack of understanding, misuse of tools, or misinterpretation of best practices, they end up causing more problems than they solve.
Here are a few examples of anti-patterns in C# .NET development:
1. God Class / Object:
This occurs when a single class or object handles an excessive number of responsibilities. This leads to tightly coupled and difficult-to-maintain code. The God class (or god object) is an anti-pattern that can be very dangerous since it makes code maintenance hard and risky.
This type of class might have many responsibilities, and the class itself usually serves as a direct parent for other, more specialized classes. As the name implies, when such classes exist in a system, they are considered gods because they often hold many responsibilities and powers they delegate to their children. While this approach might look useful, this can lead to an explosion of complex and difficult code.
Another way to recognize this class is that itβs not covered with unit testing. Usually, the class is tightly coupled with other classes or with many other dependencies. At best, you can test this class with integration tests.
To avoid God class pattern, follow these rules:
The number of responsibilities of a class should be proportional to its size. If a particular class has more than one responsibility, break it into smaller classes when necessary.
Please do not use the same name for the class you are creating and its children. They should have different names if their roles are clearly different.
Always think about whether the conditions under which a particular class exists or its responsibilities are important to the overall system. Take care to ensure that these conditions and responsibilities do not become jammed in any way, so they can be easily processed when needed.
This is also a violation of Single Responsibility Principle. By separating responsibilities by breaking down into dedicated classes, we can achieve SRP.
public class God
{
private DatabaseManager dbManager;
private ReportGenerator reportGenerator;
private EmailSender emailSender;
// Methods handling various unrelated tasks
}
2. Spaghetti Code:
This is characterized by unstructured and tangled code with poor separation of concerns, making it hard to understand, debug, and extend.
The anti-pattern known as spaghetti code emerges when code is inadequately organized and presents challenges in terms of comprehension.
This form of code lacks modular structure, a clear distinction of responsibilities, and readability. The complexity of spaghetti code renders maintenance and modifications cumbersome, owing to its intricate nature. Employing practices such as code reviews and refactoring proves effective in both averting and remedying spaghetti code. A prudent approach involves fragmenting substantial code blocks into smaller, reusable segments.
It is beneficial to maintain concise methods, each dedicated to a singular purpose. "Clean Code: A Handbook of Agile Software Craftsmanship" by Robert C. Martin extensively delves into numerous methodologies for preventing or rectifying the issue of spaghetti code.
To circumvent this anti-pattern, it's crucial to delineate the intended responsibilities of each class. Additionally, sidestepping this predicament entails crafting classes in a manner that aligns their methods coherently, fostering comprehensibility and effortless maintenance of their functionality. The primary objective should be to attain code simplicity.
Another avenue for evading this anti-pattern is the utilization of dependency injection (DI). With DI, classes can be infused with their required dependencies, a practice that enhances code legibility and manageability. Incorporating a DI container further contributes to code that is both readable and maintainable.
3. Magic numbers and strings:
When you use words or numbers in your code, it's important to give them names that make sense. This helps other people who read your code understand what's happening and why.
Using random numbers without clear names can make your code confusing and difficult to manage. If you need to change a number, you have to go through all your code and find every instance of that number to make changes.
For instance, imagine you're comparing a number in your code to the number 5. If a certain condition is met, your code does something specific. The problem is, it's hard to remember why you used the number 5 in the first place. Even if you understood it when you wrote the code, you might forget later on. This could lead to spending time trying to figure out the meaning of that number when you look at the code again.
To avoid this, it's a good idea to give meaningful names to these numbers. This way, anyone reading the code can quickly understand what's going on.
Let's understand by code sample
public class MagicNumberExample
{
public bool IsGreaterThanFive(int value)
{
if (value > 5) // 5 is a magic number here
{
return true;
}
return false;
}
}
This makes the code less readable and understandable, especially if you or someone else revisit the code later.
We can make it more clear and readable.
public class SymbolicConstantExample
{
private const int MinimumValue = 5;
public bool IsGreaterThanMinimum(int value)
{
if (value > MinimumValue) // Using the symbolic constant
{
return true;
}
return false;
}
}
In this improved example, we've defined a constant named MinimumValue
with the value of 5. Now, instead of using the magic number directly, we use the meaningful constant name. This makes the code more clear and easier to understand. If you ever need to change the minimum value, you only have to update it in one place: the constant declaration. This prevents the need to search through the entire codebase for occurrences of the magic number.
4. Yoda conditions:
Yoda conditions happen when you switch around the usual order of things in an "if" statement. Instead of saying "if the number of orders is 100," you say "if 100 is the number of orders." People sometimes do this to make their code shorter.
For example, they might write:
if (100 == numberOfOrders) {
// do something
}
This can make your code look a bit strange, like how Yoda talks in Star Wars. But it makes your code harder to understand, especially for someone who reads it later. Even though some situations might be okay, it's generally a good idea to avoid using Yoda conditions. It's better to keep things in the regular order for clarity.
// int OrderNum =100;
if (numberOfOrders == 100) {
// do something
}
5. Excessive Interface Complexity (Bloated Interface):
Excessive interface complexity, often termed as interface bloat, represents a design issue where an interface becomes needlessly crowded, perplexing, and challenging to work with.
The fundamental cause of interface bloat can be traced back to a failure in setting priorities. The creators of the interface neglected to determine which functionalities held significance and which were extraneous. Consequently, they incorporated all features and possibly even more. This amalgamation of numerous attributes combined with a lack of clarity results in a counterintuitive interface.
To circumvent falling into this pattern, it's essential to establish priorities by scrutinizing the most crucial attributes for your interface. Subsequently, consider the removal of less essential or seldom-used features. The application of the interface segregation principle can also simplify the code, promoting a more manageable and user-friendly interface.
using System;
interface IMediaPlayer
{
void Play();
void Pause();
void Stop();
void IncreaseVolume();
void DecreaseVolume();
void ChangeEqualizerSetting(string setting);
void ShufflePlaylist();
void Repeat();
void AdjustPitch();
}
class MediaPlayer : IMediaPlayer
{
public void Play() { /* Implementation */ }
public void Pause() { /* Implementation */ }
// ... other methods
}
In this example, the IMediaPlayer
interface contains a multitude of methods, some of which might not be necessary for every implementation. This interface complexity can make it harder to use and understand.
Resolution: Prioritize and Simplify the Interface
using System;
interface IMediaPlayer
{
void Play();
void Pause();
void Stop();
void IncreaseVolume();
void DecreaseVolume();
}
interface IAdvancedMediaPlayer
{
void ChangeEqualizerSetting(string setting);
void ShufflePlaylist();
void Repeat();
void AdjustPitch();
}
class MediaPlayer : IMediaPlayer
{
public void Play() { /* Implementation */ }
public void Pause() { /* Implementation */ }
// ... other methods
}
class AdvancedMediaPlayer : IMediaPlayer, IAdvancedMediaPlayer
{
public void Play() { /* Implementation */ }
public void Pause() { /* Implementation */ }
public void ChangeEqualizerSetting(string setting) { /* Implementation */ }
public void ShufflePlaylist() { /* Implementation */ }
// ... other methods
}
6. Excessive Application of the Singleton Pattern:
Singletons are utilized to ensure that a class possesses only one instance.
However, the singleton design pattern becomes "overused" when it's applied indiscriminately to most or all classes within an application. In such instances, singletons can complicate the initial stages of coding by restricting instances to just one, causing potential complications.
An illustrative example of this anti-pattern is the overemployment of the Singleton Pattern for Dependency Injection Containers, often known as ServiceLocators
. If employed extensively, this practice inhibits the injection of test doubles during unit testing.
While some seasoned developers might label any code containing a singleton pattern as suboptimal, I cannot entirely concur. The singleton pattern has garnered a negative reputation primarily due to its incorrect or excessive application. However, when judiciously and correctly employed, it can indeed enhance code quality.
Let's see the example
public class SingletonDatabase
{
private static SingletonDatabase _instance;
private SingletonDatabase() { }
public static SingletonDatabase Instance
{
get
{
if (_instance == null)
{
_instance = new SingletonDatabase();
}
return _instance;
}
}
public void Connect()
{
Console.WriteLine("Connected to the database.");
}
}
public class CustomerRepository
{
private SingletonDatabase _database;
public CustomerRepository()
{
_database = SingletonDatabase.Instance;
}
public void GetCustomerData()
{
_database.Connect();
Console.WriteLine("Fetching customer data from the database.");
}
}
In this example, both the SingletonDatabase
class and the CustomerRepository
class use the Singleton pattern, even though it might not be necessary for both classes.
Resolution: Proper Use of Singleton Pattern
public class Database
{
public void Connect()
{
Console.WriteLine("Connected to the database.");
}
}
public class CustomerRepository
{
private Database _database;
public CustomerRepository(Database database)
{
_database = database;
}
public void GetCustomerData()
{
_database.Connect();
Console.WriteLine("Fetching customer data from the database.");
}
}
In this improved example, we've removed the excessive Singleton usage and resolved the issue. The Database
class is now a regular class, and the CustomerRepository
class receives an instance of Database
through constructor injection. This approach adheres to the Single Responsibility Principle and makes the code more maintainable and flexible.
7. Excessive Use of Primitive Types (Primitive Obsession):
Primitive obsession, an anti-pattern, transpires when developers craft classes containing solely primitive data types like integers or strings. Instead of relying solely on these primitive types, developers should consider employing object-oriented classes to group these primitive attributes. This approach offers heightened flexibility.
Should you identify a set of primitive values being utilized repeatedly across your codebase, it's beneficial to avoid redundant usage by replacing them with intricate data types. These complex types can replace the overload of primitive values, augmenting your source code with improved design. By implementing small classes to represent data, you can incrementally elevate your software design through these modest enhancements.
public class Order
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal CalculateTotal()
{
return Quantity * Price;
}
}
In this example, the Order class uses primitive types like int and string directly to represent order-related data.
Resolution: Proper Use of Object-Oriented Classes
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
}
public class OrderItem
{
public Product Product { get; set; }
public int Quantity { get; set; }
public decimal CalculateSubtotal()
{
return Product.Price * Quantity;
}
}
public class Order
{
public int OrderId { get; set; }
public string CustomerName { get; set; }
public List<OrderItem> OrderItems { get; set; }
public decimal CalculateTotal()
{
return OrderItems.Sum(item => item.CalculateSubtotal());
}
}
In this improved example, we've used object-oriented classes Product
and OrderItem
to group the related attributes together. This approach promotes better code organization and encapsulation, enhancing flexibility and maintainability. The Order
class now contains a list of OrderItem
instances, each representing a product and its quantity in the order. This approach follows object-oriented principles and mitigates the problem of excessive primitive types.
8. Duplication in Programming (Copy Paste Programming):
Duplication in programming occurs when developers copy and paste existing code to different parts of their program, typically as an attempt to save time and effort.
This practice involves taking previously written code and inserting it into new sections of the program. After this insertion, minor adjustments may be made, such as altering variable and method names or making slight modifications to the logic.
However, this approach can lead to code that becomes challenging to maintain. When issues arise in the copied code, rectifying the problem requires modifications to be made. It's crucial to remember that these changes must also be propagated across all the other locations where the code was pasted.
Maintaining such vigilance in every instance is not always guaranteed, is it?
To mitigate the pitfalls of copy-paste programming and the potential for errors, it's advisable to design methods that serve distinct purposes and to maintain a practice of writing concise, focused methods.
There are more anti-patterns than these 8 like Cargo Cult Programming, Manual Memory Management, Golden Hammer etc. But these I see more often hence mentioned these only.
Top comments (7)
Thanks for sharing.
One of the best ways I've found to learn to avoid these anti-patterns is writing tests for code. As testable code needs a certain structure, I found that it reinforces a way of thinking that encourages clearly defined and smaller modules, i.e. shifting away from God classes.
Great job on this article! You've effectively explained the concept of anti-patterns in software development. Anti-patterns are common practices or solutions that may appear effective at first, but ultimately lead to poor design, inefficiency, maintainability issues, or other negative consequences in the long run. You've shed light on these seemingly reasonable solutions that, due to a lack of understanding, misuse of tools, or misinterpretation of best practices, end up causing more problems than they solve. Your article is informative and well-articulated. Keep up the good work!
thank you for paraphrasing the introduction. looks like a AI generated comment. :/
4 gave me a good chuckle
Great Post!! Learned a lot from this.. Looking forward for part 2 of anti patterns!!
Love number 8. This is what I always ask devs to not do. Copying/pasting causes a lot of problems.
Really good article : )