elizarov


Блог Романа Елизарова


Previous Entry Share Next Entry
Инкапсуляция многопоточных структур данных
elizarov

Множество книг по программированию, освещая алгоритмы, API библиотек, подходы или шаблоны, приводят куски кода, которые нельзя использовать в реальном большом проекте из-за различных недостатков. В реальном коде большого проекта есть множество аспектов, которые приходится учитывать: обработка ошибок, протоколирование, стиль кода, дизайн и т.п. Редкий книжный пример можно скопировать в свой проект без соответствующих изменений. Блоги, в том числе и мой блог, не исключение. Попытка донести до читателя определенную мысль компактно, в наименьшее число строчек кода, в первую очередь приводит к недостаткам в дизайне кода.

В предыдущей записи, я поделился шаблоном для группировки отдельных сообщений с целью их дальнейшей обработки в отдельном потоке пачками. В нем содержится весьма нетривиальный многопоточный код. Всем, кто хочет разобраться в тонкостях многопоточного программирования на Java, я в первую очередь советую прочитать книгу Java Concurrency in Practice by Brian Goetz. Один из важнейших советов, которые дает автор книги, касается инкапсуляции многопоточного кода. Инкапсуляция является одним их столпов объектно-ориентированного программирования. Правильная инкапсуляция упрощает понимание кода и его отладку. В многопоточном программировании эти соображения имеют особенную важность, ибо многопоточный код и так сложно понимать и отлаживать, а когда он представляет из себя спагетти различных аспектов запрограммированных в одном классе, то разобраться почему же код работает так, как задумывал его автор, может вообще не представляться возможным. Бывает проще сначала потратить время на рефакторинг кода, а потом уже в нем разбираться.

К счастью, во многих случаях, обнаружить недостаточную инкапсуляцию в многопоточном коде можно с первого взгляда. Верный признак недостаточной инкапсуляции, это смесь synchronized методов, защищающих структуры данных внутри объекта, с обычными методами или с другими шаблонами синхронизации, а также использование различных мониторов для защиты разных частей объекта. Именно от этого страдает пример кода, который я привел в предыдущей записи. Методы addItem, retrieveBatch и hasBatch работают со списком batch, защищая его синхронизацией на объекте BatchProcessor. Здесь явно прослеживается отдельная многопоточная структура данных с этими тремя методами, которая по случайности оказалась встроена прямо внутрь класса BatchProcessor. Давайте инкапсулируем её в отдельном классе BatchQueue.

1    
2    
3    
4    
5    
6    
7    
8    
9    
10   
11   
12   
13   
14   
15   
16   
17   
18   
19   
20   
import java.util.ArrayList;
import java.util.List;

public class BatchQueue<T> {
    private final List<T> batch = new ArrayList<T>();

    public synchronized void addItem(T item) {
        batch.add(item);
    }

    public synchronized List<T> retrieveBatch() {
        List<T> result = new ArrayList<T>(batch);
        batch.clear();
        return result;
    }

    public synchronized boolean hasBatch() {
        return !batch.isEmpty();
    }
}

Теперь класс BatchProcessor упрощается.

1    
2    
3    
4    
5    
6    
7    
8    
9    
10   
11   
12   
13   
14   
15   
16   
17   
18   
19   
20   
21   
22   
23   
24   
25   
26   
27   
28   
29   
30   
31   
32   
33   
34   
35   
36   
37   
38   
39   
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;

public abstract class BatchProcessor<T> {
    private final Executor executor;
    private final AtomicBoolean scheduled = new AtomicBoolean();
    private final BatchQueue<T> queue = new BatchQueue<T>();

    public BatchProcessor(Executor executor) {
        this.executor = executor;
    }

    public void processItem(T item) {
        queue.addItem(item);
        submitTask();
    }

    protected abstract void processBatch(List<T> batch);

    private void submitTask() {
        if (scheduled.compareAndSet(false, true))
            executor.execute(new Runnable() {
                public void run() {
                    executeTask();
                }
            });
    }

    private void executeTask() {
        try {
            processBatch(queue.retrieveBatch());
        } finally {
            scheduled.set(false);
            if (queue.hasBatch())
                submitTask();
        }
    }
}

Что дает инкапсуляция BatchQueue в отдельный класс?

Код стал проще для понимания, анализа и тестирования. BatchQueue можно протестировать отдельно от BatchProcessor. Если вдруг выяснитcя, что в вашем проекте реализация BatchQueue является узким местом системы, то можно изменить его реализацию не трогая шаблон в классе BatchProcessor. Произошло четкое разделение обязанностей. BatchQeueue отвечает за накопление сообщений и именно он может отвечать за объединение сообщений, за контроль размера очереди и другими подобными аспектами, которые могут понадобиться в вашем проекте. BatchProcessor теперь отвечает исключительно за то, чтобы пачки сообщений обрабатывались в отдельном потоке (через переданный Executor) и чтобы обработка пачек происходила последовательно.

Инкапсулировав BatchQeueue, мы спрятали от пользователей класса BatchProcessor монитор, который используется для его синхронизации. Тем самым, мы устранили потенциальное место для ошибок, которые могли бы возникнуть, если бы наследники класса BatchProcessor объявили свои собственные synchronized методы. То есть, заметив недостаток дизайна с точки зрения инкапсуляции кода, ориентируюсь исключительно на внешний признак (смесь synсhronized и не-synchronzied методов) мы заодно сделали код более надежным, защитив его пользователей от потенциальных проблем.


?

Log in

No account? Create an account