Tại sao nó không trở thành một mô hình phổ biến để sử dụng setters trong hàm tạo?


23

Bộ truy cập và bộ sửa đổi (còn gọi là setters và getters) hữu ích vì ba lý do chính:

  1. Họ hạn chế quyền truy cập vào các biến.
    • Ví dụ, một biến có thể được truy cập, nhưng không được sửa đổi.
  2. Họ xác nhận các tham số.
  3. Chúng có thể gây ra một số tác dụng phụ.

Các trường đại học, khóa học trực tuyến, hướng dẫn, bài viết trên blog và ví dụ mã trên web đều nhấn mạnh về tầm quan trọng của người truy cập và người sửa đổi, họ gần như cảm thấy như là "phải có" cho mã hiện nay. Vì vậy, người ta có thể tìm thấy chúng ngay cả khi chúng không cung cấp bất kỳ giá trị bổ sung nào, như mã bên dưới.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Điều đó đã được nói, rất phổ biến để tìm các công cụ sửa đổi hữu ích hơn, những công cụ thực sự xác nhận các tham số và đưa ra một ngoại lệ hoặc trả về một boolean nếu đầu vào không hợp lệ đã được cung cấp, đại loại như sau:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Nhưng ngay cả khi đó, tôi hầu như không bao giờ thấy các bộ sửa đổi được gọi từ một hàm tạo, vì vậy ví dụ phổ biến nhất của một lớp đơn giản mà tôi phải đối mặt là:

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Nhưng người ta sẽ nghĩ rằng cách tiếp cận thứ hai này an toàn hơn rất nhiều:

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Bạn có thấy một mô hình tương tự trong kinh nghiệm của bạn hay chỉ là tôi không may mắn? Và nếu bạn làm vậy, thì bạn nghĩ điều gì gây ra điều đó? Có một bất lợi rõ ràng cho việc sử dụng các sửa đổi từ các nhà xây dựng hoặc chúng chỉ được coi là an toàn hơn? Có phải cái gì khác không?


1
@stg, cảm ơn bạn, điểm thú vị! Bạn có thể mở rộng về nó và đưa nó vào một câu trả lời với một ví dụ về một điều xấu có thể xảy ra không?
Vlad Spreys


8
@stg Trên thực tế, đó là một mô hình chống sử dụng các phương thức có thể ghi đè trong hàm tạo và nhiều công cụ chất lượng mã sẽ chỉ ra đó là một lỗi. Bạn không biết phiên bản bị ghi đè sẽ làm gì và nó có thể làm những điều khó chịu như để "cái này" thoát ra trước khi nhà xây dựng kết thúc có thể dẫn đến tất cả các vấn đề lạ.
Michał Kosmulski

1
@gnat: Tôi không tin rằng đây là một bản sao của các phương thức của một lớp có nên gọi các getters và setters riêng của nó không? , vì bản chất của các lệnh gọi hàm tạo được gọi trên một đối tượng chưa được hình thành đầy đủ.
Greg Burghardt

3
Cũng lưu ý rằng "xác thực" của bạn chỉ khiến tuổi không được khởi tạo (hoặc bằng không, hoặc ... một cái gì đó khác, tùy thuộc vào ngôn ngữ). Nếu bạn muốn hàm tạo không thành công, bạn phải kiểm tra giá trị trả về và đưa ra một ngoại lệ khi không thành công. Khi nó đứng, xác nhận của bạn chỉ giới thiệu một lỗi khác.
Vô dụng

Câu trả lời:


37

Lý luận triết học rất chung chung

Thông thường, chúng tôi yêu cầu một nhà xây dựng cung cấp (như các điều kiện hậu) một số đảm bảo về trạng thái của đối tượng được xây dựng.

Thông thường, chúng tôi cũng hy vọng rằng các phương thức cá thể có thể giả định (như các điều kiện trước) rằng các bảo đảm này đã được giữ khi chúng được gọi và chúng chỉ phải đảm bảo không phá vỡ chúng.

Gọi một phương thức cá thể từ bên trong hàm tạo có nghĩa là một số hoặc tất cả các bảo đảm đó có thể chưa được thiết lập, điều này khiến cho việc lập luận về các điều kiện trước của phương thức cá thể có được thỏa mãn hay không. Ngay cả khi bạn hiểu đúng, nó có thể rất mong manh khi đối mặt với, vd. sắp xếp lại các cuộc gọi phương thức thể hiện hoặc các hoạt động khác.

Các ngôn ngữ cũng khác nhau về cách chúng giải quyết các cuộc gọi đến các phương thức cá thể được kế thừa từ các lớp cơ sở / bị ghi đè bởi các lớp con, trong khi hàm tạo vẫn đang chạy. Điều này thêm một lớp phức tạp.

Ví dụ cụ thể

  1. Ví dụ của riêng bạn về cách bạn nghĩ điều này sẽ trông như vậy là sai:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    Điều này không kiểm tra giá trị trả về từ setAge. Rõ ràng việc gọi setter là không đảm bảo tính chính xác sau tất cả.

  2. Những lỗi rất dễ như tùy thuộc vào thứ tự khởi tạo, chẳng hạn như:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    nơi đăng nhập tạm thời của tôi đã phá vỡ mọi thứ. Rất tiếc!

Ngoài ra còn có các ngôn ngữ như C ++ trong đó việc gọi setter từ hàm tạo có nghĩa là khởi tạo mặc định lãng phí (đối với một số biến thành viên ít nhất là đáng tránh)

Một đề nghị đơn giản

Đúng là hầu hết các mã không được viết như thế này, nhưng nếu bạn muốn giữ cho hàm tạo của bạn sạch sẽ và có thể dự đoán được, và vẫn sử dụng lại logic trước và sau điều kiện của bạn, thì giải pháp tốt hơn là:

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

hoặc thậm chí tốt hơn, nếu có thể: mã hóa ràng buộc trong loại thuộc tính và để nó xác nhận giá trị của chính nó khi gán:

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

Và cuối cùng, để hoàn thiện đầy đủ, một trình bao bọc tự xác thực trong C ++. Lưu ý rằng mặc dù nó vẫn đang thực hiện xác nhận tẻ nhạt, bởi vì lớp này không làm gì khác , nên việc kiểm tra tương đối dễ dàng

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

OK, nó không thực sự hoàn chỉnh, tôi đã bỏ đi các bản sao khác nhau và di chuyển các hàm tạo và bài tập.


4
Câu trả lời nặng nề! Hãy để tôi thêm chỉ vì OP đã thấy tình huống được mô tả trong sách giáo khoa không làm cho nó trở thành một giải pháp tốt. Nếu có hai cách trong một lớp để đặt thuộc tính (theo hàm tạo và hàm setter) và một cách muốn thực thi một ràng buộc đối với thuộc tính đó, thì cả hai cách đều phải kiểm tra ràng buộc, nếu không đó là lỗi thiết kế .
Doc Brown

Vì vậy, bạn sẽ xem xét sử dụng các phương thức setter trong một thực tiễn xấu của hàm tạo nếu có 1) không có logic trong setters hoặc 2) logic trong setters không phụ thuộc vào trạng thái? Tôi không làm điều đó thường xuyên nhưng cá nhân tôi đã sử dụng các phương thức setter trong hàm tạo chính xác cho lý do sau (khi có một số điều kiện trên setter phải luôn áp dụng cho biến thành viên
Chris Maggiulli

Nếu có một số loại điều kiện phải luôn luôn áp dụng, đó logic trong setter. Tôi chắc chắn nghĩ rằng sẽ tốt hơn nếu có thể mã hóa logic này trong thành viên, vì vậy nó buộc phải khép kín và không thể có được sự phụ thuộc vào phần còn lại của lớp.
Vô dụng

13

Khi một đối tượng đang được xây dựng, theo định nghĩa nó không được hình thành đầy đủ. Xem xét cách setter bạn cung cấp:

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Điều gì xảy ra nếu một phần của xác nhận setAge()bao gồm một kiểm tra đối với agetài sản để đảm bảo rằng đối tượng chỉ có thể tăng tuổi? Tình trạng đó có thể giống như:

if (age > this.age && age < 25) {
    //...

Đó dường như là một thay đổi khá ngây thơ, vì mèo chỉ có thể di chuyển theo thời gian theo một hướng. Vấn đề duy nhất là bạn không thể sử dụng nó để đặt giá trị ban đầu this.agevì nó giả định rằng this.ageđã có giá trị hợp lệ.

Việc tránh setters trong constructor cho thấy rõ rằng điều duy nhất mà constructor đang làm là thiết lập biến thể hiện và bỏ qua mọi hành vi khác xảy ra trong setter.


OK ... nhưng, nếu bạn không thực sự kiểm tra xây dựng đối tượng và tiếp xúc với các lỗi tiềm ẩn, có những lỗi tiềm năng một trong hai cách . Và bây giờ bạn đã có hai hai vấn đề: Bạn có mà lỗi tiềm năng ẩn bạn phải thực thi kiểm tra tuổi tầm xa ở hai nơi.
Svidgen

Không có vấn đề gì, vì trong Java / C #, tất cả các trường đều bằng 0 trước khi gọi hàm tạo.
Ded repeatator

2
Có, việc gọi các phương thức cá thể từ các nhà xây dựng có thể khó lý do và thường không được ưa thích. Bây giờ, nếu bạn muốn di chuyển logic xác thực của mình sang một phương thức tĩnh, lớp cuối cùng và gọi nó từ cả hàm tạo và setter, điều đó sẽ ổn.
Vô dụng

1
Không, các phương thức phi thể hiện tránh được sự khó khăn của các phương thức cá thể, vì chúng không chạm vào một thể hiện được xây dựng một phần. Tất nhiên, bạn vẫn cần kiểm tra kết quả xác nhận để quyết định xem việc xây dựng có thành công hay không.
Vô dụng

1
Câu trả lời này là IMHO thiếu điểm. "Khi một đối tượng đang được xây dựng, theo định nghĩa, nó không được hình thành đầy đủ" - dĩ nhiên, giữa quá trình xây dựng, nhưng khi xây dựng xong, mọi ràng buộc có chủ ý đối với các thuộc tính phải giữ, nếu không, người ta có thể tránh được ràng buộc đó. Tôi sẽ gọi đó là một lỗ hổng thiết kế nghiêm trọng.
Doc Brown

6

Một số câu trả lời tốt ở đây đã có, nhưng cho đến nay dường như không ai nhận thấy bạn đang hỏi ở đây hai điều trong một, điều này gây ra một số nhầm lẫn. IMHO bài viết của bạn được xem tốt nhất là hai câu hỏi riêng biệt :

  1. nếu có một xác nhận hợp lệ của một ràng buộc trong setter, thì nó cũng không nên nằm trong hàm tạo của lớp để đảm bảo rằng nó không thể được bỏ qua?

  2. tại sao không gọi setter trực tiếp cho mục đích này từ constructor?

Câu trả lời cho câu hỏi đầu tiên rõ ràng là "có". Một hàm tạo, khi nó không ném ngoại lệ, sẽ để đối tượng ở trạng thái hợp lệ và nếu các giá trị nhất định bị cấm đối với một số thuộc tính nhất định, thì sẽ hoàn toàn vô nghĩa khi để ctor phá vỡ điều này.

Tuy nhiên, câu trả lời cho câu hỏi thứ hai thường là "không", miễn là bạn không thực hiện các biện pháp để tránh ghi đè lên setter trong một lớp dẫn xuất. Do đó, cách thay thế tốt hơn là thực hiện xác nhận ràng buộc trong một phương thức riêng có thể được sử dụng lại từ trình thiết lập cũng như từ hàm tạo. Tôi sẽ không ở đây để lặp lại ví dụ @Usless, cho thấy chính xác thiết kế này.


Tôi vẫn không hiểu tại sao nên trích xuất logic từ setter để hàm tạo có thể sử dụng nó. ... Làm thế nào điều này thực sự khác biệt so với chỉ đơn giản là gọi các setters theo thứ tự hợp lý ?? Đặc biệt là xem xét rằng, nếu setters của bạn đơn giản như họ "nên", thì phần duy nhất của trình thiết lập của bạn, tại thời điểm này, không gọi là cài đặt thực tế của trường bên dưới ...
svidgen

2
@svidgen: xem ví dụ của JimmyJames: tóm lại, không nên sử dụng các phương thức overridable trong một ctor, điều đó sẽ gây ra vấn đề. Bạn có thể sử dụng setter trong ctor nếu nó là bản cuối cùng và không gọi bất kỳ phương thức overridable nào khác. Hơn nữa, việc tách xác thực khỏi cách xử lý khi nó không cung cấp cho bạn cơ hội để xử lý nó khác nhau trong ctor và trong setter.
Doc Brown

Chà, bây giờ tôi thậm chí còn kém thuyết phục hơn! Nhưng, cảm ơn vì đã chỉ cho tôi câu trả lời khác ...
svidgen

2
Theo một trật tự hợp lý, bạn có nghĩa là với một phụ thuộc trật tự dễ vỡ, vô hình dễ dàng bị phá vỡ bởi những thay đổi trông vô tội , phải không?
Vô dụng

@ không có gì tốt, không ... Ý tôi là, thật buồn cười khi bạn đề xuất thứ tự mà mã của bạn thực thi không quan trọng. Nhưng, đó không thực sự là vấn đề. Ngoài ví dụ giả tạo, tôi chỉ không thể nhớ một thể hiện kinh nghiệm của tôi, nơi tôi đã nghĩ đó là một ý tưởng tốt để có kiểm chứng thực chéo bất động sản trong một setter - rằng loại dẫn điều nhất thiết phải yêu cầu gọi theo đơn đặt hàng "vô hình" . Bất kể liệu những lời mời đó có xảy ra trong một nhà xây dựng không ..
svidgen

2

Đây là một đoạn mã Java ngớ ngẩn thể hiện các loại vấn đề bạn có thể gặp phải khi sử dụng các phương thức không phải là cuối cùng trong hàm tạo của bạn:

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

Khi tôi chạy cái này, tôi nhận được một NPE trong hàm tạo NextPro Hiệu. Đây là một ví dụ tầm thường tất nhiên nhưng mọi thứ có thể trở nên phức tạp một cách nhanh chóng nếu bạn có nhiều cấp độ thừa kế.

Tôi nghĩ lý do lớn hơn điều này không trở nên phổ biến là vì nó làm cho mã khó hiểu hơn một chút. Cá nhân, tôi gần như không bao giờ có các phương thức setter và các biến thành viên của tôi gần như là bất biến (ý định chơi chữ) cuối cùng. Do đó, các giá trị phải được đặt trong hàm tạo. Nếu bạn sử dụng các đối tượng bất biến (và có nhiều lý do chính đáng để làm như vậy) thì câu hỏi là phải tranh luận.

Trong mọi trường hợp, đó là một mục tiêu tốt để sử dụng lại xác thực hoặc logic khác và bạn có thể đặt nó vào một phương thức tĩnh và gọi nó từ cả hàm tạo và trình thiết lập.


Làm thế nào đây là một vấn đề của việc sử dụng setters trong hàm tạo thay vì vấn đề thừa kế được triển khai kém ??? Nói cách khác, nếu bạn vô hiệu hóa trình xây dựng cơ sở, tại sao bạn thậm chí sẽ gọi nó!?
Svidgen

1
Tôi nghĩ vấn đề là, việc ghi đè một setter, không nên làm mất hiệu lực của hàm tạo.
Chris Wohlert

@ChrisWohlert Ok ... nhưng, nếu bạn thay đổi logic setter của mình thành xung đột với logic constructor của bạn, thì đó là vấn đề bất kể constructor của bạn có sử dụng setter không. Hoặc nó thất bại trong thời gian xây dựng, nó thất bại sau đó hoặc nó thất bại vô hình (b / c bạn đã bỏ qua các quy tắc kinh doanh của bạn).
Svidgen

Bạn thường không biết Trình xây dựng của lớp cơ sở được triển khai như thế nào (nó có thể nằm trong thư viện của bên thứ 3 ở đâu đó). Ghi đè một phương thức có thể ghi đè không được phá vỡ hợp đồng của việc triển khai cơ sở, mặc dù (và có thể cho rằng ví dụ này làm như vậy bằng cách không đặt nametrường).
Hulk

@svidgen vấn đề ở đây là việc gọi các phương thức có thể ghi đè từ hàm tạo sẽ làm giảm tính hữu ích của việc ghi đè chúng - bạn không thể sử dụng bất kỳ trường nào của lớp con trong các phiên bản được ghi đè vì chúng chưa được khởi tạo. Điều tồi tệ hơn là bạn không được vượt qua một sự thisphản đối đối với một số phương pháp khác, bởi vì bạn chưa thể chắc chắn nó đã được khởi tạo hoàn toàn.
Hulk

1

Nó phụ thuộc!

Nếu bạn đang thực hiện xác nhận đơn giản hoặc đưa ra các hiệu ứng phụ nghịch ngợm trong trình thiết lập cần phải được nhấn mỗi khi thuộc tính được đặt: sử dụng trình thiết lập. Cách khác thường là trích xuất logic setter từ setter và gọi phương thức mới theo sau là set thực tế từ cả setter constructor - có hiệu quả tương tự như sử dụng setter! (Ngoại trừ bây giờ bạn đang duy trì bộ cài đặt hai dòng nhỏ, được thừa nhận ở hai vị trí.)

Tuy nhiên , nếu bạn cần thực hiện các thao tác phức tạp để sắp xếp đối tượng trước khi một setter cụ thể (hoặc hai!) Có thể thực thi thành công, đừng sử dụng setter.

Và trong cả hai trường hợp, có trường hợp thử nghiệm . Bất kể bạn đi theo con đường nào, nếu bạn có bài kiểm tra đơn vị, bạn sẽ có cơ hội tốt để vạch trần những cạm bẫy liên quan đến một trong hai lựa chọn.


Tôi cũng sẽ nói thêm, nếu trí nhớ của tôi từ thời xa xưa đó thậm chí còn chính xác từ xa, thì đây cũng là loại lý do tôi được dạy ở trường đại học. Và, nó hoàn toàn không phù hợp với các dự án thành công mà tôi có quyền riêng tư để thực hiện.

Nếu tôi phải đoán, tôi đã bỏ lỡ một bản ghi nhớ "mã sạch" tại một số điểm xác định một số lỗi điển hình có thể phát sinh từ việc sử dụng setters trong một hàm tạo. Nhưng, về phần tôi, tôi chưa từng là nạn nhân của bất kỳ lỗi nào như vậy ...

Vì vậy, tôi cho rằng quyết định đặc biệt này không cần phải càn quét và giáo điều.

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.