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。值不值?你自己掂量。

下一节,咱们聊「搬移特性」——怎么把散落在各处的逻辑,归拢到该去的地方。到时候见。