Vấn đề về kiểu mã hóa: Chúng ta có nên có các hàm lấy tham số, sửa đổi nó và sau đó TRẢ LẠI tham số đó không?


19

Tôi đang có một cuộc tranh luận với bạn tôi về việc liệu hai thực tiễn này chỉ là hai mặt của cùng một đồng tiền, hay liệu một thực sự tốt hơn.

Chúng ta có một hàm lấy tham số, điền vào một thành viên của nó và sau đó trả về nó:

Item predictPrice(Item item)

Tôi tin rằng khi nó hoạt động trên cùng một đối tượng được truyền vào, không có điểm nào xảy ra để trả lại vật phẩm. Trong thực tế, nếu có bất cứ điều gì, từ quan điểm của người gọi, nó sẽ gây nhầm lẫn vì bạn có thể mong đợi nó trả lại một mặt hàng mới mà nó không có.

Ông tuyên bố rằng nó không có gì khác biệt, và thậm chí sẽ không có vấn đề gì nếu nó đã tạo ra một Vật phẩm mới và trả lại nó. Tôi hoàn toàn không đồng ý, vì những lý do sau:

  • Nếu bạn có nhiều tham chiếu đến mục được truyền vào (hoặc con trỏ hoặc bất cứ thứ gì), thì nó phân bổ một đối tượng mới và trả lại nó có tầm quan trọng quan trọng vì các tham chiếu đó sẽ không chính xác.

  • Trong các ngôn ngữ không được quản lý bộ nhớ, hàm phân bổ một thể hiện mới xác nhận quyền sở hữu bộ nhớ và do đó chúng ta sẽ phải thực hiện một phương thức dọn dẹp được gọi tại một số điểm.

  • Phân bổ trên heap có khả năng tốn kém, và do đó, điều quan trọng là liệu hàm được gọi có làm điều đó hay không.

Do đó, tôi tin rằng nó rất quan trọng để có thể nhìn thấy thông qua một chữ ký phương thức cho dù nó sửa đổi một đối tượng hay phân bổ một đối tượng mới. Kết quả là, tôi tin rằng khi hàm chỉ sửa đổi đối tượng được truyền vào, chữ ký sẽ là:

void predictPrice(Item item)

Trong mọi cơ sở mã (được thừa nhận là cơ sở mã C và C ++, không phải Java là ngôn ngữ chúng tôi đang làm việc), tôi đã làm việc với, phong cách trên về cơ bản đã được tuân thủ và được các lập trình viên giàu kinh nghiệm hơn tuân thủ. Ông tuyên bố rằng kích thước mẫu của các cơ sở mã và đồng nghiệp của tôi là nhỏ so với tất cả các cơ sở mã và đồng nghiệp có thể, và do đó kinh nghiệm của tôi không phải là chỉ số thực sự về việc liệu ai có vượt trội hay không.

Vậy, có suy nghĩ gì không?


strcat sửa đổi một tham số tại chỗ và vẫn trả về nó. Strcat có nên trả về void?
Jerry Jeremiah

Java và C ++ có hành vi rất khác nhau liên quan đến việc truyền tham số. Nếu Itemclass Item ...và không typedef ...& Item, địa phương itemđã là một bản sao
Caleth

google.github.io/styleguide/cppguide.html#Output_Parameter Google thích sử dụng giá trị RETURN cho đầu ra
Firegun

Câu trả lời:


26

Đây thực sự là một vấn đề về quan điểm, nhưng với giá trị của nó, tôi thấy thật sai lầm khi trả lại vật phẩm đã sửa đổi nếu vật phẩm được sửa đổi tại chỗ. Một cách riêng biệt, nếu predictPricesẽ sửa đổi mục, nó sẽ có một tên cho biết nó sẽ làm điều đó (như setPredictPricehoặc một số như vậy).

Tôi muốn (theo thứ tự)

  1. Đó predictPricelà một phương phápItem (modulo một lý do chính đáng cho nó không tồn tại, có thể có trong thiết kế tổng thể của bạn)

    • Trả lại giá dự đoán, hoặc

    • Có một cái tên như setPredictedPricethay thế

  2. Điều đó predictPrice đã không sửa đổi Item, nhưng đã trả lại giá dự đoán

  3. Đó predictPricelà một voidphương pháp gọi làsetPredictedPrice

  4. Điều đó predictPricetrả về this(đối với phương thức kết nối với các phương thức khác trong bất kỳ trường hợp nào, đó là một phần của) (và được gọi setPredictedPrice)


1
Cảm ơn vi đa trả lơi. Liên quan đến 1, chúng tôi không thể có dự đoánprice bên trong Vật phẩm. 2 là một gợi ý hay - tôi nghĩ đó có lẽ là giải pháp chính xác ở đây.
Squimmy

2
@Squimmy: Và chắc chắn đủ, tôi chỉ cần bỏ ra 15 phút đối phó với một lỗi gây ra bởi người sử dụng chính xác các mẫu mà bạn đã tranh cãi chống lại: a = doSomethingWith(b) biến đổi b và trở về nó với những sửa đổi, mà thổi lên mã của tôi sau này mà nghĩ rằng nó biết những gì bđã có trong đó. :-)
TJ Crowder

11

Trong mô hình thiết kế hướng đối tượng, mọi thứ không nên sửa đổi các đối tượng bên ngoài đối tượng. Mọi thay đổi về trạng thái của một đối tượng nên được thực hiện thông qua các phương thức trên đối tượng.

Vì vậy, void predictPrice(Item item)như là một chức năng thành viên của một số lớp khác là sai . Có thể được chấp nhận trong thời của C, nhưng đối với Java và C ++, các sửa đổi của một đối tượng ngụ ý một khớp nối sâu hơn với đối tượng có thể dẫn đến các vấn đề thiết kế khác trên đường (khi bạn cấu trúc lại lớp và thay đổi các trường của nó, bây giờ, 'dự đoán' cần phải được thay đổi thành một số tệp khác.

Quay trở lại một đối tượng mới, không có tác dụng phụ liên quan, tham số truyền vào không thay đổi. Bạn (phương thức dự đoán) không biết nơi nào khác tham số đó được sử dụng. Là Itemmột chìa khóa để băm ở đâu đó? bạn đã thay đổi mã băm của nó trong việc này? Có ai khác đang giữ nó để mong nó không thay đổi?

Các vấn đề thiết kế này là những vấn đề khuyến nghị mạnh mẽ rằng bạn không nên sửa đổi các đối tượng (tôi cho rằng không thay đổi trong nhiều trường hợp) và nếu bạn thực hiện, các thay đổi về trạng thái nên được chứa và kiểm soát bởi chính đối tượng chứ không phải là điều gì đó khác ngoài lớp.


Hãy xem xét những gì sẽ xảy ra nếu một người nghịch ngợm với các lĩnh vực của một cái gì đó trong hàm băm. Hãy lấy một số mã:

import java.util.*;

public class Main {
    public static void main (String[] args) {
        Set set = new HashSet();
        Data d = new Data(1,"foo");
        set.add(d);
        set.add(new Data(2,"bar"));
        System.out.println(set.contains(d));
        d.field1 = 2;
        System.out.println(set.contains(d));
    }

    public static class Data {
        int field1;
        String field2;

        Data(int f1, String f2) {
            field1 = f1;
            field2 = f2;
        }

        public int hashCode() {
            return field2.hashCode() + field1;
        }

        public boolean equals(Object o) {
            if(!(o instanceof Data)) return false;
            Data od = (Data)o;
            return od.field1 == this.field1 && od.field2.equals(this.field2);

        }
    }
}

thần tượng

Và tôi sẽ thừa nhận rằng đây không phải là mã lớn nhất (truy cập trực tiếp vào các trường), nhưng nó phục vụ mục đích của nó là thể hiện một vấn đề với dữ liệu có thể thay đổi được sử dụng làm chìa khóa cho HashMap, hoặc trong trường hợp này, chỉ được đưa vào một Hashset.

Đầu ra của mã này là:

true
false

Điều đã xảy ra là hashCode được sử dụng khi nó được chèn là nơi đối tượng nằm trong hàm băm. Thay đổi các giá trị được sử dụng để tính toán mã băm không tính toán lại chính hàm băm. Đây là một mối nguy hiểm khi đặt bất kỳ đối tượng có thể thay đổi nào làm chìa khóa cho hàm băm.

Do đó, quay trở lại khía cạnh ban đầu của câu hỏi, phương thức được gọi không "biết" cách đối tượng mà nó nhận được như một tham số đang được sử dụng. Cung cấp "cho phép biến đổi đối tượng" là cách duy nhất để làm điều này có nghĩa là có một số lỗi tinh vi có thể xuất hiện. Giống như mất giá trị trong hàm băm ... trừ khi bạn thêm nhiều hơn và hàm băm được xử lý lại - hãy tin tôi , đó là một lỗi khó chịu để săn lùng (Tôi đã mất giá trị cho đến khi tôi thêm 20 mục nữa vào hashMap và sau đó đột nhiên nó xuất hiện trở lại).

  • Trừ khi có lý do rất chính đáng để sửa đổi đối tượng, trả lại một đối tượng mới là điều an toàn nhất để làm.
  • Khi có một lý do chính đáng để thay đổi đối tượng, sửa đổi cần được thực hiện bởi các đối tượng chính nó (phương pháp gọi) chứ không phải thông qua một số chức năng bên ngoài có thể quay vòng các lĩnh vực của mình.
    • Điều này cho phép đối tượng được tái cấu trúc với chi phí bảo trì thấp hơn.
    • Điều này cho phép đối tượng đảm bảo rằng các giá trị tính toán mã băm của nó không thay đổi (hoặc không phải là một phần của tính toán mã băm của nó)

Liên quan: Ghi đè và trả về giá trị của đối số được sử dụng làm điều kiện của câu lệnh if, bên trong cùng câu lệnh if


3
Điều đó không đúng. Rất có thể predictPricekhông nên trực tiếp đùa giỡn với Itemcác thành viên, nhưng có gì sai khi gọi các phương thức Itemđó? Nếu đó là đối tượng duy nhất được sửa đổi, có lẽ bạn có thể đưa ra một đối số rằng đó phải là một phương thức của đối tượng được sửa đổi, nhưng lần khác, nhiều đối tượng (chắc chắn không phải là cùng một lớp) đang được sửa đổi, đặc biệt là trong phương pháp khá cao cấp.

@delnan Tôi sẽ thừa nhận rằng đây có thể là một phản ứng (đã qua) đối với một lỗi mà tôi đã từng phải theo dõi một lần giá trị biến mất và xuất hiện lại trong hàm băm - ai đó đang nghịch với các trường của đối tượng trong hàm băm sau khi nó được chèn thay vì tạo một bản sao của đối tượng để sử dụng. Có các biện pháp lập trình phòng thủ mà người ta có thể thực hiện (ví dụ: các đối tượng bất biến) - nhưng những thứ như tính toán lại các giá trị trong chính đối tượng khi bạn không phải là "chủ sở hữu" của đối tượng, cũng không phải đối tượng là thứ có thể dễ dàng quay trở lại để cắn bạn chăm chỉ

Bạn biết đấy, có một lý lẽ tốt cho việc sử dụng nhiều hàm miễn phí thay vì các hàm lớp để tăng cường đóng gói. Đương nhiên, một hàm nhận một đối tượng không đổi (cho dù ẩn hoặc tham số này) không nên thay đổi nó theo bất kỳ cách nào có thể quan sát được (một số đối tượng không đổi có thể lưu trữ mọi thứ).
Ded repeatator

2

Tôi luôn tuân theo thực tiễn không làm biến đổi đối tượng của người khác. Điều đó có nghĩa là, chỉ các đối tượng đột biến thuộc sở hữu của lớp / struct / whathaveyou mà bạn đang ở trong đó.

Cho lớp dữ liệu sau:

class Item {
    public double price = 0;
}

Không sao đâu

class Foo {
    private Item bar = new Item();

    public void predictPrice() {
        bar.price = bar.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
foo.predictPrice();
System.out.println(foo.bar.price);
// Since I called predictPrice on Foo, which takes and returns nothing, it's
// apparent something might've changed

Và điều này rất rõ ràng:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public double predictPrice(Item item) {
        return item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
foo.bar.price = baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// Since I explicitly set foo.bar.price to the return value of predictPrice, it's
// obvious foo.bar.price might've changed

Nhưng điều này quá lầy lội và bí ẩn đối với thị hiếu của tôi:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public void predictPrice(Item item) {
        item.price = item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// In my head, it doesn't appear that to foo, bar, or price changed. It seems to
// me that, if anything, I've placed a predicted price into baz that's based on
// the value of foo.bar.price

Hãy kèm theo bất kỳ downvote nào với một bình luận để tôi biết cách viết câu trả lời tốt hơn trong tương lai :)
Ben Leggiero

1

Cú pháp khác nhau giữa các ngôn ngữ khác nhau và việc hiển thị một đoạn mã không dành riêng cho ngôn ngữ là vô nghĩa vì nó có nghĩa là những thứ hoàn toàn khác nhau trong các ngôn ngữ khác nhau.

Trong Java, Itemlà một kiểu tham chiếu - nó là kiểu con trỏ tới các đối tượng là các thể hiện của Item. Trong C ++, loại này được viết dưới dạng Item *(cú pháp cho cùng một thứ là khác nhau giữa các ngôn ngữ). Vì vậy, Item predictPrice(Item item)trong Java tương đương với Item *predictPrice(Item *item)trong C ++. Trong cả hai trường hợp, nó là một hàm (hoặc phương thức) đưa một con trỏ đến một đối tượng và trả về một con trỏ tới một đối tượng.

Trong C ++, Item(không có gì khác) là một loại đối tượng (giả sử Itemlà tên của một lớp). Một giá trị của loại này "là" một đối tượng và Item predictPrice(Item item)khai báo một hàm lấy một đối tượng và trả về một đối tượng. Vì &không được sử dụng, nó được truyền và trả về theo giá trị. Điều đó có nghĩa là đối tượng được sao chép khi được thông qua và được sao chép khi trả về. Không có tương đương trong Java vì Java không có các loại đối tượng.

Vậy đó là cái gì? Nhiều điều bạn đang hỏi trong câu hỏi (ví dụ: "Tôi tin rằng nó hoạt động trên cùng một đối tượng được truyền vào ...") phụ thuộc vào việc hiểu chính xác những gì được truyền và trả lại ở đây.


1

Quy ước mà tôi đã thấy các cơ sở mã tuân thủ là thích một phong cách rõ ràng từ cú pháp. Nếu hàm thay đổi giá trị, thích truyền bằng con trỏ

void predictPrice(Item * const item);

Phong cách này dẫn đến:

  • Gọi nó bạn thấy:

    dự đoán bảng giá (& my_item);

và bạn biết nó sẽ được sửa đổi, bởi sự tuân thủ phong cách của bạn.

  • Mặt khác, thích chuyển qua const ref hoặc value khi bạn không muốn hàm sửa đổi thể hiện.

Cần lưu ý rằng, mặc dù đây là cú pháp rõ ràng hơn nhiều, nhưng nó không có sẵn trong Java.
Ben Leggiero

0

Mặc dù tôi không có sở thích mạnh mẽ, tôi sẽ bỏ phiếu rằng việc trả lại đối tượng sẽ dẫn đến tính linh hoạt tốt hơn cho API.

Lưu ý rằng nó chỉ có ý nghĩa trong khi phương thức có quyền tự do trả về đối tượng khác. Nếu javadoc của bạn nói @returns the same value of parameter athì nó vô dụng. Nhưng nếu javadoc của bạn nói @return an instance that holds the same data than parameter a, thì việc trả lại cùng một ví dụ hoặc một trường hợp khác là một chi tiết triển khai.

Ví dụ, trong dự án hiện tại của tôi (Java EE) tôi có một lớp nghiệp vụ; để lưu trữ trong DB (và gán ID tự động) cho Nhân viên, giả sử, một nhân viên tôi có một cái gì đó như

 public Employee createEmployee(Employee employeeData) {
   this.entityManager.persist(employeeData);
   return this.employeeData;
 }

Tất nhiên, không có sự khác biệt với việc triển khai này vì JPA trả về cùng một đối tượng sau khi gán Id. Bây giờ, nếu tôi chuyển sang JDBC

 public Employee createEmployee(Employee employeeData) {
   PreparedStatement ps = this.connection.prepareStatement("INSERT INTO EMPLOYEE(name, surname, data) VALUES(?, ?, ?)");
   ... // set parameters
   ps.execute();
   int id = getIdFromGeneratedKeys(ps);
   ??????
 }

Bây giờ, tại ????? Tôi có ba lựa chọn.

  1. Xác định một setter cho Id và tôi sẽ trả về cùng một đối tượng. Xấu xí, vì setIdsẽ có mặt ở khắp mọi nơi.

  2. Làm một số công việc phản chiếu nặng và đặt id trong Employeeđối tượng. Bẩn, nhưng ít nhất nó bị giới hạn trong createEmployeehồ sơ.

  3. Cung cấp một hàm tạo sao chép bản gốc Employeevà cũng chấp nhận idcài đặt.

  4. Lấy đối tượng từ DB (có thể cần nếu một số giá trị trường được tính trong DB hoặc nếu bạn muốn đưa vào một thực tế).

Bây giờ, cho 1. và 2. bạn có thể trả về cùng một thể hiện, nhưng với 3. và 4. bạn sẽ trả về các thể hiện mới. Nếu từ công chúa bạn đã đặt ra trong API của mình rằng giá trị "thực" sẽ là giá trị được trả về bởi phương thức, bạn có nhiều tự do hơn.

Đối số thực sự duy nhất tôi có thể nghĩ ngược lại đó là, bằng cách sử dụng triển khai 1. hoặc 2., mọi người sử dụng API của bạn có thể bỏ qua việc gán kết quả và mã của họ sẽ bị hỏng nếu bạn thay đổi thành 3. hoặc 4. triển khai).

Dù sao, như bạn có thể thấy, sở thích của tôi dựa trên các sở thích cá nhân khác (như cấm người cài đặt Id), vì vậy có thể nó sẽ không áp dụng cho mọi người.


0

Khi mã hóa bằng Java, người ta nên cố gắng làm cho việc sử dụng từng đối tượng thuộc loại có thể thay đổi khớp với một trong hai mẫu:

  1. Chính xác một thực thể được coi là "chủ sở hữu" của đối tượng có thể thay đổi và coi trạng thái của đối tượng đó là một phần của chính đối tượng đó. Các thực thể khác có thể giữ các tham chiếu, nhưng nên coi tham chiếu là xác định một đối tượng thuộc sở hữu của người khác.

  2. Mặc dù đối tượng là có thể thay đổi, nhưng không ai có tham chiếu đến nó được phép biến đổi nó, do đó làm cho cá thể trở nên bất biến một cách hiệu quả (lưu ý rằng do các quirks trong mô hình bộ nhớ của Java, không có cách nào tốt để tạo một đối tượng bất biến hiệu quả có luồng ngữ nghĩa bất biến an toàn mà không cần thêm một mức độ gián tiếp cho mỗi truy cập). Bất kỳ thực thể nào đóng gói trạng thái bằng cách sử dụng một tham chiếu đến một đối tượng như vậy và muốn thay đổi trạng thái được đóng gói phải tạo ra một đối tượng mới có chứa trạng thái thích hợp.

Tôi không muốn có các phương thức làm thay đổi đối tượng cơ bản và trả về một tham chiếu, vì chúng cho sự xuất hiện của việc khớp mẫu thứ hai ở trên. Có một vài trường hợp cách tiếp cận có thể hữu ích, nhưng mã sẽ biến đổi các đối tượng sẽ trông giống như nó sẽ làm như vậy; mã giống như myThing = myThing.xyz(q);trông rất ít giống như nó làm biến đổi đối tượng được xác định bởi myThingđơn giản hơn myThing.xyz(q);.

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.