Làm cách nào để thực hiện nguyên tắc DRY khi sử dụng từ khóa 'bằng cách sử dụng'?


23

Hãy xem xét các phương pháp sau:

public List<Employee> GetAllEmployees()
{
    using (Entities entities = new Entities())
    {
        return entities.Employees.ToList();
    }
}

public List<Job> GetAllJobs()
{
    using (Entities entities = new Entities())
    {
        return entities.Jobs.ToList();
    }
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    using (Entities entities = new Entities())
    {
        return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

Sử dụng khối là như nhau và đã được lặp lại 3 lần ở đây (tất nhiên, hơn 100 lần trong ứng dụng thực). Làm thế nào có thể thực hiện hiệu trưởng DRY (Đừng lặp lại chính mình) cho usingkhối? Có bị coi là vi phạm tiền gốc DRY không?

Cập nhật: Tôi không nói về những gì đã được thực hiện trong usingkhối. Những gì tôi thực sự có nghĩa ở đây, là using (Entities entities = new Entities()). Dòng này được lặp lại 100 lần trở lên.


2
đây có phải là C # không? Câu trả lời cho câu hỏi của bạn có thể phụ thuộc vào ngôn ngữ
David

Vâng @David, xin lỗi vì tôi đã không đề cập đến ngôn ngữ của mình. Làm thế nào nó có thể ảnh hưởng đến câu trả lời?
Saeed Neamati

một số ngôn ngữ có thể có cú pháp cụ thể có thể giúp bạn tính một chút mã của mình. Tôi không biết C #, nhưng trong Ruby, tôi nghĩ bạn có thể sử dụng các khối để tính phần sử dụng.
David

Câu lệnh sử dụng thực sự cung cấp hỗ trợ ngôn ngữ C # để áp dụng nguyên tắc DRY để giúp tránh mã hóa lặp đi lặp lại trong khi quản lý xử lý tài nguyên với mẫu thiết kế Vứt bỏ . Điều đó không có nghĩa là chúng ta không thể tìm ra cách để làm mọi thứ DRYer! Cá nhân, tôi nghĩ về DRY như một quá trình đệ quy.
John Tobler

Câu trả lời:


24

Một ý tưởng sẽ là bọc nó với một hàm có một Func.

Một cái gì đó như thế này

public K UsingT<T,K>(Func<T,K> f) where T:IDisposable,new()
{
    using (T t = new T())
    {
        return f(t);
    }
}

Sau đó, mã trên của bạn trở thành

public List<Employee> GetAllEmployees()
{
    return UsingT<Entities,List<Employee>>(e=>e.Employees.ToList());
}

public List<Job> GetAllJobs()
{
    return UsingT<Entities,List<Job>>(e=>e.Jobs.ToList());
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return UsingT<Entities,List<Task>>(e=>e.Tasks.Where(t => t.JobId == job.Id).ToList());
}

Tôi cũng đã tạo Entitiesmột loại param, vì tôi cho rằng bạn có nhiều hơn một loại bạn đang làm điều này với. Nếu bạn không, bạn có thể xóa nó và chỉ sử dụng kiểu param cho kiểu trả về.

Thành thật mà nói mặc dù loại mã này không giúp đọc được gì cả. Theo kinh nghiệm của tôi, các đồng nghiệp của Jr cũng có một thời gian thực sự khó khăn với nó.

Cập nhật Một số biến thể bổ sung về người trợ giúp bạn có thể xem xét

//forget the Entities type param
public T UsingEntities<T>(Func<Entities,T> f)
{
    using (Entities e = new Entities())
    {
        return f(e);
    }
}
//forget the Entities type param, and return an IList
public IList<T> ListFromEntities<T>(Func<Entities,IEnumerable<T>> f)
{
    using (Entities e = new Entities())
    {
        return f(e).ToList();
    }
}
//doing the .ToList() forces the results to enumerate before `e` gets disposed.

1
+1 Giải pháp hay, mặc dù nó không giải quyết được vấn đề thực tế (không bao gồm trong câu hỏi ban đầu), tức là có nhiều sự cố xảy ra Entities.
back2dos

@Doc Brown: Tôi nghĩ rằng tôi đã đánh bại bạn với tên hàm. Tôi đã rời IEnumerablekhỏi chức năng trong trường hợp có bất kỳ thuộc IEnumerabletính nào của T mà người gọi muốn trả lại, nhưng bạn nói đúng, nó sẽ dọn sạch nó một chút. Có lẽ có một người trợ giúp cho cả Độc thân và IEnumerablekết quả sẽ tốt. Điều đó nói rằng, tôi vẫn nghĩ rằng nó làm chậm sự công nhận về những gì mã làm được, đặc biệt đối với một người không quen sử dụng nhiều thuốc generic và lambdas (ví dụ: đồng nghiệp của bạn KHÔNG ở trên SO :))
Brook

+1 Tôi nghĩ cách tiếp cận này là tốt. Và khả năng đọc có thể được cải thiện. Ví dụ: đặt "ToList" vào WithEntities, sử dụng Func<T,IEnumerable<K>>thay vì Func<T,K>và đặt tên "WithEntities" tốt hơn (như SelectEntities). Và tôi không nghĩ "Thực thể" cần phải là một tham số chung ở đây.
Doc Brown

1
Để làm cho công việc này, các ràng buộc sẽ cần phải where T : IDisposable, new(), như usingyêu cầu IDisposableđể làm việc.
Anthony Pegram

1
@Anthony Pegram: Đã sửa, cảm ơn! (đó là những gì tôi nhận được để mã hóa C # trong cửa sổ trình duyệt)
Brook

23

Đối với tôi điều này sẽ giống như lo lắng về việc tìm kiếm trên cùng một bộ sưu tập nhiều lần: đó chỉ là việc bạn cần làm. Bất kỳ nỗ lực để trừu tượng hóa nó hơn nữa sẽ làm cho mã ít dễ đọc hơn.


Sự tương tự tuyệt vời @Ben. +1
Saeed Neamati

3
Tôi không đồng ý, xin lỗi. Đọc các bình luận của OP về phạm vi giao dịch và suy nghĩ về những gì bạn phải làm khi bạn đã viết 500 lần các loại mã đó, và sau đó nhận thấy rằng bạn sẽ phải thay đổi điều tương tự 500 lần. Loại lặp lại mã này có thể chỉ ổn nếu bạn có <10 trong số các chức năng đó.
Doc Brown

Ồ, và đừng quên, nếu bạn cho mỗi bộ sưu tập giống nhau hơn 10 lần theo cách rất giống nhau, với mã trông tương tự bên trong mỗi bộ, bạn chắc chắn nên nghĩ về một số khái quát.
Doc Brown

1
Đối với tôi có vẻ như bạn đang thực hiện một lớp lót ba trong một oneliner ... bạn vẫn đang lặp lại chính mình.
Jim Wolff

Nó phụ thuộc vào ngữ cảnh, nếu foreachbộ sưu tập quá lớn hoặc logic trong foreachvòng lặp chẳng hạn. Một phương châm tôi đã áp dụng: Đừng ám ảnh mà hãy luôn chú ý đến cách tiếp cận của bạn
Coops

9

Có vẻ như bạn đang nhầm lẫn nguyên tắc "Một lần và Chỉ một lần" với nguyên tắc DRY. Nguyên tắc DRY nêu rõ:

Mỗi phần kiến ​​thức phải có một đại diện duy nhất, rõ ràng, có thẩm quyền trong một hệ thống.

Tuy nhiên, nguyên tắc Một lần và Chỉ một lần là hơi khác nhau.

Nguyên tắc [DRY] tương tự như AgainAndOnlyOnce, nhưng với một mục tiêu khác. Với AgainAndOnlyOnce, bạn được khuyến khích tái cấu trúc để loại bỏ mã và chức năng trùng lặp. Với DRY, bạn cố gắng xác định nguồn duy nhất, dứt khoát của mọi phần kiến ​​thức được sử dụng trong hệ thống của bạn và sau đó sử dụng nguồn đó để tạo các phiên bản áp dụng của kiến ​​thức đó (mã, tài liệu, bài kiểm tra, v.v.).

Nguyên tắc DRY thường được sử dụng trong bối cảnh logic thực tế, không quá nhiều sử dụng các câu lệnh:

Giữ cấu trúc của một chương trình DRY khó hơn và giá trị thấp hơn. Đó là quy tắc kinh doanh, câu lệnh if, công thức toán học và siêu dữ liệu chỉ xuất hiện ở một nơi. Nội dung WET - các trang HTML, dữ liệu kiểm tra dự phòng, dấu phẩy và {} dấu phân cách - tất cả đều dễ dàng bỏ qua, do đó, DRYing chúng ít quan trọng hơn.

Nguồn


7

Tôi không thấy việc sử dụng usingở đây:

Làm thế nào về:

public List<Employee> GetAllEmployees() {
    return (new Entities()).Employees.ToList();
}
public List<Job> GetAllJobs() {
    return (new Entities()).Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return (new Entities()).Tasks.Where(t => t.JobId == job.Id).ToList();
}

Hoặc thậm chí tốt hơn, vì tôi không nghĩ rằng bạn cần tạo một đối tượng mới mỗi lần.

private Entities entities = new Entities();//not sure C# allows for that kind of initialization, but you can do it in the constructor if needed

public List<Employee> GetAllEmployees() {
    return entities.Employees.ToList();
}
public List<Job> GetAllJobs() {
    return entities.Jobs.ToList();
}
public List<Task> GetAllTasksOfTheJob(Job job) {
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

Đối với vi phạm DRY: DRY không áp dụng ở cấp độ này. Trong thực tế không có nguyên tắc thực sự làm, ngoại trừ khả năng đọc. Cố gắng áp dụng DRY ở cấp độ đó thực sự chỉ là tối ưu hóa vi mô kiến ​​trúc, giống như tất cả tối ưu hóa vi mô chỉ là đổ xe đạp và không giải quyết được bất kỳ vấn đề nào, nhưng thậm chí có rủi ro để giới thiệu những vấn đề mới.
Từ kinh nghiệm của riêng tôi, tôi biết rằng nếu bạn cố gắng giảm sự dư thừa mã ở mức đó, bạn sẽ tạo ra tác động tiêu cực đến chất lượng mã, bằng cách che giấu những gì thực sự rõ ràng và đơn giản.

Chỉnh sửa:
Ok. Vì vậy, vấn đề không thực sự là câu lệnh sử dụng, vấn đề là sự phụ thuộc vào đối tượng bạn tạo ra mỗi lần. Tôi sẽ đề nghị tiêm một constructor:

private delegate Entities query();//this should be injected from the outside (upon construction for example)
public List<Employee> GetAllEmployees() {
    using (var entities = query()) {//AFAIK C# can infer the type here
        return entities.Employees.ToList();
    }
}
//... and so on

2
Nhưng @ back2dos, có nhiều nơi chúng tôi sử dụng using (CustomTransaction transaction = new CustomTransaction())khối mã trong mã của mình để xác định phạm vi giao dịch aa. Điều đó không thể được gói vào một đối tượng và ở mọi nơi bạn muốn sử dụng giao dịch, bạn nên viết một khối. Bây giờ nếu bạn muốn thay đổi loại giao dịch đó từ CustomTransactionthành BuiltInTransactionhơn 500 phương thức thì sao? Đây dường như là một nhiệm vụ lặp lại và là một ví dụ vi phạm của hiệu trưởng DRY.
Saeed Neamati

3
Việc "sử dụng" ở đây sẽ đóng bối cảnh dữ liệu nên không thể tải nhanh ngoài các phương thức này.
Steven Striga

@Saeed: Đó là khi bạn nhìn vào tiêm phụ thuộc. Nhưng điều đó dường như là khá khác với trường hợp như đã nêu trong câu hỏi.
một CVn

@Saeed: Đăng cập nhật.
back2dos

@WeekendWar Warrior Có phải using(trong ngữ cảnh này) vẫn là một "cú pháp thuận tiện" chưa biết nhiều hơn? Tại sao nó rất hay để sử dụng =)
Coops

4

Không chỉ sử dụng là mã trùng lặp (bằng cách đó là mã trùng lặp và thực sự so sánh với một câu lệnh try..catch..finally) mà cả toList cũng vậy. Tôi sẽ cấu trúc lại mã của bạn như thế này:

 public List<T> GetAll(Func<Entities, IEnumerable<T>> getter) {
    using (Entities entities = new Entities())
    {
        return getter().ToList();
    }
 }

public List<Employee> GetAllEmployees()
{
    return GetAll(e => e.Employees);
}

public List<Job> GetAllJobs()
{
    return GetAll(e => e.Jobs);
}

public List<Task> GetAllTasksOfTheJob(Job job)
{
    return GetAll(e => e.Tasks.Where(t => t.JobId == job.Id));
}

3

Vì không có logic kinh doanh của bất kỳ loại nào ở đây ngoại trừ cái cuối cùng. Nó không thực sự KHÔ, theo quan điểm của tôi.

Cái cuối cùng không có bất kỳ DRY nào trong khối sử dụng nhưng tôi đoán mệnh đề where sẽ thay đổi ở đâu khi nó được sử dụng.

Đây là một công việc điển hình cho các trình tạo mã. Viết và bao gồm trình tạo mã và để nó tạo cho từng loại.


Không có @arunmur. Có một sự hiểu lầm ở đây. Theo DRY, tôi có nghĩa là using (Entities entities = new Entities())khối. Ý tôi là, dòng mã này được lặp lại 100 lần và ngày càng được lặp lại nhiều hơn.
Saeed Neamati

DRY chủ yếu xuất phát từ thực tế là bạn cần phải viết các trường hợp thử nghiệm nhiều lần và một lỗi tại một nơi có nghĩa là bạn phải sửa nó ở 100 vị trí. Việc sử dụng (Thực thể ...) là một mã quá đơn giản để phá vỡ. Nó không cần phải được phân chia hoặc đưa vào một lớp khác. Nếu bạn vẫn khăng khăng đơn giản hóa nó. Tôi sẽ đề nghị một chức năng gọi lại Yeild từ ruby.
arunmur

1
@arnumur - sử dụng không phải lúc nào cũng quá đơn giản để phá vỡ. Tôi thường có một chút logic để xác định các tùy chọn sẽ sử dụng trên bối cảnh dữ liệu. Rất có khả năng chuỗi kết nối sai có thể được truyền vào.
Morgan Herlocker

1
@ironcode - Trong những tình huống đó có ý nghĩa để đẩy nó ra một chức năng. Nhưng trong những ví dụ này, nó khá khó để phá vỡ. Ngay cả khi nó phá vỡ bản sửa lỗi thường là trong định nghĩa của chính Thực thể, lớp này sẽ được bao phủ trong một trường hợp thử nghiệm riêng biệt.
arunmur

+1 @arunmur - Tôi đồng ý. Tôi thường làm điều đó bản thân mình. Tôi viết các bài kiểm tra cho chức năng đó, nhưng có vẻ hơi vượt trội để viết một bài kiểm tra cho câu lệnh sử dụng của bạn.
Morgan Herlocker

2

Vì bạn đang tạo và hủy bỏ cùng một đối tượng dùng một lần, lớp của bạn là một ứng cử viên tốt để thực hiện mẫu IDis Dùng một lần.

class ThisClass : IDisposable
{
    protected virtual Entities Context { get; set; }

    protected virtual void Dispose( bool disposing )
    {
        if ( disposing && Context != null )
            Context.Dispose();
    }

    public void Dispose()
    {
        Dispose( true );
    }

    public ThisClass()
    {
        Context = new Entities();
    }

    public List<Employee> GetAllEmployees()
    {
        return Context.Employees.ToList();
    }

    public List<Job> GetAllJobs()
    {
        return Context.Jobs.ToList();
    }

    public List<Task> GetAllTasksOfTheJob(Job job)
    {
        return Context.Tasks.Where(t => t.JobId == job.Id).ToList();
    }
}

Điều này khiến bạn chỉ cần "sử dụng" khi tạo một thể hiện của lớp. Nếu bạn không muốn lớp chịu trách nhiệm xử lý các đối tượng, thì bạn có thể làm cho các phương thức chấp nhận sự phụ thuộc như một đối số:

public static List<Employee> GetAllEmployees( Entities entities )
{
    return entities.Employees.ToList();
}

public static List<Job> GetAllJobs( Entities entities )
{
    return entities.Jobs.ToList();
}

public static List<Task> GetAllTasksOfTheJob( Entities entities, Job job )
{
    return entities.Tasks.Where(t => t.JobId == job.Id).ToList();
}

1

Một chút yêu thích của tôi về phép thuật không thể so sánh!

public class Blah
{
  IEnumerable<T> Wrap(Func<Entities, IEnumerable<T>> act)
  {
    using(var entities = new Entities()) { return act(entities); }
  }

  public List<Employee> GetAllEmployees()
  {
    return Wrap(e => e.Employees.ToList());
  }

  public List<Job> GetAllJobs()
  {
    return Wrap(e => e.Jobs.ToList());
  }

  public List<Task> GetAllTasksOfTheJob(Job job)
  {
    return Wrap(e => e.Tasks.Where(x ....).ToList());
  }
}

Wrapchỉ tồn tại để trừu tượng ra hoặc bất kỳ phép thuật nào bạn cần. Tôi không chắc chắn tôi sẽ khuyên bạn điều này mọi lúc nhưng nó có thể sử dụng. Ý tưởng "tốt hơn" sẽ là sử dụng một thùng chứa DI, như StructMap, và chỉ phạm vi lớp Thực thể cho bối cảnh yêu cầu, đưa nó vào bộ điều khiển và sau đó để nó xử lý vòng đời mà không cần bộ điều khiển của bạn.


Các tham số loại của bạn cho Func <IEnumerable <T>, Thực thể> theo thứ tự sai. (xem câu trả lời của tôi về cơ bản là giống nhau)
Brook

Vâng, đủ dễ để sửa. Tôi không bao giờ nhớ thứ tự đúng là gì. Tôi sử dụng Funcđủ tôi nên.
Travis
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.