C# 的“友元”类实现 Builder 模式

友元是 C++ 中的概念,包含友元函数和友元类。被某个类声明为友元的函数或类能够访问这个类的私有成员。友元的正确使用能提升程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,致使程序可维护性变差。所以,除了 C++ 外很难再看到友元语法特性。函数

提出问题

可是友元并不是一无可取,在某些时候确实有这样的需求。举例来讲,如今咱们须要定义一个 User 类,为了不 User 对象在使用过程当中属性被修改,须要将它设计成 Immutable 的。到目前为止尚未什么问题,但接下来问题来了——因为用户信息较多,其属性设计有十数个,为了 Immutable 所有经过构造方法的参数来设置属性是件让人悲伤的事情。ui

那么通常咱们会想到这样几个方案:this

方案简述

方案一,使用参数对象

这是 JavaScript 中经常使用的作法,使用参数对象,在构造 User 的时候,经过参数对象提供全部设置好的属性,再由 User 的构造方法从参数里把这些属性拷贝出来设置给只读成员。那么实现可能像这样:设计

为了简化代码,只定义了 IdUsernameName 三个属性。下同。指针

public sealed class User {
    public ulong Id { get; }
    public string Username { get; }
    public string Name { get; }
 
    public User(Properties props) {
        Id = props.Id;
        Username = props.Username;
        Name = props.Name;
    }

    public sealed class Properties {
        public ulong Id;
        public string Username;
        public string Name;
    }
}

一个属性就须要重复写三遍,若是代码是按行付费,这个定义会很是赚!code

一次性设置

这种作法是自定义属性的 set 函数,或者定义一个 SetXxxxx 方法,判断若是值为 null 则能够设置,一但设置将不能再设置(理论上来讲应该抛异常,但这里示例简化为无做为)。对象

下面的示例经过 UsernameName 演示了一次性设置的两种方法ip

public class User {
    public ulong Id { get; }

    public string Username { get; private set; }

    public void SetUsername(string username) {
        if (Username == null) {
            Username = username;
        }
    }

    public string Name {
        get {
            return name;
        }
        set {
            if (name == null) {
                name = value;
            }
        }
    }
    private string name;
 
    public User(ulong id) {
        Id = id;
    }
}

这种方法中的 User 并不是 Immutalbe,只是近似,由于它的属性不能从“有”到“无”,却能够从“无”到“有”。get

并且,我发现这个方法比上一个方法更赚钱。string

Builder

Builder 模式嘛,就是为了解决初始化复杂对象问题的。

public class User {
    public ulong Id { get; }
    public string Username { get; internal set; }
    public string Name { get; internal set; }

    public User(ulong id) {
        Id = id;
    }
}

public class UserBuilder {
    private readonly User user;

    public UserBuilder(ulong id) {
        user = new User(id);
    }

    public UserBuilder SetUsername(string username) {
        user.Username = username;
    }

    public UserBuilder SetName(string name) {
        user.Name = name;
    }
    
    public User Build() {
        // 验证 user 的属性
        // 或者对某个属性进行一些后期加工(好比计算,格式化处理……)
        return user;
    }
}

为了不外部访问,User 的各属性(除 Id)的 setter 都声明为 internal 的,由于只有这样 UserBuilder 才能调用它们的 setter

显然,采用这种方式在同一个 Assembly 中,好比 App Assembly 中,User 的属性仍然未能获得保护。

内部类实现“友元”特性

基于上面 Builder 模式的解决方案,很容易想到,若是把 UserBuilder 定义为 User 的内部类(嵌套类),那它直接就能够访问 User 的私有成员,其形式以下

public class User {
    // ....

    public class UserBuilder {
        // ....
    }
}

这其实和 C++ 的友元类语法仍是有类似之处——就是都须要在 User 内部去声明,C++ 是声明友元,C# 则在声明的同时进行了定义

// C++ 代码
class UserBuilder;
class User {
    friend class UserBuilder;
}

class UserBuilder {
    // ....
}

内部类实现 Builder 模式

结构上没有问题了。再利用 C# 的分部类(partial class) 特性将 User 类和 UserBuilder 类分别写在两个源文件中,而后简化一下 UserBuilder 的名称,简化为 Builder,由于它定义在 User 的内部,语义已经很是明确了。

// User.cs
public sealed partial class User {
    ulong Id { get; }
    public string Username { get; private set; }
    public string Name { get; private set; }
    
    public User(ulong id) {
        Id = id;
    }
    
    public static Builder CreateBuilder(ulong id) {
        return new Builder(id);
    }
}
// User.Builder.cs
partial sealed class User {
    public class Builder {
        private readonly User user;
        
        public Builder(ulong id) {
            user = new User(id);
        }
        
        public Builder SetUsername(string username) {
            user.Username = username;
            return this; 
        }
        
        public Builder SetName(string name) {
            user.Name = name;
            return this;
        }
        
        public User Build() {
            // 验证和后期加工
            return user;
        }
    }
}

上面这段代码就达到了 Immutable User 的目的,同时代码还很优雅,经过分部类拆分源文件,代码结构也很清晰。不过还有一点小小的瑕疵……Build() 能够重复调用,并且在调用以后仍然能够修改 user 的属性。

再严谨一点

可重复使用的 Builder

若是想把 Build() 变成可屡次调用,每次调用生成新的 User 对象,同时生成的 User 对象不受以后 BuilderSetXxxx 影响,能够在 Build() 的时候,产生一个 user 的复本返回。

另外,因为每一个 User 对象的 Id 应该不一样,因此由生成 CreateBuilder 的时候指定改成 Build() 的时候指定:

public partial class User {
    // ....
    public static Builder CreateBuilder()) {
        return new Builder();
    }
}

partial class User {
    public class Builder {
        private readonly User user;
    
        public Builder() {
            user = new User(0);
        }

        // ....
        
        public User Build(ulong id) {
            var inst = new User(id);
            inst.Username = user.Username;
            inst.Name = user.Name;
            return inst;
        }
    }
}

其实这里 Builder 内部的 user 被看成参数对象使用了。

一次性 Builder

一次性 Builder 相对简单一些,不须要在 Build() 的时候去拷贝属性。

partial class User {
    public class Builder {
        private User user;      // 这里 user 再也不是 readonly 的
    
        public Builder(ulong id) {
            user = new User(id);
        }

        // ....
        
        public User Build() {
            if (user == null) {
                throw new InvalidOperationException("Build 只能调用一次")
            }

            // 验证和后期加工
            var inst = user;
            user = null;         // 将 user 置 null
            return inst;
        }
    }
}

一次性 BuilderBuild() 以后将 user 设置为 null,那么再调用全部 SetXxxx 方法都会抛空指针异常,而再次调用 Build() 方法则会抛 InvalidOperationException 异常。

小结

其实这个很普通的 C# 的内部类实现。但它确实能够解答“C# 中没有友元怎么办”这之类的问题。Java 中也能够相似的实现,只不过 Java 没有分部类,因此代码都得写在一个源文件里,这个源文件可能会很长很长……

相关文章
相关标签/搜索