2010-04-16 45 views
9

Giả sử tôi đã tạo ra một lớp wrapper như sau:phương pháp tiếp cận cho chung chung, thời gian biên dịch phương pháp lười biếng tải an toàn

public class Foo : IFoo 
{ 
    private readonly IFoo innerFoo; 

    public Foo(IFoo innerFoo) 
    { 
     this.innerFoo = innerFoo; 
    } 

    public int? Bar { get; set; } 
    public int? Baz { get; set; } 
} 

Ý tưởng ở đây là innerFoo có thể quấn các phương pháp truy cập dữ liệu hoặc một cái gì đó đắt tương tự và tôi chỉ muốn các phương thức GetBarGetBaz được gọi một lần. Vì vậy, tôi muốn tạo một wrapper khác xung quanh nó, thao tác này sẽ lưu các giá trị thu được trong lần chạy đầu tiên.

Nó đủ đơn giản để làm được điều này, tất nhiên:

int IFoo.GetBar() 
{ 
    if ((Bar == null) && (innerFoo != null)) 
     Bar = innerFoo.GetBar(); 
    return Bar ?? 0; 
} 

int IFoo.GetBaz() 
{ 
    if ((Baz == null) && (innerFoo != null)) 
     Baz = innerFoo.GetBaz(); 
    return Baz ?? 0; 
} 

Nhưng nó được lặp đi lặp lại khá nếu tôi đang làm điều này với 10 thuộc tính khác nhau và 30 giấy gói khác nhau. Vì vậy, tôi đã tìm, hey, chúng ta hãy làm này generic:

T LazyLoad<T>(ref T prop, Func<IFoo, T> loader) 
{ 
    if ((prop == null) && (innerFoo != null)) 
     prop = loader(innerFoo); 
    return prop; 
} 

nào gần được tôi, nơi tôi muốn, nhưng không hoàn toàn, bởi vì bạn không thể ref một tính năng tự động tài sản (hoặc bất cứ tài sản nào cả). Nói cách khác, tôi không thể viết này:

int IFoo.GetBar() 
{ 
    return LazyLoad(ref Bar, f => f.GetBar()); // <--- Won't compile 
} 

Thay vào đó, tôi sẽ phải thay đổi Bar để có một lĩnh vực ủng hộ rõ ràng và viết thu khí rõ ràng và setters. Đó là tốt, ngoại trừ thực tế là tôi kết thúc bằng văn bản mã dư thừa nhiều hơn tôi đã viết ở nơi đầu tiên.

Sau đó, tôi xem xét khả năng của việc sử dụng cây biểu thức:

T LazyLoad<T>(Expression<Func<T>> propExpr, Func<IFoo, T> loader) 
{ 
    var memberExpression = propExpr.Body as MemberExpression; 
    if (memberExpression != null) 
    { 
     // Use Reflection to inspect/set the property 
    } 
} 

này đóng tốt đẹp với refactoring - nó sẽ làm việc tuyệt vời nếu tôi làm điều này:

return LazyLoad(f => f.Bar, f => f.GetBar()); 

Nhưng nó không phải là thực sự an toàn, vì ai đó ít thông minh hơn (tức là bản thân tôi trong 3 ngày kể từ bây giờ khi tôi chắc chắn quên cách thực hiện điều này trong nội bộ) có thể quyết định viết thay thế này:

return LazyLoad(f => 3, f => f.GetBar()); 

Điều gì sẽ bị hỏng hoặc dẫn đến hành vi không mong muốn/không xác định, tùy thuộc vào cách phòng thủ tôi viết phương pháp LazyLoad. Vì vậy, tôi không thực sự thích cách tiếp cận này, hoặc, bởi vì nó dẫn đến khả năng lỗi thời gian chạy mà có thể đã được ngăn chặn trong nỗ lực đầu tiên. Nó cũng dựa trên Reflection, mà cảm thấy một chút bẩn ở đây, mặc dù mã này được thừa nhận là không hiệu suất nhạy cảm.

Bây giờ tôi có thể cũng quyết định đi tất cả ra và sử dụng DynamicProxy để làm phương pháp đánh chặn và không phải viết bất kỳ mã , và trong thực tế tôi đã làm điều này trong một số ứng dụng. Nhưng mã này nằm trong một thư viện lõi mà nhiều hội đồng khác phụ thuộc vào, và có vẻ như khủng khiếp sai khi giới thiệu loại phức tạp này ở mức độ thấp như vậy. Tách việc thực hiện dựa trên chặn máy chủ từ giao diện IFoo bằng cách đặt nó vào trong hội đồng của chính nó không thực sự hữu ích; thực tế là lớp này vẫn sẽ được sử dụng khắp nơi, phải sử dụng, vì vậy đây không phải là một trong những vấn đề có thể được giải quyết một cách trivially với một chút phép thuật DI.

Tùy chọn cuối cùng tôi đã nghĩ đến việc sẽ có một phương pháp như:

T LazyLoad<T>(Func<T> getter, Action<T> setter, Func<IFoo, T> loader) { ... } 

Tùy chọn này là rất "meh" cũng - nó tránh Reflection nhưng vẫn là dễ bị lỗi, nó không thực sự làm giảm sự lặp lại nhiều. Nó gần như là xấu như phải viết getters rõ ràng và setters cho mỗi tài sản.

Có lẽ tôi chỉ là cực kỳ khó tính, nhưng ứng dụng này vẫn đang trong giai đoạn đầu, và nó sẽ tăng lên đáng kể theo thời gian, và tôi thực sự muốn giữ cho mã sạch sẽ.

Tóm lại: Tôi đang gặp khó khăn, tìm kiếm các ý tưởng khác.

Câu hỏi:

Có cách nào để làm sạch mã lười biếng nạp ở phía trên, như vậy mà thực hiện sẽ:

  • Đảm bảo thời gian biên dịch an toàn, giống như phiên bản ref;
  • Thực tế, giảm số lần lặp lại mã, như phiên bản Expression; và
  • Không nhận bất kỳ phụ thuộc bổ sung đáng kể nào?

Nói cách khác, có cách nào để làm điều này chỉ bằng các tính năng ngôn ngữ C# thông thường và có thể là một vài lớp trợ giúp nhỏ? Hay tôi chỉ cần phải chấp nhận rằng có một sự đánh đổi ở đây và tấn công một trong những yêu cầu trên từ danh sách?

+0

Bạn có thực sự cần phải có thuộc tính có thể đọc/ghi công khai có thể hoặc không được điền và một GetX() công khai đặt thuộc tính và trả về giá trị? –

+0

@ Jeffrey: Câu chuyện dài ngắn gọn, vâng, bởi vì một phần của lý do cho lớp này là các thuộc tính có thể được háo hức nạp thay thế. Ví dụ, nếu đối tượng kết thúc tốt đẹp một số cuộc gọi cơ sở dữ liệu, một thành phần cấp cao hơn có thể quyết định tải một số trong số này từ một bản ghi lớn để tránh lặp lại các cuộc gọi cơ sở dữ liệu. Tôi đang mở để tách các chức năng này - * nếu * kết quả cuối cùng vẫn đáp ứng được tất cả 3 tiêu chí. – Aaronaught

Trả lời

1

Tôi đã kết thúc triển khai một cái gì đó là kinda sorta tương tự như lớp Lazy trong .NET 4, nhưng được tùy chỉnh theo khái niệm cụ thể về "lưu vào bộ nhớ cache" hơn là "tải chậm".

Lớp bán lười biếng trông như thế này:

public class CachedValue<T> 
{ 
    private Func<T> initializer; 
    private bool isValueCreated; 
    private T value; 

    public CachedValue(Func<T> initializer) 
    { 
     if (initializer == null) 
      throw new ArgumentNullException("initializer"); 
     this.initializer = initializer; 
    } 

    public CachedValue(T value) 
    { 
     this.value = value; 
     this.isValueCreated = true; 
    } 

    public static implicit operator T(CachedValue<T> lazy) 
    { 
     return (lazy != null) ? lazy.Value : default(T); 
    } 

    public static implicit operator CachedValue<T>(T value) 
    { 
     return new CachedValue<T>(value); 
    } 

    public bool IsValueCreated 
    { 
     get { return isValueCreated; } 
    } 

    public T Value 
    { 
     get 
     { 
      if (!isValueCreated) 
      { 
       value = initializer(); 
       isValueCreated = true; 
      } 
      return value; 
     } 
    } 
} 

Ý tưởng là, không giống như các lớp Lazy<T>, điều này cũng có thể được khởi tạo từ một giá trị cụ thể. Tôi cũng đã triển khai một số toán tử chuyển đổi tiềm ẩn để có thể gán trực tiếp các giá trị cho các thuộc tính CachedValue<T> như thể chúng chỉ là T. Tôi đã không triển khai các tính năng an toàn chỉ của Lazy<T> - những trường hợp này không được thiết kế để truyền đi.

Sau đó, vì nó rất dài dòng để khởi tạo những điều này, tôi đã tận dụng một số tính năng suy luận kiểu chung để tạo một cú pháp lười biếng-init nhỏ gọn hơn:

public static class Deferred 
{ 
    public static CachedValue<T> From<TSource, T>(TSource source, 
     Func<TSource, T> selector) 
    { 
     Func<T> initializer =() => 
      (source != null) ? selector(source) : default(T); 
     return new CachedValue<T>(initializer); 
    } 
} 

Vào cuối ngày, điều này được tôi là một lớp gần như-POCO sử dụng tính năng tự động tính được khởi tạo với bộ tải chậm trong các nhà xây dựng (được null-coalescing từ Deferred):

public class CachedFoo : IFoo 
{ 
    public CachedFoo(IFoo innerFoo) 
    { 
     Bar = Deferred.From(innerFoo, f => f.GetBar()); 
     Baz = Deferred.From(innerFoo, f => f.GetBaz()); 
    } 

    int IFoo.GetBar() 
    { 
     return Bar; 
    } 

    int IFoo.GetBaz() 
    { 
     return Baz; 
    } 

    public CachedValue<int> Bar { get; set; } 
    public CachedValue<int> Baz { get; set; } 
} 

tôi không hoàn toàn vui mừng với điều này, nhưng tôi khá hạnh phúc với nó. Điều tốt cho phép người ngoài cư trú các thuộc tính theo cách triển khai, rất hữu ích khi tôi muốn ghi đè hành vi tải chậm, tức là tải trước một loạt bản ghi từ truy vấn SQL lớn:

CachedFoo foo = new CachedFoo(myFoo); 
foo.Bar = 42; 
foo.Baz = 86; 

Tôi đang gắn bó với điều này ngay bây giờ. Nó có vẻ khá khó khăn để vít lên một lớp wrapper với cách tiếp cận này. Bởi vì toán tử chuyển đổi ngầm được sử dụng, nó thậm chí còn an toàn đối với các trường hợp null.

Nó vẫn có cảm giác hơi khó hiểu và tôi vẫn mở ra những ý tưởng tốt hơn.

4

Nếu bạn có thể sử dụng .NET 4, bạn chỉ nên sử dụng Lazy<T>.

Nó cung cấp chức năng bạn đang sử dụng, và, ngoài ra, hoàn toàn an toàn chỉ với chủ đề.

Nếu bạn không thể sử dụng .NET 4, tôi vẫn khuyên bạn nên xem xét nó và "ăn cắp" nó là thiết kế và API. Nó làm cho sự khởi tạo lười biếng khá dễ dàng.

+0

Tôi đã nhìn vào lớp đó (vẫn còn trên .NET 3.5, nhưng tôi có thể tự mình thực hiện) - nhưng thực sự không thể tìm ra cách tôi sẽ phù hợp với thiết kế, hãy nhớ rằng 'innerFoo' thực sự có thể 'null' và các lớp bên ngoài sẽ có thể thay đổi các giá trị thuộc tính (do đó các bộ định cư công khai). Có lẽ tôi chỉ không nhìn thấy rừng cho cây - có lẽ bạn có thể bao gồm một ví dụ về cách này sẽ nhìn bằng cách sử dụng 'Lazy '? – Aaronaught

1

Nếu tất cả các bạn đang cố gắng làm là tránh sao chép mã này 300 lần:

private int? bar; 
public int Bar 
{ 
    get 
    { 
     if (bar == null && innerFoo != null) 
      bar = innerFoo.GetBar(); 
     return bar ?? 0;   
    } 
    set 
    { 
     bar = value; 
    } 
} 

Sau đó, bạn có thể luôn luôn chỉ cần tạo một indexer.

enum FooProperties 
{ 
    Bar, 
    Baz, 
} 

object[] properties = new object[2]; 

public object this[FooProperties property] 
{ 
    get 
    { 
     if (properties[property] == null) 
     { 
      properties[property] = GetProperty(property); 
     } 
     return properties[property];   
    } 
    set 
    { 
     properties[property] = value; 
    } 
} 

private object GetProperty(FooProperties property) 
{ 
    switch (property) 
    { 
     case FooProperties.Bar: 
       if (innerFoo != null) 
        return innerFoo.GetBar(); 
       else 
        return (int)0; 

     case FooProperties.Baz: 
       if (innerFoo != null) 
        return innerFoo.GetBaz(); 
       else 
        return (int)0; 

     default: 
       throw new ArgumentOutOfRangeException(); 
    } 
} 

Điều này đòi hỏi đúc giá trị khi nó được đọc:

int myBar = (int)myFoo[FooProperties.Bar]; 

Nhưng nó tránh được hầu hết các vấn đề khác.

Edited thêm:

OK, đây là những gì bạn nên làm, nhưng đừng nói với ai rằng bạn đã làm điều đó, hoặc là tôi đã gợi ý nó. Đến với tùy chọn này:

int IFoo.GetBar() 
{ 
    return LazyLoad(ref Bar, f => f.GetBar()); // <--- Won't compile 
} 

Làm Bar, Baz, và bạn bè các lĩnh vực công cộng thay vì tài sản.Đó phải là chính xác những gì bạn đang tìm kiếm.

Nhưng, một lần nữa, đừng nói cho ai biết rằng bạn đã làm điều đó!

+0

Cảm ơn, tôi đánh giá cao phản hồi. Tôi không chắc chắn rằng tôi muốn đi với điều này, như các chỉ số và liệt kê sẽ cần phải được tùy chỉnh bằng văn bản cho mọi lớp học lười biếng, và kiểm tra null vẫn đang được lặp đi lặp lại nhiều lần (chỉ ở một nơi khác nhau).Đó là loại nhóm tất cả các sự xấu xí với nhau, đó là một sự cải tiến, nhưng tôi muốn loại bỏ nó hoàn toàn. ;) – Aaronaught

+0

@Aaronaught - Tôi đã thêm một tùy chọn bí mật khác vào câu trả lời của mình. –

Các vấn đề liên quan