Có cách nào dễ dàng hơn để kiểm tra xác thực đối số và khởi tạo trường trong một đối tượng không thay đổi không?


20

Tên miền của tôi bao gồm rất nhiều lớp bất biến đơn giản như thế này:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        if (fullName == null)
            throw new ArgumentNullException(nameof(fullName));
        if (nameAtBirth == null)
            throw new ArgumentNullException(nameof(nameAtBirth));
        if (taxId == null)
            throw new ArgumentNullException(nameof(taxId));
        if (phoneNumber == null)
            throw new ArgumentNullException(nameof(phoneNumber));
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        FullName = fullName;
        NameAtBirth = nameAtBirth;
        TaxId = taxId;
        PhoneNumber = phoneNumber;
        Address = address;
    }
}

Viết séc null và khởi tạo thuộc tính đã trở nên rất tẻ nhạt nhưng hiện tại tôi viết kiểm tra đơn vị cho từng lớp này để xác minh rằng xác thực đối số hoạt động chính xác và tất cả các thuộc tính được khởi tạo. Điều này cảm thấy như bận rộn vô cùng nhàm chán với lợi ích không tương xứng.

Giải pháp thực sự sẽ là C # để hỗ trợ các kiểu tham chiếu không thay đổi và không thể rỗng. Nhưng tôi có thể làm gì để cải thiện tình hình trong lúc này? Có đáng để viết tất cả các bài kiểm tra này? Sẽ là một ý tưởng tốt để viết một trình tạo mã cho các lớp như vậy để tránh viết các bài kiểm tra cho từng người trong số họ?


Đây là những gì tôi có bây giờ dựa trên các câu trả lời.

Tôi có thể đơn giản hóa kiểm tra null và khởi tạo thuộc tính để trông như thế này:

FullName = fullName.ThrowIfNull(nameof(fullName));
NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
TaxId = taxId.ThrowIfNull(nameof(taxId));
PhoneNumber = phoneNumber.ThrowIfNull(nameof(phoneNumber));
Address = address.ThrowIfNull(nameof(address));

Sử dụng triển khai sau đây của Robert Harvey :

public static class ArgumentValidationExtensions
{
    public static T ThrowIfNull<T>(this T o, string paramName) where T : class
    {
        if (o == null)
            throw new ArgumentNullException(paramName);

        return o;
    }
}

Kiểm tra kiểm tra null rất dễ dàng bằng cách sử dụng GuardClauseAssertiontừ AutoFixture.Idioms(cảm ơn vì gợi ý, Esben Skov Pedersen ):

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());

Điều này có thể được nén hơn nữa:

typeof(Address).ShouldNotAcceptNullConstructorArguments();

Sử dụng phương pháp mở rộng này:

public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var assertion = new GuardClauseAssertion(fixture);

    assertion.Verify(type.GetConstructors());
}

3
Tiêu đề / thẻ nói về kiểm tra (đơn vị) các giá trị trường ngụ ý kiểm tra đơn vị sau xây dựng của đối tượng, nhưng đoạn mã hiển thị xác thực tham số / tham số đầu vào; đây không phải là những khái niệm giống nhau
Erik Eidt

2
FYI, bạn có thể viết một mẫu T4 để làm cho loại nồi hơi này dễ dàng. (Bạn cũng có thể xem xét chuỗi.IsEmpty () beyond == null.)
Erik Eidt

1
Bạn đã xem qua thành ngữ autofixture chưa? nuget.org/packages/AutoFixture.Idioms
Esben Skov Pedersen


1
Bạn cũng có thể sử dụng Fody / NullGuard , mặc dù có vẻ như nó không có chế độ cho phép mặc định.
Bob

Câu trả lời:


5

Tôi đã tạo một mẫu t4 chính xác cho loại trường hợp này. Để tránh viết nhiều bản tóm tắt cho các lớp Không thay đổi.

https://github.com/xaviergonz/T4Immutable T4Immutable là một mẫu T4 cho các ứng dụng C # .NET tạo mã cho các lớp không thay đổi.

Nói cụ thể về các bài kiểm tra không null sau đó nếu bạn sử dụng điều này:

[PreNotNullCheck, PostNotNullCheck]
public string FirstName { get; }

Hàm tạo sẽ là:

public Person(string firstName) {
  // pre not null check
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  // assignations + PostConstructor() if needed

  // post not null check
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Đã nói điều này, nếu bạn sử dụng Chú thích JetBrains để kiểm tra null, bạn cũng có thể làm điều này:

[JetBrains.Annotations.NotNull, ConstructorParamNotNull]
public string FirstName { get; }

Và hàm tạo sẽ là:

public Person([JetBrains.Annotations.NotNull] string firstName) {
  // pre not null check is implied by ConstructorParamNotNull
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  FirstName = firstName;
  // + PostConstructor() if needed

  // post not null check implied by JetBrains.Annotations.NotNull on the property
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Ngoài ra có một vài tính năng hơn cái này.


Cảm ơn bạn. Tôi vừa mới thử nó và nó hoạt động hoàn hảo. Tôi đánh dấu đây là câu trả lời được chấp nhận vì nó giải quyết tất cả các vấn đề trong câu hỏi ban đầu.
Botond Balázs

16

Bạn có thể có được một chút cải tiến với một cấu trúc lại đơn giản có thể giảm bớt vấn đề viết tất cả các hàng rào đó. Trước tiên, bạn cần phương thức mở rộng này:

internal static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
    if (o == null)
        throw new ArgumentNullException(paramName);

    return o;
}

Sau đó bạn có thể viết:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName.ThrowIfNull(nameof(fullName));
        NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
        TaxId = taxId.ThrowIfNull(nameof(taxId));
        PhoneNumber = phoneNumber.ThrowIfNull(nameof(fullName));
        Address = address.ThrowIfNull(nameof(address));
    }
}

Trả về tham số ban đầu trong phương thức mở rộng sẽ tạo ra một giao diện trôi chảy, vì vậy bạn có thể mở rộng khái niệm này với các phương thức mở rộng khác nếu muốn và xâu chuỗi tất cả chúng lại với nhau trong bài tập của bạn.

Các kỹ thuật khác thanh lịch hơn trong khái niệm, nhưng dần dần phức tạp hơn trong thực thi, chẳng hạn như trang trí tham số bằng một [NotNull]thuộc tính và sử dụng Reflection như thế này .

Điều đó nói rằng, bạn có thể không cần tất cả các thử nghiệm này, trừ khi lớp của bạn là một phần của API công khai.


3
Điều lạ lùng là điều này đã bị hạ thấp. Nó rất giống với cách tiếp cận được cung cấp bởi câu trả lời được đánh giá cao nhất, ngoại trừ việc nó không phụ thuộc vào Roslyn.
Robert Harvey

Điều tôi không thích ở đây là nó dễ bị sửa đổi đối với hàm tạo.
jpmc26

@ jpmc26: Điều đó có nghĩa là gì?
Robert Harvey

1
Khi tôi đọc câu trả lời của bạn, nó gợi ý rằng bạn không kiểm tra hàm tạo , nhưng thay vào đó, bạn tạo một số phương thức phổ biến được gọi trên mỗi tham số và kiểm tra điều đó. Nếu bạn thêm một cặp thuộc tính / đối số mới và quên kiểm tra nullđối số cho hàm tạo, bạn sẽ không bị kiểm tra thất bại.
jpmc26

@ jpmc26: Đương nhiên. Nhưng điều đó cũng đúng nếu bạn viết nó theo cách thông thường như được minh họa trong OP. Tất cả các phương thức phổ biến là di chuyển throwbên ngoài của hàm tạo; bạn không thực sự chuyển điều khiển trong hàm tạo ở nơi khác. Đó chỉ là một phương pháp tiện ích và là một cách để làm mọi thứ trôi chảy , nếu bạn chọn.
Robert Harvey

7

Trong ngắn hạn, bạn không thể làm gì nhiều về sự tẻ nhạt khi viết các bài kiểm tra như vậy. Tuy nhiên, có một số trợ giúp đi kèm với các biểu thức ném do được triển khai như một phần của phiên bản tiếp theo của C # (v7), có thể sẽ đến trong vài tháng tới:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
        NameAtBirth = nameAtBirth ?? throw new ArgumentNullException(nameof(nameAtBirth));
        TaxId = taxId ?? throw new ArgumentNullException(nameof(taxId)); ;
        PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber)); ;
        Address = address ?? throw new ArgumentNullException(nameof(address)); ;
    }
}

Bạn có thể thử nghiệm các biểu thức ném qua ứng dụng web Thử Roslyn .


Nhỏ gọn hơn nhiều, cảm ơn bạn. Một nửa số dòng. Mong C # 7!
Botond Balázs

@ BotondBalázs bạn có thể dùng thử trong bản xem trước VS15 ngay bây giờ nếu bạn muốn
user1306322

7

Tôi ngạc nhiên không ai nhắc đến NullGuard.Fody . Nó có sẵn thông qua NuGet và sẽ tự động dệt các kiểm tra null vào IL trong thời gian biên dịch.

Vì vậy, mã xây dựng của bạn sẽ chỉ đơn giản là

public Person(
    string fullName,
    string nameAtBirth,
    string taxId,
    PhoneNumber phoneNumber,
    Address address)
{
    FullName = fullName;
    NameAtBirth = nameAtBirth;
    TaxId = taxId;
    PhoneNumber = phoneNumber;
    Address = address;
}

và NullGuard sẽ thêm các kiểm tra null cho bạn chuyển đổi nó thành chính xác những gì bạn đã viết.

Tuy nhiên, xin lưu ý rằng NullGuard không tham gia , nghĩa là, nó sẽ thêm các kiểm tra null đó vào mọi đối số phương thức và hàm tạo, thuộc tính getter và setter và thậm chí kiểm tra các giá trị trả về của phương thức trừ khi bạn cho phép rõ ràng giá trị null với [AllowNull]thuộc tính.


Cảm ơn bạn, đây có vẻ là một giải pháp chung rất tốt đẹp. Mặc dù rất đáng tiếc rằng nó sửa đổi hành vi ngôn ngữ theo mặc định. Tôi muốn nó nhiều hơn nữa nếu nó hoạt động bằng cách thêm một [NotNull]thuộc tính thay vì không cho phép null là mặc định.
Botond Balázs

@ BotondBalázs: PostSharp có điều này cùng với các thuộc tính xác thực tham số khác. Không giống như Fody, nó không miễn phí. Ngoài ra, mã được tạo sẽ gọi vào DLL PostSharp, do đó bạn cần đưa chúng vào sản phẩm của mình. Fody sẽ chỉ chạy mã của nó trong quá trình biên dịch và sẽ không được yêu cầu khi chạy.
Roman Reiner

@ BotondBalázs: Ban đầu tôi cũng thấy lạ khi NullGuard áp dụng theo mặc định nhưng nó thực sự có ý nghĩa. Trong bất kỳ cơ sở mã hợp lý cho phép rỗng nên nhiều ít phổ biến hơn so với kiểm tra null. Nếu bạn thực sự muốn cho phép null ở đâu đó, phương pháp từ chối buộc bạn phải rất kỹ lưỡng với nó.
Roman Reiner

5
Vâng, điều đó hợp lý nhưng nó vi phạm Nguyên tắc tối thiểu . Nếu một lập trình viên mới không biết rằng dự án sử dụng NullGuard, thì họ sẽ gặp bất ngờ lớn! Nhân tiện, tôi cũng không hiểu downvote, đây là một câu trả lời rất hữu ích.
Botond Balázs

1
@ BotondBalázs / Roman, có vẻ như NullGuard có chế độ rõ ràng mới thay đổi cách thức hoạt động theo mặc định
Kyle W

5

Có đáng để viết tất cả các bài kiểm tra này?

Không, có lẽ là không. Khả năng bạn sẽ làm hỏng việc đó là gì? Khả năng một số ngữ nghĩa sẽ thay đổi từ bên dưới bạn là gì? Tác động nếu ai đó làm hỏng nó lên?

Nếu bạn đang dành rất nhiều thời gian để thực hiện các bài kiểm tra cho thứ gì đó hiếm khi bị hỏng, và là một sửa chữa tầm thường nếu nó đã ... có thể không đáng.

Sẽ là một ý tưởng tốt để viết một trình tạo mã cho các lớp như vậy để tránh viết các bài kiểm tra cho từng người trong số họ?

Có lẽ? Đó là điều có thể được thực hiện dễ dàng với sự phản ánh. Một cái gì đó để xem xét là thực hiện tạo mã cho mã thực, vì vậy bạn không có N lớp có thể có lỗi của con người. Ngăn chặn lỗi> phát hiện lỗi.


Cảm ơn bạn. Có thể tôi không đủ rõ ràng trong câu hỏi của mình nhưng tôi có ý định viết một trình tạo tạo ra các lớp bất biến, không phải kiểm tra cho chúng
Botond Balázs

2
Tôi cũng đã thấy một vài lần thay vì if...throwhọ sẽ có ThrowHelper.ArgumentNull(myArg, "myArg"), theo cách đó logic vẫn còn đó và bạn không bị ảnh hưởng bởi phạm vi bảo hiểm mã. Trường hợp ArgumentNullphương pháp sẽ làm if...throw.
Matthew

2

Có đáng để viết tất cả các bài kiểm tra này?

Không.
Bởi vì tôi khá chắc chắn rằng bạn đã kiểm tra các thuộc tính đó thông qua một số bài kiểm tra logic khác trong đó các lớp đó được sử dụng.

Ví dụ: bạn có thể có các bài kiểm tra cho lớp Factory có xác nhận dựa trên các thuộc tính đó ( NameVí dụ được tạo với thuộc tính được gán đúng ).

Nếu các lớp đó được hiển thị với API công khai được sử dụng bởi một số người dùng phần thứ ba / người dùng cuối (@EJoshua cảm ơn vì đã chú ý), thì các bài kiểm tra dự kiến ArgumentNullException có thể hữu ích.

Trong khi chờ C # 7, bạn có thể sử dụng phương thức mở rộng

public MyClass(string name)
{
    name.ThrowArgumentNullExceptionIfNull(nameof(name));
}

public static void ThrowArgumentNullExceptionIfNull(this object value, string paramName)
{
    if(value == null)
        throw new ArgumentNullException(paramName);
}

Để kiểm tra, bạn có thể sử dụng một phương pháp kiểm tra tham số hóa, sử dụng sự phản chiếu để tạo nulltham chiếu cho mọi tham số và khẳng định ngoại lệ dự kiến.


1
Đó là một lập luận thuyết phục cho việc không thử nghiệm khởi tạo thuộc tính.
Botond Balázs

Điều đó phụ thuộc vào cách bạn sử dụng mã. Nếu mã là một phần của thư viện công khai, bạn không bao giờ nên tin tưởng một cách mù quáng vào người khác để biết cách thư viện của bạn (đặc biệt là nếu họ không có quyền truy cập vào mã nguồn); tuy nhiên, nếu đó là thứ bạn chỉ sử dụng nội bộ để làm cho mã của riêng bạn hoạt động thì bạn phải biết cách sử dụng mã của riêng mình, vì vậy việc kiểm tra có ít mục đích hơn.
EJoshuaS - Phục hồi Monica

2

Bạn luôn có thể viết một phương thức như sau:

// Just to illustrate how to call this
private static void SomeMethod(string a, string b, string c, string d)
    {
        ValidateArguments(a, b, c, d);
        // ...
    }

    // This is the one to use as a utility function
    private static void ValidateArguments(params object[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] == null)
            {
                StackTrace trace = new StackTrace();
                // Get the method that called us
                MethodBase info = trace.GetFrame(1).GetMethod();

                // Get information on the parameter that is null so we can add its name to the exception
                ParameterInfo param = info.GetParameters()[i];

                // Raise the exception on behalf of the caller
                throw new ArgumentNullException(param.Name);
            }
        }
    }

Tối thiểu, điều đó sẽ giúp bạn tiết kiệm một số thao tác gõ nếu bạn có một số phương pháp yêu cầu loại xác nhận này. Tất nhiên, giải pháp này giả định rằng không có tham số nào trong phương thức của bạn có thể null, nhưng bạn có thể sửa đổi điều này để thay đổi điều đó nếu bạn mong muốn.

Bạn cũng có thể mở rộng điều này để thực hiện xác nhận loại cụ thể khác. Ví dụ: nếu bạn có một quy tắc rằng các chuỗi không thể hoàn toàn là khoảng trắng hoặc trống, bạn có thể thêm điều kiện sau:

// Note that we already know based on the previous condition that args[i] is not null
else if (args[i].GetType() == typeof(string))
            {
                string argCast = arg as string;

                if (!argCast.Trim().Any())
                {
                    ParameterInfo param = GetParameterInfo(i);

                    throw new ArgumentException(param.Name + " is empty or consists only of whitespace");
                }
            }

Phương thức GetParameterInfo mà tôi đề cập về cơ bản là sự phản chiếu (để tôi không phải tiếp tục gõ cùng một thứ, điều này sẽ vi phạm nguyên tắc DRY ):

private static ParameterInfo GetParameterInfo(int index)
    {
        StackTrace trace = new StackTrace();

        // Note that we have to go 2 methods back to get the ValidateArguments method's caller
        MethodBase info = trace.GetFrame(2).GetMethod();

        // Get information on the parameter that is null so we can add its name to the exception
        ParameterInfo param = info.GetParameters()[index];

        return param;
    }

1
Tại sao điều này bị hạ cấp? Nó không quá tệ
Gaspa79

0

Tôi không đề xuất ném ngoại lệ từ nhà xây dựng. Vấn đề là bạn sẽ có một thời gian khó khăn để kiểm tra điều này vì bạn phải vượt qua các tham số hợp lệ ngay cả khi chúng không liên quan đến bài kiểm tra của bạn.

Ví dụ: Nếu bạn muốn kiểm tra xem tham số thứ ba có ném ngoại lệ hay không, bạn phải vượt qua chính xác thứ nhất và thứ hai. Vì vậy, bài kiểm tra của bạn không bị cô lập nữa. khi các ràng buộc của tham số thứ nhất và thứ hai thay đổi, trường hợp kiểm tra này sẽ thất bại ngay cả khi nó kiểm tra tham số thứ ba.

Tôi đề nghị sử dụng API xác thực java và bên ngoài quá trình xác thực. Tôi đề nghị có bốn trách nhiệm (các lớp) liên quan:

  1. Một đối tượng gợi ý với các tham số chú thích xác thực Java với một phương thức xác thực trả về một Tập hợp các ràng buộc. Một lợi thế là bạn có thể vượt qua các đối tượng này mà không cần xác nhận là hợp lệ. Bạn có thể trì hoãn xác nhận cho đến khi nó không cần thiết mà không cần phải khởi tạo đối tượng miền. Đối tượng xác nhận có thể được sử dụng trong các lớp khác nhau vì nó là POJO không có kiến ​​thức cụ thể về lớp. Nó có thể là một phần của API công khai của bạn.

  2. Một nhà máy cho đối tượng miền chịu trách nhiệm tạo các đối tượng hợp lệ. Bạn chuyển vào đối tượng gợi ý và nhà máy sẽ gọi xác nhận và tạo đối tượng miền nếu mọi thứ đều ổn.

  3. Bản thân đối tượng miền mà hầu như không thể truy cập để khởi tạo cho các nhà phát triển khác. Đối với mục đích thử nghiệm, tôi đề nghị có lớp này trong phạm vi gói.

  4. Một giao diện miền công cộng để ẩn đối tượng miền cụ thể và làm cho khó hiểu.

Tôi không đề nghị kiểm tra giá trị null. Ngay khi bạn vào lớp miền, bạn sẽ thoát khỏi null hoặc trả về null miễn là bạn không có chuỗi đối tượng có kết thúc hoặc chức năng tìm kiếm cho các đối tượng đơn lẻ.

Một điểm khác: viết càng ít mã càng tốt không phải là số liệu cho chất lượng mã. Hãy suy nghĩ về các cuộc thi trò chơi 32k. Mã đó nhỏ gọn nhất nhưng cũng là mã lộn xộn nhất bạn có thể có vì nó chỉ quan tâm đến các vấn đề kỹ thuật và không quan tâm đến ngữ nghĩa. Nhưng ngữ nghĩa là những điểm làm cho mọi thứ toàn diện.


1
Tôi tôn trọng không đồng ý với điểm cuối cùng của bạn. Không phải gõ cùng một cái nồi hơi lần thứ 100 sẽ giúp giảm khả năng mắc lỗi. Hãy tưởng tượng nếu đây là Java, không phải C #. Tôi sẽ phải xác định một trường sao lưu và một phương thức getter cho mỗi thuộc tính. Mã sẽ trở nên hoàn toàn không thể đọc được và những gì quan trọng sẽ bị che khuất bởi cơ sở hạ tầng cồng kềnh.
Botond Balázs
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.