Sử dụng nhiều phương thức tĩnh có phải là một điều xấu?


97

Tôi có xu hướng khai báo là tĩnh tất cả các phương thức trong một lớp khi lớp đó không yêu cầu theo dõi các trạng thái bên trong. Ví dụ: nếu tôi cần biến đổi A thành B và không dựa vào một số trạng thái bên trong C có thể thay đổi, tôi tạo một phép biến đổi tĩnh. Nếu có một trạng thái bên trong C mà tôi muốn có thể điều chỉnh, thì tôi thêm một hàm tạo để đặt C và không sử dụng biến đổi tĩnh.

Tôi đã đọc các khuyến nghị khác nhau (bao gồm cả trên StackOverflow) KHÔNG lạm dụng các phương thức tĩnh nhưng tôi vẫn không hiểu điều đó sai với quy tắc ngón tay cái ở trên.

Đó có phải là một cách tiếp cận hợp lý hay không?

Câu trả lời:


152

Có hai loại phương thức tĩnh phổ biến:

  • Phương thức tĩnh "an toàn" sẽ luôn cung cấp cùng một đầu ra cho các đầu vào giống nhau. Nó không sửa đổi toàn cầu và không gọi bất kỳ phương thức tĩnh "không an toàn" nào của bất kỳ lớp nào. Về cơ bản, bạn đang sử dụng một loại lập trình chức năng hạn chế - đừng sợ những điều này, chúng ổn.
  • Phương thức tĩnh "không an toàn" thay đổi trạng thái toàn cục hoặc proxy thành đối tượng toàn cục hoặc một số hành vi không thể kiểm tra khác. Đây là những phản hồi đối với lập trình thủ tục và nên được cấu trúc lại nếu có thể.

Có một vài cách sử dụng tĩnh "không an toàn" phổ biến - ví dụ như trong mẫu Singleton - nhưng hãy lưu ý rằng mặc dù bất kỳ cái tên đẹp nào bạn gọi chúng, bạn chỉ đang thay đổi các biến toàn cục. Hãy suy nghĩ kỹ trước khi sử dụng tĩnh không an toàn.


Đây chính xác là vấn đề tôi phải giải quyết - việc sử dụng, hay đúng hơn là lạm dụng các đối tượng Singleton.
che khuất

Cảm ơn bạn vì câu trả lời tuyệt vời nhất. Câu hỏi của tôi là, nếu các singleton được truyền dưới dạng tham số cho các phương thức tĩnh, điều đó có làm cho phương thức tĩnh không an toàn không?
Tony D

1
Các thuật ngữ "hàm thuần túy" và "hàm không tinh khiết" là tên được đặt trong lập trình hàm cho những gì bạn gọi là tĩnh "an toàn" và "không an toàn".
Omnimike

19

Một đối tượng không có bất kỳ trạng thái bên trong nào là một điều đáng ngờ.

Thông thường, các đối tượng đóng gói trạng thái và hành vi. Một đối tượng chỉ đóng gói hành vi là kỳ quặc. Đôi khi đó là một ví dụ về Lightweight hoặc Flyweight .

Lần khác, đó là thiết kế thủ tục được thực hiện bằng một ngôn ngữ đối tượng.


6
Tôi nghe bạn đang nói gì, nhưng làm thế nào mà một thứ như đối tượng Toán học có thể đóng gói bất cứ thứ gì ngoại trừ hành vi?
JonoW

10
Anh ấy chỉ nói đáng ngờ, không sai, và anh ấy hoàn toàn đúng.
Bill K

2
@JonoW: Toán học là một trường hợp rất đặc biệt khi có nhiều hàm không trạng thái. Tất nhiên, nếu bạn đang lập trình Chức năng trong Java, bạn sẽ có nhiều chức năng không trạng thái.
S.Lott

13

Đây thực sự chỉ là một câu trả lời tiếp theo cho câu trả lời tuyệt vời của John Millikin.


Mặc dù có thể an toàn khi làm cho các phương thức không trạng thái (có khá nhiều chức năng) tĩnh, nhưng đôi khi nó có thể dẫn đến việc ghép nối khó sửa đổi. Hãy xem xét bạn có một phương thức tĩnh như vậy:

public class StaticClassVersionOne {
    public static void doSomeFunkyThing(int arg);
}

Cái mà bạn gọi là:

StaticClassVersionOne.doSomeFunkyThing(42);

Tất cả đều tốt và tốt, và rất thuận tiện, cho đến khi bạn gặp trường hợp bạn phải sửa đổi hành vi của phương thức tĩnh và thấy rằng bạn bị ràng buộc chặt chẽ StaticClassVersionOne. Có thể bạn có thể sửa đổi mã và nó sẽ ổn, nhưng nếu có những người gọi khác phụ thuộc vào hành vi cũ, họ sẽ cần được tính đến trong phần nội dung của phương thức. Trong một số trường hợp, phần thân của phương thức đó có thể trở nên khá xấu hoặc không thể xác định được nếu nó cố gắng cân bằng tất cả các hành vi này. Nếu bạn tách ra các phương thức, bạn có thể phải sửa đổi mã ở một số nơi để tính đến nó hoặc thực hiện các cuộc gọi đến các lớp mới.

Nhưng hãy xem xét nếu bạn đã tạo một giao diện để cung cấp phương thức và đưa nó cho người gọi, bây giờ khi hành vi phải thay đổi, một lớp mới có thể được tạo để triển khai giao diện, giao diện này sạch hơn, dễ kiểm tra hơn và dễ bảo trì hơn, và thay vào đó nó được trao cho những người gọi. Trong trường hợp này, các lớp gọi không cần phải thay đổi hoặc thậm chí biên dịch lại và các thay đổi được bản địa hóa.

Nó có thể là một tình huống có thể xảy ra hoặc không, nhưng tôi nghĩ nó đáng được xem xét.


5
Tôi cho rằng đây không chỉ là một kịch bản có thể xảy ra, điều này làm cho tĩnh trở thành phương sách cuối cùng. Tin học cũng khiến TDD trở thành cơn ác mộng. Bất cứ nơi nào bạn sử dụng static, bạn không thể giả lập, bạn phải biết đầu vào và đầu ra là gì để kiểm tra một lớp không liên quan. Bây giờ, nếu bạn thay đổi hành vi của tĩnh, các bài kiểm tra của bạn trên các lớp không liên quan sử dụng tĩnh đó sẽ bị hỏng. Ngoài ra, nó trở thành một phụ thuộc ẩn mà bạn không thể chuyển cho hàm tạo để thông báo cho các nhà phát triển về một phụ thuộc quan trọng tiềm ẩn.
DanCaveman

6

Tùy chọn khác là thêm chúng dưới dạng phương thức không tĩnh trên đối tượng gốc:

tức là, thay đổi:

public class BarUtil {
    public static Foo transform(Bar toFoo) { ... }
}

thành

public class Bar {
    ...
    public Foo transform() { ...}
}

tuy nhiên trong nhiều trường hợp, điều này là không thể (ví dụ: tạo mã lớp thông thường từ XSD / WSDL / v.v.), hoặc nó sẽ làm cho lớp rất dài và các phương thức chuyển đổi thường có thể là một vấn đề thực sự đối với các đối tượng phức tạp và bạn chỉ muốn chúng trong lớp riêng của họ. Vì vậy, tôi có các phương thức tĩnh trong các lớp tiện ích.


5

Các lớp tĩnh vẫn tốt miễn là chúng được sử dụng đúng nơi.

Cụ thể: Các phương thức là phương thức 'lá' (chúng không sửa đổi trạng thái, chúng chỉ biến đổi đầu vào bằng cách nào đó). Ví dụ tốt về điều này là những thứ như Path.Combine. Những thứ này hữu ích và tạo nên cú pháp ngắn gọn hơn.

Các vấn đề tôi gặp phải với tĩnh rất nhiều:

Thứ nhất, nếu bạn có các lớp tĩnh, các phụ thuộc sẽ bị ẩn. Hãy xem xét những điều sau:

public static class ResourceLoader
{
    public static void Init(string _rootPath) { ... etc. }
    public static void GetResource(string _resourceName)  { ... etc. }
    public static void Quit() { ... etc. }
}

public static class TextureManager
{
    private static Dictionary<string, Texture> m_textures;

    public static Init(IEnumerable<GraphicsFormat> _formats) 
    {
        m_textures = new Dictionary<string, Texture>();

        foreach(var graphicsFormat in _formats)
        {
              // do something to create loading classes for all 
              // supported formats or some other contrived example!
        }
    }

    public static Texture GetTexture(string _path) 
    {
        if(m_textures.ContainsKey(_path))
            return m_textures[_path];

        // How do we know that ResourceLoader is valid at this point?
        var texture = ResourceLoader.LoadResource(_path);
        m_textures.Add(_path, texture);
        return texture; 
    }

    public static Quit() { ... cleanup code }       
}

Nhìn vào TextureManager, bạn không thể biết các bước khởi tạo phải được thực hiện bằng cách nhìn vào một hàm tạo. Bạn phải đi sâu vào lớp để tìm các phụ thuộc của nó và khởi tạo mọi thứ theo đúng thứ tự. Trong trường hợp này, nó cần ResourceLoader được khởi tạo trước khi chạy. Bây giờ hãy mở rộng quy mô cơn ác mộng phụ thuộc này và bạn có thể đoán được điều gì sẽ xảy ra. Hãy tưởng tượng cố gắng duy trì mã ở nơi không có thứ tự khởi tạo rõ ràng. Ngược lại điều này với việc chèn phụ thuộc với các phiên bản - trong trường hợp đó, mã thậm chí sẽ không biên dịch nếu các phụ thuộc không được đáp ứng!

Hơn nữa, nếu bạn sử dụng statics để sửa đổi trạng thái, nó giống như một ngôi nhà của các thẻ. Bạn không bao giờ biết ai có quyền truy cập vào cái gì, và thiết kế có xu hướng giống một con quái vật mì Ý.

Cuối cùng, và cũng quan trọng không kém, việc sử dụng static liên kết một chương trình với một triển khai cụ thể. Mã tĩnh là phản đề của thiết kế để kiểm tra. Kiểm tra mã bị lỗi tĩnh là một cơn ác mộng. Một cuộc gọi tĩnh không bao giờ có thể được hoán đổi cho một cuộc thử nghiệm kép (trừ khi bạn sử dụng các khuôn khổ thử nghiệm được thiết kế đặc biệt để mô phỏng các kiểu tĩnh), vì vậy một hệ thống tĩnh khiến mọi thứ sử dụng nó trở thành một thử nghiệm tích hợp tức thì.

Nói tóm lại, tĩnh là tốt cho một số thứ và đối với các công cụ nhỏ hoặc mã vứt đi, tôi sẽ không khuyến khích việc sử dụng chúng. Tuy nhiên, bên cạnh đó, chúng là một cơn ác mộng đẫm máu về khả năng bảo trì, thiết kế tốt và dễ kiểm tra.

Đây là một bài viết hay về các vấn đề: http://gamearchitect.net/2008/09/13/an-anatomy-of-despair-managers-and-contexts/


4

Lý do bạn được cảnh báo tránh xa các phương thức tĩnh là việc sử dụng chúng làm mất đi một trong những lợi thế của đối tượng. Các đối tượng dùng để đóng gói dữ liệu. Điều này ngăn ngừa các tác dụng phụ không mong muốn xảy ra và tránh lỗi. Phương thức tĩnh không có dữ liệu được đóng gói * và do đó, không thu được lợi ích này.

Điều đó nói rằng, nếu bạn không sử dụng dữ liệu nội bộ, chúng vẫn có thể sử dụng và thực thi nhanh hơn một chút. Hãy đảm bảo rằng bạn không chạm vào dữ liệu toàn cầu trong đó.

  • Một số ngôn ngữ cũng có các biến cấp lớp cho phép đóng gói dữ liệu và các phương thức tĩnh.

4

Đó có vẻ là một cách tiếp cận hợp lý. Lý do bạn không muốn sử dụng quá nhiều lớp / phương thức tĩnh là bạn sẽ chuyển từ lập trình hướng đối tượng và hơn thế nữa sang lĩnh vực lập trình có cấu trúc.

Trong trường hợp của bạn khi bạn chỉ đơn giản là chuyển đổi A thành B, hãy nói rằng tất cả những gì chúng tôi đang làm là chuyển đổi văn bản để chuyển từ

"hello" =>(transform)=> "<b>Hello!</b>"

Sau đó, một phương thức tĩnh sẽ có ý nghĩa.

Tuy nhiên, nếu bạn đang gọi các phương thức tĩnh này trên một đối tượng thường xuyên và nó có xu hướng là duy nhất đối với nhiều lệnh gọi (ví dụ: cách bạn sử dụng nó phụ thuộc vào đầu vào), hoặc nó là một phần của hành vi vốn có của đối tượng, nó sẽ khôn ngoan để biến nó thành một phần của đối tượng và duy trì trạng thái của nó. Một cách để làm điều này là triển khai nó như một giao diện.

class Interface{
    method toHtml(){
        return transformed string (e.g. "<b>Hello!</b>")
    }

    method toConsole(){
        return transformed string (e.g. "printf Hello!")
    }
}


class Object implements Interface {
    mystring = "hello"

    //the implementations of the interface would yield the necessary 
    //functionality, and it is reusable across the board since it 
    //is an interface so... you can make it specific to the object

   method toHtml()
   method toConsole()
}

Chỉnh sửa: Một ví dụ điển hình về việc sử dụng tuyệt vời các phương thức tĩnh là các phương thức trợ giúp html trong Asp.Net MVC hoặc Ruby. Chúng tạo ra các phần tử html không bị ràng buộc với hành vi của một đối tượng và do đó là tĩnh.

Chỉnh sửa 2: Đã thay đổi lập trình chức năng thành lập trình có cấu trúc (vì một số lý do tôi đã nhầm lẫn), đạo cụ cho Torsten để chỉ ra điều đó.


2
Tôi không nghĩ rằng việc sử dụng các phương thức tĩnh đủ điều kiện là lập trình chức năng, vì vậy tôi đoán ý bạn là lập trình có cấu trúc.
Torsten Marek

3

Gần đây tôi đã cấu trúc lại một ứng dụng để loại bỏ / sửa đổi một số lớp ban đầu được triển khai dưới dạng các lớp tĩnh. Theo thời gian, các lớp này được thu thập rất nhiều và mọi người cứ gắn thẻ các hàm mới là tĩnh, vì không bao giờ có một phiên bản nào trôi nổi.

Vì vậy, câu trả lời của tôi là các lớp tĩnh vốn dĩ không tệ nhưng có thể dễ dàng hơn để bắt đầu tạo các phiên bản ngay bây giờ, sau đó phải cấu trúc lại sau.


3

Tôi coi đó là mùi thiết kế. Nếu bạn thấy mình chủ yếu sử dụng các phương thức tĩnh, có thể bạn không có một thiết kế OO tốt. Nó không nhất thiết là xấu, nhưng như với tất cả các mùi, nó sẽ khiến tôi dừng lại và đánh giá lại. Nó gợi ý rằng bạn có thể tạo ra một thiết kế OO tốt hơn hoặc có thể bạn nên đi theo hướng khác và tránh OO hoàn toàn cho vấn đề này.


2

Tôi đã từng qua lại giữa một lớp với một loạt các phương thức tĩnh và một singleton. Cả hai đều giải quyết được vấn đề, nhưng singleton có thể dễ dàng bị thay thế hơn nhiều. (Các lập trình viên luôn có vẻ chắc chắn rằng sẽ chỉ có một trong số thứ gì đó và tôi thấy mình đã sai đủ lần để từ bỏ hoàn toàn các phương thức tĩnh ngoại trừ một số trường hợp rất hạn chế).

Dù sao, singleton cung cấp cho bạn khả năng sau đó chuyển một thứ gì đó vào nhà máy để lấy một phiên bản khác và điều đó thay đổi hành vi của toàn bộ chương trình của bạn mà không cần cấu trúc lại. Thay đổi một lớp toàn cục của các phương thức tĩnh thành một thứ gì đó có dữ liệu "ủng hộ" khác hoặc một hành vi hơi khác (lớp con) là một vấn đề lớn.

Và các phương thức tĩnh không có lợi thế tương tự.

Vì vậy, có, họ là xấu.


1

Miễn là trạng thái nội bộ không phát huy tác dụng, điều này là tốt. Lưu ý rằng thông thường các phương thức tĩnh được mong đợi là an toàn cho luồng, vì vậy nếu bạn sử dụng cấu trúc dữ liệu trợ giúp, hãy sử dụng chúng theo cách an toàn cho luồng.


1

Nếu bạn biết bạn sẽ không bao giờ cần sử dụng trạng thái bên trong của C, thì tốt thôi. Tuy nhiên, nếu điều đó sẽ thay đổi trong tương lai, bạn cần phải đặt phương thức là không tĩnh. Nếu nó không tĩnh để bắt đầu, bạn có thể bỏ qua trạng thái bên trong nếu bạn không cần nó.


1

Nếu đó là một phương thức tiện ích, thật tuyệt khi làm cho nó tĩnh. Guava và Apache Commons được xây dựng dựa trên nguyên tắc này.

Ý kiến ​​của tôi về điều này là hoàn toàn thực dụng. Nếu đó là mã ứng dụng của bạn, các phương thức tĩnh thường không phải là thứ tốt nhất để có. Các phương pháp tĩnh có những hạn chế nghiêm trọng về kiểm thử đơn vị - chúng không thể dễ dàng bị chế nhạo: bạn không thể đưa chức năng tĩnh bị chế nhạo vào một số thử nghiệm khác. Bạn cũng không thể đưa chức năng vào một phương thức tĩnh.

Vì vậy, trong logic ứng dụng của tôi, tôi thường có các lệnh gọi phương thức giống tiện ích tĩnh nhỏ. I E

static cutNotNull(String s, int length){
  return s == null ? null : s.substring(0, length);
}

một trong những lợi ích là tôi không thử nghiệm các phương pháp như vậy :-)


1

Tất nhiên là không có viên đạn bạc nào. Các lớp tĩnh là ok cho các tiện ích / trợ giúp nhỏ. Nhưng sử dụng các phương thức tĩnh để lập trình logic nghiệp vụ chắc chắn là xấu. Hãy xem xét đoạn mã sau

   public class BusinessService
   {

        public Guid CreateItem(Item newItem, Guid userID, Guid ownerID)
        {
            var newItemId = itemsRepository.Create(createItem, userID, ownerID);
            **var searchItem = ItemsProcessor.SplitItem(newItem);**
            searchRepository.Add(searchItem);
            return newItemId;
        }
    }

Bạn thấy một cuộc gọi phương thức tĩnh đến ItemsProcessor.SplitItem(newItem);Nó có mùi gây ra

  • Bạn không khai báo sự phụ thuộc rõ ràng và nếu bạn không tìm hiểu kỹ mã, bạn có thể bỏ qua việc ghép nối giữa lớp và vùng chứa phương thức tĩnh của bạn
  • Bạn không thể kiểm tra BusinessServiceviệc cô lập nó khỏi nó ItemsProcessor(hầu hết các công cụ kiểm tra không mô phỏng các lớp tĩnh) và nó làm cho kiểm thử đơn vị không thể thực hiện được. Không có bài kiểm tra đơn vị nào == chất lượng thấp

0

Các phương thức tĩnh thường là một lựa chọn tồi ngay cả đối với mã không trạng thái. Thay vào đó, hãy tạo một lớp singleton với các phương thức này được khởi tạo một lần và được đưa vào các lớp muốn sử dụng các phương thức này. Các lớp học như vậy dễ dàng hơn để thử và kiểm tra. Chúng hướng tới đối tượng nhiều hơn. Bạn có thể bọc chúng bằng một proxy khi cần thiết. Tin học làm cho OO khó hơn nhiều và tôi thấy không có lý do gì để sử dụng chúng trong hầu hết các trường hợp. Không phải 100% nhưng gần như tất cả.

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.