Làm thế nào để cảnh báo các lập trình viên khác về việc thực hiện lớp


96

Tôi đang viết các lớp "phải được sử dụng theo một cách cụ thể" (tôi đoán tất cả các lớp phải ...).

Ví dụ, tôi tạo fooManagerlớp, yêu cầu gọi, nói , Initialize(string,string). Và, để đẩy ví dụ xa hơn một chút, lớp học sẽ trở nên vô dụng nếu chúng ta không lắng nghe ThisHappenedhành động của nó .

Quan điểm của tôi là, lớp tôi đang viết yêu cầu các cuộc gọi phương thức. Nhưng nó sẽ biên dịch tốt nếu bạn không gọi các phương thức đó và sẽ kết thúc với một FooManager mới trống. Tại một số điểm, nó sẽ không hoạt động, hoặc có thể sụp đổ, tùy thuộc vào lớp và những gì nó làm. Lập trình viên thực hiện lớp của tôi rõ ràng sẽ nhìn vào bên trong nó và nhận ra "Ồ, tôi đã không gọi Khởi tạo!", Và nó sẽ ổn thôi.

Nhưng tôi không thích điều đó. Điều tôi lý tưởng muốn là mã KHÔNG biên dịch nếu phương thức không được gọi; Tôi đoán điều đó đơn giản là không thể. Hoặc một cái gì đó sẽ ngay lập tức được nhìn thấy và rõ ràng.

Tôi thấy mình gặp rắc rối với cách tiếp cận hiện tại mà tôi có ở đây, đó là:

Thêm một giá trị Boolean riêng trong lớp và kiểm tra mọi nơi cần thiết nếu lớp được khởi tạo; nếu không, tôi sẽ đưa ra một ngoại lệ với nội dung "Lớp học chưa được khởi tạo, bạn có chắc là bạn đang gọi .Initialize(string,string)không?".

Tôi khá ổn với cách tiếp cận đó, nhưng nó dẫn đến rất nhiều mã được biên dịch và cuối cùng, không cần thiết cho người dùng cuối.

Ngoài ra, đôi khi còn nhiều mã hơn khi có nhiều phương thức hơn là chỉ Initiliazegọi. Tôi cố gắng giữ cho các lớp học của mình không có quá nhiều phương pháp / hành động công khai, nhưng điều đó không giải quyết được vấn đề, chỉ giữ cho nó hợp lý.

Những gì tôi đang tìm kiếm ở đây là:

  • Cách tiếp cận của tôi có đúng không?
  • Có một cái tốt hơn?
  • Các bạn làm / khuyên gì?
  • Tôi đang cố gắng giải quyết một vấn đề không? Tôi đã được các đồng nghiệp nói rằng đó là lập trình viên kiểm tra lớp trước khi thử sử dụng nó. Tôi tôn trọng không đồng ý, nhưng đó là một vấn đề khác tôi tin.

Nói một cách đơn giản, tôi đang cố gắng tìm ra một cách để không bao giờ quên thực hiện các cuộc gọi khi lớp đó được sử dụng lại sau đó hoặc bởi người khác.

XÁC NHẬN:

Để làm rõ nhiều câu hỏi ở đây:

  • Tôi chắc chắn KHÔNG chỉ nói về phần Khởi tạo của một lớp, mà là cả đời. Ngăn chặn các đồng nghiệp gọi một phương thức hai lần, đảm bảo họ gọi X trước Y, v.v ... Bất cứ điều gì cuối cùng sẽ là bắt buộc và trong tài liệu, nhưng tôi muốn mã và đơn giản và nhỏ nhất có thể. Tôi thực sự thích ý tưởng về Asserts, mặc dù tôi khá chắc chắn rằng tôi sẽ cần kết hợp một số ý tưởng khác vì Asserts sẽ không phải lúc nào cũng có thể.

  • Tôi đang sử dụng ngôn ngữ C #! Làm thế nào mà tôi không đề cập đến điều đó?! Tôi đang ở trong môi trường Xamarin và xây dựng các ứng dụng di động thường sử dụng khoảng 6 đến 9 dự án trong một giải pháp, bao gồm các dự án của PCL, iOS, Android và Windows. Tôi đã là một nhà phát triển trong khoảng một năm rưỡi (kết hợp giữa trường học và công việc), do đó những câu hỏi đôi khi lố ​​bịch của tôi. Tất cả những điều đó có lẽ không liên quan ở đây, nhưng quá nhiều thông tin không phải lúc nào cũng là điều xấu.

  • Tôi không thể luôn đặt mọi thứ bắt buộc trong hàm tạo, do hạn chế về nền tảng và việc sử dụng Dependency Injection, có các tham số khác ngoài Giao diện bị loại khỏi bảng. Hoặc có thể kiến ​​thức của tôi không đủ, điều này rất có thể. Hầu hết thời gian không phải là vấn đề Khởi tạo, nhưng nhiều hơn

Làm thế nào tôi có thể chắc chắn rằng anh ấy đã đăng ký vào sự kiện đó?

Làm thế nào tôi có thể chắc chắn rằng anh ấy đã không quên "dừng quá trình tại một số điểm"

Ở đây tôi nhớ một lớp tìm nạp quảng cáo. Miễn là chế độ xem hiển thị Quảng cáo hiển thị, lớp sẽ tìm nạp quảng cáo mới mỗi phút. Lớp đó cần một khung nhìn khi được xây dựng nơi nó có thể hiển thị Quảng cáo, rõ ràng có thể đi vào một tham số. Nhưng một khi chế độ xem không còn, StopFetching () phải được gọi. Nếu không, lớp sẽ tiếp tục tìm nạp quảng cáo cho một chế độ xem thậm chí không có ở đó và điều đó thật tệ.

Ngoài ra, lớp đó có các sự kiện phải nghe, ví dụ như "AdClicky". Mọi thứ đều hoạt động tốt nếu không được lắng nghe, nhưng chúng tôi mất theo dõi các phân tích ở đó nếu vòi không được đăng ký. Quảng cáo vẫn hoạt động, vì vậy người dùng và nhà phát triển sẽ không thấy sự khác biệt và các phân tích sẽ chỉ có dữ liệu sai. Điều đó cần phải được tránh, nhưng tôi không chắc làm thế nào nhà phát triển có thể biết họ phải đăng ký vào sự kiện tao. Tuy nhiên, đó là một ví dụ đơn giản, nhưng ý tưởng là có, "hãy chắc chắn rằng anh ta sử dụng Hành động công khai có sẵn" và vào đúng thời điểm tất nhiên!


13
Việc đảm bảo rằng các cuộc gọi đến các phương thức của một lớp xảy ra theo một thứ tự cụ thể là không thể nói chung. Đây là một trong nhiều, rất nhiều vấn đề tương đương với vấn đề tạm dừng. Mặc dù có thể làm cho việc không tuân thủ trở thành lỗi thời gian biên dịch trong một số trường hợp, không có giải pháp chung. Đó có lẽ là lý do tại sao một số ngôn ngữ hỗ trợ các cấu trúc cho phép bạn chẩn đoán ít nhất là các trường hợp rõ ràng, mặc dù phân tích luồng dữ liệu đã trở nên khá phức tạp.
Kilian Foth


5
Câu trả lời cho câu hỏi khác là không liên quan, câu hỏi không giống nhau, do đó chúng là những câu hỏi khác nhau. Nếu ai đó tìm kiếm cuộc thảo luận đó, anh ta sẽ không gõ tiêu đề của câu hỏi khác.
Gil Sand

6
Điều gì ngăn cản hợp nhất nội dung khởi tạo trong ctor? Phải gọi cuộc gọi initializemuộn sau khi tạo đối tượng? Liệu ctor có quá "mạo hiểm" theo nghĩa nó có thể ném ngoại lệ và phá vỡ chuỗi sáng tạo?
Phát hiện

7
Vấn đề này được gọi là khớp nối thời gian . Nếu bạn có thể, hãy cố gắng tránh đặt đối tượng ở trạng thái không hợp lệ bằng cách ném ngoại lệ vào hàm tạo khi gặp các đầu vào không hợp lệ, theo cách đó bạn sẽ không bao giờ vượt qua cuộc gọi newnếu đối tượng không sẵn sàng sử dụng. Dựa vào một phương pháp khởi tạo chắc chắn sẽ quay trở lại ám ảnh bạn và nên tránh trừ khi thực sự cần thiết, ít nhất đó là kinh nghiệm của tôi.
Thanh toán

Câu trả lời:


161

Trong những trường hợp như vậy, tốt nhất là sử dụng hệ thống loại ngôn ngữ của bạn để giúp bạn khởi tạo đúng cách. Làm thế nào chúng ta có thể ngăn chặn một FooManagerđược sử dụng mà không được khởi tạo? Bằng cách ngăn chặn một FooManagertừ được tạo ra mà không có thông tin cần thiết để đúng cách khởi tạo nó. Đặc biệt, tất cả các khởi tạo là trách nhiệm của nhà xây dựng. Bạn không bao giờ nên để nhà xây dựng của mình tạo một đối tượng ở trạng thái bất hợp pháp.

Nhưng người gọi cần phải xây dựng một FooManagertrước khi họ có thể khởi tạo nó, ví dụ như bởi vì FooManagernó được truyền xung quanh như là một phụ thuộc.

Đừng tạo FooManagernếu bạn không có. Thay vào đó, những gì bạn có thể làm là chuyển một đối tượng xung quanh cho phép bạn truy xuất một cấu trúc đầy đủ FooManager, nhưng chỉ với thông tin khởi tạo. (Trong lập trình chức năng nói, tôi khuyên bạn nên sử dụng một phần ứng dụng cho hàm tạo.) Eg:

ctorArgs = ...;
getFooManager = (initInfo) -> new FooManager(ctorArgs, initInfo);
...
getFooManager(myInitInfo).fooMethod();

Vấn đề với điều này là bạn phải cung cấp thông tin init mỗi khi bạn truy cập FooManager.

Nếu nó cần thiết trong ngôn ngữ của bạn, bạn có thể bao bọc getFooManager()hoạt động trong một lớp giống như nhà máy hoặc nhà xây dựng.

Tôi thực sự muốn thực hiện kiểm tra thời gian chạy rằng initialize()phương thức đã được gọi, thay vì sử dụng giải pháp cấp hệ thống loại.

Có thể tìm thấy một sự thỏa hiệp. Chúng tôi tạo một lớp bao bọc MaybeInitializedFooManagercó một get()phương thức trả về FooManager, nhưng sẽ ném nếu FooManagerkhông được khởi tạo hoàn toàn. Điều này chỉ hoạt động nếu việc khởi tạo được thực hiện thông qua trình bao bọc, hoặc nếu có một FooManager#isInitialized()phương thức.

class MaybeInitializedFooManager {
  private final FooManager fooManager;

  public MaybeInitializedFooManager(CtorArgs ctorArgs) {
    fooManager = new FooManager(ctorArgs);
  }

  public FooManager initialize(InitArgs initArgs) {
    fooManager.initialize(initArgs);
    return fooManager;
  }

  public FooManager get() {
    if (fooManager.isInitialized()) return fooManager;
    throw ...;
  }
}

Tôi không muốn thay đổi API của lớp mình.

Trong trường hợp đó, bạn sẽ muốn tránh các if (!initialized) throw;điều kiện trong từng phương thức. May mắn thay, có một mô hình đơn giản để giải quyết điều này.

Đối tượng bạn cung cấp cho người dùng chỉ là một vỏ trống để ủy quyền tất cả các cuộc gọi cho một đối tượng triển khai. Theo mặc định, đối tượng triển khai đưa ra một lỗi cho mỗi phương thức mà nó không được khởi tạo. Tuy nhiên, initialize()phương thức thay thế đối tượng thực hiện bằng một đối tượng được xây dựng đầy đủ.

class FooManager {
  private CtorArgs ctorArgs;
  private Impl impl;

  public FooManager(CtorArgs ctorArgs) {
    this.ctorArgs = ctorArgs;
    this.impl = new UninitializedImpl();
  }

  public void initialize(InitArgs initArgs) {
    impl = new MainImpl(ctorArgs, initArgs);
  }

  public X foo() { return impl.foo(); }
  public Y bar() { return impl.bar(); }
}

interface Impl {
  X foo();
  Y bar();
}

class UninitializedImpl implements Impl {
  public X foo() { throw ...; }
  public Y bar() { throw ...; }
}

class MainImpl implements Impl {
  public MainImpl(CtorArgs c, InitArgs i);
  public X foo() { ... }
  public Y bar() { ... }
}

Điều này trích xuất hành vi chính của lớp vào MainImpl.


9
Tôi thích hai giải pháp đầu tiên (+1), nhưng tôi không nghĩ đoạn trích cuối cùng của bạn với sự lặp lại của các phương thức trong FooManagervà trong UninitialisedImpllà tốt hơn so với việc lặp lại if (!initialized) throw;.
Bergi

3
@Bergi Một số người yêu thích các lớp học quá nhiều. Mặc dù nếu FooManagernhiều phương pháp, nó có thể dễ hơn là có khả năng quên một số if (!initialized)kiểm tra. Nhưng trong trường hợp đó có lẽ bạn nên thích chia tay lớp học.
Immibis

1
Một ShouldInitializedFoo dường như không tốt hơn một Foo khởi tạo quá tôi, nhưng +1 để đưa ra một số tùy chọn / ý tưởng.
Matthew James Briggs

7
+1 Nhà máy là lựa chọn chính xác. Bạn không thể cải thiện thiết kế mà không thay đổi nó, vì vậy IMHO câu trả lời cuối cùng về cách làm một nửa là nó sẽ không giúp OP.
Nathan Cooper

6
@Bergi Tôi đã tìm thấy kỹ thuật được trình bày trong phần cuối cùng rất hữu ích (xem thêm các điều kiện Thay thế thông qua kỹ thuật tái cấu trúc đa hình thái và mô hình trạng thái). Thật dễ dàng để quên kiểm tra trong một phương thức nếu chúng ta sử dụng một if / khác, khó hơn nhiều để quên thực hiện một phương thức theo yêu cầu của giao diện. Quan trọng nhất, MainImpl tương ứng chính xác với một đối tượng trong đó phương thức khởi tạo được hợp nhất với hàm tạo. Điều này có nghĩa là việc triển khai MainImpl có thể được hưởng các đảm bảo mạnh mẽ hơn, điều này giúp đơn giản hóa mã chính. Tiết
amon

79

Cách hiệu quả và hữu ích nhất để ngăn khách hàng "lạm dụng" một đối tượng là làm cho nó không thể.

Giải pháp đơn giản nhất là hợp nhất Initializevới hàm tạo. Bằng cách đó, đối tượng sẽ không bao giờ có sẵn cho máy khách ở trạng thái chưa được khởi tạo nên không thể xảy ra lỗi. Trong trường hợp bạn không thể thực hiện khởi tạo trong chính hàm tạo, bạn có thể tạo một phương thức xuất xưởng. Ví dụ, nếu lớp của bạn yêu cầu một số sự kiện nhất định phải được đăng ký, bạn có thể yêu cầu người nghe sự kiện làm tham số trong hàm tạo hoặc chữ ký phương thức nhà máy.

Nếu bạn cần có khả năng truy cập đối tượng chưa khởi tạo trước khi khởi tạo, thì bạn có thể có hai trạng thái được triển khai dưới dạng các lớp riêng biệt, vì vậy bạn bắt đầu với một UnitializedFooManagerthể hiện, có một Initialize(...)phương thức trả về InitializedFooManager. Các phương thức chỉ có thể được gọi trong trạng thái khởi tạo chỉ tồn tại trên InitializedFooManager. Cách tiếp cận này có thể được mở rộng ra nhiều trạng thái nếu bạn cần.

So với các ngoại lệ thời gian chạy, việc biểu diễn các trạng thái là các lớp riêng biệt sẽ hiệu quả hơn, nhưng nó cũng mang lại cho bạn đảm bảo thời gian biên dịch rằng bạn không gọi các phương thức không hợp lệ cho trạng thái đối tượng và nó ghi lại các trạng thái chuyển đổi rõ ràng hơn trong mã.

Nhưng nói chung, giải pháp lý tưởng là thiết kế các lớp và phương thức của bạn mà không bị ràng buộc như yêu cầu bạn gọi các phương thức vào những thời điểm nhất định và theo một thứ tự nhất định. Nó có thể không phải lúc nào cũng có thể, nhưng nó có thể tránh được trong nhiều trường hợp bằng cách sử dụng mẫu thích hợp.

Nếu bạn có khớp nối tạm thời phức tạp (phải gọi một số phương thức theo một thứ tự nhất định), một giải pháp có thể là sử dụng đảo ngược điều khiển , vì vậy bạn tạo một lớp gọi các phương thức theo thứ tự phù hợp, nhưng sử dụng các phương thức hoặc sự kiện mẫu để cho phép khách hàng thực hiện các thao tác tùy chỉnh ở các giai đoạn thích hợp trong luồng. Bằng cách đó, trách nhiệm thực hiện các hoạt động theo đúng thứ tự được đẩy từ máy khách đến chính lớp đó.

Một ví dụ đơn giản: Bạn có một File-object cho phép bạn đọc từ một tệp. Tuy nhiên, khách hàng cần gọi Opentrước khi ReadLinephương thức có thể được gọi và cần nhớ luôn gọi Close(ngay cả khi có ngoại lệ xảy ra) sau đó ReadLinephương thức không được gọi nữa. Đây là khớp nối tạm thời. Có thể tránh bằng cách sử dụng một phương thức duy nhất gọi lại hoặc ủy nhiệm làm đối số. Phương thức quản lý mở tệp, gọi lại và sau đó đóng tệp. Nó có thể chuyển một giao diện riêng biệt với một Readphương thức để gọi lại. Theo cách đó, khách hàng không thể quên gọi các phương thức theo đúng thứ tự.

Giao diện với khớp nối tạm thời:

class File {
     /// Must be called on a closed file.
     /// Remember to always call Close() when you are finished
     public void Open();

     /// must be called on on open file
     public string ReadLine();

     /// must be called on an open file
     public void Close();
}

Không có khớp nối tạm thời:

class File {
    /// Opens the file, executes the callback, and closes the file again.
    public void Consume(Action<IOpenFile> callback);
}

interface IOpenFile {
    string ReadLine();
}

Một giải pháp nặng hơn sẽ được định nghĩa Filelà một lớp trừu tượng đòi hỏi bạn phải thực hiện một phương thức (được bảo vệ) sẽ được thực thi trên tệp đang mở. Đây được gọi là một mẫu phương thức mẫu.

abstract class File {
    /// Opens the file, executes ConsumeOpenFile(), and closes the file again.
    public void Consume();

    /// override this
    abstract protected ConsumeOpenFile();

    /// call this from your ConsumeOpenFile() implementation
    protected string ReadLine();
}

Ưu điểm là như nhau: Máy khách không phải nhớ gọi các phương thức theo một thứ tự nhất định. Đơn giản là không thể gọi các phương thức theo thứ tự sai.


2
Ngoài ra tôi còn nhớ các trường hợp đơn giản là không thể đi từ nhà xây dựng, nhưng tôi không có ví dụ nào để hiển thị ngay bây giờ
Gil Sand

2
Ví dụ bây giờ mô tả một mô hình nhà máy - nhưng câu đầu tiên thực sự mô tả mô hình RAII . Vì vậy, câu trả lời này là gì để đề nghị?
Ext3h

11
@ Ext3h Tôi không nghĩ rằng mô hình nhà máy và RAII là loại trừ lẫn nhau.
Pieter B

@PieterB Không, họ không, một nhà máy có thể đảm bảo RAII. Nhưng chỉ với hàm ý bổ sung rằng khuôn mẫu / hàm tạo lập luận (được gọi UnitializedFooManager) không được chia sẻ trạng thái với các thể hiện ( InitializedFooManager), như Initialize(...)đã thực sự có Instantiate(...)ngữ nghĩa. Mặt khác, ví dụ này dẫn đến các vấn đề mới nếu cùng một nhà phát triển sử dụng hai lần. Và điều đó không thể ngăn chặn / xác nhận tĩnh nếu ngôn ngữ không hỗ trợ rõ ràng movengữ nghĩa để sử dụng mẫu một cách đáng tin cậy.
Ext3h

2
@ Ext3h: Tôi đề xuất giải pháp đầu tiên vì nó đơn giản nhất. Nhưng nếu khách hàng cần có khả năng truy cập đối tượng trước khi gọi Khởi tạo, thì bạn không thể sử dụng nó, nhưng bạn có thể sử dụng giải pháp thứ hai.
JacquesB

39

Tôi thường sẽ chỉ kiểm tra intiialisation và ném (nói) một IllegalStateExceptionnếu bạn thử và sử dụng nó trong khi không được khởi tạo.

Tuy nhiên, nếu bạn muốn an toàn thời gian biên dịch (và đó là điều đáng khen ngợi và tốt hơn), tại sao không coi việc khởi tạo như một phương thức xuất xưởng trả về một đối tượng được xây dựng và khởi tạo, ví dụ

ComponentBuilder builder = new ComponentBuilder();
Component forClients = builder.initialise(); // the 'Component' is what you give your clients

và do đó, bạn kiểm soát việc tạo đối tượng và vòng đời, và khách hàng của bạn nhận được Componentkết quả của việc khởi tạo. Đó thực sự là một khởi tạo lười biếng.


1
Câu trả lời tốt - 2 lựa chọn tốt trong một câu trả lời ngắn gọn. Các câu trả lời khác ở trên thể dục dụng cụ hệ thống hiện tại (về mặt lý thuyết) là hợp lệ; nhưng đối với phần lớn các trường hợp, 2 tùy chọn đơn giản hơn này là lựa chọn thực tế lý tưởng.
Thomas W

1
Lưu ý: Trong .NET, UnlimitedOperationException được Microsoft quảng bá là những gì bạn đã gọi IllegalStateException.
miroxlav

1
Tôi không bỏ phiếu cho câu trả lời này nhưng nó không thực sự là một câu trả lời hay. Chỉ cần ném IllegalStateExceptionhoặc InvalidOperationExceptionra khỏi màu xanh, một người dùng sẽ không bao giờ hiểu những gì anh ta đã làm sai. Những ngoại lệ này không nên trở thành một đường vòng để sửa lỗi thiết kế.
displayName

Bạn sẽ lưu ý rằng việc ném ngoại lệ là một tùy chọn có thể và tôi tiếp tục giải thích về tùy chọn ưa thích của mình - làm cho thời gian biên dịch này an toàn. Tôi đã chỉnh sửa câu trả lời của mình để nhấn mạnh điều này
Brian Agnew

14

Tôi sẽ tách ra một chút từ các câu trả lời khác và không đồng ý: không thể trả lời câu hỏi này nếu không biết bạn đang làm việc với ngôn ngữ nào. Đây có phải là một kế hoạch đáng giá hay không và là "cảnh báo" đúng đắn để đưa ra Người dùng của bạn phụ thuộc hoàn toàn vào các cơ chế mà ngôn ngữ của bạn cung cấp và các quy ước mà các nhà phát triển khác của ngôn ngữ đó có xu hướng tuân theo.

Nếu bạn có FooManagerHaskell, sẽ là tội phạm khi cho phép người dùng của bạn xây dựng một cái không có khả năng quản lý Foo, bởi vì hệ thống loại này làm cho nó rất dễ dàng và đó là những quy ước mà các nhà phát triển Haskell mong đợi.

Mặt khác, nếu bạn viết C, đồng nghiệp của bạn sẽ hoàn toàn thuộc quyền của họ để đưa bạn ra ngoài và bắn bạn để xác định các loại riêng biệt struct FooManagerstruct UninitializedFooManagerloại hỗ trợ các hoạt động khác nhau, vì nó sẽ dẫn đến mã phức tạp không cần thiết vì rất ít lợi ích .

Nếu bạn đang viết Python, không có cơ chế nào trong ngôn ngữ để cho phép bạn làm điều này.

Bạn có thể không viết Haskell, Python hoặc C, nhưng chúng là những ví dụ minh họa về mức độ / mức độ làm việc mà hệ thống loại dự kiến ​​và có thể làm được.

Thực hiện theo các kỳ vọng hợp lý của nhà phát triển bằng ngôn ngữ của bạnchống lại sự thôi thúc thiết kế quá mức một giải pháp không có triển khai tự nhiên, thành ngữ (trừ khi sai lầm rất dễ mắc phải và rất khó để nhận ra rằng nó đáng để cực đoan độ dài để làm cho nó không thể). Nếu bạn không có đủ kinh nghiệm trong ngôn ngữ của mình để đánh giá điều gì hợp lý, hãy làm theo lời khuyên của người hiểu rõ hơn bạn.


Tôi hơi bối rối bởi "Nếu bạn đang viết Python, không có cơ chế nào trong ngôn ngữ để cho phép bạn làm điều này." Python có các hàm tạo và việc tạo các phương thức nhà máy khá đơn giản. Vì vậy, có lẽ tôi không hiểu ý của bạn là "cái này"?
AL Flanagan

3
Theo "điều này", tôi có nghĩa là một lỗi thời gian biên dịch, trái ngược với lỗi thời gian chạy.
Patrick Collins

Chỉ cần nhảy bằng cách đề cập đến Python 3.5, phiên bản hiện tại có các loại thông qua hệ thống loại có thể cắm được. Chắc chắn có thể khiến Python bị lỗi trước khi chạy mã chỉ vì nó đã được nhập. Lên đến 3,5 nó hoàn toàn chính xác mặc dù.
Benjamin Gruenbaum

Ồ được thôi. Được +1. Ngay cả bối rối, điều này là rất sâu sắc.
AL Flanagan

Tôi thích phong cách của bạn Patrick.
Gil Sand

4

Vì bạn dường như không muốn gửi mã kiểm tra cho khách hàng (nhưng có vẻ tốt để làm điều đó cho lập trình viên), bạn có thể sử dụng các hàm khẳng định , nếu chúng có sẵn trong ngôn ngữ lập trình của bạn.

Bằng cách đó, bạn có các kiểm tra trong môi trường phát triển (và mọi kiểm tra mà một nhà phát triển đồng nghiệp sẽ gọi là SILL thất bại có thể dự đoán được), nhưng bạn sẽ không gửi mã cho khách hàng như các xác nhận (ít nhất là trong Java) chỉ được biên dịch có chọn lọc.

Vì vậy, một lớp Java sử dụng cái này sẽ trông như thế này:

/** For some reason, you have to call init and connect before the manager works. */
public class FooManager {
   private int state = 0;

   public FooManager () {
   }

   public synchronized void init() {
      assert state==0 : "you called init twice.";
      // do something
      state = 1;
   }

   public synchronized void connect() {
      assert state==1 : "you called connect before init, or connect twice.";
      // do something
      state = 2;
   }

   public void someWork() {
      assert state==2 : "You did not properly init FooManager. You need to call init and connect first.";
      // do the actual work.
   }
}

Các xác nhận là một công cụ tuyệt vời để kiểm tra trạng thái thời gian chạy của chương trình mà bạn yêu cầu, nhưng thực sự không mong đợi bất kỳ ai từng làm sai trong môi trường thực.

Ngoài ra, chúng khá mảnh khảnh và không chiếm phần lớn cú pháp if (), ... chúng không cần phải bị bắt, v.v.


3

Nhìn vào vấn đề này nói chung nhiều hơn các câu trả lời hiện tại, chủ yếu tập trung vào khởi tạo. Hãy xem xét một đối tượng sẽ có hai phương thức a()b(). Yêu cầu là a()luôn luôn được gọi trước b(). Bạn có thể tạo một kiểm tra thời gian biên dịch rằng điều này xảy ra bằng cách trả về một đối tượng mới từ a()và di chuyển b()đến đối tượng mới thay vì đối tượng ban đầu. Ví dụ thực hiện:

class SomeClass
{
   private int valueRequiredByMethodB;

   public IReadyForB a (int v) { valueRequiredByMethodB = v; return new ReadyForB(this); }

   public interface IReadyForB { public void b (); }

   private class ReadyForB : IReadyForB
   {
      SomeClass owner;
      private ReadyForB (SomeClass owner) { this.owner = owner; }
      public void b () { Console.WriteLine (owner.valueRequiredByMethodB); }
   }
}

Bây giờ, không thể gọi b () mà không gọi a () trước, vì nó được triển khai trong một giao diện bị ẩn cho đến khi một () được gọi. Phải thừa nhận rằng đây là một nỗ lực khá nhiều, vì vậy tôi thường không sử dụng phương pháp này, nhưng có những tình huống có thể có ích (đặc biệt là nếu lớp của bạn sẽ được sử dụng lại trong rất nhiều tình huống bởi các lập trình viên có thể không quen thuộc với việc thực hiện nó, hoặc trong mã nơi độ tin cậy là rất quan trọng). Cũng lưu ý rằng đây là một khái quát của mẫu xây dựng, như được đề xuất bởi nhiều câu trả lời hiện có. Nó hoạt động khá giống nhau, sự khác biệt thực sự duy nhất là nơi dữ liệu được lưu trữ (trong đối tượng ban đầu chứ không phải dữ liệu được trả về) và khi nó được sử dụng (bất cứ lúc nào so với chỉ trong khi khởi tạo).


2

Khi tôi triển khai một lớp cơ sở yêu cầu thêm thông tin khởi tạo hoặc liên kết đến các đối tượng khác trước khi nó trở nên hữu ích, tôi có xu hướng làm cho lớp cơ sở đó trừu tượng, sau đó định nghĩa một số phương thức trừu tượng trong lớp đó được sử dụng trong luồng của lớp cơ sở (như abstract Collection<Information> get_extra_information(args);abstract Collection<OtherObjects> get_other_objects(args);theo hợp đồng thừa kế phải được thực hiện bởi lớp cụ thể, buộc người dùng của lớp cơ sở đó phải cung cấp tất cả những thứ mà lớp cơ sở yêu cầu.

Vì vậy, khi tôi triển khai lớp cơ sở, tôi biết ngay lập tức và rõ ràng những gì tôi phải viết để làm cho lớp cơ sở hoạt động chính xác, vì tôi chỉ cần thực hiện các phương thức trừu tượng và đó là nó.

EDIT: để làm rõ, điều này gần giống như việc cung cấp các đối số cho hàm tạo của lớp cơ sở, nhưng việc triển khai phương thức trừu tượng cho phép thực hiện để xử lý các đối số được truyền cho các lệnh gọi phương thức trừu tượng. Hoặc ngay cả trong trường hợp không có đối số nào được sử dụng, nó vẫn có thể hữu ích nếu giá trị trả về của phương thức trừu tượng phụ thuộc vào trạng thái mà bạn có thể xác định trong thân phương thức, điều này không thể thực hiện được khi chuyển biến làm đối số cho người xây dựng. Cấp cho bạn vẫn có thể vượt qua một đối số sẽ có hành vi động dựa trên cùng một nguyên tắc nếu bạn muốn sử dụng thành phần hơn thừa kế.


2

Câu trả lời là: Có, không, và đôi khi. :-)

Một số vấn đề bạn mô tả được giải quyết dễ dàng, ít nhất là trong nhiều trường hợp.

Kiểm tra thời gian biên dịch chắc chắn là tốt hơn so với kiểm tra thời gian chạy, tốt hơn là không kiểm tra gì cả.

Thời gian chạy lại:

Ít nhất là về mặt khái niệm đơn giản để buộc các hàm được gọi theo một thứ tự nhất định, v.v., với kiểm tra thời gian chạy. Chỉ cần đặt các cờ cho biết chức năng nào đã được chạy và để mỗi chức năng bắt đầu bằng một cái gì đó như "nếu không phải là tiền điều kiện-1 hoặc không phải là tiền điều kiện-2 thì hãy ném ngoại lệ". Nếu kiểm tra trở nên phức tạp, bạn có thể đẩy chúng vào một chức năng riêng tư.

Bạn có thể làm cho một số điều xảy ra tự động. Lấy một ví dụ đơn giản, các lập trình viên thường nói về "dân số lười biếng" của một đối tượng. Khi cá thể được tạo, bạn đặt cờ được điền thành false hoặc một số tham chiếu đối tượng chính thành null. Sau đó, khi một hàm được gọi là cần dữ liệu liên quan, nó sẽ kiểm tra cờ. Nếu nó sai, nó sẽ điền dữ liệu và đặt cờ đúng. Nếu nó đúng thì nó cứ tiếp tục giả sử dữ liệu ở đó. Bạn có thể làm điều tương tự với các điều kiện tiên quyết khác. Đặt cờ trong hàm tạo hoặc làm giá trị mặc định. Sau đó, khi bạn đạt đến một điểm mà một số chức năng tiền điều kiện nên được gọi, nếu nó chưa được gọi, hãy gọi nó. Tất nhiên điều này chỉ hoạt động nếu bạn có dữ liệu để gọi nó vào thời điểm đó, nhưng trong nhiều trường hợp, bạn có thể bao gồm bất kỳ dữ liệu cần thiết nào trong các tham số chức năng của "

Thời gian biên dịch lại:

Như những người khác đã nói, nếu bạn cần khởi tạo trước khi một đối tượng có thể được sử dụng, điều đó sẽ đưa việc khởi tạo vào hàm tạo. Đây là lời khuyên khá điển hình cho OOP. Nếu có thể, bạn nên làm cho hàm tạo tạo một thể hiện hợp lệ, có thể sử dụng được của đối tượng.

Tôi thấy một số cuộc thảo luận về việc phải làm nếu bạn cần chuyển qua các tham chiếu đến đối tượng trước khi nó được khởi tạo. Tôi không thể nghĩ ra một ví dụ thực tế về điều đó ngoài đỉnh đầu, nhưng tôi cho rằng điều đó có thể xảy ra. Các giải pháp đã được đề xuất, nhưng các giải pháp tôi đã thấy ở đây và bất kỳ giải pháp nào tôi có thể nghĩ là lộn xộn và làm phức tạp mã. Tại một số điểm bạn phải hỏi, Có đáng để tạo mã xấu, khó hiểu để chúng tôi có thể kiểm tra thời gian biên dịch thay vì kiểm tra thời gian chạy không? Hay tôi đang tạo ra rất nhiều công việc cho bản thân và những người khác cho một mục tiêu tốt đẹp nhưng không bắt buộc?

Nếu hai hàm luôn luôn được chạy cùng nhau, giải pháp đơn giản là biến chúng thành một hàm. Giống như nếu mỗi lần bạn thêm quảng cáo vào một trang, bạn cũng nên thêm trình xử lý số liệu cho nó, sau đó đặt mã để thêm trình xử lý số liệu bên trong chức năng "thêm quảng cáo".

Một số thứ sẽ gần như không thể kiểm tra tại thời điểm biên dịch. Giống như một yêu cầu rằng một chức năng nhất định không thể được gọi nhiều hơn một lần. (Tôi đã viết nhiều chức năng như vậy. Gần đây tôi đã viết một chức năng tìm kiếm các tệp trong thư mục ma thuật, xử lý bất kỳ chức năng nào tìm thấy và sau đó xóa chúng. Tất nhiên một khi nó đã xóa tệp thì không thể chạy lại. V.v.) Tôi không biết bất kỳ ngôn ngữ nào có tính năng cho phép bạn ngăn chức năng được gọi hai lần trên cùng một ví dụ vào thời gian biên dịch. Tôi không biết làm thế nào trình biên dịch có thể chắc chắn tìm ra rằng điều đó đang xảy ra. Tất cả những gì bạn có thể làm là đưa vào kiểm tra thời gian chạy. Điều đó đặc biệt kiểm tra thời gian chạy là rõ ràng, phải không? "if yet-were-here = true thì ném ngoại lệ khác được đặt sẵn-here-here = true".

RE không thể thực thi

Không có gì lạ khi một lớp yêu cầu dọn dẹp khi bạn hoàn thành: đóng tệp hoặc kết nối, giải phóng bộ nhớ, ghi kết quả cuối cùng vào DB, v.v. Không có cách nào dễ dàng để buộc điều đó xảy ra ở cả thời gian biên dịch hoặc thời gian chạy. Tôi nghĩ rằng hầu hết các ngôn ngữ OOP đều có một số loại cung cấp cho chức năng "hoàn thiện" được gọi khi một cá thể là rác được thu thập, nhưng tôi nghĩ hầu hết cũng nói rằng chúng không thể đảm bảo điều này sẽ được chạy. Các ngôn ngữ OOP thường bao gồm chức năng "vứt bỏ" với một số loại điều khoản để đặt phạm vi sử dụng một thể hiện, mệnh đề "sử dụng" hoặc "với" và khi chương trình thoát khỏi phạm vi đó thì xử lý đó được chạy. Nhưng điều này đòi hỏi lập trình viên phải sử dụng bộ xác định phạm vi thích hợp. Nó không bắt buộc lập trình viên làm điều đó đúng,

Trong trường hợp không thể buộc lập trình viên sử dụng lớp một cách chính xác, tôi cố gắng làm cho nó để chương trình nổ tung nếu anh ta làm sai, thay vì đưa ra kết quả không chính xác. Ví dụ đơn giản: Tôi đã thấy rất nhiều chương trình khởi tạo một loạt dữ liệu thành các giá trị giả, để người dùng sẽ không bao giờ có ngoại lệ con trỏ null ngay cả khi anh ta không gọi các hàm để điền dữ liệu chính xác. Và tôi luôn tự hỏi tại sao. Bạn đang đi theo cách của bạn để làm cho lỗi khó tìm thấy hơn. Nếu, khi một lập trình viên không gọi được chức năng "tải dữ liệu" và sau đó cố gắng sử dụng dữ liệu, anh ta đã có một ngoại lệ con trỏ null, ngoại lệ sẽ nhanh chóng cho anh ta biết vấn đề ở đâu. Nhưng nếu bạn ẩn nó bằng cách làm cho dữ liệu trống hoặc bằng 0 trong những trường hợp như vậy, chương trình có thể chạy đến khi hoàn thành nhưng tạo ra kết quả không chính xác. Trong một số trường hợp, lập trình viên thậm chí có thể không nhận ra có vấn đề. Nếu anh ta để ý, anh ta có thể phải đi theo một con đường dài để tìm ra nơi sai. Thà thất bại sớm còn hơn cố gắng dũng cảm tiếp tục.

Nói chung, chắc chắn, sẽ luôn tốt khi bạn có thể sử dụng một đối tượng không chính xác. Thật tốt nếu các hàm được cấu trúc theo cách đơn giản là không có cách nào để lập trình viên thực hiện các cuộc gọi không hợp lệ hoặc nếu các cuộc gọi không hợp lệ tạo ra các lỗi thời gian biên dịch.

Nhưng cũng có một số điểm mà bạn phải nói, Lập trình viên nên đọc tài liệu về chức năng.


1
Về việc buộc dọn dẹp, có một mẫu có thể được sử dụng trong đó tài nguyên chỉ có thể được phân bổ bằng cách gọi một phương thức cụ thể (ví dụ: trong ngôn ngữ OO điển hình, nó có thể có một hàm tạo riêng). Phương thức này phân bổ tài nguyên, gọi một hàm (hoặc một phương thức của giao diện) được truyền cho nó với tham chiếu đến tài nguyên và sau đó phá hủy tài nguyên khi hàm này trả về. Khác với việc chấm dứt luồng đột ngột, mẫu này không đảm bảo tài nguyên luôn bị hủy. Đó là một cách tiếp cận phổ biến trong các ngôn ngữ chức năng, nhưng tôi cũng đã thấy nó được sử dụng trong các hệ thống OO để có hiệu quả tốt.
Jules

@Jules hay còn gọi là "bố trí"
Benjamin Gruenbaum

2

Vâng, bản năng của bạn là tại chỗ. Nhiều năm trước tôi đã viết các bài báo trên các tạp chí (giống như nơi này, nhưng trên cây chết và đưa ra mỗi tháng một lần) thảo luận về Gỡ lỗi , Bẻ khóaChống .

Nếu bạn có thể có được trình biên dịch để bắt lỗi, đó là cách tốt nhất. Những tính năng ngôn ngữ nào khả dụng tùy thuộc vào ngôn ngữ và bạn không chỉ định. Dù sao, các kỹ thuật cụ thể sẽ được chi tiết cho các câu hỏi riêng lẻ trên trang web này.

Nhưng có một thử nghiệm để phát hiện việc sử dụng vào thời gian biên dịch thay vì thời gian chạy thực sự là một loại thử nghiệm tương tự: bạn vẫn cần biết để gọi các chức năng phù hợp trước và sử dụng chúng đúng cách.

Thiết kế thành phần để tránh điều đó thậm chí là một vấn đề ở nơi đầu tiên tinh tế hơn nhiều, và tôi thích Bộ đệm giống như ví dụ Element . Phần 4 (được xuất bản hai mươi năm trước! Wow!) Có một ví dụ với cuộc thảo luận khá giống với trường hợp của bạn: Lớp này phải được sử dụng cẩn thận, vì bộ đệm () có thể không được gọi sau khi bạn bắt đầu sử dụng read (). Thay vào đó, nó chỉ phải được sử dụng một lần, ngay sau khi xây dựng. Có lớp quản lý bộ đệm tự động và vô hình cho người gọi sẽ tránh được vấn đề về mặt khái niệm.

Bạn hỏi, nếu các bài kiểm tra có thể được thực hiện trong thời gian chạy để đảm bảo sử dụng đúng cách, bạn có nên làm điều đó không?

Tôi sẽ nói . Nó sẽ tiết kiệm cho nhóm của bạn rất nhiều công việc sửa lỗi một ngày nào đó, khi mã được duy trì và việc sử dụng cụ thể bị nhiễu loạn. Việc kiểm tra có thể được thiết lập như một tập hợp chính thức của các ràng buộcbất biến có thể được kiểm tra. Điều đó không có nghĩa là chi phí không gian thừa để theo dõi trạng thái và công việc kiểm tra luôn được để lại cho mỗi cuộc gọi. Nó nằm trong lớp để nó có thể được gọi và mã ghi lại các ràng buộc và giả định thực tế.

Có lẽ việc kiểm tra chỉ được thực hiện trong một bản dựng gỡ lỗi.

Có lẽ kiểm tra rất phức tạp (ví dụ như nghĩ về heapcheck), nhưng nó hiện diện và có thể được thực hiện một lần trong một thời gian, hoặc các cuộc gọi được thêm vào đây và ở đó khi mã đang được xử lý và một số vấn đề nổi lên hoặc để kiểm tra cụ thể đảm bảo không có bất kỳ vấn đề nào như vậy trong chương trình kiểm thử đơn vị hoặc kiểm thử tích hợp có liên kết đến cùng một lớp.

Bạn có thể quyết định rằng chi phí là không đáng kể. Nếu lớp thực hiện tệp I / O, và byte trạng thái bổ sung và kiểm tra như vậy là không có gì. Các lớp iostream phổ biến kiểm tra luồng đang ở trạng thái xấu.

Bản năng của bạn là tốt. Giữ nó lên!


0

Nguyên tắc che giấu thông tin (Encapsulation) là các thực thể bên ngoài lớp không nên biết nhiều hơn những gì cần thiết để sử dụng lớp này đúng cách.

Trường hợp của bạn có vẻ như bạn đang cố gắng để một đối tượng của một lớp chạy đúng mà không cho người dùng bên ngoài biết đủ về lớp. Bạn đã ẩn nhiều thông tin hơn bạn nên có.

  • Hoặc yêu cầu rõ ràng thông tin cần thiết trong hàm tạo;
  • Hoặc giữ nguyên các hàm tạo và sửa đổi các phương thức để để gọi các phương thức, bạn phải cung cấp dữ liệu còn thiếu.

Từ của trí tuệ:

* Dù bằng cách nào bạn cũng phải thiết kế lại lớp và / hoặc hàm tạo và / hoặc phương thức.

* Bằng cách không thiết kế đúng cách và bỏ đói lớp từ thông tin chính xác, bạn đang mạo hiểm lớp của mình phá vỡ không chỉ một mà có khả năng nhiều nơi.

* Nếu bạn đã viết các ngoại lệ và thông báo lỗi kém, lớp bạn sẽ cho đi nhiều hơn về chính nó.

* Cuối cùng, nếu người ngoài được cho là phải biết rằng nó phải khởi tạo a, b và c và sau đó gọi d (), e (), f (int) thì bạn bị rò rỉ.


0

Vì bạn đã chỉ định rằng bạn đang sử dụng C # một giải pháp khả thi cho tình huống của bạn là tận dụng Bộ phân tích mã Roslyn . Điều này sẽ cho phép bạn bắt vi phạm ngay lập tức và thậm chí cho phép bạn đề xuất sửa lỗi mã .

Một cách để thực hiện nó là trang trí các phương thức được ghép theo thời gian bằng một thuộc tính chỉ định thứ tự mà các phương thức cần được gọi là 1 . Khi bộ phân tích tìm thấy các thuộc tính này trong một lớp, nó xác nhận rằng các phương thức được gọi theo thứ tự. Điều này sẽ làm cho lớp của bạn trông giống như sau:

public abstract class Foo
{
   [TemporallyCoupled(1)]
   public abstract void Init();

   [TemporallyCoupled(2)]
   public abstract void DoBar();

   [TemporallyCoupled(3)]
   public abstract void Close();
}

1: Tôi chưa bao giờ viết Trình phân tích mã Roslyn vì vậy đây có thể không phải là cách triển khai tốt nhất. Ý tưởng sử dụng Trình phân tích mã Roslyn để xác minh API của bạn đang được sử dụng chính xác là âm thanh 100%.


-1

Cách tôi đã giải quyết vấn đề này trước đây là với một hàm tạo riêng và một MakeFooManager()phương thức tĩnh trong lớp. Thí dụ:

public class FooManager
{
     public string Foo { get; private set; }
     public string Bar { get; private set; }

     private FooManager(string foo, string bar)
     {
         Foo = foo;
         Bar = bar;
     }

     public static FooManager MakeFooManager(string foo, string bar)
     {
         // Can do other checks here too.
         if(foo == null || bar == null)
         {
             return null;
         }
         else
         {
             return new FooManager(foo, bar);
         }
     }
}

Vì một hàm tạo được triển khai, không có cách nào để bất kỳ ai tạo ra một thể hiện FooManagermà không đi qua MakeFooManager().


1
điều này dường như chỉ lặp lại điểm được thực hiện và giải thích trong câu trả lời trước đó đã được đăng vài giờ trước đó
gnat

1
điều này có bất kỳ lợi thế nào không chỉ đơn giản là kiểm tra null trong ctor? có vẻ như bạn vừa thực hiện một phương thức không có gì ngoài việc gói một phương thức khác. tại sao?
sara

Ngoài ra, tại sao các biến thành viên foo và bar?
Patrick M

-1

Có một số cách tiếp cận:

  1. Làm cho lớp dễ sử dụng đúng và khó sử dụng sai. Một số câu trả lời khác tập trung hoàn toàn vào điểm này, vì vậy tôi sẽ chỉ đề cập đến RAIImẫu xây dựng .

  2. Hãy chú ý làm thế nào để sử dụng ý kiến. Nếu lớp tự giải thích, đừng viết bình luận. * Bằng cách đó mọi người có nhiều khả năng đọc bình luận của bạn khi lớp thực sự cần chúng. Và nếu bạn có một trong những trường hợp hiếm hoi này khi một lớp khó sử dụng chính xác và cần bình luận, hãy bao gồm các ví dụ.

  3. Sử dụng các xác nhận trong bản dựng gỡ lỗi của bạn.

  4. Ném ngoại lệ nếu việc sử dụng lớp không hợp lệ.

* Đây là những loại bình luận bạn không nên viết:

// gets Foo
GetFoo();
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.