每个开发者都应该知道的反模式
4 让我忍不住笑了起来。
以下是一些每个开发者都应该了解的反模式。
软件开发中的反模式指的是一些常见的做法或解决方案,它们起初看似有效,但最终会导致设计缺陷、效率低下、可维护性问题或其他长期负面影响。这些解决方案看似合理,但由于缺乏理解、工具使用不当或对最佳实践的误解,最终反而会造成更多问题。
以下是一些 C# .NET 开发中的反模式示例:
1. 上帝类/上帝对象:
当单个类或对象承担过多的职责时,就会出现这种情况。这会导致代码耦合过强,难以维护。上帝类(或上帝对象)是一种反模式,它会使代码维护变得困难且风险很高,因此非常危险。
这类类可能承担诸多职责,并且通常作为其他更专业类的直接父类。顾名思义,当这类类存在于系统中时,它们被视为“神”,因为它们往往拥有许多职责和权力,并将这些职责和权力委托给它们的子类。虽然这种方法看似有用,但却可能导致代码量激增,变得复杂且难以理解。
识别这类类的另一种方法是看它是否经过单元测试。通常,这类类与其他类或许多其他依赖项紧密耦合。最好的办法是使用集成测试来测试这类类。
为了避免出现上帝类模式,请遵循以下规则:
类的职责数量应与其规模成正比。如果某个类承担多个职责,必要时应将其拆分为更小的类。
请勿对创建的类及其子类使用相同的名称。如果它们的角色明显不同,则应使用不同的名称。
始终思考特定类存在的条件或其职责对整个系统是否重要。务必确保这些条件和职责不会出现任何冲突,以便在需要时能够轻松处理。
这也违反了单一职责原则。通过将职责分解成独立的类,我们可以实现单一职责原则。
public class God
{
private DatabaseManager dbManager;
private ReportGenerator reportGenerator;
private EmailSender emailSender;
// Methods handling various unrelated tasks
}
2. 意大利面条式代码:
这种代码结构混乱、杂乱无章,关注点分离度低,难以理解、调试和扩展。
当代码组织不当,导致理解困难时,就会出现被称为“意大利面条式代码”的反模式。
这种代码缺乏模块化结构、清晰的职责划分和可读性。由于其复杂性,意大利面条式代码的维护和修改都非常繁琐。代码审查和重构等实践已被证明能有效预防和解决意大利面条式代码问题。一种谨慎的方法是将大型代码块拆分成更小、可重用的代码段。
保持简洁的方法,每个方法专注于单一目的,也十分有益。Robert C. Martin 的《代码整洁之道:敏捷软件开发手册》深入探讨了多种预防或解决意大利面条式代码问题的方法。
为了避免这种反模式,明确每个类的预期职责至关重要。此外,规避这种困境还需要以一种能够协调一致地组织类的方法的方式来设计类,从而提高代码的可理解性和易于维护性。首要目标应该是实现代码的简洁性。
避免这种反模式的另一种方法是使用依赖注入(DI)。通过 DI,可以将所需的依赖项注入到类中,这种做法可以提高代码的可读性和可维护性。引入 DI 容器可以进一步提高代码的可读性和可维护性。
3. 魔法数字和字符串:
在代码中使用单词或数字时,务必给它们取有意义的名称。这有助于其他阅读代码的人理解代码的运行机制和原因。
使用没有明确名称的随机数会让代码变得混乱且难以管理。如果需要更改某个数字,则必须遍历所有代码,找到所有出现该数字的地方才能进行修改。
例如,假设你在代码中将一个数字与数字 5 进行比较。如果满足某个特定条件,代码会执行特定操作。问题在于,你很难记住当初为什么使用数字 5。即使你在编写代码时理解了它的含义,之后也可能忘记。这会导致你再次查看代码时,不得不花费时间去理解这个数字的含义。
为了避免这种情况,最好给这些数字赋予有意义的名称。这样,任何阅读代码的人都能快速理解代码的运行逻辑。
让我们通过代码示例来理解。
public class MagicNumberExample
{
public bool IsGreaterThanFive(int value)
{
if (value > 5) // 5 is a magic number here
{
return true;
}
return false;
}
}
这会使代码的可读性和可理解性降低,尤其是在您或其他人在以后重新查看代码时。
我们可以使其更清晰易读。
public class SymbolicConstantExample
{
private const int MinimumValue = 5;
public bool IsGreaterThanMinimum(int value)
{
if (value > MinimumValue) // Using the symbolic constant
{
return true;
}
return false;
}
}
在这个改进后的示例中,我们定义了一个名为 `min` 的常量,MinimumValue其值为 5。现在,我们不再直接使用“魔数”,而是使用更有意义的常量名称。这使得代码更清晰易懂。如果需要更改最小值,只需在一个地方进行更新:即常量声明处。这样就避免了在整个代码库中搜索“魔数”的麻烦。
4. 尤达条件:
尤达条件是指在“if”语句中颠倒通常的顺序。例如,不写“如果订单数量是100”,而是写“如果100是订单数量”。人们有时会这样做来简化代码。
例如,他们可能会写道:
if (100 == numberOfOrders) {
// do something
}
这会让你的代码看起来有点怪异,就像《星球大战》里尤达大师说话的方式一样。但这会让你的代码更难理解,尤其是对于之后阅读它的人来说。虽然某些情况下可能没问题,但通常最好避免使用这种“尤达式”的条件语句。为了清晰起见,最好保持代码的常规顺序。
// int OrderNum =100;
if (numberOfOrders == 100) {
// do something
}
5. 界面过于复杂(臃肿的界面):
界面过于复杂,通常被称为界面臃肿,是一种设计问题,指界面变得不必要地拥挤、复杂且难以使用。
界面臃肿的根本原因可以追溯到优先级设置不当。界面创建者未能确定哪些功能是重要的,哪些是多余的。因此,他们将所有功能甚至更多都塞了进去。这种大量属性的堆砌,加上缺乏清晰度,最终导致界面反直觉。
为了避免陷入这种模式,至关重要的是要通过仔细审查界面中最关键的属性来确定优先级。随后,考虑移除不太重要或很少使用的功能。应用界面隔离原则还可以简化代码,从而打造更易于管理且用户友好的界面。
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
}
在这个例子中,IMediaPlayer接口包含大量方法,其中一些方法可能并非每个实现都需要。这种接口的复杂性会增加使用和理解的难度。
解决方案:优先考虑并简化界面
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. 单例模式的过度应用:
单例模式用于确保一个类只有一个实例。
然而,当单例模式被不加区分地应用于应用程序中的大多数或所有类时,它就会被“过度使用”。在这种情况下,单例模式会将实例数量限制为一个,从而使编码的初始阶段变得复杂,并可能导致潜在的问题。
这种反模式的一个典型例子是依赖注入容器(通常称为单例模式)的过度使用ServiceLocators。如果大量使用,这种做法会阻碍单元测试期间测试替身的注入。
虽然一些经验丰富的开发者可能会认为任何包含单例模式的代码都不够理想,但我并不完全认同这种观点。单例模式之所以名声不佳,主要是因为其使用不当或过度。然而,如果运用得当,它确实可以提升代码质量。
我们来看一个例子。
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.");
}
}
在这个例子中,SingletonDatabase类 A 和类CustomerRepositoryB 都使用了单例模式,尽管对于这两个类来说可能并非必要。
解决方案:正确使用单例模式
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.");
}
}
在这个改进后的示例中,我们移除了过多的单例模式使用,解决了这个问题。Database现在,该类是一个普通的类,并通过构造函数注入CustomerRepository来接收实例Database。这种方法遵循单一职责原则,使代码更易于维护和灵活。
7. 过度使用基本类型(基本类型执念):
基本类型执念是一种反模式,指的是开发者创建的类只包含整数或字符串等基本数据类型。开发者不应仅仅依赖这些基本类型,而应考虑使用面向对象的类来组织这些基本属性。这种方法可以提供更高的灵活性。
如果您发现代码库中反复使用一组基本类型值,最好将其替换为更复杂的数据类型,以避免冗余使用。这些复杂类型可以替代基本类型的冗余值,从而改进源代码设计。通过实现用于表示数据的小型类,您可以逐步提升软件设计,实现这些适度的改进。
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;
}
}
在这个例子中,Order 类直接使用 int 和 string 等原始类型来表示与订单相关的数据。
解决方案:正确使用面向对象类
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());
}
}
在这个改进的示例中,我们使用了面向对象的类Product,OrderItem并将相关的属性分组在一起。这种方法有助于更好地组织代码和封装,从而增强灵活性和可维护性。Order现在,该类包含一个实例列表OrderItem,每个实例代表一个产品及其在订单中的数量。这种方法遵循面向对象的原则,并缓解了原始类型过多的问题。
8. 编程中的重复(复制粘贴编程):
编程中的重复是指开发人员将现有代码复制并粘贴到程序的不同部分,通常是为了节省时间和精力。
这种做法是将先前编写的代码插入到程序的新部分中。插入之后,可以进行一些细微的调整,例如更改变量和方法名称,或对逻辑进行一些小的修改。
然而,这种方法会导致代码难以维护。当复制的代码出现问题时,需要进行修改才能解决问题。务必记住,这些更改也必须同步到所有其他粘贴了该代码的位置。
在任何情况下都保持这种警惕性并非总能得到保证,对吧?
为了减轻复制粘贴编程的弊端和潜在的错误,建议设计具有明确用途的方法,并保持编写简洁、专注的方法的习惯。
除了这 8 种反模式之外,还有更多,例如货物崇拜式编程、手动内存管理、金锤效应等等。但我更常见到这几种,所以只提到了它们。
文章来源:https://dev.to/yogini16/anti-patterns-that-every-developer-should-know-4nph

