Đây có phải là mùi mã để đặt cờ trong vòng lặp để sử dụng sau này không?


30

Tôi có một đoạn mã trong đó tôi lặp lại một bản đồ cho đến khi một điều kiện nào đó là đúng và sau đó sử dụng điều kiện đó để làm thêm một số thứ.

Thí dụ:

Map<BigInteger, List<String>> map = handler.getMap();

if(map != null && !map.isEmpty())
{
    for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        fillUpList();

        if(list.size() > limit)
        {
            limitFlag = true;
            break;
        }
    }
}
else
{
    logger.info("\n>>>>> \n\t 6.1 NO entries to iterate over (for given FC and target) \n");
}

if(!limitFlag) // Continue only if limitFlag is not set
{
    // Do something
}

Tôi cảm thấy thiết lập một lá cờ và sau đó sử dụng nó để làm nhiều thứ hơn là mùi mã.

Tôi có đúng không Làm thế nào tôi có thể loại bỏ điều này?


10
Tại sao bạn cảm thấy đó là một mùi mã? loại vấn đề cụ thể nào bạn có thể thấy trước khi thực hiện điều này sẽ không xảy ra dưới một cấu trúc khác?
Ben Cottrell

13
@ gnasher729 Chỉ vì tò mò, bạn sẽ sử dụng thuật ngữ nào?
Ben Cottrell

11
-1, ví dụ của bạn không có ý nghĩa. entrykhông nơi nào được sử dụng bên trong vòng lặp chức năng và chúng ta chỉ có thể đoán nó listlà gì . Có fillUpListnghĩa vụ phải điền list? Tại sao nó không lấy nó làm tham số?
Doc Brown

13
Tôi sẽ xem xét lại việc sử dụng khoảng trắng và dòng trống của bạn.
Daniel Jour

11
Không có thứ gọi là mùi mã. "Mùi mã" là một thuật ngữ được phát minh bởi các nhà phát triển phần mềm muốn giữ mũi khi họ thấy mã không đáp ứng các tiêu chuẩn tinh hoa của họ.
Robert Harvey

Câu trả lời:


70

Không có gì sai khi sử dụng giá trị Boolean cho mục đích dự định của mình: để ghi lại sự phân biệt nhị phân.

Nếu tôi được yêu cầu cấu trúc lại mã này, có lẽ tôi đã đặt vòng lặp vào một phương thức của chính nó để phép gán + breakbiến thành một return; sau đó bạn thậm chí không cần một biến, bạn có thể nói một cách đơn giản

if(fill_list_from_map()) {
  ...

6
Thật ra mùi trong mã của anh ta là hàm dài cần được chia thành các hàm nhỏ hơn. Đề nghị của bạn là con đường để đi.
Bernhard Hiller

2
Một cụm từ tốt hơn mô tả chức năng hữu ích của phần đầu tiên của mã đó là tìm xem liệu giới hạn có bị vượt quá hay không sau khi nó tích lũy một cái gì đó từ các mục được ánh xạ đó. Chúng ta cũng có thể giả định rằng đó fillUpList()là một số mã (mà OP quyết định không chia sẻ) thực sự sử dụng giá trị entrytừ lần lặp; không có giả định này, có vẻ như thân vòng lặp không sử dụng bất cứ thứ gì từ vòng lặp.
rwong

4
@Kilian: Tôi chỉ có một mối quan tâm. Phương thức này sẽ điền vào danh sách và sẽ trả về Boolean đại diện cho kích thước danh sách đó có vượt quá giới hạn hay không, do đó, tên 'fill_list_from_map' không làm rõ rằng Boolean trả về đại diện cho cái gì vượt quá giới hạn, vv). Vì Boolean trả về là trường hợp đặc biệt không rõ ràng từ tên hàm. Có ý kiến ​​gì không? PS: chúng ta cũng có thể xem xét phân tách truy vấn lệnh.
Siddharth Trikha

2
@SiddharthTrikha Bạn nói đúng, và tôi có cùng mối quan tâm khi tôi đề xuất dòng đó. Nhưng không rõ danh sách nào được điền vào. Nếu nó luôn luôn là cùng một danh sách, bạn không cần cờ, bạn chỉ cần kiểm tra độ dài của nó sau đó. Nếu bạn cần biết liệu có bất kỳ sự lấp đầy riêng lẻ nào vượt quá giới hạn hay không, thì bạn phải vận chuyển thông tin đó ra bên ngoài bằng cách nào đó và IMO nguyên tắc tách lệnh / truy vấn không phải là lý do đủ để từ chối cách rõ ràng: thông qua trả về giá trị.
Kilian Foth

6
Chú Bob nói ở trang 45 của Clean Code : "Các hàm nên làm gì đó hoặc trả lời một cái gì đó, nhưng không phải cả hai. Hàm của bạn sẽ thay đổi trạng thái của một đối tượng hoặc nó sẽ trả về một số thông tin về đối tượng đó. sự nhầm lẫn."
CJ Dennis

25

Nó không hẳn là xấu, và đôi khi nó là giải pháp tốt nhất. Nhưng thiết lập các cờ như thế này trong các khối lồng nhau có thể làm cho mã khó theo dõi.

Vấn đề là bạn có các khối để phân định phạm vi, nhưng sau đó bạn có các cờ giao tiếp qua các phạm vi, phá vỡ sự cô lập logic của các khối. Ví dụ, limitFlagsẽ là sai nếu mapnull, vì vậy mã "làm gì đó" sẽ được thực thi nếu mapnull. Đây có thể là những gì bạn dự định, nhưng nó có thể là một lỗi dễ bỏ lỡ, bởi vì các điều kiện cho cờ này được xác định ở một nơi khác, trong phạm vi lồng nhau. Nếu bạn có thể giữ thông tin và logic trong phạm vi chặt chẽ nhất có thể, bạn nên cố gắng làm như vậy.


2
Đây là lý do tôi cảm thấy rằng đó là mùi mã, vì các khối không bị cô lập hoàn toàn và có thể khó theo dõi sau này. Vì vậy, tôi đoán mã trong câu trả lời của @Kilian gần nhất chúng ta có thể nhận được?
Siddharth Trikha

1
@SiddharthTrikha: Thật khó để nói vì tôi không biết mã thực sự phải làm gì. Nếu bạn chỉ muốn kiểm tra xem bản đồ có chứa ít nhất một mục mà danh sách lớn hơn giới hạn hay không, tôi nghĩ bạn có thể làm điều đó với một biểu thức anyMatch duy nhất.
JacquesB

2
@SiddharthTrikha: vấn đề phạm vi có thể dễ dàng được giải quyết bằng cách thay đổi thử nghiệm ban đầu thành mệnh đề bảo vệ như if(map==null || map.isEmpty()) { logger.info(); return;}Điều này sẽ chỉ hoạt động nếu mã chúng ta thấy là toàn bộ chức năng và // Do somethingkhông bắt buộc phải có phần trong trường hợp bản đồ là null hoặc rỗng.
Doc Brown

14

Tôi khuyên bạn không nên suy luận về 'mùi mã'. Đó chỉ là cách lười nhất có thể để hợp lý hóa những thành kiến ​​của bạn. Theo thời gian, bạn sẽ phát triển rất nhiều thành kiến, và rất nhiều trong số chúng sẽ hợp lý, nhưng rất nhiều trong số chúng sẽ là ngu ngốc.

Thay vào đó, bạn nên có những lý do thực tế (nghĩa là không giáo điều) để thích cái này hơn cái khác, và tránh nghĩ rằng bạn nên có câu trả lời giống nhau cho tất cả các câu hỏi tương tự.

"Mùi mã" là khi bạn không suy nghĩ. Nếu bạn thực sự sẽ nghĩ về mã, thì hãy làm đúng!

Trong trường hợp này, quyết định thực sự có thể đi theo bất kỳ cách nào tùy thuộc vào mã xung quanh. Nó thực sự phụ thuộc vào những gì bạn nghĩ là cách rõ ràng nhất để suy nghĩ về những gì mã đang làm. (Mã "sạch" là mã truyền đạt rõ ràng những gì nó đang làm cho các nhà phát triển khác và giúp họ dễ dàng xác minh rằng đó là chính xác)

Rất nhiều lần, mọi người sẽ viết các phương thức được cấu trúc thành các giai đoạn, trong đó mã trước tiên sẽ xác định những gì nó cần biết về dữ liệu và sau đó hành động theo nó. Nếu phần "xác định" và phần "hành động trên nó" hơi phức tạp một chút, thì có thể có ý nghĩa tốt để làm điều này và thường "những gì nó cần biết" có thể được thực hiện giữa các pha trong cờ Boolean. Tôi thực sự muốn rằng bạn đã đặt cho cờ một cái tên tốt hơn, mặc dù. Một cái gì đó như "LargeEntryExists" sẽ làm cho mã sạch hơn rất nhiều.

Mặt khác, nếu mã "// Làm gì đó" rất đơn giản, thì việc đặt nó vào trong ifkhối thay vì đặt cờ sẽ có ý nghĩa hơn . Điều đó đặt hiệu ứng gần với nguyên nhân hơn và người đọc không phải quét phần còn lại của mã để đảm bảo rằng cờ giữ lại giá trị bạn sẽ đặt.


5

Vâng, đó là một mùi mã (cue downvote từ tất cả những người làm điều đó).

Điều quan trọng đối với tôi là việc sử dụng break tuyên bố. Nếu bạn không sử dụng nó thì bạn sẽ lặp đi lặp lại nhiều mục hơn mức yêu cầu, nhưng sử dụng nó sẽ cho hai điểm thoát có thể từ vòng lặp.

Không phải là một vấn đề lớn với ví dụ của bạn, nhưng bạn có thể tưởng tượng rằng khi các điều kiện hoặc điều kiện bên trong vòng lặp trở nên phức tạp hơn hoặc thứ tự của danh sách ban đầu trở nên quan trọng thì lỗi dễ dàng hơn để chui vào mã.

Khi mã đơn giản như ví dụ của bạn, nó có thể được giảm xuống thành một while vòng lặp hoặc bản đồ tương đương, bộ lọc xây dựng.

Khi mã đủ phức tạp để yêu cầu cờ và phá vỡ, nó sẽ dễ bị lỗi.

Vì vậy, như với tất cả các mã có mùi: Nếu bạn thấy một lá cờ, hãy thử thay thế nó bằng một while. Nếu bạn không thể, hãy thêm các bài kiểm tra đơn vị.


+1 từ tôi. Đó là một mùi mã chắc chắn và bạn nói rõ tại sao, và làm thế nào để xử lý nó, tốt.
David Arno

@Ewan: SO as with all code smells: If you see a flag, try to replace it with a whileBạn có thể giải thích về điều này với một ví dụ không?
Siddharth Trikha

2
Có nhiều điểm thoát khỏi vòng lặp có thể khiến bạn khó lý do hơn, nhưng trong trường hợp này sẽ tái cấu trúc nó để làm cho điều kiện vòng lặp phụ thuộc vào cờ - điều đó có nghĩa là thay thế for (Map.Entry<BigInteger, List<String>> entry : map.entrySet())bằng for (Iterator<Map.Entry<BigInteger, List<String>>> iterator = map.entrySet().iterator(); iterator.hasNext() && !limitFlag; Map.Entry<BigInteger, List<String>> entry = iterator.next()). Đó là một mô hình đủ phổ biến mà tôi gặp nhiều khó khăn hơn để hiểu nó hơn là một sự phá vỡ tương đối đơn giản.
James_pic

@James_pic java của tôi hơi bị rỉ sét, nhưng nếu chúng tôi đang sử dụng bản đồ thì tôi sẽ sử dụng một bộ sưu tập để tổng hợp số lượng vật phẩm và lọc ra những thứ đó sau giới hạn. Tuy nhiên, như tôi nói ví dụ là "không tệ", mùi mã là một quy tắc chung cảnh báo bạn về một vấn đề tiềm ẩn. Không phải là một luật thiêng liêng, bạn phải luôn tuân theo
Ewan

1
Bạn không có nghĩa là "cue" chứ không phải "xếp hàng"?
psmears

0

Chỉ cần sử dụng một tên khác ngoài giới hạn cho biết những gì bạn đang thực sự kiểm tra. Và tại sao bạn đăng nhập bất cứ điều gì khi bản đồ vắng mặt hoặc trống? limtFlag sẽ là sai, tất cả những gì bạn quan tâm. Vòng lặp chỉ tốt nếu bản đồ trống, vì vậy không cần phải kiểm tra điều đó.


0

Thiết lập một giá trị boolean để truyền đạt thông tin bạn đã có là một thực tế xấu theo quan điểm của tôi. Nếu không có sự thay thế dễ dàng thì có lẽ đó là dấu hiệu của một vấn đề lớn hơn như đóng gói kém.

Bạn nên di chuyển logic vòng lặp for vào phương thức fillUpList để phá vỡ nó nếu đạt đến giới hạn. Sau đó kiểm tra kích thước của danh sách trực tiếp sau đó.

Nếu điều đó phá vỡ mã của bạn, tại sao?


0

Trước hết là trường hợp chung: Sử dụng cờ để kiểm tra xem một số phần tử của bộ sưu tập có đáp ứng một điều kiện nhất định không phải là hiếm. Nhưng mô hình mà tôi đã thấy thường xuyên nhất để giải quyết điều này là chuyển séc theo một phương thức bổ sung và trực tiếp trở về từ nó (như Kilian Foth được mô tả trong câu trả lời của anh ấy ):

private <T> boolean checkCollection(Collection<T> collection)
{
    for (T element : collection)
        if (checkElement(element))
            return true;
    return false;
}

Vì Java 8 có một cách ngắn gọn hơn bằng cách sử dụng Stream.anyMatch(…):

collection.stream().anyMatch(this::checkElement);

Trong trường hợp của bạn, điều này có thể sẽ trông như thế này (giả sử list == entry.getValue()trong câu hỏi của bạn):

map.values().stream().anyMatch(list -> list.size() > limit);

Vấn đề trong ví dụ cụ thể của bạn là cuộc gọi bổ sung fillUpList(). Câu trả lời phụ thuộc rất nhiều vào những gì phương pháp này được cho là làm.

Lưu ý bên lề: Vì nó đứng, cuộc gọi đến fillUpList()không có nhiều ý nghĩa, bởi vì nó không phụ thuộc vào yếu tố bạn hiện đang lặp lại. Tôi đoán đây là hậu quả của việc tước mã thực tế của bạn để phù hợp với định dạng câu hỏi. Nhưng chính xác điều đó dẫn đến một ví dụ nhân tạo khó diễn giải và do đó khó lý luận. Do đó, điều rất quan trọng là cung cấp một ví dụ Tối thiểu, Hoàn chỉnh và Có thể kiểm chứng .

Vì vậy, tôi giả sử rằng mã thực tế truyền hiện tại entrycho phương thức.

Nhưng có nhiều câu hỏi hơn để hỏi:

  • Các danh sách trong bản đồ có trống không trước khi đạt được mã này? Nếu vậy, tại sao đã có bản đồ và không chỉ là danh sách hoặc bộ BigIntegerchìa khóa? Nếu chúng không trống, tại sao bạn cần điền vào danh sách? Khi đã có các yếu tố trong danh sách, đó không phải là một bản cập nhật hoặc một số tính toán khác trong trường hợp này?
  • Điều gì khiến một danh sách trở nên lớn hơn giới hạn? Đây có phải là một điều kiện lỗi hoặc dự kiến ​​sẽ xảy ra thường xuyên? Có phải do đầu vào không hợp lệ?
  • Bạn có cần các danh sách được tính đến điểm bạn đạt đến một danh sách lớn hơn giới hạn không?
  • Phần " Làm gì đó " làm gì?
  • Bạn có khởi động lại điền sau phần này?

Đây chỉ là một số câu hỏi xuất hiện trong đầu tôi khi tôi cố gắng hiểu đoạn mã. Vì vậy, theo tôi, đó là mùi mã thực sự : Mã của bạn không truyền đạt rõ ràng ý định.

Nó có thể có nghĩa này ("tất cả hoặc không có gì" và đạt đến giới hạn cho thấy có lỗi):

/**
 * Computes the list of all foo strings for each passed number.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 * @return all foo strings for each passed number. Never {@code null}.
 * @throws InvalidArgumentException if any number produces a list that is too long.
 */
public Map<BigInteger, List<String>> computeFoos(Set<BigInteger> numbers)
        throws InvalidArgumentException
{
    if (numbers.isEmpty())
    {
        // Do you actually need to log this here?
        // The caller might know better what to do in this case...
        logger.info("Nothing to compute");
    }
    return numbers.stream().collect(Collectors.toMap(
            number -> number,
            number -> computeListForNumber(number)));
}

private List<String> computeListForNumber(BigInteger number)
        throws InvalidArgumentException
{
    // compute the list and throw an exception if the limit is exceeded.
}

Hoặc nó có thể có nghĩa này ("cập nhật cho đến khi vấn đề đầu tiên"):

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @throws InvalidArgumentException if any new foo list would become too long.
 *             Some other lists may have already been updated.
 */
public void updateFoos(Map<BigInteger, List<String>> map)
        throws InvalidArgumentException
{
    map.replaceAll(this::computeUpdatedList);
}

private List<String> computeUpdatedList(
        BigInteger number, List<String> currentValues)
        throws InvalidArgumentException
{
    // compute the new list and throw an exception if the limit is exceeded.
}

Hoặc cái này ("cập nhật tất cả các danh sách nhưng giữ danh sách gốc nếu nó quá lớn"):

/**
 * Refreshes all foo lists after they have become invalid because of bar.
 * Lists that would become too large will not be updated.
 * 
 * @param map the numbers with all their current values.
 *            The values in this map will be modified.
 *            Must not be {@code null}.
 * @return {@code true} if all updates have been successful,
 *         {@code false} if one or more elements have been skipped
 *         because the foo list size limit has been reached.
 */
public boolean updateFoos(Map<BigInteger, List<String>> map)
{
    boolean allUpdatesSuccessful = true;
    for (Entry<BigInteger, List<String>> entry : map.entrySet())
    {
        List<String> newList = computeListForNumber(entry.getKey());
        if (newList.size() > limit)
            allUpdatesSuccessful = false;
        else
            entry.setValue(newList);
    }
    return allUpdatesSuccessful;
}

private List<String> computeListForNumber(BigInteger number)
{
    // compute the new list
}

Hoặc thậm chí sau đây (sử dụng computeFoos(…)từ ví dụ đầu tiên nhưng không có ngoại lệ):

/**
 * Processes the passed numbers. An optimized algorithm will be used if any number
 * produces a foo list of a size that justifies the additional overhead.
 * 
 * @param numbers the numbers to process. Must not be {@code null}.
 */
public void process(Collection<BigInteger> numbers)
{
    Map<BigInteger, List<String>> map = computeFoos(numbers);
    if (isLimitReached(map))
        processLarge(map);
    else
        processSmall(map);
}

private boolean isLimitReached(Map<BigInteger, List<String>> map)
{
    return map.values().stream().anyMatch(list -> list.size() > limit);
}

Hoặc nó có thể có nghĩa là một cái gì đó hoàn toàn khác nhau ;-)

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.