谈到面向对象,你可能会想到面向对象的三个特点:封装、继承和多态。
引用
封装
- 把紧密相关的信息放在一起,形成一个基础单元。
- 将一个一个基础单元组合,一层一层逐步向上构建出更大的单元。
我们设计一个类,先要考虑其对象应该提供哪些行为。然后,我们根据这些行为提供对应的方法,最后才是考虑实现这些方法要有哪些字段。
现实中,我们往往是面向过程去编程,例如最早的DBFirst的设计思想,我们首先设计数据库的表,表有哪些字段,使用ORM生成实体,给每一个实体对象的字段进行赋值。其次我们在BLL层或者Service层做业务逻辑操作。这基本就偏离了面向对象封装的设计理念。
封装的重点在于对象提供了哪些行为,而不是有哪些数据。也就是说,即便我们把对象理解成数据加函数,数据和函数也不是对等的地位。函数是接口,而数据是内部的实现,正如我们一直说的那样,接口是稳定的,实现是易变的。
C# 9.0 提供了一个新的类型:records
record RecordPerson
{
public string Name { get; init; }
public int Age { get; init; }
}
复制代码
简单理解,对于Record对象,他的属性只允许初始化,不允许修改。当一个对象对外暴露过多的细节,就会导致这个对象不稳定。随着需求的变化,如果一个业务处理四处都在修改对象的Age属性,那么出现bug的概率会越来越高,并且难以排查。
对于一个对象来说,封装在于我们提供了哪些行为,而不是getter 和 setter这种暴露内部细节的字段或者属性。
很多团队非常随意地在系统里面添加接口,一个看似不那么复杂的系统里,随随便便就有成百上千个接口。
如果你想改造系统去掉一些接口时,很有可能会造成线上故障,因为你根本不知道哪个团队在什么时候用到了它。所以,在软件设计中,暴露接口需要非常谨慎。
最小化接口暴露。也就是,每增加一个接口,你都要找到一个合适的理由。
继承
在我大学和工作中,部门铜须理解面向对象的第一个概念,就是我这个对象继承另一个对象,这就是面向对象了。比如老师和学生,我们抽象出一个Person类,他们都有Name,Age,在Person中实现一个Introduction的接口,那么我的老师和学生就能复用这个方法了。
Demo
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void Introduction()
{
Console.WriteLine($"I am {Name}, Age:{Age}!");
}
}
public class Student : Person {}
public class Teacher : Person{}
复制代码
这种使用方式,在C#、Java的语法规范中并没有什么问题,但这个真的是继承使用的正确姿势吗?
组合优于继承
如果一个方案既能用组合实现,也能用继承实现,那就选择用组合实现。
在C#、Java都是属于单继承的语言设计,每个类只能有一个父类,一旦继承被实现继承占据了,在想做接口继承就会很麻烦。也就是说,把实现继承当作一种代码复用的方式,并不是一种值得鼓励的做法。
总结来说:组合优于继承,继承是为了实现多态,而不是复用。
多态
一个接口,多种形态
Java多态实现
interface Shape {
// 绘图接口
void draw();
}
class Square implements Shape {
void draw() {
// 画一个正方形
}
}
class Circle implements Shape {
void draw() {
// 画一个圆形
}
}
复制代码
Go多态实现
type Shape interface {
draw()
}
type Square struct
{
}
func (s Square) draw() {
// 画一个正方形
}
type Circle struct
{
}
func (c Circle) draw() {
// 画一个圆形
}
复制代码
理解多态,还要理解好接口。它是将变的部分和不变的部分隔离开来,在二者之间建立起一个边界。一个重要的编程原则就是面向接口编程,这是很多设计原则的基础。