C#面向对象编程核心知识与设计模式详解
简介:面向对象编程(OOP)是一种广泛应用的编程范式,以对象为核心,将数据与操作封装在一起。本文聚焦C#语言中的面向对象核心特性,深入讲解封装、继承、多态三大基础概念,并结合设计模式与SOLID设计原则,帮助开发者构建高内聚、低耦合、易于维护的软件系统。通过工厂模式、单例模式、观察者模式等经典设计模式的实战解析,结合接口隔离、依赖倒置等设计原则,提升代码的灵活性与可扩展性。 
1. 面向对象三大核心概念:封装、继承、多态
面向对象编程(OOP)是现代软件开发的基础范式,其核心理念围绕三大支柱: 封装(Encapsulation) 、 继承(Inheritance) 与 多态(Polymorphism) 。它们共同构建了模块化、可复用、可扩展的代码结构。
封装 通过将数据设为私有(private),仅通过公开(public)方法访问,保护内部状态。例如在C#中,使用属性(property)包装字段,实现数据访问控制。
继承 允许派生类复用基类的属性和方法,形成类的层级结构,提升代码复用性和逻辑组织能力。
多态 则通过虚方法(virtual)和重写(override),使同一接口在不同对象中有不同实现,增强了系统的灵活性与可扩展性。
这三大特性相辅相成,构成了C#中设计模式与高级架构设计的基础。
2. C#访问修饰符与信息隐藏
在面向对象编程中,访问修饰符是控制类成员可见性和可访问性的关键机制。C# 提供了丰富的访问修饰符,帮助开发者在设计类结构时实现信息隐藏(Information Hiding),从而增强系统的安全性与封装性。本章将深入探讨 C# 中各类访问修饰符的使用场景与默认规则,并通过具体代码示例展示信息隐藏的实现方式。最终,我们将结合实践构建具有合理访问控制的类结构,防止信息泄露与滥用。
2.1 访问修饰符的种类与作用
C# 提供了五种主要的访问修饰符: public 、 private 、 protected 、 internal 与 protected internal 。这些修饰符决定了类成员(如字段、方法、属性等)在不同作用域下的可访问性。
2.1.1 public、private、protected、internal与protected internal的使用场景
以下是对每种访问修饰符的详细说明及其典型使用场景:
| 修饰符 | 可访问范围 | 使用场景 |
|---|---|---|
public |
同一程序集内或外部引用的任何代码 | 类、接口、公共方法、公开属性等 |
private |
仅限定义该成员的类内部 | 类的私有方法、字段、辅助函数等 |
protected |
类及其派生类内部 | 基类中需要被子类继承并重写的成员 |
internal |
同一程序集内的任何代码 | 同一项目或 DLL 内部使用的成员,对外不可见 |
protected internal |
同一程序集内或派生类中的代码 | 允许程序集内部和继承链中的访问 |
代码示例:访问修饰符应用
// 文件:Person.cs
namespace AccessModifiersDemo
{
public class Person
{
// 公共字段,外部可直接访问
public string Name;
// 私有字段,仅限Person类内部访问
private int age;
// 受保护字段,仅限Person类及其子类访问
protected string Gender;
// 内部字段,仅限同一程序集访问
internal string Nationality;
// 受保护且内部字段,同一程序集或子类中可访问
protected internal string Email;
// 公共方法,用于设置年龄
public void SetAge(int age)
{
// 只有合法年龄才设置
if (age > 0 && age < 150)
this.age = age;
}
// 私有方法,仅在类内部调用
private void LogInfo()
{
Console.WriteLine($"Name: {Name}, Age: {age}");
}
}
}
逐行解读与逻辑分析:
- 第6行:
public class Person定义了一个公共类,意味着其他程序集可以引用该类。 - 第9行:
public string Name;是一个公开字段,允许外部直接读写。 - 第12行:
private int age;是私有字段,只能在Person类内部访问。 - 第15行:
protected string Gender;仅允许Person类及其子类访问。 - 第18行:
internal string Nationality;仅限当前程序集访问。 - 第21行:
protected internal string Email;表示在当前程序集内或子类中可访问。 - 第24~29行:
SetAge是一个公共方法,用于安全设置age字段的值。 - 第32~35行:
LogInfo是私有方法,用于记录日志,仅在类内部调用。
流程图:访问修饰符的作用范围
graph TD
A[Public] --> B[外部程序集]
A --> C[同一程序集]
D[Private] --> E[仅限类内部]
F[Protected] --> G[类本身]
F --> H[派生类]
I[Internal] --> J[同一程序集]
K[Protected Internal] --> L[同一程序集]
K --> M[派生类]
2.1.2 默认访问级别的规则与影响
在 C# 中,如果不显式指定访问修饰符,编译器会使用默认的访问级别。默认规则如下:
- 类成员(字段、方法、属性等)的默认访问级别是
private - 类(class)的默认访问级别是
internal - 接口(interface)的默认访问级别是
internal - 枚举(enum)的默认访问级别是
internal
影响分析:
默认访问级别有助于简化代码,但也可能导致安全隐患。例如,如果一个类没有显式指定为 public ,那么它只能在当前程序集内访问,这对于封装性是有利的;但如果类成员默认为 private ,则可能需要显式声明 protected 或 internal 才能被其他类或派生类访问。
示例:默认访问级别的影响
class DefaultAccess
{
int number; // 默认 private
void Display() // 默认 private
{
Console.WriteLine(number);
}
}
逐行解读:
- 第1行:类
DefaultAccess默认为internal,只能在当前程序集内访问。 - 第3行:字段
number默认为private,只能在类内部访问。 - 第4行:方法
Display默认为private,只能在类内部调用。
建议:
- 对于类成员,应根据实际需求显式指定访问级别,避免因默认规则导致意外暴露。
- 如果类需要被外部访问,应显式声明为
public。 - 对于仅在派生类中使用的成员,应使用
protected。
2.2 信息隐藏的实现机制
信息隐藏是面向对象设计中的核心原则之一,旨在限制对类内部状态的直接访问,以增强系统的安全性和可维护性。C# 提供了多种机制来实现信息隐藏,包括使用访问修饰符、属性封装、只读字段等。
2.2.1 封装数据与行为的必要性
封装(Encapsulation)是指将数据(字段)和操作数据的行为(方法)绑定在一起,并通过访问控制机制限制外部对其的直接访问。这样做的好处包括:
- 防止非法访问和修改对象状态
- 提高代码的可维护性
- 支持模块化设计,降低耦合度
示例:未封装与封装的对比
// 未封装的类
public class BankAccount
{
public decimal Balance;
}
// 封装后的类
public class BankAccountSafe
{
private decimal balance;
public decimal GetBalance()
{
return balance;
}
public void Deposit(decimal amount)
{
if (amount > 0)
balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount > 0 && balance >= amount)
balance -= amount;
}
}
逻辑分析:
BankAccount类中Balance是public,外部可以直接修改其值,存在安全隐患。BankAccountSafe类将balance设为private,并通过Deposit和Withdraw方法控制其修改,确保数据的合法性和一致性。
2.2.2 使用属性与方法控制访问
在 C# 中, 属性(Property) 是实现封装的重要手段。属性提供了一种类似于字段的语法,但背后通过 get 和 set 方法控制访问逻辑。
示例:使用属性封装字段
public class Person
{
private string name;
public string Name
{
get { return name; }
set
{
if (!string.IsNullOrEmpty(value))
name = value;
else
throw new ArgumentException("Name cannot be null or empty.");
}
}
}
逐行解读:
- 第3行:
private string name;是类的私有字段。 - 第5~13行:
Name属性提供对外访问接口。 get方法返回字段值。set方法进行输入验证,避免非法值被赋值。
优势:
- 外部调用者使用
person.Name = "John"的方式访问,与访问字段一致。 - 内部可以通过
set添加验证逻辑,提高安全性。
表格:字段 vs 属性
| 特性 | 字段(Field) | 属性(Property) |
|---|---|---|
| 可访问性 | 直接访问 | 通过 get/set 方法访问 |
| 数据验证 | 无 | 可添加逻辑验证 |
| 线程安全性 | 不易控制 | 可在 get/set 中加入锁机制 |
| 接口实现 | 不支持 | 支持 |
| 序列化 | 支持 | 支持 |
2.3 实践:构建安全的类结构
在实际开发中,构建具有合理访问控制的类结构是保障系统安全与稳定的关键。本节将通过一个完整示例,演示如何设计一个具备信息隐藏机制的安全类,并讨论避免信息泄露与滥用的技巧。
2.3.1 设计具有合理访问控制的类
示例:设计一个安全的 User 类
public class User
{
private string username;
private string passwordHash;
private bool isActive;
public string Username
{
get { return username; }
private set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Username cannot be empty.");
username = value;
}
}
public bool IsActive
{
get { return isActive; }
set { isActive = value; }
}
public User(string username, string password)
{
SetUsername(username);
SetPassword(password);
}
private void SetUsername(string username)
{
if (username.Length < 3)
throw new ArgumentException("Username must be at least 3 characters.");
this.username = username;
}
public void SetPassword(string password)
{
if (password.Length < 6)
throw new ArgumentException("Password must be at least 6 characters.");
this.passwordHash = HashPassword(password);
}
private string HashPassword(string password)
{
// 模拟密码哈希处理
return "Hashed_" + password.GetHashCode();
}
}
逐行解读与逻辑分析:
- 第1~4行:定义类成员字段,全部设为
private,防止外部直接访问。 - 第6~15行:
Username属性提供只读访问,set为private,防止外部赋值。 - 第17~20行:
IsActive属性允许外部读取和设置。 - 第22~27行:构造函数接收用户名和密码,调用私有方法进行设置。
- 第29~33行:
SetUsername方法对用户名进行长度验证。 - 第35~39行:
SetPassword方法验证密码长度,并调用哈希方法存储。 - 第41~45行:
HashPassword是私有方法,防止外部调用。
优势:
- 密码不会以明文形式存储。
- 所有字段都通过属性或私有方法控制访问。
- 构造函数与私有方法共同确保对象状态的合法性。
2.3.2 避免信息泄露与滥用的技巧
在类设计中,开发者应遵循以下原则来避免信息泄露与滥用:
- 最小访问权限原则 :只对外暴露必要的成员,其余设为
private或internal。 - 只读属性 :对于不应被外部修改的属性,提供
get访问器,set设为private。 - 敏感数据加密/哈希处理 :如密码、密钥等数据应加密或哈希后存储。
- 避免返回引用类型字段的引用 :防止外部修改对象内部状态。
- 使用
sealed类与readonly字段 :防止类被继承或字段被修改。
示例:避免返回引用类型字段
public class Employee
{
private List<string> roles = new List<string>();
public IReadOnlyList<string> Roles => roles.AsReadOnly();
public void AddRole(string role)
{
roles.Add(role);
}
}
分析:
Roles属性返回IReadOnlyList<string>,外部只能读取不能修改。- 若直接返回
roles,外部可调用Add方法修改列表内容。
通过本章的学习,我们深入理解了 C# 中访问修饰符的种类与使用场景,掌握了信息隐藏的实现机制,并通过实际案例构建了具备合理访问控制的安全类结构。这些知识为后续章节中类的继承、多态与设计模式的应用打下了坚实的基础。
3. 类的继承与方法重写实现
面向对象编程的核心之一在于继承机制,它允许一个类(派生类)基于另一个类(基类)的结构和行为进行扩展。这种机制不仅促进了代码复用,也支持了程序结构的层次化设计,为后续的多态与接口设计奠定了基础。在本章中,我们将深入探讨类的继承机制、方法重写的实现方式,以及如何通过继承构建可扩展的类层次结构。
3.1 继承的基本机制
继承是面向对象编程中最基础、也是最强大的特性之一。它允许一个类(派生类)从另一个类(基类)中继承字段、属性、方法等成员,从而实现代码的复用和结构的扩展。
3.1.1 基类与派生类的关系
在 C# 中,类可以通过使用冒号 : 指定其基类,从而建立继承关系。例如:
public class Animal
{
public void Eat()
{
Console.WriteLine("This animal is eating.");
}
}
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("The dog is barking.");
}
}
代码逻辑分析:
Animal是基类,定义了一个公共方法Eat()。Dog是派生类,继承自Animal,并新增了Bark()方法。- 在
Dog类中可以直接调用Eat(),因为它继承了该方法。
参数说明:
Dog : Animal表示Dog类继承自Animal。public void Eat():公共访问权限允许派生类和外部调用。public void Bark():新增方法,仅在Dog类及其派生类中可见。
继承关系结构图(Mermaid 流程图):
classDiagram
Animal <|-- Dog
Animal : +Eat()
Dog : +Bark()
3.1.2 继承中的构造函数调用顺序
构造函数的调用顺序是继承机制中的重要知识点。C# 中,派生类的构造函数默认会调用基类的无参构造函数。如果基类没有无参构造函数,必须显式调用某个构造函数。
public class Animal
{
public Animal(string name)
{
Name = name;
Console.WriteLine($"Animal constructor called: {Name}");
}
public string Name { get; }
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
Console.WriteLine($"Dog constructor called: {Name}");
}
}
代码逻辑分析:
Animal类的构造函数接受一个name参数。Dog类的构造函数通过base(name)显式调用基类构造函数。- 输出结果将显示先调用基类构造函数,再执行派生类构造函数。
输出示例:
Animal constructor called: Buddy
Dog constructor called: Buddy
构造函数调用顺序说明:
- 创建
Dog实例时,首先调用Animal的构造函数。 - 然后执行
Dog的构造函数体。 - 如果基类有多个构造函数,派生类可以选择调用特定的构造函数。
3.2 方法重写与虚方法
在继承的基础上,方法重写(Override)允许派生类重新定义基类中定义的方法,从而实现多态行为。C# 提供了 virtual 和 override 关键字来支持方法重写。
3.2.1 override 与 virtual 关键字的作用
为了实现方法重写,基类中的方法需要被标记为 virtual ,派生类使用 override 来覆盖该方法。
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal makes a sound.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Dog barks.");
}
}
代码逻辑分析:
MakeSound()在基类中被标记为virtual,表示该方法可以在派生类中被重写。Dog类中使用override重写了该方法,改变了其行为。- 多态调用时,会根据实际对象类型执行相应的方法。
使用示例:
Animal myAnimal = new Dog();
myAnimal.MakeSound(); // 输出 "Dog barks."
输出说明:
- 虽然变量类型是
Animal,但实际对象是Dog,因此调用的是Dog的MakeSound()方法。
3.2.2 避免隐藏方法与正确使用 new 关键字
如果派生类定义了一个与基类同名但未使用 override 的方法,则称为方法隐藏(Method Hiding)。C# 中应使用 new 显式声明此行为,以避免歧义。
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal makes a sound.");
}
}
public class Cat : Animal
{
public new void MakeSound()
{
Console.WriteLine("Cat meows.");
}
}
代码逻辑分析:
Cat类使用new显式隐藏了Animal的MakeSound()方法。- 如果使用
Animal类型变量调用,将执行基类方法;如果是Cat类型变量,执行派生类方法。
示例对比:
Animal animal = new Cat();
animal.MakeSound(); // 输出 "Animal makes a sound."
Cat cat = new Cat();
cat.MakeSound(); // 输出 "Cat meows."
使用建议:
- 避免在非
virtual方法上使用new,否则可能导致难以察觉的逻辑错误。 - 明确使用
new或override,以增强代码的可读性和维护性。
3.3 实践:构建可扩展的类层次结构
继承的最终目标是构建具有清晰层次、易于扩展的类结构。在实际开发中,合理的继承设计能够提升系统的可维护性与灵活性。
3.3.1 设计继承树以支持业务扩展
考虑一个业务场景:开发一个动物管理系统,支持不同种类的动物具有不同的行为。
public abstract class Animal
{
public abstract void MakeSound();
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow!");
}
}
public class Cow : Animal
{
public override void MakeSound()
{
Console.WriteLine("Moo!");
}
}
代码逻辑分析:
- 使用
abstract抽象类Animal定义抽象方法MakeSound()。 - 每个具体动物类(如
Dog,Cat,Cow)都实现了该方法。 - 可以通过统一接口管理所有动物行为。
优势说明:
- 扩展性强:新增动物类型只需继承并实现
MakeSound()。 - 一致性高:所有动物遵循相同接口,便于统一处理。
3.3.2 利用继承实现代码复用与定制
在实际项目中,很多类会共享一些通用逻辑,此时可以通过继承复用这些逻辑,并在子类中进行定制。
public class Vehicle
{
public virtual void Start()
{
Console.WriteLine("Vehicle is starting.");
}
public void Stop()
{
Console.WriteLine("Vehicle is stopping.");
}
}
public class Car : Vehicle
{
public override void Start()
{
Console.WriteLine("Car engine is starting...");
}
}
public class Bike : Vehicle
{
public override void Start()
{
Console.WriteLine("Bike is being pedaled to start.");
}
}
代码逻辑分析:
Vehicle类定义了通用的Start()和Stop()方法。Car和Bike分别重写了Start()方法,实现了不同的启动方式。Stop()方法未被重写,在所有子类中保持一致。
方法调用示例:
Vehicle car = new Car();
car.Start(); // 输出 "Car engine is starting..."
car.Stop(); // 输出 "Vehicle is stopping."
Vehicle bike = new Bike();
bike.Start(); // 输出 "Bike is being pedaled to start."
优势总结:
Stop()方法在多个子类中复用,避免重复代码。Start()方法根据不同子类定制实现,体现多态特性。
总结:
本章详细介绍了 C# 中类的继承机制、方法重写的实现方式以及如何通过继承构建可扩展的类结构。通过合理使用继承,我们不仅可以提高代码复用率,还能使系统具备良好的可维护性和可扩展性。在下一章中,我们将进一步探讨多态的实现机制,特别是虚方法和接口在运行时多态中的作用。
4. 虚方法与接口实现多态
4.1 多态的基本原理
4.1.1 编译时多态与运行时多态的区别
多态(Polymorphism)是面向对象编程的三大核心特性之一,其核心在于对象在不同上下文中表现出不同的行为。C# 中多态分为两种形式: 编译时多态(静态多态) 和 运行时多态(动态多态) 。
| 类型 | 特点 | 实现方式 |
|---|---|---|
| 编译时多态 | 在编译阶段确定调用哪个方法 | 方法重载(Overload) |
| 运行时多态 | 在运行阶段根据对象实际类型确定 | 方法重写(Override)+ 虚方法 |
示例代码:
// 编译时多态:方法重载
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
// 运行时多态:虚方法与重写
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal speaks");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Dog barks");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}
逻辑分析:
- 编译时多态 :
Calculator类中定义了两个Add方法,参数类型不同。编译器根据传入参数的类型选择具体方法,调用时无需运行时判断。 - 运行时多态 :
Animal类中定义了虚方法Speak,Dog和Cat分别重写该方法。当通过Animal类型引用实际对象时,会根据对象的真实类型在运行时决定调用哪一个Speak方法。
参数说明:
virtual:表示该方法可以在派生类中被重写。override:用于在派生类中覆盖基类的虚方法。
4.1.2 多态在面向对象设计中的意义
多态在设计和实现面向对象系统中具有重要意义,主要体现在以下几个方面:
- 解耦调用与实现 :客户端代码可以面向接口或基类编程,无需关心具体实现类。
- 增强可扩展性 :新增功能只需继承或实现接口,无需修改已有代码。
- 提高代码复用性 :多个类共享相同接口,行为实现可复用。
- 支持策略模式 :不同算法或行为可以通过多态实现动态切换。
示例:使用多态处理不同动物叫声
class Program
{
static void Main(string[] args)
{
Animal[] animals = new Animal[] { new Dog(), new Cat() };
foreach (var animal in animals)
{
animal.Speak(); // 根据实际对象类型动态调用方法
}
}
}
执行逻辑说明:
animals数组中的每个元素虽然声明为Animal类型,但实际指向Dog或Cat的实例。- 调用
Speak()时,由于是虚方法,C# 运行时会根据对象的实际类型调用对应的方法。 - 输出结果为:
Dog barks Cat meows
总结:
- 编译时多态适用于静态方法、构造函数、操作符等,运行时多态适用于虚方法和接口实现。
- 多态是实现“面向接口编程”的基础,也是设计模式(如策略模式、工厂模式)的核心机制。
4.2 虚方法与抽象方法的使用
4.2.1 通过虚方法实现运行时绑定
虚方法是实现运行时多态的关键。在 C# 中,通过 virtual 关键字定义虚方法,并在派生类中使用 override 来重写方法。
流程图说明:虚方法调用流程
graph TD
A[调用虚方法] --> B{对象是否被重写?}
B -->|是| C[调用派生类实现]
B -->|否| D[调用基类实现]
示例代码:
public class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing a shape.");
}
}
public class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle.");
}
}
public class Square : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a square.");
}
}
逻辑分析:
Shape类中的Draw方法被定义为virtual,允许派生类重写。Circle和Square都重写了Draw方法,分别输出不同内容。- 客户端代码可以通过统一的
Shape引用来调用不同子类的方法。
使用场景:
- 当多个子类具有相似行为但实现方式不同时。
- 当希望客户端代码无需关心具体实现细节,仅依赖于基类或接口。
4.2.2 抽象类与接口的选择策略
在 C# 中,抽象类和接口都可以用于实现多态,但它们有本质区别和适用场景。
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 成员实现 | 可包含具体方法实现 | 默认不包含实现(C# 8+可默认) |
| 构造函数 | 支持 | 不支持 |
| 多继承 | 不支持 | 支持 |
| 状态维护 | 支持 | 不支持 |
| 适用场景 | 紧密相关的类族共享实现 | 不相关类共享方法契约 |
示例代码:抽象类与接口对比
// 抽象类
public abstract class Animal
{
public abstract void Speak();
public void Sleep()
{
Console.WriteLine("Animal is sleeping.");
}
}
// 接口
public interface IDrawable
{
void Draw();
}
逻辑分析:
- 抽象类
Animal包含抽象方法Speak和具体方法Sleep,子类必须实现Speak,可直接使用Sleep。 - 接口
IDrawable定义契约方法Draw,实现类必须提供具体实现。
选择策略:
- 优先使用接口 :当你希望多个不相关的类共享方法定义,或需要支持多继承时。
- 使用抽象类 :当你希望提供部分实现、维护状态或定义构造函数时。
4.3 接口的设计与实现
4.3.1 接口的定义与实现方式
接口定义了一组行为规范,任何实现该接口的类都必须提供这些行为的具体实现。
定义接口:
public interface ILogger
{
void Log(string message);
}
实现接口:
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
public class FileLogger : ILogger
{
public void Log(string message)
{
// 写入文件逻辑
Console.WriteLine($"Writing to file: {message}");
}
}
逻辑分析:
ILogger接口定义了一个Log方法,任何日志类都必须实现该方法。ConsoleLogger和FileLogger分别实现了控制台和文件日志功能。
使用方式:
class Program
{
static void Main(string[] args)
{
ILogger logger = new ConsoleLogger();
logger.Log("Application started.");
logger = new FileLogger();
logger.Log("User logged in.");
}
}
执行逻辑说明:
ILogger类型引用可以指向任意实现类的实例。- 根据实际对象类型,
Log方法调用不同的实现。 - 输出结果:
Log: Application started. Writing to file: User logged in.
4.3.2 显式接口实现与隐式接口实现的对比
C# 支持两种接口实现方式: 显式接口实现 和 隐式接口实现 。
| 实现方式 | 特点 | 适用场景 |
|---|---|---|
| 隐式接口实现 | 类成员直接实现接口方法,访问权限为 public | 一般场景,希望方法公开访问 |
| 显式接口实现 | 方法名前加上接口名,只能通过接口引用访问 | 避免命名冲突、限制访问范围 |
示例代码:
public interface ILogin
{
void Authenticate();
}
public class UserService : ILogin
{
// 隐式实现
public void Authenticate()
{
Console.WriteLine("User authenticated.");
}
// 显式实现
void ILogin.Authenticate()
{
Console.WriteLine("Explicit interface implementation.");
}
}
使用方式:
class Program
{
static void Main(string[] args)
{
UserService user = new UserService();
user.Authenticate(); // 调用隐式实现
ILogin login = user;
login.Authenticate(); // 调用显式实现
}
}
执行结果:
User authenticated.
Explicit interface implementation.
逻辑分析:
- 隐式实现的
Authenticate方法可以直接通过类实例调用。 - 显式实现的
Authenticate方法只能通过接口引用调用,类实例无法直接访问。 - 显式实现可以避免命名冲突,尤其在实现多个接口时非常有用。
4.4 实践:多态在业务逻辑解耦中的应用
4.4.1 利用多态实现灵活的策略模式
策略模式是一种行为型设计模式,它允许在运行时选择算法或行为。多态是策略模式实现的核心。
结构图:
classDiagram
class Context {
+SetStrategy(IStrategy)
+ExecuteStrategy()
}
class IStrategy {
<<interface>>
+Execute()
}
class ConcreteStrategyA {
+Execute()
}
class ConcreteStrategyB {
+Execute()
}
Context --> IStrategy
IStrategy <|-- ConcreteStrategyA
IStrategy <|-- ConcreteStrategyB
示例代码:
public interface IStrategy
{
void Execute();
}
public class StrategyA : IStrategy
{
public void Execute()
{
Console.WriteLine("Executing Strategy A");
}
}
public class StrategyB : IStrategy
{
public void Execute()
{
Console.WriteLine("Executing Strategy B");
}
}
public class Context
{
private IStrategy _strategy;
public void SetStrategy(IStrategy strategy)
{
_strategy = strategy;
}
public void ExecuteStrategy()
{
_strategy.Execute();
}
}
使用方式:
class Program
{
static void Main(string[] args)
{
Context context = new Context();
context.SetStrategy(new StrategyA());
context.ExecuteStrategy(); // 输出 Strategy A
context.SetStrategy(new StrategyB());
context.ExecuteStrategy(); // 输出 Strategy B
}
}
执行结果:
Executing Strategy A
Executing Strategy B
逻辑分析:
Context类通过接口IStrategy调用具体策略的Execute方法。- 策略可在运行时动态更换,实现行为解耦。
- 多态机制确保
Execute调用正确的方法实现。
4.4.2 接口驱动开发在系统设计中的优势
接口驱动开发(Interface-Driven Development)是一种以接口为核心的设计理念,强调在设计初期就定义清晰的行为契约。
优势:
- 模块解耦 :接口定义行为,实现类独立变化,降低模块间依赖。
- 易于测试 :可以通过 Mock 接口实现快速单元测试。
- 支持多态扩展 :通过接口引用调用不同实现,实现灵活扩展。
- 提升代码可读性 :接口定义清晰,便于团队协作与维护。
示例:订单处理系统
public interface IPaymentProcessor
{
bool ProcessPayment(decimal amount);
}
public class CreditCardProcessor : IPaymentProcessor
{
public bool ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount:C}");
return true;
}
}
public class PayPalProcessor : IPaymentProcessor
{
public bool ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount:C}");
return true;
}
}
public class OrderProcessor
{
private IPaymentProcessor _processor;
public OrderProcessor(IPaymentProcessor processor)
{
_processor = processor;
}
public void ProcessOrder(decimal amount)
{
if (_processor.ProcessPayment(amount))
{
Console.WriteLine("Order processed successfully.");
}
}
}
使用方式:
class Program
{
static void Main(string[] args)
{
var creditCard = new CreditCardProcessor();
var order1 = new OrderProcessor(creditCard);
order1.ProcessOrder(100.0m);
var payPal = new PayPalProcessor();
var order2 = new OrderProcessor(payPal);
order2.ProcessOrder(200.0m);
}
}
执行结果:
Processing credit card payment of $100.00
Order processed successfully.
Processing PayPal payment of $200.00
Order processed successfully.
逻辑分析:
OrderProcessor通过IPaymentProcessor接口处理支付,具体支付方式由构造函数注入。- 支持多种支付方式的扩展,且无需修改
OrderProcessor类。 - 符合开闭原则,增强系统的可扩展性和可测试性。
本章小结:
- 多态是面向对象编程的核心,分为编译时多态与运行时多态。
- 虚方法和接口是实现运行时多态的关键机制,各有适用场景。
- 显式接口实现可以限制访问权限,避免命名冲突。
- 策略模式是多态在实际业务中的一种典型应用,实现行为解耦。
- 接口驱动开发提升系统设计的灵活性、可测试性和可维护性。
本章内容为后续章节中设计模式的应用(如策略模式、工厂模式)奠定了基础。
5. 工厂模式设计与实现
工厂模式是一种创建型设计模式,广泛应用于面向对象编程中,其核心目标是 将对象的创建过程封装起来 ,从而实现调用方与具体类之间的解耦。这种设计方式提高了代码的可维护性、可扩展性和可测试性,是构建大型系统时不可或缺的设计思想之一。
在C#中,工厂模式主要有三种实现形式: 简单工厂模式(Simple Factory) 、 工厂方法模式(Factory Method) 和 抽象工厂模式(Abstract Factory) 。每种模式适用于不同的场景,具有各自的优缺点。
本章将从这三种模式的基本结构入手,通过C#代码示例详细说明其实现机制,并探讨其在实际项目中的应用方式。同时,还将介绍如何将工厂模式与 依赖注入(Dependency Injection) 相结合,以进一步提升系统的灵活性与可维护性。
5.1 工厂模式的分类与结构
工厂模式的三种主要形式在结构和适用场景上有所不同,理解它们的差异有助于我们在实际开发中做出合适的选择。
5.1.1 简单工厂模式(Simple Factory)
简单工厂模式 并不是GoF(四人组)定义的23种设计模式之一,但由于其实现简单、结构清晰,广泛应用于实际项目中。
特点:
- 一个工厂类负责所有具体类的创建逻辑。
- 客户端只需通过工厂类传入参数即可获取所需对象。
- 工厂类通常使用条件判断或反射机制来决定创建哪种对象。
适用场景:
- 对象种类较少,且创建逻辑不复杂。
- 客户端不需要关心对象的具体实现细节。
示例代码:
public enum ProductType
{
ProductA,
ProductB
}
public interface IProduct
{
void Use();
}
public class ProductA : IProduct
{
public void Use()
{
Console.WriteLine("Product A is in use.");
}
}
public class ProductB : IProduct
{
public void Use()
{
Console.WriteLine("Product B is in use.");
}
}
public class SimpleFactory
{
public IProduct CreateProduct(ProductType type)
{
switch (type)
{
case ProductType.ProductA:
return new ProductA();
case ProductType.ProductB:
return new ProductB();
default:
throw new ArgumentException("Invalid product type.");
}
}
}
逻辑分析:
IProduct是产品接口,ProductA和ProductB是其实现类。SimpleFactory是工厂类,根据传入的ProductType枚举创建相应的实例。- switch-case 控制对象的创建逻辑。
优点:
- 结构简单,易于理解和实现。
- 客户端只需知道工厂类即可获取所需对象。
缺点:
- 扩展性差,新增产品类型时需要修改工厂类。
- 违反了 开闭原则(Open-Closed Principle) 。
UML类图(mermaid格式):
classDiagram
class IProduct {
<<interface>>
+Use()
}
class ProductA {
+Use()
}
class ProductB {
+Use()
}
class SimpleFactory {
+CreateProduct(type: ProductType): IProduct
}
IProduct <|-- ProductA
IProduct <|-- ProductB
SimpleFactory --> IProduct
5.1.2 工厂方法模式(Factory Method)
工厂方法模式 是GoF定义的正式设计模式之一。它将对象的创建延迟到子类中实现,使得系统更具扩展性。
特点:
- 定义一个用于创建对象的接口,但让子类决定实例化哪一个类。
- 工厂方法使一个类的实例化延迟到其子类。
适用场景:
- 需要将对象的创建过程与使用过程分离。
- 产品种类可能不断扩展,希望避免修改已有代码。
示例代码:
public interface IProduct
{
void Use();
}
public class ProductA : IProduct
{
public void Use()
{
Console.WriteLine("Product A is in use.");
}
}
public class ProductB : IProduct
{
public void Use()
{
Console.WriteLine("Product B is in use.");
}
}
public abstract class Factory
{
public abstract IProduct CreateProduct();
}
public class ProductAFactory : Factory
{
public override IProduct CreateProduct()
{
return new ProductA();
}
}
public class ProductBFactory : Factory
{
public override IProduct CreateProduct()
{
return new ProductB();
}
}
逻辑分析:
Factory是抽象工厂类,包含抽象方法CreateProduct()。- 每个具体工厂类(如
ProductAFactory)负责创建特定的产品实例。 - 客户端通过具体工厂类获取产品实例,无需知道具体类名。
优点:
- 符合开闭原则,新增产品类型时只需添加新的工厂类。
- 将创建逻辑与使用逻辑分离,增强系统灵活性。
缺点:
- 类的数量增加,结构略显复杂。
- 每个产品都需要一个对应的工厂类,适合产品种类较多的场景。
UML类图(mermaid格式):
classDiagram
class IProduct {
<<interface>>
+Use()
}
class ProductA {
+Use()
}
class ProductB {
+Use()
}
class Factory {
<<abstract>>
+CreateProduct(): IProduct
}
class ProductAFactory {
+CreateProduct(): IProduct
}
class ProductBFactory {
+CreateProduct(): IProduct
}
IProduct <|-- ProductA
IProduct <|-- ProductB
Factory <|-- ProductAFactory
Factory <|-- ProductBFactory
ProductAFactory --> ProductA
ProductBFactory --> ProductB
5.1.3 抽象工厂模式(Abstract Factory)
抽象工厂模式 用于创建一组相关或依赖对象的家族,而无需指定它们的具体类。它适用于多个产品族的创建场景。
特点:
- 提供一个接口,用于创建一组相关或依赖对象的家族。
- 所有产品族遵循相同的抽象接口。
- 保证客户端在使用产品族时的一致性。
适用场景:
- 系统中存在多个产品族(如UI组件库中的按钮、文本框等)。
- 要求客户端与具体类解耦,并能够切换产品族。
示例代码:
// 定义产品族接口
public interface IButton
{
void Render();
}
public interface ITextBox
{
void Render();
}
// 具体产品A
public class WinButton : IButton
{
public void Render()
{
Console.WriteLine("Windows Button rendered.");
}
}
public class WinTextBox : ITextBox
{
public void Render()
{
Console.WriteLine("Windows TextBox rendered.");
}
}
// 具体产品B
public class MacButton : IButton
{
public void Render()
{
Console.WriteLine("Mac Button rendered.");
}
}
public class MacTextBox : ITextBox
{
public void Render()
{
Console.WriteLine("Mac TextBox rendered.");
}
}
// 抽象工厂接口
public interface IUIFactory
{
IButton CreateButton();
ITextBox CreateTextBox();
}
// 具体工厂A
public class WinUIFactory : IUIFactory
{
public IButton CreateButton()
{
return new WinButton();
}
public ITextBox CreateTextBox()
{
return new WinTextBox();
}
}
// 具体工厂B
public class MacUIFactory : IUIFactory
{
public IButton CreateButton()
{
return new MacButton();
}
public ITextBox CreateTextBox()
{
return new MacTextBox();
}
}
逻辑分析:
IButton和ITextBox是两个产品族的接口。WinButton、WinTextBox、MacButton、MacTextBox是具体实现类。IUIFactory是抽象工厂接口,定义了创建产品族的方法。WinUIFactory和MacUIFactory是具体工厂类,用于创建特定平台的产品族。
优点:
- 支持多个产品族,确保产品之间的一致性。
- 符合开闭原则,新增产品族只需添加新的工厂类。
缺点:
- 类结构复杂,类数量成倍增长。
- 增加新种类产品时,所有工厂类都需要修改。
UML类图(mermaid格式):
classDiagram
class IButton {
<<interface>>
+Render()
}
class ITextBox {
<<interface>>
+Render()
}
class WinButton {
+Render()
}
class WinTextBox {
+Render()
}
class MacButton {
+Render()
}
class MacTextBox {
+Render()
}
class IUIFactory {
<<interface>>
+CreateButton(): IButton
+CreateTextBox(): ITextBox
}
class WinUIFactory {
+CreateButton(): IButton
+CreateTextBox(): ITextBox
}
class MacUIFactory {
+CreateButton(): IButton
+CreateTextBox(): ITextBox
}
IButton <|-- WinButton
IButton <|-- MacButton
ITextBox <|-- WinTextBox
ITextBox <|-- MacTextBox
IUIFactory <|-- WinUIFactory
IUIFactory <|-- MacUIFactory
WinUIFactory --> WinButton
WinUIFactory --> WinTextBox
MacUIFactory --> MacButton
MacUIFactory --> MacTextBox
5.2 工厂模式的实践应用
在实际开发中,工厂模式经常用于 解耦调用方与具体类 ,提高系统的可维护性与可测试性。例如,在依赖注入框架中,工厂模式常用于创建服务实例。
5.2.1 使用工厂模式实现依赖注入
我们可以通过一个示例展示工厂模式与依赖注入的结合使用:
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
public class FileLogger : ILogger
{
public void Log(string message)
{
// 假设写入文件
Console.WriteLine($"File Log: {message}");
}
}
public class LoggerFactory
{
public ILogger CreateLogger(string type)
{
switch (type)
{
case "console":
return new ConsoleLogger();
case "file":
return new FileLogger();
default:
throw new ArgumentException("Invalid logger type.");
}
}
}
public class Service
{
private readonly ILogger _logger;
public Service(ILogger logger)
{
_logger = logger;
}
public void DoWork()
{
_logger.Log("Service is doing work.");
}
}
说明:
LoggerFactory根据传入的字符串参数创建不同的ILogger实例。Service类通过构造函数注入日志器实例,实现了松耦合。
优点:
- 客户端代码与具体日志实现类解耦。
- 易于扩展,新增日志类型只需修改工厂类。
5.3 工厂模式与设计原则的关系
工厂模式的使用体现了面向对象设计中的多个原则:
| 设计原则 | 说明 |
|---|---|
| 开闭原则 | 新增产品类型时,不需修改已有代码,符合“对扩展开放,对修改关闭”。 |
| 依赖倒置原则 | 客户端依赖于抽象接口,而不是具体类。 |
| 单一职责原则 | 工厂类负责对象的创建,业务类负责使用对象,职责清晰。 |
| 里氏替换原则 | 子类可以替换父类,满足多态需求。 |
5.4 小结
工厂模式作为创建型设计模式的核心之一,在C#中具有广泛的应用价值。通过本章的学习,我们掌握了以下内容:
- 简单工厂 适用于对象种类少、创建逻辑简单的场景;
- 工厂方法 通过子类实现对象创建,提高扩展性;
- 抽象工厂 支持多个产品族,适用于产品一致性要求高的系统;
- 工厂模式与 依赖注入 结合,可以显著提升系统的可测试性与灵活性;
- 工厂模式的实现体现了多个面向对象设计原则,是构建高质量软件系统的重要工具。
在实际开发中,合理选择工厂模式的类型,有助于构建结构清晰、易于维护和扩展的系统。
6. 单例模式设计与实现
单例模式是一种创建型设计模式,其核心目标是确保一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。它在系统配置管理、日志记录、数据库连接池、缓存服务等场景中被广泛使用。然而,单例的滥用可能导致全局状态污染,降低代码的可测试性与可维护性。因此,深入理解其不同实现方式、线程安全机制以及适用场景,对于高质量软件设计至关重要。
本章将从单例模式的基本结构出发,依次介绍懒汉式、饿汉式、线程安全实现方式,探讨C#中静态类与单例的异同,并通过实际代码演示其在项目中的应用方式。同时,我们还将分析单例模式的优缺点,以及其在现代架构中的使用建议。
6.1 单例模式的基本结构与设计原理
单例模式的核心在于控制类的实例化过程,确保无论何时访问该类,都只能获得一个实例。其基本结构包括以下几个关键要素:
- 私有构造函数 :防止外部通过
new关键字创建实例。 - 静态私有字段 :用于存储唯一的实例。
- 公共静态访问方法 :提供全局访问入口。
6.1.1 经典实现结构
public class Singleton
{
private static Singleton _instance;
private Singleton() { }
public static Singleton GetInstance()
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
代码逻辑分析:
- 构造函数私有化 :
private Singleton()确保外部无法通过new创建实例。 - 延迟初始化 :只有在第一次调用
GetInstance()时才创建实例。 - 静态字段
_instance:保存唯一的实例引用。
参数说明:
_instance是一个静态私有字段,用于存储类的唯一实例。GetInstance()是对外暴露的访问方法,确保外部只能通过该方法获取实例。
适用场景:
适用于资源开销较大或需全局唯一访问的对象,例如日志记录器、数据库连接管理器等。
6.2 单例模式的多种实现方式对比
在实际开发中,单例模式有多种实现方式,每种方式适用于不同的场景,尤其在并发环境下需要考虑线程安全问题。
6.2.1 懒汉式(Lazy Initialization)
定义 :对象在第一次调用时才被创建。
public class LazySingleton
{
private static LazySingleton _instance;
private LazySingleton() { }
public static LazySingleton Instance
{
get
{
if (_instance == null)
{
_instance = new LazySingleton();
}
return _instance;
}
}
}
逻辑分析:
- 该实现方式简单,但在多线程环境下可能创建多个实例。
- 若多个线程同时进入
if (_instance == null)判断,可能重复初始化。
缺点:
- 非线程安全,需额外同步机制。
6.2.2 饿汉式(Eager Initialization)
定义 :类加载时即创建实例。
public class EagerSingleton
{
private static readonly EagerSingleton _instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton Instance => _instance;
}
逻辑分析:
readonly保证实例只初始化一次。- 利用静态字段在类加载时创建实例,避免了延迟加载的问题。
优点:
- 线程安全,无需额外同步。
- 适合初始化成本不高、使用频率高的场景。
缺点:
- 无法延迟加载,可能浪费资源。
6.2.3 线程安全的懒汉式(Double-Check Locking)
定义 :通过双重检查锁机制实现线程安全的延迟加载。
public class ThreadSafeSingleton
{
private static ThreadSafeSingleton _instance;
private static readonly object _lock = new object();
private ThreadSafeSingleton() { }
public static ThreadSafeSingleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new ThreadSafeSingleton();
}
}
}
return _instance;
}
}
}
逻辑分析:
- 使用
lock对象进行同步,防止多个线程同时进入创建实例的代码块。 - 第一次检查
_instance == null避免每次调用都加锁。 - 第二次检查是为了防止多个线程同时通过第一次判断后重复创建实例。
参数说明:
_lock是用于线程同步的对象,通常使用object实例。Instance属性是只读的公共访问点。
优点:
- 兼顾了线程安全与延迟加载。
缺点:
- 代码复杂度增加,需注意锁的使用方式。
6.2.4 使用 Lazy<T> 实现延迟加载
C# 提供了泛型 Lazy<T> 类型,用于简化延迟初始化逻辑。
public class LazySingletonWithLazy
{
private static readonly Lazy<LazySingletonWithLazy> _instance =
new Lazy<LazySingletonWithLazy>(() => new LazySingletonWithLazy());
private LazySingletonWithLazy() { }
public static LazySingletonWithLazy Instance => _instance.Value;
}
逻辑分析:
Lazy<T>内部已经处理了线程安全和延迟加载逻辑。- 构造函数仍为私有,确保单例性。
优点:
- 简洁高效,推荐在 C# 中使用。
缺点:
- 不适用于需要复杂初始化逻辑的场景。
6.2.5 实现方式对比总结
| 实现方式 | 是否线程安全 | 是否延迟加载 | 是否推荐 |
|---|---|---|---|
| 懒汉式 | ❌ | ✅ | ❌ |
| 饿汉式 | ✅ | ❌ | ⚠️(适合初始化成本低的场景) |
| 双重检查锁 | ✅ | ✅ | ✅(推荐) |
| Lazy | ✅ | ✅ | ✅✅(强烈推荐) |
6.3 C#中单例的高级实现与替代方案
除了传统的单例实现方式,C#还支持使用静态类、依赖注入等方式实现类似功能。
6.3.1 使用静态类作为替代
public static class StaticLogger
{
public static void Log(string message)
{
Console.WriteLine($"Log: {message}");
}
}
特点:
- 不能继承,不能实现接口。
- 所有成员必须为静态。
- 类不能实例化。
适用场景:
- 不需要维护状态的工具类(如日志、数学计算等)。
优点:
- 简洁,无需处理实例化逻辑。
缺点:
- 无法多态,不利于测试和扩展。
6.3.2 依赖注入中的单例生命周期管理
现代框架如 ASP.NET Core 支持依赖注入(DI)机制,其中可通过服务生命周期控制单例行为。
// 在 Startup.cs 中注册
services.AddSingleton<ICacheService, InMemoryCacheService>();
逻辑分析:
- 通过 DI 容器统一管理对象生命周期。
- 单例服务在整个应用程序中只创建一次。
优点:
- 解耦调用方与具体实现。
- 支持接口抽象,便于替换与测试。
缺点:
- 依赖框架,不适用于小型项目。
6.4 单例模式在项目中的实际应用案例
6.4.1 日志记录服务
public class Logger
{
private static readonly Lazy<Logger> _instance =
new Lazy<Logger>(() => new Logger());
private Logger() { }
public static Logger Instance => _instance.Value;
public void Log(string message)
{
Console.WriteLine($"[LOG] {DateTime.Now}: {message}");
}
}
// 使用方式
Logger.Instance.Log("Application started.");
说明:
- 日志记录器通常需要在整个应用程序中访问。
- 使用
Lazy<T>保证线程安全与延迟加载。
6.4.2 数据库连接池管理器
public class DbConnectionManager
{
private static readonly Lazy<DbConnectionManager> _instance =
new Lazy<DbConnectionManager>(() => new DbConnectionManager());
private List<SqlConnection> _connections = new List<SqlConnection>();
private DbConnectionManager()
{
// 初始化连接池
}
public static DbConnectionManager Instance => _instance.Value;
public SqlConnection GetConnection()
{
// 从连接池获取连接
return new SqlConnection("your_connection_string");
}
}
说明:
- 数据库连接池管理器需要确保全局唯一。
- 使用单例模式可集中管理连接资源。
6.5 单例模式的优缺点与设计建议
6.5.1 优点
- 全局访问 :便于在应用程序的任何地方访问。
- 资源节约 :避免重复创建对象,节省系统资源。
- 集中管理 :适用于配置、状态等集中管理的场景。
6.5.2 缺点
- 全局状态 :可能导致系统状态难以追踪,降低可维护性。
- 测试困难 :依赖全局状态的对象难以进行单元测试。
- 违反单一职责原则 :若单例承担过多职责,违背设计原则。
6.5.3 设计建议
- 优先使用依赖注入 :在现代应用中,应优先使用 DI 管理对象生命周期。
- 避免过度使用 :仅在真正需要全局唯一对象时使用。
- 封装状态 :将状态封装在服务内部,减少全局副作用。
6.6 小结与后续讨论
本章详细讲解了单例模式的多种实现方式及其在C#中的应用。通过对比懒汉式、饿汉式、线程安全实现与 Lazy<T> 的优缺点,我们明确了不同场景下的选择策略。同时,我们也探讨了静态类与依赖注入在单例实现中的替代方案,并通过实际代码案例演示了其在项目中的应用方式。
在后续章节中,我们将继续探讨观察者模式,了解如何通过对象间的订阅机制实现事件驱动的设计。同时,我们也将对比单例与观察者模式在系统解耦中的作用,帮助构建更灵活、可维护的软件架构。
7. 观察者模式设计与实现
7.1 观察者模式的核心结构
观察者模式(Observer Pattern)是一种行为型设计模式,它定义了一种一对多的依赖关系,使得多个观察者对象能够自动监听并响应某个主题(Subject)对象的状态变化。其核心结构包含两个主要角色:
- Subject(主题) :维护一组观察者,并提供注册、注销与通知的方法。
- Observer(观察者) :定义一个更新接口,用于在主题状态变化时接收通知。
示例类图(使用 Mermaid 流程图表示)
classDiagram
class Subject {
+Register(observer)
+Unregister(observer)
+Notify()
}
class Observer {
+Update()
}
Subject <|-- ConcreteSubject
Observer <|-- ConcreteObserverA
Observer <|-- ConcreteObserverB
ConcreteSubject : +State
ConcreteObserverA : -name
ConcreteObserverB : -name
Subject "1" -- "0..*" Observer : observes
在上述类图中, ConcreteSubject 是具体的主题类,它包含状态 State ; ConcreteObserverA 和 ConcreteObserverB 是具体的观察者类,各自实现 Update() 方法以响应状态变化。
7.2 C# 中观察者模式的实现
在 C# 中,观察者模式可以通过 委托(Delegate) 与 事件(Event) 机制高效实现,这也是 .NET 框架中事件处理的底层机制。
7.2.1 使用委托与事件实现观察者模式
以下是一个基于 C# 的基础实现示例:
// 定义更新事件的参数类
public class StateChangedEventArgs : EventArgs
{
public string NewState { get; set; }
}
// 主题类
public class Subject
{
// 定义事件委托
public event EventHandler<StateChangedEventArgs> StateChanged;
private string _currentState;
public string CurrentState
{
get => _currentState;
set
{
if (_currentState != value)
{
_currentState = value;
OnStateChanged(new StateChangedEventArgs { NewState = value });
}
}
}
protected virtual void OnStateChanged(StateChangedEventArgs e)
{
// 调用事件处理器
StateChanged?.Invoke(this, e);
}
}
// 观察者类
public class Observer
{
private string _name;
public Observer(string name)
{
_name = name;
}
public void OnSubjectChanged(object sender, StateChangedEventArgs e)
{
Console.WriteLine($"{_name} received new state: {e.NewState}");
}
}
代码说明:
Subject类中定义了一个StateChanged事件,当状态CurrentState变化时会触发此事件。Observer类通过注册OnSubjectChanged方法作为事件处理函数,接收主题的状态更新。StateChangedEventArgs继承自EventArgs,用于封装传递给观察者的数据。
7.2.2 使用示例代码
class Program
{
static void Main(string[] args)
{
// 创建主题
var subject = new Subject();
// 创建观察者
var observer1 = new Observer("Observer A");
var observer2 = new Observer("Observer B");
// 注册观察者到主题
subject.StateChanged += observer1.OnSubjectChanged;
subject.StateChanged += observer2.OnSubjectChanged;
// 改变主题状态,观察者将自动收到通知
subject.CurrentState = "New State 1";
subject.CurrentState = "New State 2";
// 可选择注销观察者
subject.StateChanged -= observer1.OnSubjectChanged;
// 此时只有 observer2 会收到通知
subject.CurrentState = "Final State";
}
}
输出结果:
Observer A received new state: New State 1
Observer B received new state: New State 1
Observer A received new state: New State 2
Observer B received new state: New State 2
Observer B received new state: Final State
参数说明与逻辑分析:
EventHandler<StateChangedEventArgs>是 .NET 提供的泛型委托,用于封装事件处理函数。StateChangedEventArgs封装了传递给观察者的数据,如NewState。StateChanged?.Invoke(this, e)是线程安全地调用事件处理函数的方式。- 通过
+=和-=操作符,可以注册或注销观察者。
下一节将继续深入讨论观察者模式在实际业务场景中的应用,如 UI 更新与消息广播机制。
简介:面向对象编程(OOP)是一种广泛应用的编程范式,以对象为核心,将数据与操作封装在一起。本文聚焦C#语言中的面向对象核心特性,深入讲解封装、继承、多态三大基础概念,并结合设计模式与SOLID设计原则,帮助开发者构建高内聚、低耦合、易于维护的软件系统。通过工厂模式、单例模式、观察者模式等经典设计模式的实战解析,结合接口隔离、依赖倒置等设计原则,提升代码的灵活性与可扩展性。
更多推荐


所有评论(0)