Là sửa đổi một đối tượng thông qua tham chiếu là một thực tiễn xấu?


12

Trước đây, tôi thường thực hiện hầu hết các thao tác đối tượng của mình trong phương thức chính mà nó được tạo / cập nhật, nhưng gần đây tôi thấy mình có một cách tiếp cận khác và tôi tò mò liệu đó có phải là một thực tiễn xấu.

Đây là một ví dụ. Giả sử tôi có một kho lưu trữ chấp nhận một Userthực thể, nhưng trước khi chèn thực thể đó, chúng tôi gọi một số phương thức để đảm bảo tất cả các trường của nó được đặt theo những gì chúng tôi muốn. Bây giờ, thay vì gọi các phương thức và thiết lập các giá trị trường từ bên trong phương thức Chèn, tôi gọi một loạt các phương thức chuẩn bị định hình đối tượng trước khi chèn.

Phương pháp cũ:

public void InsertUser(User user) {
    user.Username = GenerateUsername(user);
    user.Password = GeneratePassword(user);

    context.Users.Add(user);
}

Phương pháp mới:

public void InsertUser(User user) {
    SetUsername(user);
    SetPassword(user);

    context.Users.Add(user);
}

private void SetUsername(User user) {
    var username = "random business logic";

    user.Username = username;
}

private void SetPassword(User user) {
    var password = "more business logic";

    user.Password = password;
}

Về cơ bản, việc thực hành thiết lập giá trị của một tài sản từ một phương pháp khác có phải là một thực tiễn xấu?


6
Chỉ cần nói rằng ... bạn đã không thông qua bất cứ điều gì bằng cách tham khảo ở đây. Truyền tham chiếu theo giá trị hoàn toàn không phải là điều tương tự.
cHao

4
@cHao: Đó là một sự khác biệt không có sự khác biệt. Hành vi của mã là như nhau bất kể.
Robert Harvey

2
@JDDavis: Về cơ bản, sự khác biệt thực sự duy nhất giữa hai ví dụ của bạn là bạn đã đặt một tên có ý nghĩa cho tên người dùng được đặt và đặt hành động mật khẩu.
Robert Harvey

1
Tất cả các cuộc gọi của bạn là bằng cách tham khảo ở đây. Bạn phải sử dụng loại giá trị trong C # để truyền theo giá trị.
Frank Hileman

2
@FrankHileman: Tất cả các cuộc gọi đều theo giá trị ở đây. Đó là mặc định trong C #. Đi qua một tài liệu tham khảo và đi qua bằng cách tham khảo là những con thú khác nhau, và sự khác biệt không thành vấn đề. Nếu userđược chuyển qua tham chiếu, mã có thể lấy nó ra khỏi tay người gọi và thay thế nó bằng cách đơn giản là nói, nói , user = null;.
cHao

Câu trả lời:


10

Vấn đề ở đây là một cái Userthực sự có thể chứa hai thứ khác nhau:

  1. Một thực thể Người dùng hoàn chỉnh, có thể được chuyển đến kho lưu trữ dữ liệu của bạn.

  2. Tập hợp các yếu tố dữ liệu được yêu cầu từ người gọi để bắt đầu quá trình tạo thực thể Người dùng. Hệ thống phải thêm tên người dùng và mật khẩu trước khi nó thực sự là Người dùng hợp lệ như trong # 1 ở trên.

Điều này bao gồm một sắc thái không có giấy tờ đối với mô hình đối tượng của bạn mà không được thể hiện trong hệ thống loại của bạn. Bạn chỉ cần "biết" nó là một nhà phát triển. Điều đó không tuyệt vời, và nó dẫn đến các mẫu mã kỳ lạ giống như mẫu bạn đang gặp phải.

Tôi đề nghị bạn cần hai thực thể, ví dụ một Userlớp và một EnrollRequestlớp. Cái sau có thể chứa mọi thứ bạn cần biết để tạo Người dùng. Nó trông giống như lớp Người dùng của bạn, nhưng không có tên người dùng và mật khẩu. Sau đó, bạn có thể làm điều này:

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

Người gọi chỉ bắt đầu với thông tin đăng ký và lấy lại người dùng đã hoàn thành sau khi được chèn. Bằng cách này, bạn tránh làm biến đổi bất kỳ lớp nào và bạn cũng có sự phân biệt loại an toàn giữa người dùng được chèn và người không.


2
Một phương thức có tên như thế InsertUsercó khả năng có tác dụng phụ của việc chèn. Tôi không chắc "icky" nghĩa là gì trong bối cảnh này. Đối với việc sử dụng một "người dùng" chưa được thêm vào, bạn dường như đã bỏ lỡ điểm. Nếu nó chưa được thêm, đó không phải là người dùng, chỉ là yêu cầu tạo người dùng. Bạn được tự do làm việc với yêu cầu, tất nhiên.
John Wu

@candiedorange Những tác dụng phụ không phải là tác dụng mong muốn của việc gọi phương thức. Nếu bạn không muốn thêm ngay lập tức, bạn tạo một phương thức khác thỏa mãn trường hợp sử dụng. Nhưng đó không phải là trường hợp sử dụng được thông qua trong câu hỏi vì vậy có thể nói rằng mối quan tâm không quan trọng.
Andy

@candiedorange Nếu khớp nối làm cho mã máy khách dễ dàng hơn tôi sẽ nói điều đó tốt. Những gì dev hoặc pos có thể nghĩ trong tương lai là không liên quan. Nếu thời điểm đó đến, bạn thay đổi mã. Không có gì lãng phí hơn là cố gắng xây dựng mã có thể xử lý bất kỳ trường hợp sử dụng nào trong tương lai.
Andy

5
@CandiedOrange Tôi khuyên bạn không nên kết hợp chặt chẽ hệ thống loại được xác định chính xác của việc triển khai với các thực thể tiếng Anh đơn giản về khái niệm của doanh nghiệp. Một khái niệm kinh doanh về "người dùng" chắc chắn có thể được thực hiện trong hai lớp, nếu không muốn nói là đại diện cho các biến thể có ý nghĩa về mặt chức năng. Người dùng không kiên trì không được đảm bảo tên người dùng duy nhất, không có khóa chính nào có thể bị ràng buộc và thậm chí không thể đăng nhập, vì vậy tôi nói rằng nó bao gồm một biến thể quan trọng. Đối với một giáo dân, tất nhiên, cả EnrollRequest và Người dùng đều là "người dùng", bằng ngôn ngữ mơ hồ của họ.
John Wu

5

Tác dụng phụ là ok miễn là chúng không đến bất ngờ. Vì vậy, nói chung không có gì sai khi kho lưu trữ có phương thức chấp nhận người dùng và thay đổi trạng thái bên trong của người dùng. Nhưng IMHO một tên phương thức như InsertUser không truyền đạt rõ ràng điều này và đó là nguyên nhân khiến nó dễ bị lỗi. Khi sử dụng repo của bạn, tôi sẽ mong đợi một cuộc gọi như

 repo.InsertUser(user);

để thay đổi trạng thái nội bộ của kho lưu trữ, không phải trạng thái của đối tượng người dùng. Vấn đề này tồn tại trong cả hai triển khai của bạn, làm thế nào InsertUserđể nội bộ này hoàn toàn không liên quan.

Để giải quyết điều này, bạn có thể

  • khởi tạo riêng biệt với việc chèn (vì vậy người gọi InsertUsercần cung cấp một Userđối tượng được khởi tạo đầy đủ , hoặc,

  • xây dựng khởi tạo vào quy trình xây dựng của đối tượng người dùng (như được đề xuất bởi một số câu trả lời khác), hoặc

  • chỉ cần cố gắng tìm một tên tốt hơn cho phương thức thể hiện rõ hơn những gì nó làm.

Vì vậy, chọn một tên phương pháp như PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordhoặc bất cứ điều gì bạn thích để làm cho tác dụng phụ rõ ràng hơn.

Tất nhiên, tên phương thức dài như vậy chỉ ra rằng phương thức có thể đang thực hiện "quá nhiều" (vi phạm SRP), nhưng đôi khi bạn không muốn hoặc không thể dễ dàng khắc phục điều này và đây là giải pháp thực dụng, ít xâm phạm nhất.


3

Tôi đang xem xét hai lựa chọn bạn đã chọn và tôi phải nói rằng từ trước đến nay tôi thích phương pháp cũ hơn là phương pháp mới được đề xuất. Có một vài lý do cho điều đó mặc dù về cơ bản họ cũng làm điều tương tự.

Trong cả hai trường hợp, bạn đang thiết lập user.UserNameuser.Password. Tôi có đặt chỗ về mục mật khẩu, nhưng những bảo lưu đó không ảnh hưởng đến chủ đề.

Ý nghĩa của việc sửa đổi các đối tượng tham chiếu

  • Nó làm cho việc lập trình đồng thời trở nên khó khăn hơn - nhưng không phải tất cả các ứng dụng đều là đa luồng
  • Những sửa đổi đó có thể gây ngạc nhiên, đặc biệt nếu không có gì về phương pháp cho thấy điều đó sẽ xảy ra
  • Những bất ngờ đó có thể làm cho việc bảo trì khó khăn hơn

Phương pháp cũ so với Phương pháp mới

Phương pháp cũ giúp mọi thứ dễ kiểm tra hơn:

  • GenerateUserName()là thử nghiệm độc lập. Bạn có thể viết các bài kiểm tra theo phương pháp đó và đảm bảo rằng các tên được tạo chính xác
  • Nếu tên yêu cầu thông tin từ đối tượng người dùng, thì bạn có thể thay đổi chữ ký thành GenerateUserName(User user)và duy trì khả năng kiểm tra đó

Phương pháp mới ẩn các đột biến:

  • Bạn không biết rằng Userđối tượng đang thay đổi cho đến khi bạn đi sâu 2 lớp
  • Những thay đổi Userđối tượng là đáng ngạc nhiên hơn trong trường hợp đó
  • SetUserName()không chỉ đặt tên người dùng. Đó không phải là sự thật trong quảng cáo khiến các nhà phát triển mới khó khám phá cách mọi thứ hoạt động trong ứng dụng của bạn

1

Tôi hoàn toàn đồng ý với câu trả lời của John Wu. Đề nghị của ông là một trong những tốt. Nhưng nó hơi bỏ lỡ câu hỏi trực tiếp của bạn.

Về cơ bản, việc thực hành thiết lập giá trị của một tài sản từ một phương pháp khác có phải là một thực tiễn xấu?

Không phải vốn có.

Bạn không thể đi quá xa, vì bạn sẽ gặp phải hành vi bất ngờ. Ví dụ, PrintName(myPerson)không nên thay đổi đối tượng người, vì phương thức ngụ ý rằng nó chỉ quan tâm đến việc đọc các giá trị hiện có. Nhưng đó là một đối số khác với trường hợp của bạn, vì SetUsername(user)mạnh mẽ ngụ ý rằng nó sẽ đặt giá trị.


Đây thực sự là một cách tiếp cận tôi thường sử dụng cho các bài kiểm tra đơn vị / tích hợp, trong đó tôi tạo một phương thức đặc biệt để thay đổi một đối tượng để đặt giá trị của nó thành một tình huống cụ thể mà tôi muốn kiểm tra.

Ví dụ:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Tôi rõ ràng mong đợi ArrrangeContractDeletedStatusphương thức thay đổi trạng thái của myContractđối tượng.

Lợi ích chính là phương pháp cho phép tôi kiểm tra việc xóa hợp đồng với các hợp đồng ban đầu khác nhau; ví dụ: hợp đồng có lịch sử trạng thái dài hoặc hợp đồng không có lịch sử trạng thái trước đó, hợp đồng có lịch sử trạng thái có lỗi cố ý, hợp đồng mà người dùng thử nghiệm của tôi không được phép xóa.

Nếu tôi đã hợp nhất CreateEmptyContractArrrangeContractDeletedStatusthành một phương thức duy nhất; Tôi sẽ phải tạo nhiều biến thể của phương pháp này cho mọi hợp đồng khác nhau mà tôi muốn thử nghiệm ở trạng thái bị xóa.

Và trong khi tôi có thể làm một cái gì đó như:

myContract = ArrrangeContractDeletedStatus(myContract);

Điều này là dư thừa (vì dù sao tôi cũng đang thay đổi đối tượng) hoặc bây giờ tôi buộc bản thân phải tạo một bản sao sâu sắc của myContractđối tượng; Điều này là quá khó nếu bạn muốn bao quát mọi trường hợp (hãy tưởng tượng nếu tôi muốn các thuộc tính điều hướng có giá trị của nhiều cấp độ. Tôi có cần sao chép tất cả chúng không? Chỉ có thực thể cấp cao nhất? ... Rất nhiều câu hỏi, rất nhiều kỳ vọng ngầm )

Thay đổi đối tượng là cách dễ nhất để có được những gì tôi muốn mà không phải làm thêm việc chỉ để tránh không thay đổi đối tượng như một vấn đề nguyên tắc.


Vì vậy, câu trả lời trực tiếp cho câu hỏi của bạn là nó không thực sự xấu, miễn là bạn không che giấu rằng phương pháp này có thể thay đổi đối tượng đã qua. Đối với tình hình hiện tại của bạn, điều đó được làm rõ ràng thông qua tên phương thức.


0

Tôi thấy cái cũ cũng như cái mới có vấn đề.

Cách cũ:

1) InsertUser(User user)

Khi đọc tên phương thức này xuất hiện trong IDE của tôi, điều đầu tiên, xuất hiện trong đầu tôi là

Người dùng chèn vào đâu?

Tên phương thức nên đọc AddUserToContext. Ít nhất, đây là, những gì phương pháp làm cuối cùng.

2) InsertUser(User user)

Điều này vi phạm rõ ràng nguyên tắc ít bất ngờ nhất. Nói rằng, tôi đã làm công việc của mình cho đến nay và tạo ra một ví dụ mới về a Uservà đưa cho anh ta namevà đặt một password, tôi sẽ bị bất ngờ:

a) điều này không chỉ chèn người dùng vào một cái gì đó

b) nó cũng không thực hiện ý định chèn người dùng của tôi; tên và mật khẩu đã bị ghi đè.

c) điều này cho thấy, đó cũng là một sự vi phạm nguyên tắc trách nhiệm duy nhất.

Cách mới:

1) InsertUser(User user)

Vẫn vi phạm SRP và nguyên tắc ít bất ngờ nhất

2) SetUsername(User user)

Câu hỏi

Đặt tên người dùng? Để làm gì?

Tốt hơn: SetRandomNamehoặc một cái gì đó phản ánh ý định.

3) SetPassword(User user)

Câu hỏi

Đặt mật khẩu? Để làm gì?

Tốt hơn: SetRandomPasswordhoặc một cái gì đó phản ánh ý định.


Gợi ý:

Những gì tôi muốn đọc sẽ là một cái gì đó như:

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

Về câu hỏi ban đầu của bạn:

Là sửa đổi một đối tượng thông qua tham chiếu là một thực tiễn xấu?

Không. Đó là vấn đề của hương vị.

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.