Определение

Шаблон «Шаблонный метод» - это шаблон поведенческого проектирования, который улучшает коммуникацию / взаимодействие между объектами и сохраняет их слабую связь.

Шаблон «Шаблонный метод» определяет структуру с каркасом алгоритма и делегирует ответственность за некоторые из его шагов подкомпонентам.

Когда нам следует использовать этот паттерн?

Этот шаблон следует использовать, когда два или более компонентов имеют сходство в реализации алгоритма.

Конкретный пример

Допустим, мы работаем над приложением для интеллектуального анализа данных, которое извлекает информацию из файлов различного типа (Doc, CSV и PDF).

На данный момент в нашем приложении есть 3 разных класса для извлечения данных из файлов Doc, CSV и PDF. Если мы внимательно посмотрим на методы, используемые для извлечения данных, мы увидим, что они следуют аналогичной последовательности: openFile → extractData → parseData → analysisData → sendReport → closeFile.

AnalyzeData и sendReport - единственные две функции, которые имеют реализацию по умолчанию, независимо от типа файла. Это означает, что существует много дублирования, поскольку эти 2 метода находятся в компонентах Doc, CSV и PDF. Все остальные функции имеют индивидуальную реализацию.

Как нам использовать этот паттерн?

Этот шаблон определяет «шаблонный метод», объединяющий все шаги нашего алгоритма. В приведенном выше примере показано использование общей серии методов для анализа файлов различного типа: нашего алгоритма. Давайте подумаем о структуре, в которой компоненты Doc, CSV и PDF исключают дублирование.

В этой структуре мы видим компонент DataMiner с набором функций, которые будут использоваться субкомпонентами doc, CSV и PDF. Есть два разных способа реализовать это: протокол и наследование классов.

Что нам следует использовать: протокол или наследование классов?

Наследование протоколов и классов можно использовать для создания компонентов с общим поведением. Согласно принципам ООП, полиморфное поведение и повторное использование кода должно достигаться композицией¹.

Возможность комбинирования не подходит, если объекты состоят из конкретных типов. В результате объекты связаны, не могут использоваться изолированно, а также не может использоваться только одна часть объекта. Принципы SOLID² помогают обеспечить правильную компоновку, потому что они создают крошечные составляемые компоненты (= отдельные компоненты и компоненты для одного назначения). Чем более компонуемой является система, тем более удобной в обслуживании, расширяемой, тестируемой и многоразовой становится система.

Поскольку Swift не поддерживает множественное наследование классов³, проблема в том, что это может привести к созданию контейнеров функций, нарушающих Принцип единой ответственности. В этом случае наследование подразумевает повторное использование кода, но оно должно расширять поведение класса (Принцип открытости-закрытости). Подкласс не должен зависеть от функций, которые они не используют. Таким образом, при наследовании классов следует использовать либо все унаследованные функции, либо ни одну из них. Если от его суперкласса требуется только одна конкретная функция, наследование не является решением. Swift создает сложную композицию с наследованием классов.

Класс может наследовать от нескольких протоколов, поэтому он может быть реализован только с необходимыми функциями из разных протоколов. Компоненты реализуют только те функции, которые они используют (Принцип разделения интерфейса) и зависят от абстракции (Принцип инверсии зависимостей). Композиция с протоколами помогает иметь компоненты единого назначения и делает их многократно используемыми в нескольких компонентах. Их также можно расширить для реализации протоколов.

По этим причинам мы предпочитаем наследование протокола, если мы строим отношения наследования.

[1] Композиция - это способ объединять объекты в более сложные.

[2] Принципы SOLID - это стандарты кодирования ООП, предназначенные для создания понятного, поддерживаемого и гибкого программного обеспечения.

[3] Наследование классов - это способ достижения полиморфного поведения посредством расширения класса.

Реализация

Все следующие разделы кода написаны на Swift, а typealias ниже используются для выразительности.

typealias File     = String
typealias RawData  = String
typealias `Data`   = String
typealias Analysis = String

Вот интерфейс с планом необходимых шагов / методов:

/// Blueprint of all the steps/methods from the DataMiner component
protocol DataMiner {
    func mine(path: String)
    func openFile(path: String) -> File
    func extractData(file: File) -> RawData
    func parseData(data: RawData) -> `Data`
    func analyzeData(analysis: `Data`) -> Analysis
    func sendReport(analysis: Analysis)
    func closeFile(file: File)
}

Проблема с этим кодом заключается в том, что компоненты (Doc, CSV и PDF), соответствующие протоколу DataMiner, должны будут реализовать все эти методы. Это по-прежнему приводит к дублированию, поскольку mine, analysisData и sendReport имеют один и тот же код. Этот протокол не решает нашу проблему дублирования. Однако в Swift есть замечательная функция, называемая расширением протокола, которая обеспечивает реализации методов.

Реализация по умолчанию

/// Blueprint of the functions that will require custom implementations
protocol DataMiner {
    func openFile(path: String) -> File
    func extractData(file: File) -> RawData
    func parseData(data: RawData) -> `Data`
    func closeFile(file: File)
}
extension DataMiner {
    /// TEMPLATE METHOD: series of functions defining the algorithm
    func mine(path: String) {
        let file = openFile(path: path)
        let rawData = extractData(file: file)
        let data = parseData(data: rawData)
        let analysis = analyzeData(analysis: data)
        sendReport(analysis: analysis)
        closeFile(file: file)
    }
    // MARK: Default implementations
    func analyzeData(analysis: `Data`) -> Analysis {
        print("ℹ️ analyze data")
        return "analysis"
    }
    func sendReport(analysis: Analysis) {
        print("ℹ️ send report")
    }
}

Mine, analysisData и sendReport были удалены из методов, определенных в объявлении протокола. Однако они были добавлены в расширение протокола.

В результате компоненты (Doc, CSV и PDF), соответствующие протоколу DataMiner, не нуждаются в реализации методов mine, analysisData и sendReport. поскольку их реализации уже предусмотрены в расширении протокола.

Реализация PDFDataMiner

// PDFDataMiner conforms to the DataMiner protocol
class PDFDataMiner: DataMiner {
    init() {
        // Call the template method
        mine(path: "PDFFilePath")
    }
    func openFile(path: String) -> File {
        print("📃️ open PDF File")
        return "PDF file opened"
    }
    func extractData(file: File) -> RawData {
        print("📃️ extract PDF data")
        return "PDF raw data extracted"
    }
    func parseData(data: RawData) -> `Data` {
        print("📃️ parse PDF data")
        return "PDF data parsed"
    }
    func closeFile(file: File) {
        print("📃️ close PDF File")
    }
}

Запускать код на игровой площадке

Вот Online Swift Playground, поэтому не нужно создавать Xcode Playground для тестирования этой реализации шаблона Template Method. Затем скопируйте приведенный ниже код, который соответствует полной реализации шаблона Шаблонный метод для нашего приложения интеллектуального анализа данных.

typealias File     = String
typealias RawData  = String
typealias `Data`   = String
typealias Analysis = String
/// Blueprint of the functions that will require custom implementations
protocol DataMiner {
    func openFile(path: String) -> File
    func extractData(file: File) -> RawData
    func parseData(data: RawData) -> `Data`
    func closeFile(file: File)
}
extension DataMiner {
    /// TEMPLATE METHOD: series of functions defining the algorithm
    func mine(path: String) {
        let file = openFile(path: path)
        let rawData = extractData(file: file)
        let data = parseData(data: rawData)
        let analysis = analyzeData(analysis: data)
        sendReport(analysis: analysis)
        closeFile(file: file)
    }
    // MARK: Default implementations
    func analyzeData(analysis: `Data`) -> Analysis {
        print("ℹ️ analyze data")
        return "analysis"
    }
    func sendReport(analysis: Analysis) {
        print("ℹ️ send report")
    }
}
// PDFDataMiner conforms to the DataMiner protocol
class PDFDataMiner: DataMiner {
    init() {
        // Call the template method
        mine(path: "PDFFilePath")
    }
    func openFile(path: String) -> File {
        print("📃️ open PDF File")
        return "PDF file opened"
    }
    func extractData(file: File) -> RawData {
        print("📃️ extract PDF data")
        return "PDF raw data extracted"
    }
    func parseData(data: RawData) -> `Data` {
        print("📃️ parse PDF data")
        return "PDF data parsed"
    }
    func closeFile(file: File) {
        print("📃️ close PDF File")
    }
}
// Testing the PDF implementation
_ = PDFDataMiner()

Наконец, вставьте и запустите код.