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 沒有分部類,因此代碼都得寫在一個源文件裏,這個源文件可能會很長很長……

相關文章
相關標籤/搜索