学习面向对象编程的 SOLID 原则
面向对象编程(OOP)是一种编程风格,它将数据和行为封装到称为对象的模型中。这样,相关的代码被分组在一起,与其他代码隔离,并提供可重用的代码块,用于解决当前问题。
面向对象编程 (OOP) 可能是最常见的编程形式之一,许多流行的编程语言,包括 C#、Java 和 JavaScript,都是基于这种理念而设计的。
在许多编程语言中,对象的蓝图被称为类。类包含了该对象的所有定义,包括属性和函数(更常见的说法是方法)。
要创建一个实际对象,我们称之为创建类的一个实例,在这个实例中,所有属性定义都会被赋予实际值。重要的是,我们可以同时存在同一个类的多个实例,每个实例可以具有不同的值。
例如,我们可以创建一个简单的 Person 类,它有一个 name 属性和一个 speak 方法:
public class Person
{
public Person(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public void Speak()
{
Console.WriteLine($"My name is {Name}");
}
}
这就是蓝图。现在我们可以创建该类的两个独立实例,它们的 speak 方法会给出不同的输出,因为每个实际对象都使用不同的 name 属性值进行实例化。
var bernard = new Person("Bernard");
var sally = new Person("Sally");
bernard.Speak(); // outputs "My name is Bernard"
sally.Speak(); // outputs "My name is Sally"
虽然这些例子非常简单,但希望您能从中看到创建这些对象蓝图并创建实际对象的独立实例的好处。
然而,如果不小心,很容易误用对象,导致代码难以阅读或不稳定。例如,你可能会得到一个“上帝”类,其中塞满了各种不相关的行为。
为了帮助编写良好、可维护、稳定的代码,下面描述了 SOLID 原则。
- S - 单一职责原则
- O - 开/闭原理
- L-里氏替换原理
- I - 界面隔离原理
- D - 依赖倒置原理
S——单一职责原则
标题已经很清楚地说明了这一点。一个类应该只承担一项职责。这样做是为了避免出现“上帝类”的情况,即一个类包揽所有工作,同时也有助于将代码拆分成更小、更合理的模块。
虽然单一职责原则很容易理解,但在实践中,判断某个东西应该归入哪个类,又应该移到另一个类,并非总是那么简单。这种判断主要靠经验,随着时间的推移,你会越来越擅长。
如果我们回到 Person 类,可能会决定将每个人的信息保存到数据库中。因此,在 Person 类中创建一个方法似乎是合理的:
public class Person
{
// ... other properties and methods
public void SaveToDatabase()
{
// logic to save person
}
}
然而,问题在于如何将人员信息保存到数据库的细节是一项额外的职责,因此这项职责应该移到另一个类中。
因此,我们可以创建一个数据库类,其中包含一个 SavePerson 方法,并将 Person 实例传递给该类。这样,Person 类只处理人员的详细信息,而数据库类则处理保存人员的详细信息。
public class Database
{
public void SavePerson(Person person)
{
// logic to save person
}
}
O - 开/闭原理
开放/封闭原则指出,一个物体应该对扩展开放,对修改封闭。这意味着,你应该设计出易于扩展的物体,而无需直接修改它们。
例如,我们可以定义两个新类,Employee 和 Manager,它们都派生自 Person 类,以及一个 SalaryCalculator 类。
public class Employee : Person
{
// employee methods and properties
}
public class Manager : Person
{
// manager methods and properties
}
public class SalaryCalculator
{
public decimal CalculateSalary(Person person)
{
if (person is Employee)
{
return 100 * 365;
}
else if (person is Manager)
{
return 200 * 365;
}
}
}
在这个例子中,SalaryCalculator 类违反了开闭原则,因为如果我们通过添加 Director 类来扩展程序,我们就必须修改 CalculateSalary 方法来解决这个问题。
为了解决这个问题,我们可以给每个 Person 类型添加一个 DailyRate 属性。这样,我们就可以添加任意数量的 Person 类型,而无需修改 SalaryCalculator。
public class Person
{
public virtual decimal DailyRate => 0;
}
public class Employee : Person
{
public override decimal DailyRate => 100;
}
public class Manager : Person
{
public override decimal DailyRate => 200;
}
public class Director : Person
{
public override decimal DailyRate => 300;
}
public class SalaryCalculator
{
public decimal CalculateSalary(Person person)
{
return person.DailyRate * 365;
}
}
L-里氏替换原理
里氏替换原则指出,每个子类都应该能够替换它的基类,并且程序仍然会按预期运行。
开闭原理的例子也是里氏替换原理的一个很好的例子。
在 SalaryCalculator 类中,`CalculateSalary` 方法接受基类 `Person`,但在运行时,我们也可以传入它的任何子类。由于我们在 `Person` 和子类中分别使用了 `virtual` 和 `override` 关键字,因此 `CalculateSalary` 方法中的 `DailyRate` 值将是子类的值,也就是说,即使我们在 `CalculateSalary` 方法中用子类替换了基类,该方法仍然能够正常工作。
举例来说,我们可以将类重新定义为违反里氏替换原则:
public class Person
{
public decimal DailyRate => 0;
}
public class Employee
{
public new() decimal DailyRate => 100;
}
根据此定义,我们违反了里氏替换原则,因为在 SalaryCalculator 中,DailyRate 的值始终为 0,而不是子类的值。
I - 界面隔离原理
接口隔离原则指出,不应强制客户端实现它不会使用的接口属性和方法。因此,定义许多小型、具体的接口比定义少数大型、通用的接口更好。
例如,我们可以定义一个 IRepository 接口,用于对数据库上的 Person 类执行 CRUD 操作,并实现该接口:
public interface IRepository
{
bool Create(Person person);
Person Get(int id);
IEnumerable<Person> GetAll();
bool Update(Person person);
bool Delete(int id);
}
public class Repository : IRepository
{
public bool Create(Person person)
{
// create logic
}
public Person Get(int id)
{
// get logic
}
// etc
}
如果我们知道始终需要完整的 CRUD 功能,那当然没问题。但如果实际上我们只需要存储库是只读的呢?目前,即使我们不需要,也被迫实现完整的 CRUD 操作。
相反,我们可以定义多个接口,然后只实现我们需要的接口。
public interface ICreatableRepository
{
bool Create(Person person);
}
public interface IGettableRepository
{
Person Get(int id);
Person GetAll();
}
public interface IUpdateableRepository
{
bool Update(Person person);
}
public interface IDeletableRepository
{
bool Delete(int id);
}
然后我们可以选择实现只读存储库或完整的 CRUD 存储库。
public class ReadonlyRepository : IGettableRepository
{
public Person Get(int id)
{
// get logic
}
public IEnumerable<Person> GetAll()
{
// get all logic
}
}
public class CrudRepository : ICreateableRepository, IGettableRepository, IUpdateableRepository, IDeleteableRepository
{
public bool Create(Person person)
{
// create logic
}
public Person Get(int id)
{
// get logic
}
// etc
}
D - 依赖倒置原理
依赖倒置原则指出,类不应该依赖于其他类,而应该依赖于这些类所实现的接口。这会产生依赖关系方向反转的效果。
例如,一个仅由类组成的传统三层应用程序可能包含一个 PersonPresenter 类,该类依赖于一个 PersonLogic 类,而 PersonLogic 类又依赖于一个 PersonRepository 类。
PersonPresenter --> PersonLogic --> PersonRepository
这样做的问题在于它将高层表示与底层实现细节紧密耦合。更好的做法是编写松耦合的代码,而这可以通过使用接口来实现。关于使用接口的更多原因,请参阅我之前的博客文章:
为了反转依赖关系,我们可以为每个底层类创建接口,并让这些类实现这些接口:
PersonPresenter --> IPersonLogic <-- PersonLogic --> IPersonRepository <-- PersonRepository
结论
以上就是面向对象编程的 5 个 SOLID 原则。如果您觉得有用,请点赞并分享这篇文章。也欢迎在Twitter上关注我。如果您愿意,还可以请我喝杯咖啡!😊
文章来源:https://dev.to/dr_sam_walpole/learn-the-solid-principles-for-object-orient-programming-53e1