진행 상황을보고하면서 다양한 형식으로 개체 내보내기

Aug 18 2020

기술

WinForms 응용 프로그램에는 다음 유형의 개체를 다양한 형식으로 내보내는 기능이 있습니다.

class Item
{
    public int id { get; set; }
    public string description { get; set; }
}

창에서 버튼을 클릭하면 a SaveFileDialog가 표시되며 현재 데이터를 .txt, .csv 또는 .xlsx 형식으로 저장하는 옵션을 제공합니다. 때로는 수백 또는 수천 개의 개체가 있고 UI가 고정되어서는 안되므로이 Task작업을 실행 하는 데 a 가 사용됩니다. 이 구현은 작동하지만 개선 될 수 있습니다.

암호

public partial class ExportWindow : Form
{
    // objects to be exported
    List<Item> items;

    // event handler for the "Export" button click
    private async void exportButton_click(object sender, System.EventArgs e)
    {
        SaveFileDialog exportDialog = new SaveFileDialog();
        exportDialog.Filter = "Text File (*.txt)|*.txt|Comma-separated values file (*.csv)|*.csv|Excel spreadsheet (*.xlsx)|*.xlsx";
        exportDialog.CheckPathExists = true;
        DialogResult result = exportDialog.ShowDialog();
        if (result == DialogResult.OK)
        {
            var ext = System.IO.Path.GetExtension(saveExportFileDlg.FileName);

            try
            { 
                // update status bar
                // (it is a custom control)
                statusBar.text("Exporting");

                // now export it
                await Task.Run(() =>
                {
                    switch (ext.ToLower())
                    {
                        case ".txt":
                            saveAsTxt(exportDialog.FileName);
                            break;

                        case ".csv":
                            saveAsCsv(exportDialog.FileName);
                            break;
                    
                        case ".xlsx":
                            saveAsExcel(exportDialog.FileName);
                            break;

                        default:
                            // shouldn't happen
                            throw new Exception("Specified export format not supported.");
                    }
                });
            }
            catch (System.IO.IOException ex)
            {
                 statusBar.text("Export failed");
                 logger.logError("Export failed" + ex.Message + "\n" + ex.StackTrace);

                 return;
            }
        }
    }

    private delegate void updateProgressDelegate(int percentage);

    public void updateProgress(int percentage)
    {
        if (statusBar.InvokeRequired)
        {
            var d = updateProgressDelegate(updateProgress);
            statusBar.Invoke(d, percentage);
        }
        else
        {
            _updateProgress(percentage);
        }
    }

    private void saveAsTxt(string filename)
    {
        IProgress<int> progress = new Progress<int>(updateProgress);
        
        // save the text file, while reporting progress....
    }

    private void saveAsCsv(string filename)
    {
        IProgress<int> progress = new Progress<int>(updateProgress);
        
        using (StreamWriter writer = StreamWriter(filename))
        {
            // write the headers and the data, while reporting progres...
        }
    }

    private void saveAsExcel(string filename)
    {
        IProgress<int> progress = Progress<int>(updateProgress);

        // EPPlus magic to write the data, while reporting progress...
    }
}

질문

확장 성을 높이기 위해 어떻게 리팩토링 할 수 있습니까? 즉, 더 많은 파일 유형에 대한 지원을 추가하려면 쉽고 빠르게 수정할 수 있습니다. switch 문은 매우 길어질 수 있습니다. 기본적으로 개방 / 폐쇄 원칙을 준수하는 방법은 무엇입니까?

답변

5 CharlesNRice Aug 18 2020 at 20:04

실제 수출품을 자신의 클래스로 옮기는 것이 좋습니다. 수출용 인터페이스를 만들 수 있습니다. 라인을 따라 뭔가

public interface IExport<T>
{
    Task SaveAsync(string fileName, IEnumerable<T> items, IProgress<int> progress = null);
    string ExportType { get; }
}

그러면 각 내보내기 유형이이 인터페이스를 구현할 수 있습니다.

public class ExportItemsToText : IExport<Item>
{
    public Task SaveAsync(string fileName, IEnumerable<Item> items, IProgress<int> progress = null)
    {
        throw new NotImplementedException();
    }

    public string ExportType => "txt";
}

그런 다음 ExportWindow의 생성자에서

public ExportWindow(IEnumerable<IExport<Item>> exports)
{
    // if using DI otherwise could just fill in dictionary here
    ExportStrategy = exports.ToDictionary(x => x.ExportType, x => x);
}

switch 문 대신 이제 딕셔너리에서 키를 검색하여 어떤 내보내기를 실행해야하는지 찾을 수 있으며 찾을 수없는 경우 기본 사례와 동일합니다.

IExport<Item> exporter;
if (ExportStrategy.TryGetValue(ext.ToLower(), out exporter))
{
    await exporter.SaveAsync(exportDialog.FileName, items, new Progress<int>(updateProgress))
}
else
{
    throw new Exception("Specified export format not supported.");
}

이제 앞으로 더 많은 유형에 대한 지원을 추가하는 경우 인터페이스를 구현하고 DI 컨테이너를 업데이트하기 만하면됩니다. 또는 DI를 사용하지 않는 경우 ExportWindow의 생성자에 DI를 추가해야합니다.

나는 이것이 좋은 생각이라고 생각 하지 않지만 내보내기마다 클래스를 만들고 싶지 않다면 딕셔너리를 IDictionary<string, Action<string>>만든 다음 거기에 메소드를 넣고 새 유형을 추가 할 때 create 방법 및 사전을 업데이트하십시오.

2 iSR5 Aug 21 2020 at 06:22

이전 프로젝트 중 하나 (ASP.NET에 있음)에서 이미이 (일종)를 구현했기 때문에 내가 가진 것을 공유하고 싶지만 다른 환경에서도 적용 할 수 있습니다. 구현은 CharlesNRice 제안과 유사했습니다. 그러나 요구 사항은 시스템 보고서 (하나의 보고서 템플릿 만 사용됨)를 Pdf, Excel 및 Word로 내보낼 수있는 옵션 만 가지고 있어야하며 향후 더 많은 내보내기 옵션을 갖도록 협상했습니다. 그래서 이것이 내가 한 방법입니다.

먼저 인터페이스 :

public interface IExportTo<T>
{
    IExportTo<T> Generate();

    void Download(string fileName);

    void SaveAs(string fileFullPath);
}

그런 다음 컨테이너 클래스 :

public class ExportTo : IDisposable
{
    private readonly IList<T> _source;

    public ExportTo(IList<T> source)
    {
        _source = source;
    }

    public ExportExcel Excel()
    {
        return new ExportExcel(_source);
    }

    public ExportPdf Pdf()
    {
        return new ExportPdf(_source);
    }
    
    public ExportWord Word()
    {
        return new ExportPdf(_source);
    }
    

    #region IDisposable

    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                Dispose();
            }

            _disposed = true;
        }
    }


    ~ExportTo()
    {
        Dispose(false);
    }

    #endregion
}

위의 클래스에서 볼 수 있듯이 각 내보내기 유형에 대한 클래스를 구현했습니다. 하나의 클래스를 공유 할 것입니다 (실제 클래스에서 단순화하겠습니다).

public sealed class ExportPdf : IExportTo<T>, IDisposable
{
    private readonly IList<T> _source;

    private ExportPdf() { }

    public ExportPdf(IList<T> source) : this() => _source = source ?? throw new ArgumentNullException(nameof(source));

    public IExportTo<T> Generate()
    {
        // some implementation 
        return this;
    }

    // another overload to generate by Id 
    public IExportTo<T> Generate(long reportId)
    {
        // do some work 
        return this;
    }

    // Download report as file 
    public void Download(string fileName)
    {
       // do some work 
    }

    public void SaveAs(string fileFullPath)
    {
        throw new NotImplementedException("This function has not been implemented yet. Only download is available for now.");
    }


    #region IDisposable

    private bool _disposed = false;

    public void Dispose()
    {   
        Dispose(true);
        GC.SuppressFinalize(this);
    }


    private void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                Dispose();
            }

            _disposed = true;
        }
    }


    ~ExportPdf()
    {
        Dispose(false);
    }

    #endregion
}

DownloadSaveAs(동일하지 않음) 다르다. Download내 보낸 파일을 다운로드하고 SaveAs개체 인스턴스를 저장합니다. 그러나 이것은 의존성을 사용했기 때문에 이렇게 구현되었습니다.

이제 사용법은 다음과 같습니다.

new ExportTo(someList)
.Pdf()
.Generate()
.Download(fileName);

이것이 내가 그 프로젝트에서 구현 한 방법이며 개선 될 수 있지만 비즈니스 요구 사항에는 충분합니다.

새로운 내보내기 유형을 추가해야 할 때마다 새 sealed클래스를 만든 다음 IExportTo<T>, IDisposable해당 클래스에서 구현 하면됩니다. 마지막으로 컨테이너 클래스를 새 유형으로 업데이트하면 (이 메서드의 새 인스턴스를 여는 메서드 추가) 준비가 완료됩니다.