4、封装与抽象:封装字段、自封装字段、以工厂函数取代构造函数
好,咱们今天聊点实在的。封装与抽象,这四个字听着挺玄乎,但说白了,就是「把不该暴露的藏起来,把该暴露的讲清楚」。我在项目里见过太多人,一上来就把字段搞成 public,满世界随便改。结果呢?改一个字段,牵出一堆 bug,排查到天亮。
这一节,我带你搞定三个最常用的重构手法:封装字段、自封装字段、以工厂函数取代构造函数。它们就像代码世界的「三件套」,穿上之后,你的代码会干净很多。
4.1 封装字段(Encapsulate Field)
先问个问题:你的类里,字段是 public 还是 private?
如果你还在用 public 字段,我建议你立刻改掉。为什么?因为 public 字段等于把家门钥匙挂在大街上。谁都能进来,谁都能改。你根本不知道哪天哪个同事会往里面塞个 null。
封装字段,就是把字段变成 private,然后提供 getter/setter。听起来简单,但很多人做错了。
错误示范:
public class Order
{
public decimal Amount; // 直接暴露字段
}
这种写法,我见过不下百次。你想想看,如果有一天你需要加个校验,比如金额不能为负数,你怎么办?满世界找调用方,一个一个改?
正确做法:
public class Order
{
private decimal _amount;
public decimal Amount
{
get => _amount;
set
{
if (value < 0)
throw new ArgumentException("金额不能为负数");
_amount = value;
}
}
}
嗯,这里要注意:封装字段不只是「加个属性」那么简单。它给了你一个「控制点」。以后想加日志、加校验、加缓存,都在这个点上做,调用方完全无感知。
我的习惯:所有字段一律 private,哪怕现在不需要校验。因为你永远不知道明天需求会怎么变。先封住,后面改起来成本最低。
4.2 自封装字段(Self-Encapsulate Field)
这个手法,很多人第一次听会觉得「多此一举」。我刚开始也这么想。但后来踩了坑,才明白它的价值。
自封装字段的意思是:在类内部访问自己的字段时,也通过 getter/setter,而不是直接访问字段。
举个例子:
public class Employee
{
private decimal _salary;
// 直接访问字段
public decimal CalculateBonus()
{
return _salary * 0.1m;
}
// 也通过属性访问
public decimal Salary
{
get => _salary;
set => _salary = value;
}
}
你看,CalculateBonus 方法直接用了 _salary 字段。这有什么问题?
问题大了。假设有一天,你需要在获取薪资时做特殊处理,比如扣税或者汇率转换。你改了 Salary 属性的 getter,但 CalculateBonus 还是直接读字段,它根本不会走你的新逻辑。这就是 bug 的温床。
自封装后的版本:
public class Employee
{
private decimal _salary;
public decimal Salary
{
get => _salary;
set => _salary = value;
}
public decimal CalculateBonus()
{
return Salary * 0.1m; // 通过属性访问
}
}
我曾经踩过的坑:有一次重构一个老系统,里面有个类,内部方法直接访问字段,而属性 getter 里加了缓存逻辑。结果缓存根本没生效,因为内部调用绕过了属性。排查了整整一个下午。从那以后,我强制团队所有成员:类内部也走属性,除非有明确的性能理由。
自封装字段的好处,说白了就是「统一入口」。不管外部还是内部,都走同一个门。将来改逻辑,只改一个地方。
4.3 以工厂函数取代构造函数(Replace Constructor with Factory Method)
构造函数,听起来很基础对吧?但它在实际项目中,经常成为「代码坏味道」的重灾区。
你想想看,构造函数有什么限制?
- 名字固定,只能叫
类名 - 不能有返回值
- 不能重载太多(参数多了调用方根本分不清)
- 不能做复杂的初始化逻辑
所以,当你的对象创建逻辑变得复杂时,就该用工厂函数了。
一个典型的坏味道:
public class User
{
public User(string name, string role, bool isActive)
{
// 各种初始化逻辑
}
}
// 调用方:
var user = new User("张三", "admin", true);
问题在哪?调用方根本不知道每个参数是什么意思。而且如果以后创建逻辑变了,比如需要从数据库读取默认值,你怎么办?改构造函数?那所有调用方都得改。
用工厂函数重构后:
public class User
{
private User(string name, string role, bool isActive)
{
// 构造函数保持简单
}
// 工厂函数
public static User CreateAdmin(string name)
{
return new User(name, "admin", true);
}
public static User CreateGuest(string name)
{
return new User(name, "guest", false);
}
}
// 调用方:
var admin = User.CreateAdmin("张三");
var guest = User.CreateGuest("李四");
你看,调用方现在一目了然。而且工厂函数里可以做更多事情,比如校验、日志、从配置读取默认值。
核心区别:
| 构造函数 | 工厂函数 |
|---|---|
| 名字固定 | 名字可以自定义(如 CreateAdmin) |
| 不能有返回值 | 可以返回子类或接口类型 |
| 逻辑复杂时难以维护 | 可以封装复杂逻辑 |
| 每次调用都创建新对象 | 可以实现对象池、缓存等 |
我个人的建议:当构造函数参数超过 3 个,或者创建逻辑涉及条件判断时,果断用工厂函数。别犹豫。我在一个项目中,用工厂函数替换了 20 多个「参数爆炸」的构造函数,代码可读性直接翻倍。
4.4 三者如何配合使用?
这三个手法不是孤立的。在实际项目中,它们经常一起出现。
举个例子:
public class Product
{
private string _name;
private decimal _price;
// 构造函数私有化,强制使用工厂函数
private Product(string name, decimal price)
{
_name = name;
_price = price;
}
// 自封装字段
public string Name
{
get => _name;
private set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("名称不能为空");
_name = value;
}
}
public decimal Price
{
get => _price;
private set
{
if (value < 0)
throw new ArgumentException("价格不能为负数");
_price = value;
}
}
// 工厂函数
public static Product CreateNormal(string name, decimal price)
{
return new Product(name, price);
}
public static Product CreateDiscount(string name, decimal price, decimal discount)
{
return new Product(name, price * (1 - discount));
}
}
你看,这个类里:
- 字段全部 private(封装字段)
- 内部方法通过属性访问字段(自封装字段)
- 构造函数私有,对外只暴露工厂函数
三者配合,代码既安全又灵活。调用方想犯错都难。
注意:不要为了用而用。如果你的类很简单,就两三个字段,没有校验逻辑,那直接 public 属性就够了。过度设计也是坏味道。我见过有人给一个只有两个字段的 DTO 类写了五个工厂函数,那纯粹是画蛇添足。
4.5 小结
好,咱们总结一下今天的内容:
- 封装字段:把字段藏起来,通过属性暴露。给你一个控制点。
- 自封装字段:类内部也走属性。统一入口,避免绕开逻辑。
- 以工厂函数取代构造函数:当构造函数不够用时,用静态方法创建对象。名字清晰,逻辑灵活。
这三个手法,说白了就是「把控制权握在自己手里」。你想想看,代码写出来是给人看的,也是要维护的。今天偷懒写个 public 字段,明天可能就要花两小时排查 bug。值不值?你自己掂量。
下一节,咱们聊「搬移特性」——怎么把散落在各处的逻辑,归拢到该去的地方。到时候见。