常见的架构设计原则包括以下几个方面:
1. 单一职责原则(Single Responsibility Principle,SRP):一个模块或者类只负责一项功能。
2. 开闭原则(Open-Closed Principle,OCP):软件实体应该对扩展开放,对修改关闭。
3. 里氏替换原则(Liskov Substitution Principle,LSP):所有引用基类对象的代码都能够透明地使用其子类对象。
4. 接口隔离原则(Interface Segregation Principle,ISP):客户端不应该依赖于它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上。
5. 依赖倒置原则(Dependency Inversion Principle,DIP):高层模块不应该依赖于低层模块,二者应该依赖于抽象接口。同时,抽象不应该依赖于细节,细节应该依赖于抽象。
这些原则可以指导我们设计出更加可扩展、可维护、可测试、可复用的架构。
为了更加详细的说明,以下是各个原则的示例代码:
1、单一职责原则(SRP)
// 错误的示例
public class User {
public void requestBook(string bookName) {
// 做一些请求书籍的事情
// ...
// 做一些记录用户行为的事情
logUserAction("request a book");
}
private void logUserAction(string action) {
// 记录用户行为到日志中
// ...
}
}
// 正确的示例
public class User {
public void requestBook(string bookName) {
// 做一些请求书籍的事情
// ...
}
}
public class UserActionLogger {
public void logUserAction(string action) {
// 记录用户行为到日志中
// ...
}
}
在错误的示例中,`User` 类不仅要负责请求书籍的事情,还要负责记录用户行为。这不仅让代码变得复杂,而且不符合单一职责原则。正确的示例分离了不同的职责,把记录用户行为的功能独立成了一个新的类。
2、开闭原则(OCP)
// 错误的示例
public class UserManager {
public void addUser(User user) {
// 添加用户到数据库中
// ...
}
public void deleteUser(User user) {
// 从数据库中删除用户
// ...
}
}
// 新需求:修改用户信息
public class UserManager {
public void addUser(User user) {
// 添加用户到数据库中
// ...
}
public void deleteUser(User user) {
// 从数据库中删除用户
// ...
}
// 新需求:修改用户信息
public void updateUser(User user) {
// 修改用户信息
// ...
}
}
// 正确的示例
public interface IUserManager {
void addUser(User user);
void deleteUser(User user);
}
public class UserManager : IUserManager {
public void addUser(User user) {
// 添加用户到数据库中
// ...
}
public void deleteUser(User user) {
// 从数据库中删除用户
// ...
}
}
public class AdvancedUserManager : IUserManager {
public void addUser(User user) {
// 添加用户到数据库中
// ...
}
public void deleteUser(User user) {
// 从数据库中删除用户
// ...
}
// 新需求:修改用户信息
public void updateUser(User user) {
// 修改用户信息
// ...
}
}
在错误的示例中,当需要添加新的需求(比如修改用户信息)时,我们需要修改 `UserManager` 类。这显然不符合开闭原则。正确的示例使用了接口和不同的实现类分离不同的功能,这样当我们需要添加新的需求时,只需要创建一个新的实现类即可。
3、里氏替换原则(LSP)
// 错误的示例
public class Animal {
public virtual void eat() {
Console.WriteLine("Animal eat");
}
}
public class Dog : Animal {
public override void eat() {
Console.WriteLine("Dog eat");
}
}
public class Cat : Animal {
public override void eat() {
Console.WriteLine("Cat eat");
}
}
public class AnimalFeeder {
public void feed(Animal animal) {
animal.eat();
}
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog());
animalFeeder.feed(new Cat());
}
// 输出:
// Dog eat
// Cat eat
// 错误的示例,违反了 LSP
public class WildAnimal : Animal {
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog());
animalFeeder.feed(new WildAnimal()); // 报错!
}
在错误的示例中,我们定义了一个 `WildAnimal` 类继承自 `Animal` 类,但是这个类并没有重写 `eat()` 方法,而是直接继承了父类的实现。当我们尝试用 `WildAnimal` 对象来调用 `AnimalFeeder` 的 `feed()` 方法时,会出现运行时异常,因为 `WildAnimal` 对象没有正确处理 `eat()` 方法。
4、接口隔离原则(ISP)
// 错误的示例
public interface IAnimal {
void eat();
void fly();
}
public class Dog : IAnimal {
public void eat() {
Console.WriteLine("Dog eat");
}
public void fly() {
throw new NotImplementedException(); // 错误的设计,狗不能飞行
}
}
public class Bird : IAnimal {
public void eat() {
Console.WriteLine("Bird eat");
}
public void fly() {
Console.WriteLine("Bird fly");
}
}
public class AnimalFeeder {
public void feed(IAnimal animal) {
animal.eat();
}
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog()); // 报错!
animalFeeder.feed(new Bird());
}
// 正确的示例
public interface IEatable {
void eat();
}
public interface IFlyable {
void fly();
}
public class Dog : IEatable {
public void eat() {
Console.WriteLine("Dog eat");
}
}
public class Bird : IEatable, IFlyable {
public void eat() {
Console.WriteLine("Bird eat");
}
public void fly() {
Console.WriteLine("Bird fly");
}
}
public class AnimalFeeder {
public void feed(IEatable animal) {
animal.eat();
}
}
static void Main(string[] args) {
AnimalFeeder animalFeeder = new AnimalFeeder();
animalFeeder.feed(new Dog());
animalFeeder.feed(new Bird());
}
在错误的示例中,我们定义了一个 `IAnimal` 接口,其中包含了 `eat()` 和 `fly()` 两个方法。但是不是所有的动物都可以飞行,例如狗就不能飞行。正确的示例把 `IAnimal` 接口拆分成了 `IEatable` 和 `IFlyable` 两个接口,这样我们可以根据实际需要选择使用哪个接口来表示一个动物的能力。
5、依赖倒置原则(DIP)
// 错误的示例
public class UserService {
private readonly UserDAO userDAO;
public UserService() {
this.userDAO = new UserDAO(); // 依赖了具体的实现
}
public void addUser(User user) {
userDAO.addUser(user);
}
}
public class UserDAO {
public void addUser(User user) {
// 添加用户到数据库中
// ...
}
}
// 正确的示例
public interface IUserDAO {
void addUser(User user);
}
public class UserDAO : IUserDAO {
public void addUser(User user) {
// 添加用户到数据库中
// ...
}
}
public class UserService {
private readonly IUserDAO userDAO;
public UserService(IUserDAO userDAO) {
this.userDAO = userDAO; // 依赖抽象接口
}
public void addUser(User user) {
userDAO.addUser(user);
}
}
static void Main(string[] args) {
IUserDAO userDAO = new UserDAO();
UserService userService = new UserService(userDAO);
userService.addUser(new User());
}
在错误的示例中,`UserService` 类需要访问数据库添加用户,但是直接依赖了 `UserDAO` 类。这使得 `UserService` 类不灵活,不能适应变化。正确的示例中,我们把 `UserDAO` 类抽象成了 `IUserDAO` 接口,并且通过构造函数注入了依赖。这样做不仅遵循了依赖倒置原则,而且还能够灵活地替换不同的实现类。