部分クラスを使用したCの剰余符号のリファクタリング


我々のコードが成長するにつれて、我々は定期的にそれがよく構造化され、組織化する新しい方法を求めて自分自身を見つける.体系的リファクタリングは必要ですが、しばしば非常に簡単に来ません.
我々がしばしば直面する課題の1つは、より大きいクラスの異なる部分をまとめる方法を決めることです.良い程度の分離でさえ、時々、我々は理由についてあまりにたくさんあるかもしれないクラスで終わります.
言語の最も初期のバージョンから、C -経号は、regionsと呼ばれている構成を提供しました.コードを整理しようとするとき、それが役に立ちますが、ほとんどは地域を使用することがgenerally an anti-patternであると同意するようです.たとえ彼らの使用が正当化されることができるとしても、彼らの利点はしばしば読みやすさに関してかなり急なコストで来ます.
私は、論理ブロックを形成するためにグループ・コードができることが役に立つと思っています、しかし、私は地域が彼らが解決するより多くの問題を引き起こすことに同意します.そういうわけで、私は積極的に部分的なクラスを使用していました.
Partial classesは、潜在的にそれ自身のファイルの中で潜在的に複数のパーツにタイプの定義を分けることができるCの角機能です.ビルド中に、コンパイラはすべてのパーツを収集し、1つの場所で定義されているかのように、単一のクラスを生成するためにそれらをまとめます.これは、定義でpartialのキーワードを追加することによって有効です.
この記事では、私自身のコードをリファクタリングするとき、私は通常、部分的なクラスをどのように利用するかを示します.うまくいけば、ここでの例では、同様にこのアプローチを試すことができます.

静的メンバーの抽出
私がほとんどすべての時間が好きである1つの事はクラスの残りの部分から別々の静的プロパティとメソッドです.それは、任意の基準のように見えるかもしれませんが、私たちは、異なる方法で静的および非静的メンバーについての理由を行うので、それは意味があることがわかります.
例を見てみましょう.我々は、ローリングファイルの概念を実装するPartitionedTextWriterと呼ばれる抽象化に取り組んでいる想像してください-それは、以前の1つの特定の文字のしきい値に到達した後、自動的に新しいファイルに切り替えるストリーミングテキストライターとして動作します.
クラスは基本パスで初期化され、各パーティションのファイル名を生成するために使用する必要があります.それが副作用のない純粋なビジネスロジックであるので、静的なヘルパー方法にそれを置くのは、完全な意味を作ります.
通常、静的および非静的メンバーを混合することは全く混乱することができます.代わりに、部分クラスを使用するときの様子を見てみましょう.
public partial class PartitionedTextWriter : TextWriter
{
    private readonly string _baseFilePath;
    private readonly long _partitionLimit;

    private int _partitionIndex;
    private TextWriter _innerWriter;
    private long _partitionCharCount;

    public override Encoding Encoding { get; } = Encoding.UTF8;

    public PartitionedTextWriter(string baseFilePath, long partitionLimit)
    {
        _baseFilePath = baseFilePath;
        _partitionLimit = partitionLimit;
    }

    private void InitializeInnerWriter()
    {
        // Get current file path by injecting partition identifier in the file name
        // E.g. MyFile.txt, MyFile [part 2].txt, etc
        var filePath = GetPartitionFilePath(_baseFilePath, _partitionIndex);

        _innerWriter = File.CreateText(filePath);
    }

    public override void Write(char value)
    {
        // Make sure the underlying writer is initialized
        if (_innerWriter == null)
            InitializeInnerWriter();

        // Write content
        _innerWriter.Write(value);
        _partitionCharCount++;

        // When the char count exceeds the limit,
        // start writing to a new file
        if (_partitionCharCount >= _partitionLimit)
        {
            _partitionIndex++;
            _partitionCharCount = 0;

            _innerWriter?.Dispose();
            _innerWriter = null;
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
            _innerWriter?.Dispose();

        base.Dispose(disposing);
    }
}

public partial class PartitionedTextWriter
{
    // Pure helper function
    private static string GetPartitionFilePath(string baseFilePath, int partitionIndex)
    {
        if (partitionIndex <= 0)
            return baseFilePath;

        // Inject "[part x]" in the file name
        var fileNameWithoutExt = Path.GetFileNameWithoutExtension(baseFilePath);
        var fileExt = Path.GetExtension(baseFilePath);
        var fileName = $"{fileNameWithoutExt} [part {partitionIndex + 1}]{fileExt}";

        var dirPath = Path.GetDirectoryName(baseFilePath);
        if (!string.IsNullOrWhiteSpace(dirPath))
            return Path.Combine(dirPath, fileName);

        return fileName;
    }
}
開発者として初めてこのコードを読んで、あなたはこの分離を最も感謝します.私たちが新しいファイルを作成する概念を扱っているとき、私たちは、GetPartitionFilePathがどのように実装されるかについて、あまり気にしません.同様に、GetPartitionFilePathがどのように機能しているかを知りたいならば、残りのコードは無関係なノイズとして作用するでしょう.
代わりに、別の静的クラスにヘルパーメソッドを移動させることができたと主張できました.その方法が特に他の場所で再利用されることになっているならば、それはいくつかのケースで働くことができました.しかし、それはまた、方法がより発見できないようにするでしょう、そして、私は一般に、認知オーバーヘッドを減らすために、依存関係をできるだけソースに近づけるのを好みます.
この例では、クラスの部分定義は同じファイルに配置されます.私たちの主な目標は、それが断片に切断されたよりもむしろコードをグループ化することですので、物事を近い保つことはより意味があります.私はパーティションを1つの場所に保つには大きすぎる場合のみファイルを個別に移動すると思います.
このアイデアは、特に"Resource acquisition is initialization"パターンと組み合わせるときによく動作します.部分クラスを使用すると、初期化を行うメソッドをグループ化でき、残りのクラスからそれらを分離できます.
次の例では、Windowsオペレーティングシステムのデバイスコンテキストリソースのラッパーです.クラスは、ネイティブリソースにハンドルを提供することで構築することができますが、消費者は手動でこれを行うことはありません.代わりに、それらは、それらのための初期化の面倒を見るNativeDeviceContextのような利用可能な静的メソッドの1つを呼び出しています.
再び、静的メソッドを分割するときにどのように見えますか
// Resource management concerns
public sealed partial class NativeDeviceContext : IDisposable
{
    public IntPtr Handle { get; }

    public NativeDeviceContext(IntPtr handle)
    {
        Handle = handle;
    }

    ~NativeDeviceContext()
    {
        Dispose();
    }

    public void SetGammaRamp(GammaRamp ramp)
    {
        // Call a WinAPI method via p/invoke
        NativeMethods.SetDeviceGammaRamp(Handle, ref ramp);
    }

    public void Dispose()
    {
        NativeMethods.DeleteDC(Handle);
        GC.SuppressFinalize(this);
    }
}

// Resource acquisition concerns
public partial class NativeDeviceContext
{
    public static NativeDeviceContext? FromDeviceName(string deviceName)
    {
        var handle = NativeMethods.CreateDC(deviceName, null, null, IntPtr.Zero);

        return handle != IntPtr.Zero
            ? new NativeDeviceContext(handle)
            : null;
    }

    public static NativeDeviceContext? FromPrimaryMonitor() { /* ... */ }

    public static IReadOnlyList<NativeDeviceContext> FromAllMonitors() { /* ... */ }
}
前の例と同様に、これは視覚的に2つの関連しない(Albeit結合)懸念―リソース初期化とリソース管理―を分離することによって、コードを大いに読みやすくします.

インタフェース実装の分離
部分クラスでできる別の興味深いことは別のインターフェイス実装です.多くの場合、インターフェイスの実装に責任を持つメンバーは、クラスのコア動作に本当に貢献しないので、それらをプッシュするのは理にかなっています.
例えば、HTML DOMの要素を表すクラスFromDeviceName(...)を見てみましょう.それは、深いコピーを容易にするためにその子供とHtmlElementの上で繰り返されるために、IEnumerable<T>を実行します.
部分クラスを使用すると、次のようにコードを配置できます.
// Core concerns
public partial class HtmlElement : HtmlNode
{
    public string TagName { get; }
    public IReadOnlyList<HtmlAttribute> Attributes { get; }
    public IReadOnlyList<HtmlNode> Children { get; }

    public HtmlElement(string tagName,
        IReadOnlyList<HtmlAttribute> attributes,
        IReadOnlyList<HtmlNode> children)
    {
        /* ... */
    }

    public HtmlElement(HtmlElement other)
    {
        /* ... */
    }

    public string? GetAttributeValue(string attributeName) { /* ... */ }

    public IEnumerable<HtmlNode> GetDescendants() { /* ... */ }
}

// Implementation of IEnumerable<T>
public partial class HtmlElement : IEnumerable<HtmlNode>
{
    public IEnumerator<HtmlNode> GetEnumerator() => Children.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// Implementation of ICloneable
public partial class HtmlElement : ICloneable
{
    public object Clone() => new HtmlElement(this);
}
部分的なクラスでインタフェース実装を置くことは、上流に呼ぶ前に進むメソッドによって引き起こされる「ルーティング雑音」を減らすのを助けます.また、C - Chorzは各パーティションのクラスシグネチャを別々に指定することができますので、同じインターフェイスに属するメンバーを便利にグループ化できます.
このアプローチは、条件付きコンパイルと組み合わせたときにも非常に有用です.時折、フレームワークの特定のバージョンで利用可能な機能に依存するAPIを導入したい場合があります.そのためには、ICloneableのディレクティブを使用しなければなりません.
部分クラスは、物事をtidierに役立つことができます.#ifをオーバーライドする例を見てみましょう.NET標準2.1 :
public partial class SegmentedHttpStream : Stream
{
    private readonly HttpClient _httpClient;
    private readonly string _url;
    private readonly long _segmentSize;

    private Stream? _currentStream;

    public SegmentedHttpStream(HttpClient httpClient,
        string url, long length, long segmentSize)
    {
        /* ... */
    }

    /* Skipped overrides for Stream methods */

    protected override void Dispose(bool disposing)
    {
        if (disposing)
            _currentStream?.Dispose();

        base.Dispose(disposing);
    }
}

#if NETSTANDARD2_1
public partial class SegmentedHttpStream
{
    // This method is not available in earlier versions of the standard
    public override async ValueTask DisposeAsync()
    {
        if (_currentStream != null)
            await _currentStream.DisposeAsync();

        await base.DisposeAsync();
    }
}
#endif
このような場合、部分クラスを使用するという明確な利点は、条件付きブロックによって引き起こされるノイズを完全に緩和することができるということです.彼らがコードの間にいる代わりに外へ押し出されるとき、それはずっとよく見えます.

プライベートクラスの編成
プライベートクラスを持つのは珍しくない.つの場所でのみ使用されるタイプを定義している間、名前空間汚染を避けたいとき、これらは便利です.典型的なケースは、サードパーティライブラリやフレームワーク内の特定の動作をオーバーライドするカスタムインターフェイスを実装する必要がある場合です.
例として、我々はHTMLドキュメントとして販売報告書をエクスポートしていると想像し、我々はそれを行うには、Scribanエンジンを使用している.この特定のシナリオでは、ファイルシステムからではなく、アセンブリに埋め込まれたリソースからテンプレートを解決できるように設定する必要があります.そのために、フレームワークは私たちにDisposeAsyncのカスタム実装を提供することを期待します.
私たちのカスタムローダーがこのクラスの中でのみ使用されるようになるのを見て、それはプライベートクラスとして定義するのに完全な意味をなします.しかし、Cが少し冗長であるので、プライベートクラスは我々のコードに不必要な雑音を導入するかもしれません.
部分クラスを使用すると、以下のようにクリーンアップできます.
public partial class HtmlReportRenderer : IReportRenderer
{
    public async ValueTask<string> RenderReportAsync(SalesReport report, string templateCode)
    {
        var template = Template.Parse(templateCode);

        var templateContext = new TemplateContext
        {
            TemplateLoader = new CustomTemplateLoader(), // reference the private class
            StrictVariables = true
        };

        var model = new ScriptObject();
        model.SetValue("report", report, true);

        templateContext.PushGlobal(model);

        return await template.RenderAsync(templateContext);
    }
}

public partial class HtmlReportRenderer
{
    // This type is only used within HtmlReportRenderer
    private class CustomTemplateLoader : ITemplateLoader
    {
        private static readonly string ResourceRootNamespace =
            $"{typeof(HtmlReportRenderer).Namespace}.Templates";

        private static StreamReader GetTemplateReader(string templatePath)
        {
            var resourceName = $"{ResourceRootNamespace}.{templatePath}";

            var assembly = Assembly.GetExecutingAssembly();

            using var stream = assembly.GetManifestResourceStream(resourceName);
            if (stream == null)
                throw new MissingManifestResourceException("Template not found.");

            return new StreamReader(stream);
        }

        public string GetPath(
            TemplateContext context,
            SourceSpan callerSpan,
            string templateName) => templateName;

        public string Load(
            TemplateContext context,
            SourceSpan callerSpan,
            string templatePath) => GetTemplateReader(templatePath).ReadToEnd();

        public async ValueTask<string> LoadAsync(
            TemplateContext context,
            SourceSpan callerSpan,
            string templatePath) => await GetTemplateReader(templatePath).ReadToEndAsync();
    }
}

任意符号のグループ化
部分クラスの使用を決定する特別な場合は必ずしも必要ありません.実際には、時々私たちのコードの一部をいくつかの論理ブロックに分割する権利を感じます.
この例では、ファイルをフォーマットするコマンドラインアプリケーションを使用します.オプションとコマンドの振舞いは、単一のクラスの一部として定義されます.
部分クラスを使用することで、クラスの異なる部分を分割してグループ化できます.
// Core options
public partial class FormatCommand
{
    [CommandOption("files", 'f', IsRequired = true, Description = "List of files to process.")]
    public IReadOnlyList<FileInfo> Files { get; set; }

    [CommandOption("config", 'c', Description = "Configuration file.")]
    public FileInfo? ConfigFile { get; set; }
}

// Options related to formatting
public partial class FormatCommand
{
    [CommandOption("indent-size", Description = "Override: indent size.")]
    public int? IndentSize { get; set; } = 4;

    [CommandOption("line-length", Description = "Override: line length.")]
    public int? LineLength { get; set; } = 80;

    [CommandOption("insert-eof-newline", Description = "Override: insert new line at EOF.")]
    public bool? InsertEofNewLine { get; set; } = false;
}

// Command implementation
[Command("format", Description = "Format files.")]
public partial class FormatCommand : ICommand
{
    private readonly IFormattingService _formattingService;

    public FormatCommand(IFormattingService formattingService)
    {
        _formattingService = formattingService;
    }

    private Config LoadConfig() { /* ... */ }

    public async ValueTask ExecuteAsync(IConsole console)
    {
        var config = LoadConfig();

        foreach (var file in Files)
        {
            await _formattingService.FormatAsync(config, file.FullName);
            console.Output.WriteLine($"Formatted: {file.FullName}");
        }
    }
}

概要
部分クラスは、単に自動生成されたコードよりも使用できます.それは、より小さな論理的に独立した単位にコードを手配する創造的な方法を可能にする強力な言語機能です.これは、我々が認知負荷を減らすか、単にものをもう少し組織化したいとき、非常に役に立つことがありえます.
私たちはリファクタリングのトピックにあるので、また、クリーナーコードを書くためにa few interesting ways we can use extension methodsをチェックアウトします.部分クラスと同様に、それらはあなたが考えたより多くの用途を持っているかもしれません.
新しい記事を投稿するときに通知する✨