Русский
Русский
English
Статистика
Реклама

Objective-c

Конструктор Lego и объектно-ориентированное программирование в Tcl. Разбор сертификата x509.v3

21.12.2020 18:22:45 | Автор: admin
imageЧасто приходится слышать, что скриптовому языку Tcl не хватает поддержки объектно-ориентированного стиля программирования. Сам я до последнего времени мало прибегал к объектно-ориентированному программированию, тем более в среде Tcl. Но за Tcl стало обидно. Я решил разобраться. И оказалось, что практически с момента своего появления появилась возможность объектно-ориентированного программирования (ООП) в среде Tcl. Все неудобство заключалось в необходимости подключить пакет с поддержкой ООП. А таких пакетом было и есть несколько, как говорится на любой вкус. Это и Incr Tcl, Snit и XoTcl.
Программисты, привыкшие к языку C++, чувствуют себя как дома, программируя в среде Incr Tcl. Это было одним из первых широко используемых расширений для OOП на основе Tcl.
Пакет Snit в основном используется при построении Tk-виджетов, а XoTcl и его преемник nx предназначались для исследования динамического объектно-ориентированного программирования.
Обобщение опыта, полученного при использовании этих систем, позволило внедрить ООП в ядро Tcl начиная с версии 8.6. Так появился TclOO Tcl Object Oriented.
Сразу отметим, что Tcl не просто поддерживает объектно-ориентированное программирование, а в полном смысле динамическое объектно-ориентированное программирование.
Разрабатывая приложения на Tcl/Tk, например удостоверяющий центр CAFL63, я не прибегал к ООП. И, как сейчас понимаю, зря. Где, где, а в УЦ объектов хватает. Это и запросы на сертификаты, это и сами сертификаты, списки отозванных сертификатов и много чего другого:



Начать было решено с рассмотрения сертификата x509.v3 с учетом российской специфики как объекта при ООП. Тем более, что имеется опыт разбора квалифицированного сертификата на Python. Именно на примере разбора и работы с сертификатом мы и покажем объектно-ориентированный стиль программирования в TclOO.

О DER и BER кодировках

Для доступа к сертификату будет создан класс certificate, в конструкторе которого при создании объекта конкретного сертификата будет проводится разбор его на составные части. Для этого нам потребуется в первую очередь пакет asn (package require asn), который поможет с разбором asn-структуры сертификата. К сожалению, этот пакет (кстати, в других скриптовых языках встречается аналогичная проблема) заточен на разбор asn-структур в DER-кодировке. Но сегодня еще встречаются сертификаты (и электронные подписи и много чего другого) в BER-кодировке. Но оказалось решить эту проблему можно достаточно просто, заменив процедуру ::asn::asnLength из пакета ASN на новую, которая будет подсчитывать длины тега как в DER, так и BER-кодировках:
package require asn#Переименовываем оригинальную процедуру подсчета длины rename ::asn::asnGetLength ::asn::asnGetLength.orig#Новая процедура подсчета длиныproc ::asn::asnGetLength {data_var length_var} {    upvar 1 $data_var data  $length_var length    asnGetByte data length    if {$length == 0x080} {#Поддержка BER-кодировкиset lendata [string length $data]set tvl 1set length 0set data1 $datawhile {$tvl != 0} {    ::asn::asnGetByte data1 peek_tag     ::asn::asnPeekByte data1 peek_tag1    if {$peek_tag == 0x00 && $peek_tag1 == 0x00} {incr tvl -1::asn::asnGetByte data1 tag incr length 2continue    }    if {$peek_tag1 == 0x80} {incr tvlif {$tvl > 0} {    incr length 2}::asn::asnGetByte data1 tag     } else {set l1 [string length $data1]::asn::asnGetLength data1 llset l2 [string length $data1]set l3 [expr $l1 - $l2]incr length $l3incr length $llincr length::asn::asnGetBytes data1 $ll strt    }}return    }    if {$length > 0x080} {        set len_length [expr {$length & 0x7f}]          if {[string length $data] < $len_length} {            return -code error \"length information invalid, not enough octets left"         }        asnGetBytes data $len_length lengthBytes        switch $len_length {            1 { binary scan $lengthBytes     cu length }            2 { binary scan $lengthBytes     Su length }            3 { binary scan \x00$lengthBytes Iu length }            4 { binary scan $lengthBytes     Iu length }            default {                                binary scan $lengthBytes H* hexstrscan $hexstr %llx length            }        }    }    return}

Что нам еще потребуется? Любая ASN-структура, особенно такая как сертификат X509.v3 содержит большое количество OID-ов, для которых могут существовать достаточно общепризнанные символьные обозначения. Значительная часть OID-ов, которые используются в сертификатах, присутствует в пакете pki. Мы его тоже будем использовать (package require pki). Естественно, что в этом пакете ничего не известно об OID-ах, которые используются в квалифицированных сертификатах и об OID-ах для российской криптографии. Их тоже целесообразно добавить в массив ::pki::oids:
set ::pki::oids(1.2.643.100.1)  "OGRN"set ::pki::oids(1.2.643.100.5)  "OGRNIP"set ::pki::oids(1.2.643.3.131.1.1) "INN"set ::pki::oids(1.2.643.100.3) "SNILS"#Для КПП ЕГАИСset ::pki::oids(1.2.840.113549.1.9.2) "UN"#set ::pki::oids(1.2.840.113549.1.9.2) "unstructuredName"#Алгоритмы подписиset ::pki::oids(1.2.643.2.2.3) "GOST R 34.10-2001 with GOST R 34.11-94"set ::pki::oids(1.2.643.2.2.19) "GOST R 34.10-2001"set ::pki::oids(1.2.643.7.1.1.1.1) "GOST R 34.10-2012-256"set ::pki::oids(1.2.643.7.1.1.1.2) "GOST R 34.10-2012-512"set ::pki::oids(1.2.643.7.1.1.3.2) "GOST R 34.10-2012-256 with GOSTR 34.11-2012-256"set ::pki::oids(1.2.643.7.1.1.3.3) "GOST R 34.10-2012-512 with GOSTR 34.11-2012-512"set ::pki::oids(1.2.643.100.113.1) "KC1 Class Sign Tool"set ::pki::oids(1.2.643.100.113.2) "KC2 Class Sign Tool"set ::pki::oids(2.5.4.42)  "givenName"

Для полноты не мешает также добавить символьное представление параметров подписи:
#Параметры подписи
#Параметры подписиset ::pki::oids((1.2.643.2.2.35.1)"id-GostR3410-2001-CryptoPro-A-ParamSet"set ::pki::oids(1.2.643.2.2.35.2)"id-GostR3410-2001-CryptoPro-B-ParamSet"set ::pki::oids(1.2.643.2.2.35.3)"id-GostR3410-2001-CryptoPro-C-ParamSet"set ::pki::oids(1.2.643.2.2.36.0)"id-GostR3410-2001-CryptoPro-XchA-ParamSet"set ::pki::oids(1.2.643.2.2.36.1)"id-GostR3410-2001-CryptoPro-XchB-ParamSet"set ::pki::oids(1.2.643.7.1.2.1.1.1)"id-tc26-gost-3410-2012-256-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.1.2)"id-tc26-gost-3410-2012-256-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.1.3)"id-tc26-gost-3410-2012-256-paramSetC"set ::pki::oids(1.2.643.7.1.2.1.1.4)"id-tc26-gost-3410-2012-256-paramSetD"set ::pki::oids(1.2.643.7.1.2.1.2.1)"id-tc26-gost-3410-2012-512-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.2.2)"id-tc26-gost-3410-2012-512-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.2.3)"id-tc26-gost-3410-2012-512-paramSetC"


Создание класса

Объявление класса в TclOO мало чем отличается от объявления класса в других языках. Класс в TclOO также содержит область данных, конструктор, область объектно-ориентированных методов и деструктор. При этом область данных, конструктор и деструктор могут опускаться. Напомним, что конструктор вызывается при создание объекта (экземпляра объекта) заданного класса, а деструктор при его уничтожении. Конструктор (в отличии от деструктора), также как и методы, может иметь параметры. В нашем случае параметром для конструктора выступает сертификат в DER или PEM кодировке.
Области данных может предшествовать область наследуемых классов (superclass). Её будем рассматривать ниже. Но, для написания универсального класса certificate, эта область будет нами задействована.
В TclOO можно узнать какие классы в данный момент доступны в программе. Для этих целей служит команда следующего вида:
info class instances oo::class

В последующем, мы будем задействовать для наследования класс pubkey. Поэтому в нашем определении класса certificate присутствует проверка наличия класса pubkey и, если он присутствует, он объявляется как наследуемый (superclass pubkey).
Итак, ниже представлен класс для сертификата пока что с одним методом parse_cert, который возвращает список элементов сертификата:
Объявление класса certificate
oo::class create certificate {#Список доступных классов    foreach cl  "[info class instances oo::class]" {if {$cl == "::pubkey" } {#Если класс pubkey есть, то наследуем его. Это будет использовано в примере 3    superclass pubkey    break}    }#Переменные класса#Доступны только в пределах класса#Переменная для хранения разобранного сертификата.     variable ret#Переменная для хранения расширений сертификата    variable extcert#Конструктор    constructor {cert} {array set parsed_cert [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]set cert_seq $parsed_cert(data)array set ret [list]#Полный сертификат der в hexbinary scan $cert_seq H* ret(cert_full)  # Decode X.509 certificate, which is an ASN.1 sequence::asn::asnGetSequence cert_seq wholething::asn::asnGetSequence wholething cert#tbs - сертификатset ret(tbsCert) [::asn::asnSequence $cert]binary scan $ret(tbsCert) H* ret(tbsCert)::asn::asnPeekByte cert peek_tagif {$peek_tag != 0x02} {    # Version number is optional, if missing assumed to be value of 0    ::asn::asnGetContext cert - asn_version    ::asn::asnGetInteger asn_version ret(version)    incr ret(version)} else {    set ret(version) 1}::asn::asnGetBigInteger cert ret(serial_number)::asn::asnGetSequence cert data_signature_algo_seq::asn::asnGetObjectIdentifier data_signature_algo_seq ret(data_signature_algo)::asn::asnGetSequence cert issuer    set ret(issuer) $issuer::asn::asnGetSequence cert validity::asn::asnGetUTCTime validity ret(notBefore)::asn::asnGetUTCTime validity ret(notAfter)::asn::asnGetSequence cert subject    set ret(subject) $subject::asn::asnGetSequence cert pubkeyinfobinary scan $pubkeyinfo H* ret(pubkeyinfo_hex)::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid ret(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset extensions_list [list]while {$cert != ""} {    ::asn::asnPeekByte cert peek_tag    switch -- [format {0x%02x} $peek_tag] {    "0x81" {    ::asn::asnGetContext cert - issuerUniqueID        }    "0x82" {    ::asn::asnGetContext cert - subjectUniqueID        }    "0xa1" {    ::asn::asnGetContext cert - issuerUniqID        }    "0xa2" {    ::asn::asnGetContext cert - subjectUniqID        }    "0xa3" {    ::asn::asnGetContext cert - extensions_ctx    ::asn::asnGetSequence extensions_ctx extensions#Убираем перевод oid в текстset ::pki::oids1 [array get ::pki::oids]array unset ::pki::oids     while {$extensions != ""} {            ::asn::asnGetSequence extensions extension            ::asn::asnGetObjectIdentifier extension ext_oid        ::asn::asnPeekByte extension peek_tag        if {$peek_tag == 0x1} {        ::asn::asnGetBoolean extension ext_critical            } else {        set ext_critical false            }        ::asn::asnGetOctetString extension ext_value_seq        set ext_oid [::pki::_oid_number_to_name $ext_oid]        set ext_value [list $ext_critical]        switch -- $ext_oid {                        id-ce-basicConstraints {                ::asn::asnGetSequence ext_value_seq ext_value_bin                if {$ext_value_bin != ""} {            ::asn::asnGetBoolean ext_value_bin allowCA                } else {            set allowCA "false"                }            if {$ext_value_bin != ""} {            ::asn::asnGetInteger ext_value_bin caDepth                } else {            set caDepth -1                }                lappend ext_value $allowCA $caDepth                        }                        default {                binary scan $ext_value_seq H* ext_value_seq_hex                lappend ext_value $ext_value_seq_hex                        }                    }        lappend extensions_list $ext_oid $ext_value    }#Возвращаем перевод oid-ов в текстarray set ::pki::oids $::pki::oids1        }    }}set ret(extensions) $extensions_listarray set extcert $extensions_list::asn::asnGetSequence wholething signature_algo_seq::asn::asnGetObjectIdentifier signature_algo_seq ret(signature_algo)::asn::asnGetBitString wholething ret(signature)set ret(serial_number) [::math::bignum::tostr $ret(serial_number)]set ret(signature) [binary format B* $ret(signature)]binary scan $ret(signature) H* ret(signature)#Инициируем класс pubkeyinfo при наследовании - superclassif {[llength [self next]]} {#Если есть наследуемый класс, то вызываем его конструкторnext $ret(pubkeyinfo_hex)}    }    method parse_cert {} {        return [array get ret]    }}


В области данных командой variable определяются данные/переменные объекта через, которые доступны во всех методах класса.
Метод method определяется точно так же, как процедура proc Tcl. Методы могут иметь произвольное количество параметров. Внутри метода можно определять свои данные командой
my variable <идентификатор переменной>
. Методы могут быть публичными (экспортируемыми) и приватными.
Экспортируемые методы методы видимы за пределами класса. По умолчанию экспортируются методы начинаются со строчной буквы. По умолчанию методы, чьи имена начинаются с прописной буквы считаются неэкспортируемыми (приватными) методами. Область видимости независимо от первого символа можно задать явно. Для указания того, что метод является публичным служит следующая команда:
export <идентификатор метода>
.
Для запрета экспорта метода используется следующая команда:
unexport <идентификатор метода>
.
Для вызова одного метода из другого метода внутри класса используется команда my:
my <идентификатор метода>
.
Для этой же цели можно использовать внутреннюю команда класса self, которая возвращает идентификатор текущего объекта:
[self] <идентификатор метода>

Ниже мы увидим всё это.
Для дальнейшей работы соберем весь рассмотренный код в файле classparsecert.tcl.
Содержимое файла classparsecert.tcl
package require asn#Переименовываем оригинальную процедуру подсчета длины rename ::asn::asnGetLength ::asn::asnGetLength.orig#Новая процедура подсчета длиныproc ::asn::asnGetLength {data_var length_var} {    upvar 1 $data_var data  $length_var length    asnGetByte data length    if {$length == 0x080} {#Поддержка BER-кодировкиset lendata [string length $data]set tvl 1set length 0set data1 $datawhile {$tvl != 0} {    ::asn::asnGetByte data1 peek_tag     ::asn::asnPeekByte data1 peek_tag1    if {$peek_tag == 0x00 && $peek_tag1 == 0x00} {incr tvl -1::asn::asnGetByte data1 tag incr length 2continue    }    if {$peek_tag1 == 0x80} {incr tvlif {$tvl > 0} {    incr length 2}::asn::asnGetByte data1 tag     } else {set l1 [string length $data1]::asn::asnGetLength data1 llset l2 [string length $data1]set l3 [expr $l1 - $l2]incr length $l3incr length $llincr length::asn::asnGetBytes data1 $ll strt    }}return    }    if {$length > 0x080} {        set len_length [expr {$length & 0x7f}]        if {[string length $data] < $len_length} {            return -code error \"length information invalid, not enough octets left"         }        asnGetBytes data $len_length lengthBytes        switch $len_length {            1 { binary scan $lengthBytes     cu length }            2 { binary scan $lengthBytes     Su length }            3 { binary scan \x00$lengthBytes Iu length }            4 { binary scan $lengthBytes     Iu length }            default {                                binary scan $lengthBytes H* hexstrscan $hexstr %llx length            }        }    }    return}package require pkiset ::pki::oids(1.2.643.100.1)  "OGRN"set ::pki::oids(1.2.643.100.5)  "OGRNIP"set ::pki::oids(1.2.643.3.131.1.1) "INN"set ::pki::oids(1.2.643.100.3) "SNILS"#Для КПП ЕГАИСset ::pki::oids(1.2.840.113549.1.9.2) "UN"#set ::pki::oids(1.2.840.113549.1.9.2) "unstructuredName"#Алгоритмы подписиset ::pki::oids(1.2.643.2.2.3) "GOST R 34.10-2001 with GOST R 34.11-94"set ::pki::oids(1.2.643.2.2.19) "GOST R 34.10-2001"set ::pki::oids(1.2.643.7.1.1.1.1) "GOST R 34.10-2012-256"set ::pki::oids(1.2.643.7.1.1.1.2) "GOST R 34.10-2012-512"set ::pki::oids(1.2.643.7.1.1.3.2) "GOST R 34.10-2012-256 with GOSTR 34.11-2012-256"set ::pki::oids(1.2.643.7.1.1.3.3) "GOST R 34.10-2012-512 with GOSTR 34.11-2012-512"set ::pki::oids(1.2.643.100.113.1) "KC1 Class Sign Tool"set ::pki::oids(1.2.643.100.113.2) "KC2 Class Sign Tool"set ::pki::oids(2.5.4.42)  "givenName"#Параметры подписиset ::pki::oids((1.2.643.2.2.35.1)"id-GostR3410-2001-CryptoPro-A-ParamSet"set ::pki::oids(1.2.643.2.2.35.2)"id-GostR3410-2001-CryptoPro-B-ParamSet"set ::pki::oids(1.2.643.2.2.35.3)"id-GostR3410-2001-CryptoPro-C-ParamSet"set ::pki::oids(1.2.643.2.2.36.0)"id-GostR3410-2001-CryptoPro-XchA-ParamSet"set ::pki::oids(1.2.643.2.2.36.1)"id-GostR3410-2001-CryptoPro-XchB-ParamSet"set ::pki::oids(1.2.643.7.1.2.1.1.1)"id-tc26-gost-3410-2012-256-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.1.2)"id-tc26-gost-3410-2012-256-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.1.3)"id-tc26-gost-3410-2012-256-paramSetC"set ::pki::oids(1.2.643.7.1.2.1.1.4)"id-tc26-gost-3410-2012-256-paramSetD"set ::pki::oids(1.2.643.7.1.2.1.2.1)"id-tc26-gost-3410-2012-512-paramSetA"set ::pki::oids(1.2.643.7.1.2.1.2.2)"id-tc26-gost-3410-2012-512-paramSetB"set ::pki::oids(1.2.643.7.1.2.1.2.3)"id-tc26-gost-3410-2012-512-paramSetC"#Класс certificateoo::class create certificate {#Наследуем класс pubkey#    superclass pubkey#Переменные класса#Доступны только в пределах класса#Переменная для хранения разобранного сертификата.     variable ret#Переменная для хранения расширений сертификатаvariable extcert#Конструктор    constructor {cert} {array set parsed_cert [::pki::_parse_pem $cert "-----BEGIN CERTIFICATE-----" "-----END CERTIFICATE-----"]set cert_seq $parsed_cert(data)array set ret [list]#Полный сертификат der в hexbinary scan $cert_seq H* ret(cert_full)  # Decode X.509 certificate, which is an ASN.1 sequence::asn::asnGetSequence cert_seq wholething::asn::asnGetSequence wholething cert#tbs - сертификатset ret(tbsCert) [::asn::asnSequence $cert]binary scan $ret(tbsCert) H* ret(tbsCert)::asn::asnPeekByte cert peek_tagif {$peek_tag != 0x02} {    # Version number is optional, if missing assumed to be value of 0    ::asn::asnGetContext cert - asn_version    ::asn::asnGetInteger asn_version ret(version)    incr ret(version)} else {    set ret(version) 1}::asn::asnGetBigInteger cert ret(serial_number)::asn::asnGetSequence cert data_signature_algo_seq::asn::asnGetObjectIdentifier data_signature_algo_seq ret(data_signature_algo)::asn::asnGetSequence cert issuer    set ret(issuer) $issuer::asn::asnGetSequence cert validity::asn::asnGetUTCTime validity ret(notBefore)::asn::asnGetUTCTime validity ret(notAfter)::asn::asnGetSequence cert subject    set ret(subject) $subject::asn::asnGetSequence cert pubkeyinfobinary scan $pubkeyinfo H* ret(pubkeyinfo_hex)::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid ret(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset extensions_list [list]while {$cert != ""} {    ::asn::asnPeekByte cert peek_tag    switch -- [format {0x%02x} $peek_tag] {    "0x81" {    ::asn::asnGetContext cert - issuerUniqueID        }    "0x82" {    ::asn::asnGetContext cert - subjectUniqueID        }    "0xa1" {    ::asn::asnGetContext cert - issuerUniqID        }    "0xa2" {    ::asn::asnGetContext cert - subjectUniqID        }    "0xa3" {    ::asn::asnGetContext cert - extensions_ctx    ::asn::asnGetSequence extensions_ctx extensions#Убираем перевод oid в текстset ::pki::oids1 [array get ::pki::oids]array unset ::pki::oids     while {$extensions != ""} {            ::asn::asnGetSequence extensions extension            ::asn::asnGetObjectIdentifier extension ext_oid        ::asn::asnPeekByte extension peek_tag        if {$peek_tag == 0x1} {        ::asn::asnGetBoolean extension ext_critical            } else {        set ext_critical false            }        ::asn::asnGetOctetString extension ext_value_seq        set ext_oid [::pki::_oid_number_to_name $ext_oid]        set ext_value [list $ext_critical]        switch -- $ext_oid {                        id-ce-basicConstraints {                ::asn::asnGetSequence ext_value_seq ext_value_bin                if {$ext_value_bin != ""} {            ::asn::asnGetBoolean ext_value_bin allowCA                } else {            set allowCA "false"                }            if {$ext_value_bin != ""} {            ::asn::asnGetInteger ext_value_bin caDepth                } else {            set caDepth -1                }                           lappend ext_value $allowCA $caDepth                        }                        default {                binary scan $ext_value_seq H* ext_value_seq_hex                lappend ext_value $ext_value_seq_hex                        }                    }        lappend extensions_list $ext_oid $ext_value    }#Возвращаем перевод oid-ов в текстarray set ::pki::oids $::pki::oids1        }    }}set ret(extensions) $extensions_listarray set extcert $extensions_list::asn::asnGetSequence wholething signature_algo_seq::asn::asnGetObjectIdentifier signature_algo_seq ret(signature_algo)::asn::asnGetBitString wholething ret(signature)set ret(serial_number) [::math::bignum::tostr $ret(serial_number)]set ret(signature) [binary format B* $ret(signature)]binary scan $ret(signature) H* ret(signature)#Инициируем класс pubkeyinfo при наследовании - superclass#next $ret(pubkeyinfo_hex)    }    method parse_cert {} {        return [array get ret]    }}


После того как был определен класс можно создавать конкретный объект (экземпляр объекта). Для этого может быть использована одна из следующих команд:
<имя класса> create <идентификатор экземпляра класса> [параметры для констуктура]

или
set <переменная для идентификатора экземпляра класса > <имя класса> new [параметры для констуктура]

В первом случае программист сам назначает идентификатор для создаваемого экземпляра объекта. Этот идентификатор фактически будет командой, через которую осуществляется доступ к объекту и его методам:
<идентификатор объекта>  <идентификатор метода> [<параметры>]

Во втором случае идентификатор создаваемого объекта назначается интерпретатором и возвращается как результат выполнения команды new для указанного класса. В этом случае идентификатор объекта будет браться из этой переменной.
Интересно сравнить с созданием объекта в Python. И что мы видим? Несущественную синтаксическую разницу.

Напишем небольшой пример example1.tcl использования этого класса:
#Загружаем описание классаsource ./classparsecert.tcl#Загружаем сертификатset file [lindex $argv 0]if {$argc != 1 || ![file exists $file]} {    puts "Usage: tclsh example1 <файл с сертификатом>"    exit}puts "Loading file: $file"set fd [open $file]chan configure $fd -translation binaryset data [read $fd]close $fdif {[catch {certificate create cert1 $data} er1]} {puts "Файл не содержит СЕРТИФИКАТ"exit}array set cert_parse [cert1 parse_cert]#parray cert_parseputs "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}

Выполним пример:
$tclsh ./example1.tclLoading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        cert_parse(pubkeyinfo_hex)        cert_parse(extensions)        cert_parse(issuer)        cert_parse(data_signature_algo)        cert_parse(cert_full)        cert_parse(serial_number)        cert_parse(signature)        cert_parse(pubkey_algo)        cert_parse(notAfter)        cert_parse(signature_algo)        cert_parse(notBefore)        cert_parse(version)        cert_parse(tbsCert)$ 

О конструкторе Lego

У читателя, наверное, так и хочет сорваться с языка вопрос:- А причем здесь конструктор Lego? А вот при чем. Если, скажем в C++ класс объекта должен быть определен сразу, то в TclOO класс может собираться постепенно как модель в конструкторе. Более того одни части класса могут удаляться и заменяться другими и т.д. Более того, такой метод конструирования класса распространяется и на объекты, да на конкретные объекты.
Предположим, что необходимо вывести информацию и о владельце и об издателе сертификата. Для этого нам потребуется два публичных issur и subject и один приватный метод parse_dn для разбора отличительного имени (DN) издателя и владельца. Традиционно нам пришлось бы переписать класс certificate, добавив в него указанные методы. В TclOO можно поступить по другому. Можно просто в нужном месте программы выполнить оператор добавления в существующий класс новых членов.
Для добавления в класс новых членов в область данных используется команда (модуль конструктора) вида:
oo::define <идентификатор класса>  {#Область данных классаvariable <идентификатор переменной>  [<идентификатор переменной>][ variable <идентификатор переменной> ]}

Может быть несколькокоманд variable, каждая из которых определяет один или несколько элементов данных.
Аналогично добавляются методы:
oo::define <идентификатор класса>  {#методыmethod <идентификатор метода 1>  {<параметры>} {<тело метода>}[method <идентификатор метода N>  {<параметры>} {<тело метода>}]}

Любой метод можно удалить в любое время с помощьюкоманды deletemethod внутри сценария определения класса. Эта команды будет рассмотрена ниже при рассмотрении примера с отзывом сертификата.
Про видимость методов (публичные, приватные методы) мы уже говорили выше.
Отметим, что первоначально класс может создаваться абсолютно пустым:
oo::class create <Идентификатор класса>

с последующим наполнением его через команду:
oo::define <идентификатор класса>  {}

Итак, добавляем новые методы в класс Certificate:
oo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}

Теперь дополним наш пример кодом для распечатки информации об издателе и владельце:
...puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}...

Таким образом мы получим второй пример.
Тестовый пример example2.tcl
source ./classparsecert.tcl
#Загружаем сертификат
set file [lindex $argv 0]
if {$argc != 1 || ![file exists $file]} {
puts Usage: tclsh example1 <файл с сертификатом>
exit
}
puts Loading file: $file
set fd [open $file]
chan configure $fd -translation binary
set data [read $fd]
close $fd
if {[catch {certificate create cert1 $data} er1]} {
puts Файл не содержит СЕРТИФИКАТ
exit
}
array set cert_parse [cert1 parse_cert]
#parray cert_parse
puts Распарсенный сертификат
foreach ind [array names cert_parse] {
puts "\tcert_parse($ind)"
}
#Добавляем новые методы
oo::define certificate {
method issuer {} {
return [ my parse_dn $ret(issuer)]
}
method subject {} {
return [ my parse_dn $ret(subject)]
}
method parse_dn {asnblock} {
set lret {}
while {[string length $asnblock]} {
asn::asnGetSet asnblock AttributeValueAssertion
asn::asnGetSequence AttributeValueAssertion valblock
asn::asnGetObjectIdentifier valblock oid
set name [::pki::_oid_number_to_name $oid]
::asn::asnGetString valblock value
lappend lret [string toupper $name]
lappend lret $value
}
return $lret
}
#Приватный метод
unexport parse_dn
}
#Применяем методы
puts Сведения о владельце:
foreach {oid value} [cert1 subject] {
puts "\t$oid=$value"
}
puts Сведения об издателе:
foreach {oid value} [cert1 issuer] {
puts "\t$oid=$value"
}

Попробуем выполнить этот пример:
$tclsh example2.tcl minenergo.cer  Loading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        . . .         cert_parse(tbsCert)Сведения о владельце:        EMAIL=xxxxxxxxxxx        INN=xxxxxxxxxxx        OGRN=............. . .        ST=77 г. Москва        C=RU        CN=Мин РоссииСведения об издателе: . . .        C=RU        ST=77 Москва        L=Москва        CN=Тестовый удостоверяющий центр$

О наследовании


Определяющей характеристикой объектно-ориентированных систем является поддержка наследования.Наследование относится к способностипроизводногокласса (также называемогоподклассом) наследовать область данных и методы из наследуемого класса (из супер класса).
При разборе сертификата, естественно, требуется получить и полную информацию о его публичном ключе. Предположим у нас уже есть класс pubkey, который на основе asn-структуры pubkeyinfo выдает полную информацию о публичном ключе, включая RSA, EC, GOST:
oo::class create pubkey {#Внутренняя переменная класса для хранения asn-структуры pubkeyinfo    variable infopk    constructor {pubkinfo} {set infopk $pubkinfo    }    method infopubkey {} {array set retpk [list]set pubkeyinfo [binary format H* $infopk]::asn::asnGetSequence pubkeyinfo pubkey_algoid::asn::asnGetObjectIdentifier pubkey_algoid retpk(pubkey_algo)::asn::asnGetBitString pubkeyinfo pubkeyset pubkey [binary format B* $pubkey]binary scan $pubkey H* retpk(pubkey)set retpk(pkcs11id_hex) [::sha1::sha1  $pubkey]if {"1 2 643" == [string range $retpk(pubkey_algo) 0 6]} {#ГОСТ-ключ        set retpk(type) gost    ::asn::asnGetSequence pubkey_algoid pubalgost  #OID - параметра    ::asn::asnGetObjectIdentifier pubalgost retpk(paramkey)    set retpk(paramkey) [::pki::_oid_number_to_name $retpk(paramkey)]    if {$pubalgost != ""} {  #OID - Функция хэша::asn::asnGetObjectIdentifier pubalgost retpk(hashkey)    } else {set retpk(hashkey) ""    }} elseif {"1 2 840 10045 2 1" == $retpk(pubkey_algo) } {#EC-key        set retpk(type) ec    ::asn::asnGetObjectIdentifier pubkey_algoid retpk(pubkey_algo_par)} elseif {"1 2 840 113549 1 1 1" == $retpk(pubkey_algo) }  {#RSA- key        set retpk(type) rsa    binary scan $pubkey H* retpk(pubkey)    ::asn::asnGetSequence pubkey pubkey_parts    ::asn::asnGetBigInteger pubkey_parts retpk(n)    ::asn::asnGetBigInteger pubkey_parts retpk(e)    set retpk(n) [::math::bignum::tostr $retpk(n)]    set retpk(e) [::math::bignum::tostr $retpk(e)]    set retpk(l) [expr {int([::pki::_bits $retpk(n)] / 8.0000 + 0.5) * 8}]} else {        set retpk(type) unknown}return [array get retpk]    }}

Сохраним этот класс в файле classpubkeyinfo.tcl.
Для того, чтобы наследовать метод infopubkey для объектов класса certificate, в определение класса certificate добавляется определение суперкласса, методы которого будут наследоваться:
superclass pubkey

Также добавляем в конструктор класса certificate вызов конструктора класса pubkey с передачей ему в качестве параметра asn-структуры pubkeyinfo:
next $ret(pubkeyinfo_hex)

Команда next вызывает одноименный метод (в данном случае constructor) из суперкласса, т.е. из класса pubkey. Конструктор в классе pubkey просто сохранит в переменной класса infopk asn-структуру публичного ключа. Этот код с соответствующей проверкой наличия в теле программы класса pubkey и его конструктора был включен при определении класса certificate.
Полный техт example3.tcl здесь.
source ./classpubkeyinfo.tclsource ./classparsecert_and_pk.tclset file [lindex $argv 0]if {$argc != 1 || ![file exists $file]} {    puts "Usage: tclsh example1 <файл с сертификатом>"    exit}puts "Loading file: $file"set fd [open $file]chan configure $fd -translation binaryset data [read $fd]close $fdif {[catch {certificate create cert1 $data} er1]} {puts "Файл не содержит сертификата"exit}array set cert_parse [cert1 parse_cert]puts "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}#Добавляем новые методыoo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}puts "INFO PUB KEY"foreach {oid value} [cert1 infopubkey] {    puts "\t$oid=$value"}#Создаем объект pubkeyputs "КЛАСС INFO PUB KEY"if {[catch {pubkey create pk1 $cert_parse(pubkeyinfo_hex)} er1]} {puts "НЕ PUBKEYINFO"exit}foreach {oid value} [pk1 infopubkey] {    puts "\t$oid=$value"}puts "Публичные методы класса certificate"puts "\t[info class methods certificate]"puts "Все методы класса certificate, включая приватные"puts "\t[info class methods certificate -private]"

Выполним пример example3.tcl:
$ tclsh example3.tcl minenergo.cer Loading file: minenergo.cerРаспарсенный сертификат        cert_parse(subject)        . . .        cert_parse(tbsCert)Сведения о владельце:        . . .        ST=77 г. Москва        C=RU        CN=Мин РоссииСведения об издателе:        C=RU        ST=77 Москва        . . .        CN=Тестовый удостоверяющий центрINFO PUB KEY        pkcs11id_hex=842205ac57465fd853a158544f1ea1ba1de58569        pubkey=04401dc81447918c7694a74dbe6bb4e4c10a63ca21d6b95a41ae20837deda4700f2404a0c1141d9b535b95707bb751791eb684bd09ce8f0c98d912dea947e4b8bbdb        hashkey=1 2 643 7 1 1 2 2        paramkey=id-GostR3410-2001-CryptoPro-XchA-ParamSet        type=gost        pubkey_algo=1 2 643 7 1 1 1 1Публичные методы класса certificate        subject parse_cert issuerВсе методы класса certificate, включая приватные        parse_dn subject parse_cert issuer . . .$

Отметим также, что TclOO допускает и множественное наследование, но это тема для отдельной публикации.

Информационная поддержка


В результатах выполнения примера мы видим перечень методов доступных в классе certificate.
Для получения списка методов используется следующая команда:
info class methods <идентификатор класса> [-private]

Если флаг "-private" не задан, то выдается список публичных методов. В противном случае, выдается весь перечень методов, включая приватные.
Проверить принадлежность объекта тому или иному классу можно командой:
info object clacc <идентификатор объекта>
.
В нашем примере объект cert1 принадлежит двум классам: certuficate и pubkey.
Если требуется узнать какие классы наследует тот или иной класс, достаточно выполнить коиманду:
info class superclasses <идентификатор класса>

А если требуется получить информацию о том, какими классами наследуется тот или иной класс, то достаточно выполнить следующую команду:
info class subclasses <идентификатор класса>
.
В нашем примере мы имеем:
$ . . .Публичные методы класса certificate        subject parse_cert issuerВсе методы класса certificate, включая приватные        parse_dn subject parse_cert issuerПринадлежность объекта cert1 классу certificate        1Принадлежность объекта cert1 классу pubkey        1Супер классы класса certificate        ::pubkeyСупер классы класса pubkey        ::oo::objectПодклассы класса certificateПодклассы класса pubkey        ::certificate$ 

Подмешивание (mix in) методов в класс


Для расширения возможность класса, прежде всего с точки зрения его функциональности, помимо наследования можно использовать так называемый метод подмешивания (mix in).
Если мы хотим распечатать сертификат в текстовом виде, то нам потребуется разбор asn-структур расширений сертификата. Это и начначение ключа сертификата, это свойства квалифицированного сертификата и многое другое. Оформим разбор расширений сертификата в отдельный класс parseexts, в котором отсутствует констуктор и деструктор:
#Класс разбора расширений сертификатаoo::class create parseexts {#Переменные с распарсенным сертификатом и его расширениями#Область данных берется их класса, к которому будем плдмешивать    variable ret    variable extcert#Подмешиваемые методы    method issuerSignTool {} {set member {"Наименование СКЗИ УЦ" "Наименование УЦ" "Сертификат СКЗИ УЦ" "Сертификат УЦ"}#Проверка наличия расширенияif {![info exists extcert(1.2.643.100.112)]} {    return [list ]}set rr [list]set iss [binary format H* [lindex $extcert(1.2.643.100.112) 1]]::asn::asnGetSequence iss iss_polfor {set i 0} {[string length $iss_pol] > 0}  {incr i} {    ::asn::asnGetUTF8String iss_pol retist    lappend rr [lindex $member $i]    lappend rr $retist}return $rr  }    method subjectSignTool {} {#Проверка наличия расширенияif {![info exists extcert(1.2.643.100.111)]} {    return [list ]}set iss [binary format H* [lindex $extcert(1.2.643.100.111) 1]]lappend rr "User CKZI"::asn::asnGetUTF8String iss retsstlappend rr $retsstreturn $rr    }    method keyUsage {} {    #keyUsageset critcert "No"array set ist [list]#Проверка наличия расширенияif {![info exists extcert(2.5.29.15)]} {    return [array get ist]}    set ku_hex [lindex $extcert(2.5.29.15) 1]if {[lindex $extcert(2.5.29.15) 0] == 1} {    set critcert "Yes"}set ku_options {"Digital signature" "Non-Repudiation" "Key encipherment" "Data encipherment" "Key agreement" "Certificate signature" "CRL signature" "Encipher Only" "Decipher Only" "Revocation list signature"}set ku [binary format H* $ku_hex]::asn::asnGetBitString ku ku_binset retku {}for {set i 0} {$i < [string length $ku_bin]}  {incr i} {    if {[string range $ku_bin $i $i] > 0 } {    lappend retku [lindex $ku_options $i]    }}array set aku [list]set aku(keyUsage) $retkuset aku(critcert) $critcertreturn [array get aku]    }}

Область данных подмешиваемого класса должна включать те данные, из класса к которому будет подмешиваться данный класс, которые будут использоваться в его методах.
Для подмещивания используется команда mixin:
mixin <идентификатор подмешиваемого класса> 

Для нашего примера это будет выглядеть следующим образом:
oo::define certificate {mixin parseexts}

Полный пример использования подмешивания example4.tcl находится здесь.

source ./classpubkeyinfo.tcl
source ./classparsecert.tcl
set file [lindex $argv 0]
if {$argc != 1 || ![file exists $file]} {
puts Usage: tclsh example1 <файл с сертификатом>
exit
}
puts Loading file: $file
set fd [open $file]
chan configure $fd -translation binary
set data [read $fd]
close $fd
if {[catch {certificate create cert1 $data} er1]} {
puts Файл не содержит сертификата
exit
}
array set cert_parse [cert1 parse_cert]
if {0} {
puts Распарсенный сертификат
foreach ind [array names cert_parse] {
puts "\tcert_parse($ind)"
}
}
#Добавляем новые методы
oo::define certificate {
method issuer {} {
return [ my parse_dn $ret(issuer)]
}
method subject {} {
return [ my parse_dn $ret(subject)]
}
method parse_dn {asnblock} {
set lret {}
while {[string length $asnblock]} {
asn::asnGetSet asnblock AttributeValueAssertion
asn::asnGetSequence AttributeValueAssertion valblock
asn::asnGetObjectIdentifier valblock oid
set name [::pki::_oid_number_to_name $oid]
::asn::asnGetString valblock value
lappend lret [string toupper $name]
lappend lret $value
}
return $lret
}
unexport parse_dn
}
puts Сведения о владельце:
foreach {oid value} [cert1 subject] {
puts "\t$oid=$value"
}
puts Сведения об издателе:
foreach {oid value} [cert1 issuer] {
puts "\t$oid=$value"
}
puts INFO PUB KEY
foreach {oid value} [cert1 infopubkey] {
puts "\t$oid=$value"
}
#Класс разбора расширений сертификата
oo::class create parseexts {
#Переменная с распарсенным сертификатом
variable ret
variable extcert
method issuerSignTool {} {
set member {Наименование СКЗИ УЦ Наименование УЦ Сертификат СКЗИ УЦ Сертификат УЦ}
#Проверка наличия расширения
if {![info exists extcert(1.2.643.100.112)]} {
return [list ]
}
set rr [list]
set iss [binary format H* [lindex $extcert(1.2.643.100.112) 1]]
::asn::asnGetSequence iss iss_pol
for {set i 0} {[string length $iss_pol] > 0} {incr i} {
::asn::asnGetUTF8String iss_pol retist
lappend rr [lindex $member $i]
lappend rr $retist
}
# unset extcert(1.2.643.100.112)
return $rr
}
method subjectSignTool {} {
#Проверка наличия расширения
if {![info exists extcert(1.2.643.100.111)]} {
return [list ]
}
set iss [binary format H* [lindex $extcert(1.2.643.100.111) 1]]
lappend rr User CKZI
::asn::asnGetUTF8String iss retsst
lappend rr $retsst
# unset extcert(1.2.643.100.111)
return $rr
}
method keyUsage {} {
#keyUsage
set critcert No
array set ist [list]
#Проверка наличия расширения
if {![info exists extcert(2.5.29.15)]} {
return [array get ist]
}
set ku_hex [lindex $extcert(2.5.29.15) 1]
if {[lindex $extcert(2.5.29.15) 0] == 1} {
set critcert Yes
}
set ku_options {Digital signature Non-Repudiation Key encipherment Data encipherment Key agreement Certificate signature CRL signature Encipher Only Decipher Only Revocation list signature}
set ku [binary format H* $ku_hex]
::asn::asnGetBitString ku ku_bin
set retku {}
for {set i 0} {$i < [string length $ku_bin]} {incr i} {
if {[string range $ku_bin $i $i] > 0 } {
lappend retku [lindex $ku_options $i]
}
}
array set aku [list]
set aku(keyUsage) $retku
set aku(critcert) $critcert
return [array get aku]
}
}
oo::define certificate {
mixin parseexts
}
puts keyUsage
foreach {oid value} [cert1 keyUsage] {
puts "\t$oid=$value"
}
puts issuerSignTool
foreach {oid value} [cert1 issuerSignTool] {
puts "\t$oid=$value"
}
puts subjectSignTool
foreach {oid value} [cert1 subjectSignTool] {
puts "\t$oid=$value"
}
puts Публичные методы класса certificate
puts "\t[info class methods certificate]"
puts Все методы класса certificate, включая приватные
puts "\t[info class methods certificate -private]"
puts Принадлежность объекта cert1 классу certificate
puts "\t[info object class cert1 certificate]"
puts Принадлежность объекта cert1 классу pubkey
puts "\t[info object class cert1 pubkey]"
puts Супер классы класса certificate
puts "\t[info class superclasses certificate]"
puts Супер классы класса pubkey
puts "\t[info class superclasses pubkey]"
puts Подклассы класса certificate
puts "\t[info class subclasses certificate]"
puts Подклассы класса pubkey
puts "\t[info class subclasses pubkey]"
puts Mixin-ы класса certificate
puts "\t[info class mixins certificate]"

Результат выполнения примера:
$tclsh example4.tcl cert.cer. . .Сведения об издателе:. . .        C=RU        ST=77 Москва        L=Москва. . .        CN=Тестовый удостоверяющий центрINFO PUB KEY        pkcs11id_hex=842205ac57465fd853a158544f1ea1ba1de58569        pubkey=04401dc81447918c7694a74dbe6bb4e4c10a63ca21d6b95a41ae20837deda4700f2404a0c1141d9b535b95707bb751791eb684bd09ce8f0c98d912dea947e4b8bbdb        hashkey=1 2 643 7 1 1 2 2        paramkey=id-GostR3410-2001-CryptoPro-XchA-ParamSet        type=gost        pubkey_algo=1 2 643 7 1 1 1 1keyUsage        critcert=Yes        keyUsage={Digital signature} Non-Repudiation {Key encipherment} {Data encipherment}issuerSignTool        Наименование СКЗИ УЦ="CSP"         Наименование УЦ="Удостоверяющий центр" версии         Сертификат СКЗИ УЦ=Сертификат соответствия         Сертификат УЦ=Сертификат соответствия  subjectSignTool        User CKZI=CSP Публичные методы класса certificate        subject parse_cert issuerВсе методы класса certificate, включая приватные        parse_dn subject parse_cert issuerПринадлежность объекта cert1 классу certificate        1Принадлежность объекта cert1 классу pubkey        1Супер классы класса certificate        ::pubkeyСупер классы класса pubkey        ::oo::objectПодклассы класса certificateПодклассы класса pubkey        ::certificateMixin-ы класса certificate        ::parseexts

Добавление/переопределение методов у объектов


В принципе этого материала достаточно, чтобы начать использовать ООП в Tcl. Но мы упомянули и то, что в TcllOO можно динамически конструировать не только сам класс, то и экземпляры класса, т.е. объекты. На одной из таких возможностей хотелось бы остановится.
Для этого добавим в класс certificate еще один метод для подписания этим сертификатом некоторого документа:
#Метод для Подписания документаoo::define certificate {method signDoc {doc} {set sign "Здесь должна находиться подпись документа  $doc"#Счетчик подписанных документовmy variable signedDoc#Количество подписанных документовincr signedDocreturn [list $signedDoc $sign]}}

При вызове этого метода должно происходить подписание документа и увеличение счетчика подписанных документов на единицу. В качестве результата работы этого метода возвращается общее число подписанных на данный момент документов и сама подпись:
. . . set doc "Подпись1"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}. . .

Результат будет выглядеть так:
. . .Подписание документа Подпись1        Подписано документов на данный момент=1        Подпись документа="Здесь должна находиться подпись документа  Подпись1". . .

Сам алгорит подписи здесь не рассматривается, но его можно найти в утилите cryptoarmpkcs:

image

А теперь представим, что владелец сертификата убывает в отпуск. Он знает, что в отпуске он будет отдыхать и не в коем случае не будет работать с документами и тем более что-либо подписывать. Он хочет отозвать сертификат, а когда вернется восстановить его действие. Для этих целей служит следующая функция:
#Процедура отзыва сертификатаproc revoke {cert_obj} {    oo::objdefine $cert_obj {#Переопределяем метод подписи для конкретного объекта        method signDoc {args} {#Переменная accessCert хранит число несанкционированных попыток подписания            my variable accessCert             set sign "Сертификат временно отозван. Не пытайтесь им подписывать!"#Число попыток несанкционированного использования возрастает на 1            incr accessCert            return [list $accessCert $sign]        }        method unrevoke {} {            my variable accessCert#Вызов метод  unrevoke удалит метод подписи для конкретного объекта,#восстанавливая тем самым действие  метода signDoc из класса и #удалит сам метод unrevoke            oo::objdefine [self] { deletemethod signDoc unrevoke }            if {![info exist accessCert]} {                return 0            }            return $accessCert        }    }}

Вызов этой функции определяет новый функционал методв signDoc для конкретного объекта. Для остальных объектов, как существующих и так и новых, сохраняется действие метода, определенного для класса. Также определяется новый метод unrevoke, вызов которого сотрудником по возвращению из отпуска приведет к восстановлению метода signDoc из класса certificate, путем удаления метода signDoc для объекта, а также удалит и сам метод unrevoke.
Полный текст примера example5.tcl находится здесь
source ./classpubkeyinfo.tclsource ./classparsecert.tcl#Примерset file [lindex $argv 0]if {$argc != 1 || ![file exists $file]} {    puts "File $file not exist"    puts "Usage: tclsh example1 <файл с сертификатом>"    exit}puts "Loading file: $file"set fd [open $file]chan configure $fd -translation binaryset data [read $fd]close $fdif {$data == "" } {    puts "Bad file with certificate=$file"    usage 1    exit}if {[catch {certificate create cert1 $data} er1]} {puts "НЕ СЕРТИФИКАТ"exit}array set cert_parse [cert1 parse_cert]#parray cert_parseif {0} {puts "Распарсенный сертификат"foreach ind [array names cert_parse] {    puts "\tcert_parse($ind)"}}#Добавляем новые методыoo::define certificate {    method issuer {} {return [ my parse_dn $ret(issuer)]    }    method subject {} {return [ my parse_dn $ret(subject)]    }    method parse_dn {asnblock} {set lret {}      while {[string length $asnblock]} {        asn::asnGetSet asnblock AttributeValueAssertion        asn::asnGetSequence AttributeValueAssertion valblock        asn::asnGetObjectIdentifier valblock oidset name [::pki::_oid_number_to_name $oid]::asn::asnGetString valblock  valuelappend lret [string toupper $name]lappend lret $value      }return $lret    }    unexport parse_dn}puts "Сведения о владельце:"foreach {oid value} [cert1 subject] {    puts "\t$oid=$value"}puts "Сведения об издателе:"foreach {oid value} [cert1 issuer] {    puts "\t$oid=$value"}puts "INFO PUB KEY"foreach {oid value} [cert1 infopubkey] {    puts "\t$oid=$value"}#Метод для Подписания документаoo::define certificate {method signDoc {doc} {set sign "Здесь должна находиться подпись документа  $doc"#Счетчик подписанных документовmy variable signedDoc#Количество подписанных документовincr signedDocreturn [list $signedDoc $sign]}}set doc "Подпись1"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}set doc "Подпись2"puts "Подписание документа $doc"foreach {count sign} [cert1 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""}#Процедура отзыва сертификатаproc revoke {cert_obj} {    oo::objdefine $cert_obj {#Переопределяем метод подписи для конкретного объекта        method signDoc {args} {#Переменная accessCert хранит число несанкционированных попыток подписания            my variable accessCert             set sign "Сертификат временно отозван. Не пытайтесь им подписывать!"#Число попыток несанкционированного использования возрастает на 1            incr accessCert            return [list $accessCert $sign]        }        method unrevoke {} {            my variable accessCert#Вызов метод  unrevoke удалит метод подписи для конкретного объекта,#восстанавливая тем самым действие  метода signDoc из класса и #удалит сам метод unrevoke            oo::objdefine [self] { deletemethod signDoc unrevoke }            if {![info exist accessCert]} {                return 0            }            return $accessCert        }    }}#Клонируем объектoo::copy cert1 cert11#Отзыв сертификатаputs "Отзыв сертификата"revoke cert1foreach doc "Подпись3 подпись4" {    puts "Попытка подписать документ $doc"    foreach {count sign} [cert1 signDoc $doc] {puts "\tПопыток несанкционированного доступа=$count"puts "\tПодпись документа=\"$sign\""    }}#Для клонированного объекта отзыв не действуетforeach doc "Подпись3к подпись4к" {    puts "Попытка подписать документ $doc клонированным объектом"    foreach {count sign} [cert11 signDoc $doc] {    puts "\tПодписано документов на данный момент=$count"    puts "\tПодпись документа=\"$sign\""    }}#Восстанавливаем действие сертификатаforeach {count info} [cert1 unrevoke] {    puts "Действие сертификата восстанвлено"    puts "\tЗа время его отзыва было $count попытки несанкционированного досьупа"}foreach doc "\"Подпись после восстановления\"" {    puts "Попытка подписать документ $doc"    foreach {count sign} [cert1 signDoc $doc] {puts "\tПодписано документов на данный момент=$count"puts "\tПодпись документа=\"$sign\""    }}

Ниже приведен фрагмент выполнения примера example5.tcl:
. . . Подписание документа Подпись1        Подписано документов на данный момент=1        Подпись документа="Здесь должна находиться подпись документа  Подпись1"Подписание документа Подпись2        Подписано документов на данный момент=2        Подпись документа="Здесь должна находиться подпись документа  Подпись2"Отзыв сертификатаПопытка подписать документ Подпись3        Попыток несанкционированного доступа=1        Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"Попытка подписать документ подпись4        Попыток несанкционированного доступа=2        Подпись документа="Сертификат временно отозван. Не пытайтесь им подписывать!"Действие сертификата восстанвлено        За время его отзыва было 2 попытки несанкционированного досьупаПопытка подписать документ Подпись после восстановления        Подписано документов на данный момент=3        Подпись документа="Здесь должна находиться подпись документа  Подпись после восстановления". . .

Упомянем еще один оператор. Это оператор клонирования объекта:
oo::copy <идентификатор исходного объекта> <идентификатор клона>
Говорить и писать об ООП на TclOO можно долго и долго.
Еще интересней его исследовать.
Подробнее..

Реализация наследования в файлах локализации iOS

20.07.2020 10:13:25 | Автор: admin


Приветствую, дорогие хабражители!

Сегодня я хочу поделиться интересным опытом в решении проблемы локализации. В iOS локализация устроена достаточно удобно с точки зрения одного таргета, либо нескольких таргетов, в которых ключи в localizable.strings не сильно повторяются. Но всё становится сложнее, когда у вас появляется с десяток таргетов, в которых больше половины ключей повторяются, но при этом частично имеют разные значения, а так же есть набор уникальных для конкретного таргета ключей.



Для тех, кто с этим пока не сталкивался, объясню проблему подробнее на примере.

Допустим, у нас есть большой проект, в котором 90% общего кода и 3 таргета: MyApp1, MyApp2, MyApp3, которые имеют некоторое количество специфичных экранов, а так же каждый имеет своё название и тексты. По сути таргет представляет из себя самостоятельное приложение. Каждый из них должен быть переведен на 10 языков. При этом мы НЕ хотим добавлять ключи локализации типа app1_localizable_key1, app2_localizable_key1 и т.д. Хотим, чтобы в коде всё было красиво и локализация происходила одной строчкой
NSLocalizedString(@"localizable_key1", nil)

Без всяких if и ifdef, чтобы при добавлении нового таргета нам не пришлось искать по всему коду огромного проекта места с NSLocalizedString и прописывать там новые ключи. Так же хотим, чтобы часть ключей была привязана к специфичным экранам таргета, т.е. были ключи app2_screen1_key, app3_screen2_key.

Штатными средствами Xcode сейчас можно сделать следующее:
  • Скопировать общую часть localizable.strings в каждый таргет, при этом мы получим 3 копии этих файлов.
  • Добавить в соответствующие localizable.strings ключи специфичные для конкретного таргета.


Каким проблемы мы получаем:
  • Добавить новый общий ключ в проект достаточно накладно. Число мест равняется числу таргетов помноженному на число языков. В нашем примере это 30 мест.
  • Есть вероятность ошибки, когда добавили строку в 1-2 текущих таргета, с которыми идёт активная работа, а через год решили воскресить еще один или несколько таргетов. Придется вручную синхронизировать между собой локализации, либо писать для этого скрипт. А если была проявлена некоторая неряшливость при добавлении или мерже веток, и общие ключи смешаны со специфичными, то тут будет самый настоящий квест.
  • Объём файлов локализации. Они все постоянно растут, это затрудняет работу с ними и увеличивает шансы конфликта при мерже веток.


Что хотелось бы:
  • Чтобы все общие ключи хранились в отдельном файле.
  • Для каждого таргета был файл, в котором хранились только специфичные для него ключи, а так же общие ключи со значениями для данного таргета.


Для нашего примера имея общий файл localizable.strings со строками
"shared_localizable_key1" = "MyApp title""shared_localizable_key2" = "MyApp description""shared_localizable_key3" = "Shared text1""shared_localizable_key4" = "Shared text2"


Хотелось бы иметь файл localizable_app2.strings, в котором были бы ключи
"shared_localizable_key1" = "MyApp2 another title""shared_localizable_key2" = "MyApp2 another description""app2_screen1_key" = "Profile screen title"


Т.е. организовать в файлах локализации принцип наследования.

К сожалению Xcode не заточен под это, по-этому пришлось изобретать свой велосипед, который долго не хотел ехать из-за того, что Xcode то тут, то там вставлял палки в колеса.

Мы имеем проект с 18 таргетами и 12 языками. И это не шутка, проект действительно большой и такое количество таргетов там необходимо. Каждый раз, когда нам нужно добавить новый общий ключ для перевода, мы имеем дело с 216 файлами локализации. Это отнимает достаточно много времени. А добавление нового таргета приводит к тому, что нужно скопировать в него еще 12 localizable.strings. В общем в какой-то момент мы поняли, что так больше жить нельзя и нужно искать решение.

Не буду долго рассказывать про все методы, которые я успел опробовать в процессе, перейду сразу к рабочему решению.

Итак, для начала нам нужно было найти все общие ключи. Это можно сделать с помощью скрипта, не буду вдаваться в подробности, это достаточно тривиальная задача.

Далее, когда мы получили общий (базовый) файл локализации, а точнее 12 физических файлов, а так же набор файлов для каждого таргета, идем в Xcode, добавляем туда все файлы. При этом не прикрепляем файлы к какому-либо таргету, т.е. в правой панели в разделе Target Membership не должно быть отметок.



Эти отметки мы поставим только для файла, который будет результатом работы скрипта по сборке файлов.

Далее начинается тот самый велосипед:
  • Создаём в корне папку Localization, там будет лежать скрипт build_localization.py.
  • Создаём рядом со скриптом папку Localizable. В неё скрипт будет генерировать файлы localizable.strings.
  • Копируем в папку Localizable базовую локализацию.




Она нам нужна просто для того, чтобы корректно добавить ссылку на файлы в проект, и чтобы Xcode правильно их распознал. Иначе он не будет их использовать для поиска ключей. Например, если создать папку Localizable с правильно разложенными файлами localizable.strings внутри, и добавить в проект как ссылку на папку (create folder references), то не смотря ни на что Xcode не поймет, что мы дали ему ключи локализации. По-этому берем папку Localizable, перетаскиваем как группу (create group) и снимаем галочку copy items if needed, чтобы получилось как на картинке ниже.



Удаляем папку Localizable и вносим её в исключения для гита. Потому что результат работы скрипта нам в гите не нужен, он будет меняться для каждого таргета и засорять коммиты.

Теперь нам нужно добавить скрипт в фазу сборки. Для этого в Build Phases нажимаем New Run Script Phase и прописываем наш скрипт с параметрами.
python3 ${SRCROOT}/Localization/build_localization.py -b ${SRCROOT}/BaseLocalization" -s "${SRCROOT}/Target1Localization" -d "${SRCROOT}/Localization/Localizable"

b это папка с базовой локализацией, s локализация текущего таргета, d папка результата.

Перемещаем новую фазу вверх, она должна быть не ниже фазы Copy Bundle Resources. Т.е. сначала скрипт генерирует файлы, а уже потом они забираются в бандл.



Теперь важно сообщить Xcode, что в процессе выполнения скрипта меняются файлы, иначе при сборке он будет выкидывать ошибку, что не смог найти файлы. Причем ошибка будет только на чистой сборке, и не сразу будет понятно в чем проблема. В фазе сборки добавляем в output files все файлы локализации



Это нужно проделать для каждого таргета. Проще всего это сделать открыв проект с помощью текстового редактора, потому что Xcode не сумеет скопировать/вставить фазу между таргетами. Соответственно параметр скрипта -s для каждого таргета будет свой.

Теперь при каждой сборке скрипт будет брать базовый файл локализации, накатывать на него изменения из файла таргета (добавлять, перезаписывать ключи) и генерировать локализацию в папку Localizable, которую iOS будет использовать для поиска ключей.

В целом получили то, что и планировалось при реализации механизма наследования:
  • Общие ключи лежат в одном файле и не мешаются в других. Время на процесс внесения новых ключей сокращено в 18! раз.
  • Ключи, относящиеся к конкретному таргету, лежат в соответствующем файле.
  • Размер файлов значительно снизился. Избавились от захламления повторяющимися строками.
  • Процесс добавления нового языка в проект так же значительно упрощён.
  • При создании нового таргета не нужно копировать локализацию с кучей ненужных строк. Создаём новый файл localizable.strings и добавляем туда только нужное для этого таргета.
  • Если решили реанимировать старый таргет, то со строками вообще ни чего делать не надо, всё подтянется из базового файла.
  • Скрипт не захламляет гит, результат работы остаётся локально и его можно безболезненно удалить.


Готовый скрипт можно взять тут: github.com/iBlacksus/iOSLocalizationInheritance
Не претендую на идеальность скрипта, пул-реквесты приветствуются.
Подробнее..

Перевод Связанные не явные выражения в Swift 5.4

14.04.2021 00:14:01 | Автор: admin

В Swift 5.4: не явные выражения для членов классов (также известные как точечный синтаксис) теперь могут использоваться даже при обращении к свойству или методу в результате такого выражения, пока окончательный тип возвращаемого значения остается прежним.

Обратите внимание, что на момент написания статьи Swift 5.4 находится в стадии бета-тестирования в качестве части Xcode 12.5.

На практике это означает, что всякий раз, когда мы создаем объект или значение с помощью статического API, или при обращении обращении к перечисляемому типу, мы теперь можем напрямую вызвать метод или свойство в этом экземпляре класса, и компилятор по-прежнему сможет вывести тот тип, к которому мы обращаемся.

Например, при создании экземпляра UIColor с использованием одного из встроенных статических API-интерфейсов, предоставленных в качестве части системы, мы теперь можем легко изменить альфа-компонент такого цвета без необходимости явно ссылаться на сам UIColor в таких ситуациях, как:

// In Swift 5.3 and earlier, an explicit type reference is always// required when dealing with chained expressions:let view = UIView()view.backgroundColor = UIColor.blue.withAlphaComponent(0.5)...// In Swift 5.4, the type of our expression can now be inferred:let view = UIView()view.backgroundColor = .blue.withAlphaComponent(0.5)...

Конечно, вышеупомянутый подход также работает при использовании наших собственных статических API, например, любых пользовательских определений UIColor, которые мы добавили с помощью расширения:

extension UIColor {    static var chiliRed: UIColor {        UIColor(red: 0.89, green: 0.24, blue: 0.16, alpha: 1)    }}let view = UIView()view.backgroundColor = .chiliRed.withAlphaComponent(0.5)...

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

extension ImageFilter {    static var dramatic: Self {        ImageFilter(            name: "Dramatic",            icon: .drama,            transforms: [                .portrait(withZoomMultipler: 2.1),                .contrastBoost,                .grayScale(withBrightness: .dark)            ]        )    }}

При использовании Swift 5.4 (или более поздних версий в будущем) мы могли бы добавить что-то вроде такого, что позволяет нам легко объединить два экземпляра ImageFilter, путем объединения их .transforms:

extension ImageFilter {    func combined(with filter: Self) -> Self {        var newFilter = self        newFilter.transforms += filter.transforms        return newFilter    }}

С учетом вышеизложенного теперь мы сможем работать с фильтрами, которые комбинируются динамически. Теперь мы можем использовать тот же упрощенный точечный синтаксис, который раньше мог быть применен только к предварительно определенному фильтру.

let filtered = image.withFilter(.dramatic.combined(with: .invert))

Довольно круто! Я собираюсь продолжить изучение того, какие API-интерфейсы позволяет мне разрабатывать эта новая языковая функциональность, и, конечно же, продолжу делиться своими знаниями с вами в будущих статьях.

Подробнее..

Как мы подружили Flutter с CallKit Call Directory

21.04.2021 14:19:32 | Автор: admin

Flutter+CallKitCallDirectory=Love


Привет!


В этом лонгриде я расскажу о том, как мы в Voximplant пришли к реализации собственного Flutter плагина для использования CallKit во Flutter приложении, и в итоге оказались первыми, кто сделал поддержку блокировки/определения номеров через Call Directory для Flutter.


Что такое CallKit


Apple CallKit это фреймворк для интеграции звонков стороннего приложения в систему.


Если звонок из стороннего приложения отображается как нативный, то тут задействован CallKit. Если звонок из стороннего приложения отображается в списке звонков системного приложения Phone тоже CallKit. Сторонние приложения, выступающие в качестве определителя номера CallKit. Звонки из сторонних приложений, которые не могут пробиться через режим Не беспокоить ну вы поняли.



CallKit предоставляет сторонним разработчикам системный UI для отображения звонков



А что с CallKit на Flutter?


CallKit является частью iOS SDK, во Flutter он не представлен, однако доступ к нему из Flutter возможен путём взаимодействия с нативным кодом. Для использования функциональности этого фреймворка потребуется подключить сторонний плагин, инкапсулирующий взаимодействие Flutter с iOS, или реализовывать всё самостоятельно, например, так:



Пример реализации CallKit сервиса для Flutter, где код iOS приложения (platform code) связывает приложение Flutter с системой




Готовые решения с CallKit на Flutter


Итак, нам потребовалось интегрировать наше Flutter приложение для VoIP звонков с системой. Первым делом мы рассмотрели большинство из существующих сторонних решений и на какое-то время воспользовались одним из них. Однако этот и остальные доступные варианты вели по пути наименьшего сопротивления, которому сопутствовали характерные проблемы.


Существующие плагины частично или полностью оборачивали CallKit API в собственный высокоуровневый API. Таким образом терялась гибкость, а некоторые возможности становились недоступными. Из-за собственной реализации архитектуры и интерфейсов такие плагины содержали свои баги. Документация хромала или отсутствовала, а авторы некоторых из них прекратили поддержку почти сразу, что особенно опасно на быстроразвивающемся Flutter.



Как мы пришли к созданию своего решения


Для простых сценариев на первое время это было приемлемо, однако, как только появлялся специфичный кейс, тут же появлялись неудобства. Приходилось изучать исходный код, чтобы выяснить, как именно этот плагин взаимодействует с CallKit. В конце концов могло обнаружиться, что реализовать требуемое вообще не выйдет из-за ограничений, накладываемых высокоуровневым API.


Мы задумались о том, чтобы реализовать своё решение с учетом этих недостатков.


Хотелось пойти по пути сохранения архитектуры и интерфейсов CallKit. Таким образом оставить пользователям всю гибкость, возможность использовать оригинальную документацию и оградить от потенциальных багов в собственной реализации.



Наша Реализация


Нам удалось перенести всё CallKit API на Dart с сохранением иерархии классов и механизмов взаимодействия с ними.



Наш плагин закрывает собой всю работу с платформой, при этом предоставляет идентичный интерфейс


Коммуникация между Flutter и iOS асинхронна, так что пришлось поломать голову с реализацией некоторых деталей. Основной сложностью был функционал, требующий синхронного взаимодействия с той или иной стороны.


Например, нативное CallKit API CXProviderDelegate.provider(_:execute:) требует синхронно возвращать Bool значение:


optional func provider(_ provider: CXProvider,     execute transaction: CXTransaction) -> Bool

Этот метод вызывается каждый раз, когда нужно обработать новую транзакцию CXTransaction. Можно вернуть true, чтобы обработать транзакцию самостоятельно и уведомить об этом систему. Вернув false, получим дефолтное поведение, при котором для каждого CXAction, содержащегося в транзакции, будет вызван соответствующий метод обработчик в CXProviderDelegate.


Для переноса этого API в плагин требовалось объявить его в Dart коде так, чтобы пользователь мог управлять этим поведением, несмотря на асинхронный характер обмена данными между платформами. Возвращая в нативном коде значение true, мы смогли перенести управление транзакциями в Dart код, где выполняем ручную или автоматическую обработку CXTransaction в зависимости от значения, полученного от пользователя.


Проблемы с асинхронностью возникают и в нативной части. Например, есть iOS фреймворк PushKit, он не является частью CallKit, но часто они используются вместе, так что интеграция с ним была необходима. При получении VoIP пуша требуется немедленно уведомить CallKit о входящем звонке в нативном коде, в противном случае приложение упадет. Для обработки этого нюанса мы решили дать возможность репортить входящие звонки напрямую в CallKit из нативного кода без асинхронного крюка в виде Flutter. В итоге для этой интеграции реализовали несколько хелперов в нативной части плагина (доступны через FlutterCallkitPlugin iOS класс) и несколько на стороне Flutter (доступны через FCXPlugin Dart класс).


Дополнительные возможности плагина мы объявили в его собственном классе, чтобы отделить интерфейс плагина от интерфейса CallKit.

Как зарепортить входящий звонок напрямую в CallKit

При получении VoIP пуша вызывается один из методов PKPushRegistryDelegate.pushRegistry(_: didReceiveIncomingPushWith:). Здесь необходимо создать экземпляр CXProvider и вызвать reportNewIncomingCall для уведомления CallKit о звонке. Так как для дальнейшей работы со звонком необходим тот же экземпляр провайдера, мы добавили метод FlutterCallkitPlugin.reportNewIncomingCallWithUUID с нативной стороны плагина. При его вызове плагин сам зарепортит звонок в CXProvider, а так же вызовет FCXPlugin.didDisplayIncomingCall хендлер на стороне Dart для продолжения работы со звонком.


func pushRegistry(_ registry: PKPushRegistry,                  didReceiveIncomingPushWith payload: PKPushPayload,                  for type: PKPushType,                  completion: @escaping () -> Void) {    // Достаем необходимые данные из пуша    guard let uuidString = payload["UUID"] as? String,        let uuid = UUID(uuidString: uuidString),        let localizedName = payload["identifier"] as? String    else {        return    }    let callUpdate = CXCallUpdate()    callUpdate.localizedCallerName = localizedName    let configuration = CXProviderConfiguration(        localizedName: "ExampleLocalizedName"    )        // Репортим звонок в плагин, а он зарепортит его в CallKit    FlutterCallkitPlugin.sharedInstance.reportNewIncomingCall(        with: uuid,        callUpdate: callUpdate,        providerConfiguration: configuration,        pushProcessingCompletion: completion    )}


Подводя итог: главной фишкой нашего плагина является то, что его использование на Flutter практически не отличается от использования нативного CallKit на iOS.


One more thing


Но оставалось ещё кое-что в Apple CallKit, что мы не реализовали у себя (и не реализовал никто в доступных сторонних решениях). Это поддержка Call Directory App Extension.



Что такое Call Directory


CallKit умеет блокировать и определять номера, доступ к этим возможностям для разработчиков открыт через специальное системное расширение Call Directory. Подробнее про iOS app extensions можно почитать в App Extension Programming Guide.



Call Directory app extension позволяет блокировать и/или идентифицировать номера


Если вкратце, то это отдельный таргет iOS проекта, который запускается независимо от основного приложения по требованию системы.


Например, при получении входящего звонка iOS пытается определить или найти звонящего в списке заблокированных стандартными средствами. Если номер не был найден, система может запросить данные у доступных Call Directory расширений, чтобы так или иначе обработать звонок. В этот момент расширение должно эти номера достать из некого хранилища номеров. Само приложение может заполнять это хранилище номерами из своих баз в любое время. Таким образом, взаимодействия между расширением и приложением нет, обмен данными происходит через общее хранилище.



Пример архитектуры для реализации Call Directory


Примеры с передачей номеров в Call Directory уже есть на хабре: раз и два.


Подробнее про iOS App Extensions: App Extension Programming Guide.



Call Directory Extension на Flutter


Не так давно нам написал пользователь с запросом на добавление поддержки Call Directory. Начав изучать возможность реализации этой фичи, мы выяснили, что сделать Flutter API без необходимости написания пользователями нативного кода не выйдет. Проблема заключается в том, что, как было сказано выше, Call Directory работает в расширении. Оно запускается системой, работает очень короткое время и не зависит от приложения (и в том числе от Flutter). Таким образом, для поддержки этого функционала пользователю плагина так или иначе потребуется реализовать app extension и хранилище данных самостоятельно.



Пример работы с Call Directory во Flutter приложении



Принятое решение


Несмотря на сложности с нативным кодом, мы твёрдо решили сделать использование Call Directory максимально удобным для пользователей нашего фреймворка.


Проверив возможность работы такого расширения в связке с Flutter приложением, мы принялись за проектирование. Решение должно было сохранить все Call Directory Manager API, а также требовать от пользователя минимум написания нативного кода и быть удобным для взаимодействия через Flutter.


Так мы сделали версию 1.2.0 с поддержкой Call Directory Extension.



Как мы реализовывали Call Directory для Flutter


Итак, для реализации этого функционала требовалось учесть несколько аспектов:


  • Перенести интерфейс класса CXCallDirectoryManager (CallKit объект позволяющий управлять Call Directory)
  • Решить, что делать с app extension и хранилищем номеров для него
  • Создать удобный способ передачи данных из Dart в натив и обратно для управления списками номеров из Flutter приложения


Перенос интерфейсов CXCallDirectoryManager во Flutter


Код, приведенный в статье, был специально упрощен для облегчения восприятия, полную версию кода можно найти по ссылкам в конце статьи. Для реализации плагина мы использовали Objective-C, так как он был выбран основным в проекте ранее. Интерфейсы CallKit представлены на Swift для простоты.


Интерфейс


Первым делом посмотрим, что конкретно требуется перенести:


extension CXCallDirectoryManager {    public enum EnabledStatus : Int {        case unknown = 0        case disabled = 1        case enabled = 2    }}open class CXCallDirectoryManager : NSObject {    open class var sharedInstance: CXCallDirectoryManager { get }    open func reloadExtension(        withIdentifier identifier: String,        completionHandler completion: ((Error?) -> Void)? = nil    )    open func getEnabledStatusForExtension(        withIdentifier identifier: String,        completionHandler completion: @escaping (CXCallDirectoryManager.EnabledStatus, Error?) -> Void    )    open func openSettings(        completionHandler completion: ((Error?) -> Void)? = nil    )}

Воссоздадим аналог CXCallDirectoryManager.EnabledStatus энама в Dart:


enum FCXCallDirectoryManagerEnabledStatus {  unknown,  disabled,  enabled}

Теперь можно объявить класс и методы. Необходимости в sharedInstance в нашем интерфейсе нет, так что сделаем обычный Dart класс со static методами:


class FCXCallDirectoryManager {  static Future<void> reloadExtension(String extensionIdentifier) async { }  static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(    String extensionIdentifier,  ) async { }  static Future<void> openSettings() async { }}

Сохранение API важно, но так же важно учитывать платформенные и языковые code-style, чтобы использование интерфейса было понятно и удобно для пользователей плагина.


Для API в Dart мы использовали более короткое название без слов-связок (длинное название пришло из objective-C) и заменили completion блок на Future. Future является стандартным механизмом, используемым для получения результата выполнения асинхронных методов в Dart. Мы также возвращаем Future из большинства Dart методов плагина, потому что коммуникация с нативным кодом происходит асинхронно.


Было getEnabledStatusForExtension(withIdentifier:completionHandler:)


Стало Future getEnabledStatus(extensionIdentifier)




Реализация


Для коммуникации между Flutter и iOS будем использовать FlutterMethodChannel.


Подробнее про особенности работы этого канала связи можно почитать здесь.



On the Flutter side


Создадим объект MethodChannel:


const MethodChannel _methodChannel =  const MethodChannel('plugins.voximplant.com/flutter_callkit');


On the iOS side


Первым делом iOS класс плагина нужно подписать на протокол FlutterPlugin, чтобы иметь возможность взаимодействовать с Flutter:


@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>@end

При инициализации плагина создадим FlutterMethodChannel с таким же идентификатором, что мы использовали выше:


+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {    FlutterMethodChannel *channel        = [FlutterMethodChannel           methodChannelWithName:@"plugins.voximplant.com/flutter_callkit"          binaryMessenger:[registrar messenger]];    FlutterCallkitPlugin *instance         = [FlutterCallkitPlugin sharedPluginWithRegistrar:registrar];    [registrar addMethodCallDelegate:instance channel:channel];}

Теперь можно использовать этот канал для вызова iOS методов из Flutter.



Рассмотрим подробно реализацию методов в Dart и нативной части плагина на примере getEnabledStatus.



On the Flutter side


Реализация на Dart будет максимально проста и будет заключаться в вызове MethodChannel.invokeMethod с необходимыми аргументами, а также в обработке результата этого вызова.


Про MethodChannel

MethodChannel API позволяет асинхронно получить результат вызова из нативного кода посредством Future, но накладывает ограничения на передаваемые типы данных.




Итак, нам потребуется передать имя метода (его будем использовать в нативном коде для того, чтобы идентифицировать вызов) и аргумент extensionIdentifier в MethodChannel.invokeMethod, а затем преобразовать результат из простейшего типа int в FCXCallDirectoryManagerEnabledStatus. На случай ошибки в нативном коде следует обработать PlatformException.


static Future<FCXCallDirectoryManagerEnabledStatus> getEnabledStatus(  String extensionIdentifier,) async {  try {    // Воспользуемся объектом MethodChannel для вызова    // соответствующего метода в платформенном коде    // с аргументом extensionIdentifier.    int index = await _methodChannel.invokeMethod(      'Plugin.getEnabledStatus',      extensionIdentifier,    );    // Преобразуем результат в энам     // FCXCallDirectoryManagerEnabledStatus    // и вернем его значение пользователю    return FCXCallDirectoryManagerEnabledStatus.values[index];  } on PlatformException catch (e) {    // Если что-то пошло не так, обернем ошибку в собственный тип     // и отдадим пользователю    throw FCXException(e.code, e.message);  }}

Обратите внимание на идентификатор метода который мы использовали:


Plugin.getEnabledStatus


Слово перед точкой используется, для определения модуля ответственного за тот или иной метод.


getEnabledStatus идентично названию метода во Flutter, а не в iOS (или Android).




On the iOS side


Теперь переместимся в платформенный код и реализуем бэкенд для этого метода.


Вызовы через FlutterMethodChannel попадают в метод handleMethodCall:result:.


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


- (void)handleMethodCall:(FlutterMethodCall*)call                  result:(FlutterResult)result {    // Вызовы из Flutter можно идентифицировать по названию,    // которое передается в `FlutterMethodCall.method` проперти    if ([@"Plugin.getEnabledStatus" isEqualToString:call.method]) {        // При передаче аргументов с помощью MethodChannel,         // они упаковываются в `FlutterMethodCall.arguments`        // Извлечем extensionIdentifier, который         // мы передали сюда ранее из Flutter кода        NSString *extensionIdentifier = call.arguments;        if (isNull(extensionIdentifier)) {            // Если аргументы не валидны, вернём ошибку через             // `result` обработчик            // Ошибка должна быть упакована в `FlutterError`            // Она вылетит в виде PlatformException в Dart коде            result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);            return;}        // Теперь, когда метод обнаружен,        // а аргументы извлечены и провалидированы,         // можно реализовать саму логику        // Для взаимодействия с этой функциональностью CallKit // потребуется экземпляр CallDirectoryManager        CXCallDirectoryManager *manager             = CXCallDirectoryManager.sharedInstance;        // Вызываем метод CallDirectoryManager        // с требуемой функциональностью        // и ожидаем результата        [manager             getEnabledStatusForExtensionWithIdentifier:extensionIdentifier            completionHandler:^(CXCallDirectoryEnabledStatus status,                                            NSError * _Nullable error) {            // completion с результатом вызова запустился,             // можем пробросить результат в Dart            // предварительно сконвертировав его в подходящие типы,             // так как через MethodChannel можно передавать            // лишь некоторые определенные типы данных.            if (error) {                // Ошибки передаются упакованные в `FlutterError`                result([FlutterError errorFromCallKitError:error]);            } else {                // Номера передаются упакованные в `NSNumber`                // Так как этот энам представлен значениями `NSInteger`,                 // выполним требуемое преобразование                result([self convertEnableStatusToNumber:enabledStatus]);            }}];    }}


По аналогии реализуем оставшиеся два метода FCXCallDirectoryManager



On the Flutter side


static Future<void> reloadExtension(String extensionIdentifier) async {  try {    // Задаем идентификатор, передаем аргумент     // и вызываем платформенный метод    await _methodChannel.invokeMethod(      'Plugin.reloadExtension',      extensionIdentifier,    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}static Future<void> openSettings() async {  try {    // А этот метод не принимает аргументов     await _methodChannel.invokeMethod(      'Plugin.openSettings',    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.reloadExtension" isEqualToString:call.method]) {    NSString *extensionIdentifier = call.arguments;    if (isNull(extensionIdentifier)) {        result([FlutterError errorInvalidArguments:@"extensionIdentifier must not be null"]);        return;    }    CXCallDirectoryManager *manager         = CXCallDirectoryManager.sharedInstance;    [manager         reloadExtensionWithIdentifier:extensionIdentifier        completionHandler:^(NSError * _Nullable error) {        if (error) {            result([FlutterError errorFromCallKitError:error]);        } else {            result(nil);        }    }];}if ([@"Plugin.openSettings" isEqualToString:call.method]) {    if (@available(iOS 13.4, *)) {        CXCallDirectoryManager *manager             = CXCallDirectoryManager.sharedInstance;        [manager             openSettingsWithCompletionHandler:^(NSError * _Nullable error) {            if (error) {                result([FlutterError errorFromCallKitError:error]);            } else {                result(nil);            }        }];    } else {        result([FlutterError errorLowiOSVersionWithMinimal:@"13.4"]);    }}


Готово, CallDirectoryManager реализован и может быть использован.


Подробнее про Platform-Flutter взаимодействие



App Extension и хранилище номеров


Так как из-за нахождения Call Directory в iOS расширении мы не сможем предоставить его реализацию с плагином, а работа с платформенным кодом обычно непривычна для Flutter разработчиков, не знакомых с нативной разработкой, постараемся по максимуму помочь им с помощью Документации!


Реализуем полноценный пример app extension и хранилища и подключим их к example app нашего плагина.


В качестве простейшего варианта хранилища используем UserDefaults, которые обернем в propertyWrapper.


Примерно так выглядит интерфейс нашего хранилища:


// Доступ к хранилищу из iOS приложения@UIApplicationMainfinal class AppDelegate: FlutterAppDelegate {    @UserDefault("blockedNumbers", defaultValue: [])    private var blockedNumbers: [BlockableNumber]    @UserDefault("identifiedNumbers", defaultValue: [])    private var identifiedNumbers: [IdentifiableNumber]}// Доступ к хранилищу из app extensionfinal class CallDirectoryHandler: CXCallDirectoryProvider {    @UserDefault("blockedNumbers", defaultValue: [])    private var blockedNumbers: [BlockableNumber]    @UserDefault("identifiedNumbers", defaultValue: [])    private var identifiedNumbers: [IdentifiableNumber]    @NullableUserDefault("lastUpdate")    private var lastUpdate: Date?}


Код имплементации хранилища:


UserDefaults


Код iOS приложения:


iOS App Delegate


Код iOS расширения:


iOS App Extension


Обратите внимание, что примеры хранилища и расширения это не часть плагина, а часть example приложения, идущего в комплекте с ним.


Передача номеров из Flutter в iOS и обратно


Итак, app extension настроен и связан с хранилищем, необходимые методы CallDirectoryManager реализованы, осталась последняя деталь научиться передавать номера из Flutter в платформенное хранилище или, наоборот, запрашивать номера оттуда.


Наиболее простым вариантом кажется взвалить передачу данных на пользователя плагина, тогда ему придется самостоятельно организовывать MethodChannel или использовать другие сторонние решения по управлению хранилищем. И, безусловно, кому-то это даже подойдет! :) А для остальных сделаем простое и удобное API, чтобы пробрасывать номера прямо через наш фреймворк. Этот функционал будем делать опциональным, чтобы не ограничивать тех, кому удобнее использовать свои способы передачи данных.



Интерфейс


Посмотрим, какие интерфейсы могут понадобиться:


  • Добавление блокируемых/идентифицируемых номеров в хранилище
  • Удаление блокируемых/идентифицируемых номеров из хранилища
  • Запрос блокируемых/идентифицируемых номеров из хранилища


On the Flutter side


Для методов-хелперов мы ранее решили использовать классы плагина FCXPlugin (Flutter) и FlutterCallkitPlugin (iOS). Однако Call Directory является узкоспециализированным функционалом, который используется далеко не в каждом проекте. Поэтому хотелось вынести это в отдельный файл, но оставить доступ через объект класса FCXPlugin, для этого подойдет extension:


extension FCXPlugin_CallDirectoryExtension on FCXPlugin {  Future<List<FCXCallDirectoryPhoneNumber>> getBlockedPhoneNumbers()    async { }  Future<void> addBlockedPhoneNumbers(    List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeBlockedPhoneNumbers(List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeAllBlockedPhoneNumbers() async { }  Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers()    async { }  Future<void> addIdentifiablePhoneNumbers(List<FCXIdentifiablePhoneNumber> numbers,  ) async { }  Future<void> removeIdentifiablePhoneNumbers(List<FCXCallDirectoryPhoneNumber> numbers,  ) async { }  Future<void> removeAllIdentifiablePhoneNumbers() async { }}


On the iOS side


Чтобы со стороны Flutter получить доступ к номерам, которые находятся в неком хранилище на стороне iOS, пользователю плагина нужно будет как-то связать свою базу номеров с плагином. Для этого дадим ему такой интерфейс:



@interface FlutterCallkitPlugin : NSObject<FlutterPlugin>@property(strong, nonatomic, nullable)NSArray<FCXCallDirectoryPhoneNumber *> *(^getBlockedPhoneNumbers)(void);@property(strong, nonatomic, nullable)void(^didAddBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveBlockedPhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveAllBlockedPhoneNumbers)(void);@property(strong, nonatomic, nullable)NSArray<FCXIdentifiablePhoneNumber *> *(^getIdentifiablePhoneNumbers)(void);@property(strong, nonatomic, nullable)void(^didAddIdentifiablePhoneNumbers)(NSArray<FCXIdentifiablePhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveIdentifiablePhoneNumbers)(NSArray<FCXCallDirectoryPhoneNumber *> *numbers);@property(strong, nonatomic, nullable)void(^didRemoveAllIdentifiablePhoneNumbers)(void);@end


Для каждого типа взаимодействия с хранилищем предусмотрен свой обработчик, который будет вызываться нашим фреймворком каждый раз, когда с Flutter стороны был вызван соответствующий хелпер-метод.


Обработчики опциональны, что позволяет использовать лишь необходимый минимум или вовсе не брать эту функциональность, а воспользоваться собственным решением для передачи номеров.


Реализация


Теперь реализуем связь между объявленными методами-хелперами во Flutter и обработчиками в iOS.


Методов много, а работают они +- одинаково, поэтому будем рассматривать два из них с противоположным направлением движения данных.

Get identifiable numbers



On the Flutter side


Future<List<FCXIdentifiablePhoneNumber>> getIdentifiablePhoneNumbers() async {  try {    // Вызываем платформенный метод и сохраняем результат    List<dynamic> numbers = await _methodChannel.invokeMethod(      'Plugin.getIdentifiablePhoneNumbers',    );    // Типизируем результат и возвращаем пользователю    return numbers      .map(        (f) => FCXIdentifiablePhoneNumber(f['number'], label: f['label']))      .toList();  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.getIdentifiablePhoneNumbers" isEqualToString:call.method]) {    if (!self.getIdentifiablePhoneNumbers) {        // Проверяем существует-ли обработчик,        // если нет  возвращаем ошибку        result([FlutterError errorHandlerIsNotRegistered:@"getIdentifiablePhoneNumbers"]);        return;    }    // Используя обработчик, запрашиваем номера у пользователя    NSArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers        = self.getIdentifiablePhoneNumbers();    NSMutableArray<NSDictionary *> *phoneNumbers        = [NSMutableArray arrayWithCapacity:identifiableNumbers.count];    // Оборачиваем каждый номер в словарь,     // чтобы иметь возможность передать их через MethodChannel     for (FCXIdentifiablePhoneNumber *identifiableNumber in identifiableNumbers) {        NSMutableDictionary *dictionary             = [NSMutableDictionary dictionary];        dictionary[@"number"]             = [NSNumber numberWithLongLong:identifiableNumber.number];        dictionary[@"label"]             = identifiableNumber.label;        [phoneNumbers addObject:dictionary];    }    // Отправляем номера во Flutter    result(phoneNumbers);}


Add identifiable numbers



On the Flutter side


Future<void> addIdentifiablePhoneNumbers(  List<FCXIdentifiablePhoneNumber> numbers,) async {  try {    // Готовим номера для передачи через MethodChannel    List<Map> arguments = numbers.map((f) => f._toMap()).toList();    // Отправляем номера в нативный код    await _methodChannel.invokeMethod(      'Plugin.addIdentifiablePhoneNumbers',      arguments    );  } on PlatformException catch (e) {    throw FCXException(e.code, e.message);  }}


On the iOS side


if ([@"Plugin.addIdentifiablePhoneNumbers" isEqualToString:call.method]) {    if (!self.didAddIdentifiablePhoneNumbers) {        // Проверяем существует-ли обработчик,        // если нет  возвращаем ошибку        result([FlutterError errorHandlerIsNotRegistered:@"didAddIdentifiablePhoneNumbers"]);        return;    }    // Достаем переданные в аргументах номера    NSArray<NSDictionary *> *numbers = call.arguments;    if (isNull(numbers)) {        // Проверяем их валидность        result([FlutterError errorInvalidArguments:@"numbers must not be null"]);        return;    }    NSMutableArray<FCXIdentifiablePhoneNumber *> *identifiableNumbers        = [NSMutableArray array];    // Типизируем номера    for (NSDictionary *obj in numbers) {        NSNumber *number = obj[@"number"];        __auto_type identifiableNumber            = [[FCXIdentifiablePhoneNumber alloc] initWithNumber:number.longLongValue                                                                                     label:obj[@"label"]];        [identifiableNumbers addObject:identifiableNumber];    }    // Отдаём типизированные номера в обработчик пользователю    self.didAddIdentifiablePhoneNumbers(identifiableNumbers);    // Сообщаем во Flutter о завершении операции    result(nil);}


Остальные методы реализуются по аналогии, полный код:




Примеры использования


Теперь переместимся на сторону пользователя получившегося плагина и посмотрим, как он может воспользоваться нашими интерфейсами.



Reload extension


Метод reloadExtension(withIdentifier:completionHandler:) используется для перезагрузки расширения Call Directory. Это может потребоваться, например, после добавления новых номеров в хранилище, чтобы они попали в CallKit.


Использование идентично нативному CallKit API: обращаемся к FCXCallDirectoryManager и запрашиваем перезагрузку по заданному extensionIdentifier:


final String _extensionID =  'com.voximplant.flutterCallkit.example.CallDirectoryExtension';Future<void> reloadExtension() async {  await FCXCallDirectoryManager.reloadExtension(_extensionID);}


Get identified numbers



On the Flutter side


Запрашиваем список идентифицируемых номеров через класс плагина из Flutter:


final FCXPlugin _plugin = FCXPlugin();Future<List<FCXIdentifiablePhoneNumber>> getIdentifiedNumbers() async {  return await _plugin.getIdentifiablePhoneNumbers();}


On the iOS side


Добавляем обработчик getIdentifiablePhoneNumbers, который плагин использует для передачи заданных номеров во Flutter. Будем передавать в него номера из нашего хранилища identifiedNumbers:


private let callKitPlugin = FlutterCallkitPlugin.sharedInstance@UserDefault("identifiedNumbers", defaultValue: [])private var identifiedNumbers: [IdentifiableNumber]// Добавляем обработчик событий запроса номеровcallKitPlugin.getIdentifiablePhoneNumbers = { [weak self] in    guard let self = self else { return [] }    // Возвращаем номера из хранилища в обработчик    return self.identifiedNumbers.map {        FCXIdentifiablePhoneNumber(number: $0.number, label: $0.label)    }}


Теперь номера из пользовательского хранилища будут попадать в обработчик, а из него через плагин во Flutter.



Add identified numbers



On the Flutter side


Передаем номера, которые хотим идентифицировать, в объект плагина:


final FCXPlugin _plugin = FCXPlugin();Future<void> addIdentifiedNumber(String number, String id) async {  int num = int.parse(number);  var phone = FCXIdentifiablePhoneNumber(num, label: id);  await _plugin.addIdentifiablePhoneNumbers([phone]);}


On the iOS side


Добавляем обработчик didAddIdentifiablePhoneNumbers, который плагин использует для уведомления платформенного кода о получении новых номеров из Flutter. В обработчике сохраняем полученные номера в хранилище номеров:


private let callKitPlugin = FlutterCallkitPlugin.sharedInstance@UserDefault("identifiedNumbers", defaultValue: [])private var identifiedNumbers: [IdentifiableNumber]// Добавляем обработчик событий добавления номеровcallKitPlugin.didAddIdentifiablePhoneNumbers = { [weak self] numbers in    guard let self = self else { return }    // Сохраняем в хранилище номера, переданные плагином в обработчик    self.identifiedNumbers.append(        contentsOf: numbers.map {            IdentifiableNumber(identifiableNumber: $0)        }    )    // Номера в Call Directory обязательно должны быть отсортированы    self.identifiedNumbers.sort()}


Теперь номера из Flutter будут попадать в плагин, из него в обработчик события, а оттуда в пользовательское хранилище номеров. При следующей перезагрузке Call Directory расширения они станут доступны CallKit для идентификации звонков.


Полные примеры:




Итог


У нас получилось дать возможность использовать CallKit Call Directory из Flutter!


Детали платформенных коммуникаций по прежнему скрыты в недрах плагина, нативное API сохранено, а необходимая к написанию пользовательская iOS реализация хорошо задокументирована.


Теперь во Flutter можно относительно просто блокировать и/или определять номера с помощью нативного Call Directory.



Пример работы с Call Directory в Flutter приложении с использованием flutter_callkit_voximplant



Результаты:


  • Интерфейс CallDirectoryManager полностью перенесен
  • Добавлен простой способ передачи номеров из Flutter кода в iOS, оставлена возможность использовать собственные решения передачи данных
  • Архитектура решения описана в README с визуальными схемами для лучшего понимания
  • Добавлен полноценный работоспособный example app, использующий всю функциональность Call Directory, реализующий пример платформенных модулей (таких как iOS расширение и хранилище данных)


Полезные ссылки


Source код flutter_callkit на GitHub


Example app код на GitHub


Полная документация по использованию Call Directory с flutter_callkit


CallKit Framework Documentation by Apple


App Extension Programming Guide by Apple


Writing custom platform-specific code by Flutter

Подробнее..

Модуляризация iOS-приложения Badoo борьба с последствиями

21.01.2021 20:07:17 | Автор: admin

В предыдущей статье я рассказывал о том, как мы выделили модуль чата в нашем приложении. Всё прошло успешно, и мы собирались распространить этот опыт начать тотальную модуляризацию в iOS-разработке Badoo. Даже презентовали подход продуктовым командам, командам, занимающимся тестированием и непрерывной интеграцией, и постепенно стали внедрять модуляризацию в наши процессы.

Мы сразу понимали, что будут сложности, поэтому не торопились и раскатывали решение поэтапно. Это помогло нам вовремя понять проблемы, о которых сегодня пойдёт речь.

В этой статье я расскажу:

  • как мы не потерялись в сложном графе зависимостей;

  • как спасли CI от чрезмерной нагрузки;

  • что делать, если с каждым новым модулем приложение запускается всё медленнее;

  • мониторинг каких показателей стоит предусмотреть и почему это необходимо.

Сложный граф зависимостей

Только когда количество модулей стало сильно расти, мы поняли, что связывание и поддержка развесистого графа зависимостей это действительно сложно. Наши представления о 50 модулях, отображённых в красивой иерархии, разбились о суровую реальность.

Так выглядел граф зависимостей Badoo к моменту, когда у нас было около 50 модулей:

Было потрачено немало усилий в попытках сделать визуализацию удобной и читабельной, но безрезультатно. Сложный граф помог нам понять несколько вещей:

  1. Визуализация всех зависимостей одновременно сложная и бессмысленная задача. При работе с графом нужно понимать, какая проблема ищется или решается и каких модулей она касается. Этим инструментом можно пользоваться эффективно только в рамках графа вокруг одного, максимум нескольких модулей.

  2. Сложности визуализации порождают сложности отладки. Найти фундаментальные проблемы в сложном графе зависимостей крайне непросто.

  3. Добавление зависимостей, особенно промежуточных (о них мы говорили в первой части), ресурсоёмкая задача, влекущая за собой дополнительную нагрузку на разработчиков и отвлекающая от решения продуктовых задач.

Мы поняли, что простой визуализацией не обойтись, работу с графом зависимостей нужно автоматизировать. Так появилась наша внутренняя утилита deps. Она была призвана решать наши новые задачи: искать проблемы в графе зависимостей, указывать на них разработчикам и исправлять типовые ошибки линковки.

Основные характеристики утилиты:

  • это консольное Swift-приложение;

  • работает с xcodeproj-файлами с помощью фреймворка XcodeProj;

  • понимает сторонние зависимости (мы не очень активно и охотно принимаем их в проект, но некоторые всё же используем; загружаются и собираются они через Carthage);

  • включена в процессы непрерывной интеграции;

  • знает о требованиях к нашему графу зависимостей и работает в соответствии с ними.

Последний пункт объясняет, почему не стоит надеяться на появление подобных утилит с открытым исходным кодом. Сегодня не существует универсальной хорошо описанной структуры проектов и их настроек, которая подойдёт всем. В разных компаниях могут отличаться различные параметры графа:

  • статическая или динамическая линковка;

  • инструменты поддержки сторонних зависимостей (Carthage, CocoaPods, Swift Package Manager);

  • хранение ресурсов в отдельных фреймворках или на уровне приложения;

  • и другие.

Поэтому, если вы смотрите в сторону 100+ модулей, на каком-то этапе вам, скорее всего, придётся задуматься о написании подобной утилиты.

Итак, для автоматизации работы с графом зависимостей мы разработали несколько команд:

  1. Doctor. Команда проверяет, все ли зависимости корректно связаны и встроены в приложение. После исполнения мы либо получаем список ошибок в графе (например, отсутствие чего-либо в фазе Link with binaries или Embedded frameworks), либо скрипт говорит, что всё хорошо и можно двигаться дальше.

  2. Fix. Развитие команды doctor. Эта команда в автоматическом режиме исправляет проблемы, найденные командой doctor.

  3. Add. Добавляет зависимость между модулями. Пока у вас простое небольшое приложение, добавление зависимости между двумя фреймворками кажется простой задачей. Но когда граф сложный и многоуровневый, а вы работаете с включёнными явными зависимостями, добавление нужных зависимостей становится задачей, которую вы не захотите из раза в раз делать руками. Благодаря команде add разработчики могут просто указать два названия фреймворков (зависимый и зависящий) и все фазы сборки заполнятся необходимыми зависимостями в соответствии с графом.

Впоследствии скрипт создания нового модуля по шаблону также стал частью утилиты deps. Что мы получили в итоге?

  1. Автоматизированную поддержку графа. Мы находим ошибки прямо в pre-commit hook, сохраняя стабильность и правильность графа и давая возможность разработчику в автоматическом режиме эти ошибки исправлять.

  2. Упрощённое редактирование. Добавить новый модуль или связи между модулями можно одной командой, что сильно упрощает их разработку.

Непрерывная интеграция не справлялась

Перед переходом на модуляризацию у нас было семь приложений, и мы придерживались стратегии проверь всё. Вне зависимости от того, что именно было изменено в нашем монорепозитории, мы просто пересобирали все приложения и прогоняли все тесты. Если всё было хорошо, то мы позволяли изменениям попасть в основную ветку разработки.

Модуляризация быстро дала нам понять, что этот подход плохо масштабируется. Количество модулей и количество CI-агентов, необходимых для распараллеливания проверки изменений, линейно связаны. Когда количество модулей увеличилось, увеличились и очереди на CI. Конечно, до какого-то момента можно просто покупать новые билд-агенты, но мы пошли более рациональным путём.

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

Мы безуспешно попробовали несколько подходов. Расскажу о них подробнее, чтобы вы не повторяли наши ошибки.

Очевидным решением было перестать собирать и проверять всё и всегда. Нужно было, чтобы CI проверял только то, что нужно проверить. Что не сработало:

  1. Вычисление изменений по структуре директорий. Мы пробовали положить в репозиторий файл с маппингом директория модуль, чтобы понимать, какие модули нужно проверить, но буквально через неделю из-за увеличившегося количества падений приложения на проде обнаружили, что файл был перенесён в новый модуль и изменён одновременно, а файл маппинга при этом обновлён не был. Контролировать обновление файла с маппингом автоматически не представлялось возможным и мы перешли к поиску иных решений.

  2. Запуск интеграционных проверок на CI ночью. В целом это неплохая идея, но разработчики не сказали нам за это спасибо. Регулярными стали ситуации, когда ты уходишь домой в надежде, что всё хорошо, а утром в корпоративном мессенджере получаешь сообщение от CI, что 25 проверок не прошли, и первое, что нужно делать, разбираться с ночными проблемами, которые, вероятно, ещё кого-то блокируют. В общем, мы не захотели портить завтраки людям и продолжили поиски оптимального решения.

  3. Разработчик указывает, что проверять. Этот эксперимент завершился быстрее всех буквально за пару дней. Благодаря ему мы узнали, что разработчики бывают двух типов:

    1. Те, кто на всякий случай проверяет всё.

    2. Те, кто уверен, что не мог ничего сломать.

Как вы поняли, из-за первых очереди на CI почти не становились меньше, а из-за вторых у нас ломалась основная ветка разработки.

В итоге мы вернулись к идее автоматизации вычисления изменений, но немного с другой стороны. У нас была утилита deps, которая знала про граф зависимостей и файлы проекта. А Git позволяла получить список изменённых файлов. Мы расширили deps командой affected, с помощью которой можно было получить список затронутых модулей, исходя из изменений, отражаемых системой контроля версий. Ещё более важно то, что она учитывала зависимости между модулями (если от затронутого модуля зависят другие модули, их тоже необходимо проверить, чтобы, например, в случае изменения интерфейса более низкого модуля верхний не перестал собираться).

Пример: изменения в блоках Регистрация и Аналитика на нашей схеме указывают на необходимость проверить также модули Чат, Sign In with Apple, Видеостриминг и само приложение.

Это был инструмент для CI. Но любой разработчик тоже имел возможность локально посмотреть, что он потенциально мог затронуть своими изменениями, чтобы проверить работоспособность зависящих модулей вручную.

В результате такой модернизации мы получили ряд бонусов:

  1. Мы проверяем в CI только то, что действительно было затронуто прямо или косвенно.

  2. Продолжительность CI-проверок перестала линейно зависеть от количества модулей.

  3. Разработчик понимает, что его изменения могут затронуть и где нужно быть осторожным.

  4. Механизм впоследствии был адаптирован не только для проверки безошибочной компиляции модулей, но и для запуска юнит-тестов затронутых модулей, что также существенно снизило нагрузку на CI и ускорило попадание изменений в мастер.

Ждём завершения Е2Е-тестов

Для приложения Badoo у нас есть более 2000 сквозных (end-to-end) тестов, которые его запускают и проходят по сценариям использования для проверки ожидаемых результатов. Если запустить все эти тесты на одной машине, то прогон всех сценариев займёт около 60 часов. Поэтому на CI все тесты запускаются параллельно насколько это позволяет количество свободных агентов.

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

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

Для каждого нового модуля отдельный скрипт в CI проверяет наличие модуля в этом маппинге, чтобы разработчики не забывали синхронизироваться с командой тестирования для пометки модуля необходимыми группами тестов.

Подобное решение едва ли можно назвать оптимальным, но всё же преимущества от его внедрения были ощутимы:

  1. Нагрузка на CI существенно снизилась. Чтобы не быть голословным, привожу график времени, которое задача на прогон сквозных тестов провела в очереди:

  2. Уменьшился шум инфраструктурных проблем (меньше запусков тестов меньше падений из-за зависших агентов, сломавшихся симуляторов, недостатка места и т. д.).

  3. Карта модулей и их тестов стала точкой синхронизации отделов разработки и тестирования. В рамках разработки программист и тестировщик обсуждают, например, какие из имеющихся групп тестов могут подойти для проверки в том числе нового модуля.

Медленный запуск приложения

Apple открыто говорит о том, что динамическое связывание замедляет запуск приложения, однако именно оно является вариантом по умолчанию для любого нового проекта, созданного в Xcode. Так выглядит реальный график времени запуска одного из наших проектов:

В середине графика видно резкое снижение времени. Причина переход на статическую линковку. С чем это связано? Инструмент динамической загрузки модулей dyld от Apple выполняет трудоёмкие задачи не совсем оптимальными способами, время исполнения которых линейно зависит от количества модулей. В этом и была основная причина замедления запуска нашего приложения: мы добавляли новые модули dyld работал всё медленнее (на графике синяя линия отражает количество добавляемых модулей).

После перехода на статическое связывание количество модулей перестало влиять на скорость запуска приложения, а с появлением iOS 13 Apple перешла на использование dyld3 для запуска приложений, что тоже поспособствовало ускорению этого процесса.

Стоит сказать, что статическая линковка несёт с собой и ряд ограничений:

  1. Одно из наиболее неудобных невозможность хранения ресурсов в статических модулях. Эта проблема частично решается в относительно новых XCFrameworks, но технологию пока что нельзя считать проверенной временем и полноценно готовой к Enterprise. Мы решили эту проблему созданием рядом с модулем отдельных бандлов, которые уже упаковываются в готовое приложение или тестовый бандл для прогона тестов. Обеспечением целостности графа при работе с ресурсами также занимается утилита deps, которая получила несколько новых правил.

  2. После перехода на статическую линковку нужно хорошенько протестировать приложение на предмет рантайм-падений. Чтобы исправить многие из них, вам просто придётся использовать не самые оптимальные параметры оптимизаций. Например, почти для всех Objective-C-модулей придётся включить флаг -all-load. Отмечу ещё раз, что решение всех этих проблем с вынесенными xcconfigами (про xcconfig в первой части) не было таким мучительным, каким могло бы быть.

Итак, мы побороли две основные проблемы, вынесли ресурсы в отдельные бандлы, поправили конфигурации сборки. В результате:

  • количество модулей в приложении перестало влиять на скорость его запуска;

  • размер приложения уменьшился примерно на 30%;

  • количество крашей уменьшилось в три раза за счёт ускорения запуска. iOS убивает приложения, если они долго запускаются, а на устройствах старше трёх лет со слабенькими батареями, где процессор давно не работает на максимуме, это реальные случаи, которых стало значительно меньше.

Цифры подскажут, куда двигаться дальше

Мы рассмотрели глобальные изменения, которые нам пришлось сделать, чтобы жить в мире и согласии в процессе модуляризации:

  • автоматизация работы с графом зависимостей: следите за графом, сделайте так, чтобы было легко понять, что от чего зависит и где на графе узкие места;

  • уменьшение нагрузки на CI за счёт фильтрации проверяемых модулей и умных тестов: не попадайтесь в ловушку прямой зависимости продолжительности CI-проверок от количества модулей;

  • статическая линковка: скорее всего, вам придётся перейти на статическое связывание, так как уже к 50-60 модулям регресс в скорости запуска приложения станет заметен не только вам, но и вашим менеджерам.

Я не говорил об этом напрямую, но, как вы могли заметить, у нас есть графики времени, которое задачи проводят в очереди CI, скорости запуска приложения и т. д. Необходимость всех изменений, описанных ранее, была обусловлена значениями важных при разработке приложения метрик. Какие бы преобразования процессов вы ни задумали, необходимо иметь возможность измерить их результаты. Если такой возможности нет, скорее всего, изменения не имеют смысла.

В нашем случае мы понимали, что изменения сильно затронут разработчиков, поэтому основная метрика, за которой мы следили и следим, это время сборки проекта на железе разработчика. Да, есть показатели времени чистой сборки с CI, но разработчики редко делают чистую сборку; они используют другое оборудование и т. д., то есть и конфигурации сборки, и их окружение другие.

Поэтому у нас есть график среднего времени сборки наших приложений на компьютерах разработчиков:

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

Ещё один интересный вывод, к которому мы пришли, получив подобную аналитику: медленно собирающиеся модули не всегда повод для оптимизации.

Мы построили график с расположенными по осям средней продолжительностью сборки и количеством сборок в день. Стало ясно, что среди самых долго собирающихся модулей есть такие, которые собираются всего лишь два раза в день, и в целом их влияние на общий опыт работы с проектом крайне мало. Есть же, наоборот, модули, сборка которых занимает порядка 10 секунд, но в день их собирают более 1500 раз: за ними нужно внимательно следить. В общем, старайтесь смотреть не на один модуль, а на картину в целом.

Кроме того, мы стали понимать, какое оборудование ещё подходит для работы над нашим проектом, а с каким пришло время прощаться.

Например, видно, что iMac Pro 5K 2017 года выпуска не лучшее железо для сборки Badoo, в то время как MacBook Pro 15 2018 года ещё вполне неплох.

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

Измеряем время сборки

Чтобы получать данные о продолжительности сборки на компьютерах разработчиков, мы создали специальное macOS-приложение Zuck. Оно сидит в статус-баре и следит за всеми xcactivitylog-файлами в DerivedData. xcactivitylog файлы, которые содержат ту же информацию, которую мы видим в билд-логах Xcode в непростом для парсинга формате от Apple. По ним можно понять, когда началась и закончилась сборка отдельного модуля и в какой последовательности они собирались.

В утилите есть white- и black-листы, так что мы отслеживаем только рабочие проекты. Если разработчик скачал демо-проект какой-то библиотеки с GitHub, мы не будем отправлять данные о её сборке куда-либо.

Информацию о сборке наших проектов мы передаем во внутреннюю систему аналитики, где имеется широкий инструментарий для построения графиков и анализа данных. Например, у нас есть инструмент Anomaly Detection, который предсказывает аномалии в виде слишком сильных отклонений от прогнозируемых значений. Если время сборки резко изменяется по сравнению с предыдущим днём, Core-команда получает уведомление и начинает разбираться, где и что пошло не так.

P. S. Мы дорабатываем Zuck, чтобы выпустить его в open source.

В целом измерение локального времени сборки даёт важные результаты:

  • мы измеряем влияние изменений на разработчиков;

  • имеем возможность сравнивать чистые и инкрементальные сборки;

  • знаем, что надо улучшить;

  • быстро получаем результаты экспериментов. Например, мы изменили настройки оптимизаций и уже на следующий день видим на графике, как это отразилось на жизни разработчиков. Если эксперимент не удался, мы быстро откатываем изменения до лучших времён.

Далее я приведу краткую сводку метрик, за которыми нужно следить, если вы начали двигаться в сторону модуляризации:

  1. Время запуска приложения. Последние версии Xcode предоставляют эту информацию в разделе Organizer. Метрика быстро укажет на появившиеся проблемы.

  2. Размер артефактов. Эта метрика поможет быстро обнаружить проблемы линковки. Возможны случаи, когда линковщик не сообщает о том, что, например, какой-то модуль дублировался. Однако об этом скажет увеличившийся размер скомпилированного приложения. Обращайте внимание на этот график. Проще всего подобную информацию получить из CI.

  3. Инфраструктурные показатели (время сборки, время задач в очереди CI и т. п.). Модуляризация скажется на многих процессах компании, но инфраструктура особенно важна, потому что её придётся масштабировать. Не попадайтесь в ловушку, как мы, когда до мержа в мастер изменениям приходилось находиться в очереди по пять-шесть часов.

Конечно, нет предела совершенству, но это основные показатели, которые позволят вам отслеживать самые критичные проблемы и ошибки. Некоторые метрики можно собирать и локально, если нет возможности инвестировать в сложные системы мониторинга: заведите локальные скрипты и хотя бы раз в неделю смотрите на основные показатели. Это поможет понять, в правильном ли вообще направлении вы движетесь.

Если вам кажется, что я сейчас сделаю вот так и по-любому станет лучше, но вы не можете это лучше измерить, стоит подождать с таким экспериментом. У вас ведь наверняка тоже есть менеджер, которому нужно будет как-то презентовать результаты своей работы.

Заключение

После моего рассказа у вас могло сложиться впечатление, что требуется очень много сил и ресурсов, чтобы построить в компании процесс модуляризации. Отчасти это правда, но я приведу небольшую статистику: на самом деле мы не такие уж и большие.

  1. Всего сейчас у нас работают 43 iOS-разработчика.

  2. Четыре из них в Core-команде.

  3. Сейчас у нас два основных приложения и N экспериментальных.

  4. Около 2 миллионов строк кода.

  5. Около 78% из них находятся в модулях.

Последняя цифра постепенно растёт: старые фичи и переписанный legacy-код потихоньку перетекают из основного таргета приложений в модули.

В двух статьях я рекламировал вам модуляризацию, но, конечно, у неё есть свои минусы:

  • усложнение процессов: вам придётся решить ряд вопросов в процессах как вашего департамента и рядовых iOS-разработчиков, так и во взаимодействии с другими департаментами: QA, CI, менеджерами продуктов и т. д.;

  • в процессе перехода на модуляризацию вскроются проблемы, которых вы не ждали, всё не предусмотреть: возможно, потребуются дополнительные ресурсы и других команд;

  • всё, что вы построите, будет нуждаться в дополнительной поддержке: процессы, новые внутренние инструменты кто-то должен будет за это отвечать;

  • приложения, базирующиеся на модулях, сильнее зависят друг от друга. Например, при изменении чего-то в модуле работы с базой данных для приложения Badoo через несколько минут можно получить от CI сообщение о том, что в приложении Bumble не прошли тесты.

Но хорошая новость заключается в том, что все эти минусы управляемы. Их не стоит бояться, поскольку это не те проблемы, которые невозможно решить. Нужно просто быть к ним готовыми.

О плюсах модуляризации уже было сказано, но я повторюсь:

  • гибкое масштабирование iOS-разработки: новые команды начинают работать в своих модулях, подход к созданию и поддержке которых унифицирован;

  • явные зоны ответственности: с одной стороны, есть конкретные люди, ответственные за конкретные участки кода, с другой в рамках команд возможна ротация, так как подходы общие, отличается лишь начинка модулей;

  • развитие инфраструктуры и мониторинга: это неизбежные улучшения, без которых вам будет очень сложно создать и поддерживать процесс модуляризации.

Напоследок скажу, что совсем не обязательно брать крупную часть приложения как первый модуль на старте внедрения модуляризации. Берите кусочки поменьше и попроще, экспериментируйте. Не стоит слепо следовать любому из примеров других компаний у всех разные подходы к разработке, и вероятность того, что вам полностью подойдёт чей-то опыт, небольшая. Обязательно развивайте инфраструктуру и мониторьте результаты. Успехов!

Подробнее..

Разделяй и властвуй. Модульное приложение из монолита на Objective-C и Swift

11.08.2020 14:11:07 | Автор: admin


Привет, Хабр! Меня зовут Василий Козлов, я iOS-техлид в Delivery Club, и застал проект в его монолитном виде. Признаюсь, что приложил руку к тому, борьбе с чем посвящена эта статья, но раскаялся и трансформировал своё сознание вместе с проектом.

Я хочу рассказать, как разбивал существующий проект на Objective-C и Swift на отдельные модули frameworkи. Согласно Apple, framework это директория определенной структуры.

Изначально мы поставили цель: обособить код, реализующий функцию чата для поддержки пользователей, и уменьшить длительность сборки. Это привело к полезным последствиям, которым сложно следовать, не имея привычки и существуя в монолитном мире одного проекта.

Неожиданно пресловутые принципы SOLID начали обретать очертания, а главное сама постановка задачи вынуждала организовывать код в соответствии с ними. Вынося какую-то сущность в отдельный модуль, вы автоматически сталкиваетесь со всеми её зависимостями, которые не должны находиться в этом модуле, а также дублироваться в главном проекте приложения. Поэтому назрел вопрос об организации дополнительного модуля с общей функциональностью. Это ли не принцип единой ответственности, когда одна сущность должна иметь одно предназначение?

Сложность разделения на модули проекта с двумя языками и большим наследием может отпугнуть при первом взгляде, что со мной и произошло, но интерес к новой задаче одержал победу.

В предварительно найденных статьях авторы обещали безоблачное будущее при выполнении простых и четких шагов, характерных для нового проекта. Но когда я перенёс первый базовый класс в модуль для общего кода, выявилось столько неочевидных зависимостей, столько строчек кода покрылось красным в Xcode, что продолжать дальше не хотелось.

Проект содержал много legacy-кода, перекрестных зависимостей от классов на Objective-C и Swift, разных targetов в терминах iOS-разработки, внушительный список CocoaPods. Любой шаг в сторону от этого монолита приводил к тому, что проект переставал собираться в Xcode, обнаруживая порой ошибки в самых неожиданных местах.

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

Первые шаги


Они очевидны, о них написано много статей. Apple постаралась сделать их максимально удобными.

1. Создаем первый модуль: File New Project Cocoa Touch Framework

2. Добавляем модуль в workspace проекта





3. Создаем зависимость основного проекта от модуля, указав последний в разделе Embedded Binaries. Если в проекте несколько targetов, то модуль надо будет включить в раздел Embedded Binaries каждого зависящего от него targetа.

От себя добавлю только один комментарий: не спешите.

Знаете ли вы, что будет размещено в этом модуле, по какому признаку будут разделены модули? В моём варианте это должен был быть UIViewController для чата с таблицей и ячейками. К модулю должен был быть привязан Cocoapods с чатом. Но вышло всё немного по-другому. Реализацию чата мне пришлось отложить, потому что и UIViewController, и его presenter, и даже ячейка основывались на базовых классах и протоколах, о которых новый модуль ничего не знал.

Как выделить модуль? Наиболее логичный подход по фичам (features), то есть по какой-то пользовательской задаче. Например, чат с техподдержкой, экраны регистрации/авторизации, bottom sheet с настройками основного экрана. Кроме этого, скорее всего, понадобится какая-то базовая функциональность, которая представляет из себя не feature, а лишь набор UI-элементов, базовых классов и т.д. Эту функциональность следует вынести в общий модуль, аналогичный знаменитому файлу Utils. Не бойтесь раздробить и этот модуль. Чем меньше кубики, тем проще их вписать в основную постройку. Мне кажется, так можно сформулировать еще один из принципов SOLID.

Есть готовые советы по разделению на модули, которыми я не воспользовался, потому и сломал столько копий, и даже решил рассказать о наболевшем. Однако такой подход сначала действовать, потом думать как раз и открыл мне глаза на ужас зависимого кода в монолитном проекте. Когда вы в начале пути, вам сложно объять всё количество изменений, которые потребуются для устранения зависимостей.

Поэтому просто переместите класс из одного модуля в другой, посмотрите, что покраснело в Xcode, и постарайтесь разобраться с зависимостями. Xcode 10 хитёр: при перемещении ссылок на файлы из одного модуля в другой он оставляет файлы на прежнем месте. Потому следующий шаг будет таким

4. Перемещайте файлы в файловом менеджере, удаляйте старые ссылки в Xcode и заново добавляйте файлы в новый модуль. Если делать это по классу за раз, будет легче не запутаться в зависимостях.

Чтобы сделать все обособленные сущности доступными извне модуля, придётся принять во внимание особенности Swift и Objective-C.

5. В Swift все классы, перечисления и протоколы должны быть помечены модификатором доступа public, тогда к ним можно будет получить доступ снаружи модуля. Если в отдельный framework перемещается базовый класс, его следует пометить модификатором open, иначе не получится создать от него класс-потомок.

Сразу следует вспомнить (или впервые узнать), какие есть уровни доступа в Swift, и получить profit!



При изменении уровня доступа у перенесённого класса Xcode потребует изменить уровень доступа у всех переопределённых методов на идентичный.



Затем необходимо добавить импорт нового frameworkа в Swift-файл, где используется выделенная функциональность, наряду с каким-нибудь UIKit. После этого ошибок в Xcode должно стать меньше.

import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {//..}

С Objective-C последовательность действий немного сложнее. Кроме того, использование bridging headerа для импорта классов Objective-C в Swift не поддерживается во frameworkах.



Поэтому поле Objective-C Bridging Header должно быть пустым в настройках frameworkа.



Из сложившейся ситуации есть выход, и почему это так тема отдельного исследования.

6. У каждого frameworkа есть собственный заголовочный файл umbrella header, через который будут смотреть во внешний мир все публичные интерфейсы Objective-C.

Если в этом umbrella header указать импорт всех прочих заголовочных файлов, то они будут доступны в Swift.



import UIKitimport FeatureOneimport FeatureTwoclass ViewController: UIViewController {        var vc: Obj2ViewController?        override func viewDidLoad() {        super.viewDidLoad()        // Do any additional setup after loading the view, typically from a nib.    }

В Objective-C, чтобы получить доступ к классам снаружи модуля, нужно поиграться с его настройками: сделать заголовочные файлы публичными.



7. Когда все файлы поодиночке перенесены в отдельный модуль, нужно не забыть о Cocoapods. Файл Podfile требует реорганизации, если какая-то функциональность окажется в отдельном frameworkе. У меня так и было: pod с графическими индикаторами надлежало вынести в общий framework, а чат новый pod был включён в свой собственный отдельный framework.

Необходимо явно указать, что проект теперь не просто проект, а рабочее пространство с подпроектами:

workspace 'myFrameworkTest'

Общие для frameworkов зависимости следует вынести в отдельные переменные, например, networkPods и uiPods:

def networkPods     pod 'Alamofire'end def uiPods     pod 'GoogleMaps' end

Тогда зависимости основного проекта будут описаны следующим образом:

target 'myFrameworkTest' doproject 'myFrameworkTest'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend 

Зависимости frameworkа с чатом таким образом:

target 'FeatureOne' do    project 'FeatureOne/FeatureOne'    uiPods    pod 'ChatThatMustNotBeNamed'end


Подводные камни


Наверное, на этом можно было бы закончить, но в дальнейшем я обнаружил несколько неявных проблем, о которых также хочется упомянуть.

Все общие зависимости вынесены в один отдельный framework, чат в другой, код стал немного чище, проект собирается, но при запуске падает.

Первая проблема скрывалась в реализации чата. На просторах сети проблема встречается и в других podах, достаточно загуглить Library not loaded: Reason: image not found. Именно с таким сообщением происходило падение.

Более элегантного решения я не нашёл и был вынужден продублировать подключение podа с чатом в основном приложении:

target 'myFrameworkTest' do    project 'myFrameworkTest'    pod 'ChatThatMustNotBeNamed'    networkPods    uiPods    target 'myFrameworkTestTests' do    endend

Таким образом Cocoapods позволяет приложению видеть динамически подключенную библиотеку при запуске и при компиляции проекта.

Другая проблема заключалась в ресурсах, про которые я благополучно забыл и нигде не встречал упоминания о том, что этот аспект надо держать в уме. Приложение падало при попытке зарегистрировать xib-файл ячейки: Could not load NIB in bundle.

Конструктор init(nibName:bundle:) класса UINib по умолчанию ищет ресурс в модуле главного приложения. Естественно, об этом ничего не знаешь, когда разработка ведется в монолитном проекте.

Решение указывать bundle, в котором определен класс ресурса, либо позволить компилятору сделать это самому, используя конструктор init(for:) класса Bundle. Ну и, конечно, впредь не забывать о том, что ресурсы теперь могут быть общими для всех модулей или специфичными для одного модуля.

Если в модуле используются xibы, то Xcode будет, как обычно, предлагать для кнопок и UIImageView выбирать графические ресурсы из всего проекта, но в run time все расположенные в других модулях ресурсы окажутся не загруженными. Я загружал изображения в коде, используя конструктор init(named:in:compatibleWith:) класса UIImage, где вторым параметром идёт Bundle, в котором расположен файл изображения.

Ячейки в UITableView и UICollectionView теперь также должны регистрироваться подобным образом. Причем надо помнить, что Swift-классы в строковом представлении включают в себя ещё и имя модуля, а метод NSClassFromString() из Objective-C возвращает nil, поэтому рекомендую регистрировать ячейки, указывая не строку, а класс. Для UITableView можно воспользоваться таким вспомогательным методом:

@objc public extension UITableView {    func registerClass(_ classType: AnyClass) {        let bundle = Bundle(for: classType)        let name = String(describing: classType)        register(UINib(nibName: name, bundle: bundle), forCellReuseIdentifier: name)    }}


Выводы


Теперь можно не переживать, если в одном pull request окажутся изменения в структуре проекта, сделанные в разных модулях, потому что у каждого модуля свой xcodeproj-файл. Можно распределять работу так, чтобы не приходилось тратить несколько часов на сведение файла проекта воедино. Полезно иметь модульную архитектуру в больших и распределенных командах. Как следствие, должна увеличиться скорость разработки, но верно и обратное. На свой самый первый модуль я потратил гораздо больше времени, чем если бы создавал чат внутри монолита.

Из очевидных плюсов, на которые также указывает Apple, возможность снова использовать код. Если в приложении имеются различные targetы (app extensions), то это самый доступный подход. Возможно, чат не самый лучший вариант для примера. Следовало начать с вынесения сетевого слоя, но давайте будем честными сами с собой, это очень длинная и опасная дорога, которую лучше разбить на небольшие отрезки. А так как за последние пару лет это было внедрение второго сервиса для организации технической поддержки, хотелось внедрить его не внедряя. Где гарантии, что скоро не появится третий?

Один неочевидный эффект при разработке модуля более продуманные, чистые интерфейсы. Разработчику приходится проектировать классы так, чтобы определенные свойства и методы были доступны извне. Поневоле приходится задумываться, что сокрыть и как сделать модуль таким, чтобы его можно было с легкостью использовать снова.
Подробнее..

Сценарий идеального технического собеседования

07.10.2020 14:10:47 | Автор: admin


Дисклеймер: это сценарий идеального технического собеседования в Delivery Club Tech. Мнение нашей команды может не совпадать с мнением читателей.

Привет, Хабр! Меня зовут Василий Козлов, я iOS-техлид в Delivery Club. Я часто и много провожу собеседования. В этой статье я собрал накопленный опыт и собственные наблюдения, которыми хочу поделиться. Во второй части статьи приведу пример собеса с комментариями со своей стороны. Итак, начнём.

1. Собесы бывают разные: жёлтые, зелёные, красные (лирическое отступление)


Есть мнение, что сложные технические собесы не работают. Сооснователь платформы для рекрутинга Interviewing.io Алин Лернер ранее писала, что компании, которые подбирают сотрудников, опираясь на сложные технические собеседования, тратят ресурсы на множество кандидатов, которые не понимают игровую сущность собеседований. В результате на финишную прямую в таких компаниях выходят кандидаты, которые хороши именно в прохождении интервью.

Добавьте сюда стресс от собеседований, разнообразие и непредсказуемость вопросов на технических собеседованиях в разных компаниях. И вспомните свои неожиданные неудачи на этих встречах. Статистика это лишь подтверждает: только около 25% кандидатов способны раскрыть и продемонстрировать свой потенциал, и даже первоклассные специалисты в 22% случаев заваливают технические собеседования.

Эта особенность задавать на технических интервью сложные, не имеющие никакой связи с реальностью, головоломкие вопросы появилась в 1950-х годах в Соединенных Штатах в период холодной войны. Тренд задала лаборатория полупроводников Шокли из долины, тогда ещё не носившей имя Кремниевой, вынужденная привлекать на службу безумных гениев для противостояния красной угрозе. Невозможность писать код на техническом собеседовании по телефону заставляла интервьюеров искать альтернативные варианты для быстрой оценки аналитических способностей, интеллекта и потенциала собеседника. Так появились задачи про фальшивую монету и два взвешивания.

В 1990-х годах с бумом доткомов последовал рост найма технических специалистов, и Microsoft взяла на вооружение подход прошлых лет. Их примеру некоторое время следовала Google.

Впоследствии Google и Microsoft отказались от популярных головоломок из серии как передвинуть гору Фудзи. Что касается найма, то мы обнаружили, что головоломки это пустая трата времени. Сколько мячей для гольфа вы можете поместить в самолет? Сколько заправочных станций на Манхэттене? Полная трата времени. Они ничего не предсказывают. Они служат, в первую очередь, для того, чтобы интервьюер чувствовал себя умным, признал старший вице-президент по работе с персоналом в Google в интервью New York Times.

Итак, есть мнение, что сложные технические собесы не работают, и я его разделяю, поэтому все технические собеседования в Delivery Club я проводил, не задавая подобных вопросов. Невозможно без соответствующей подготовки к собеседованию взять и пройти таковое. На практические тренировки может уйти 1-2 месяца хождения. Это нормальный процесс, с которым так или иначе согласны все опрошенные мной коллеги.

Появление компьютеров позволяет решать проблемы, которые не проявлялись до их существования. Так и подготовка к собеседованиям позволяет решать проблемы, зачастую возникающие только на собеседованиях. Очень часто это именно так.

IT-индустрия в России с точки зрения IT-найма никак не стандартизована. Способы оценки знаний принимают, зачастую, очень изощренные формы. Один из самых бестолковых примеров на моей памяти телефонное интервью с HR-специалистом, который записывал ответы кандидата на технические вопросы, чтобы далее передать их техническому специалисту. В этом случае какой-либо диалог полностью исключается, и невозможно поделиться ни мнением, ни оспорить вариант ответа. Всякий диалог также исключен, когда перед соискателем предстает online-тест с выбором из заготовленных ответов, также порой являющийся порождением ума другого технического специалиста с его собственным, уникальным опытом и знанием английского языка. На мой взгляд, английский в разработке важен настолько, что порой проще на собесе объясняться на нём.

Другой пример собеседования, с одной стороны вполне объяснимый, это желание работодателей получить психологический портрет соискателя, предупреждая собеседование по существу встречей с психологом. Самый ушлый работодатель на моей практике всё же провел техническое собеседование, а потом предложил пройти испытание на детекторе лжи, аргументируя такую последовательность тем, что услуга эта платная, а кандидатов много.

Следует признать, что это редкие случаи, и вся совокупность проверки технических знаний сводится, в итоге, к тестовому заданию или очной встрече. Как эти простые инструменты сделать эффективными в поиске подходящего технического спеца? Для начала, как и в любой технической задаче, следует определиться с требованиями к кандидату и к собесу.

2. Идеальный формат, идеальный кандидат (формируем требования)


После каждого интервью у вас должно сложиться чёткое ощущение, может ли этот человек повысить вероятность успеха вашей компании, говорит Нил Роузман в статье Анатомия идеального технического интервью от бывшего вице-президента Amazon. Пусть эти слова для кого-то прозвучат высокопарно, особенно от вице-президента, пусть и бывшего, но это правда. В конечном счёте именно вам или вашему коллеге предстоит работать с этим кандидатом, если он успешно пройдет собес.

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

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

На помощь пришел давнишний подход, впервые применённый в Гарвардской бизнес-школе в 1924 году ситуационное интервью, или кейс-интервью. Условно его можно разделить на три большие части:

  • ценности и взгляды кандидата, soft skills;
  • профессиональные навыки и умения, hard skills;
  • модели поведения и индивидуально-личностные качества.

Вопрос о том, как совместить ситуационное интервью с известными подходами к оценке технических знаний, оставался открытым. Важно предложить кандидату структурированный, прозрачный план собеса и уместить его в тесное расписание современного разработчика.

Согласно статистическим данным Zety, консультационного сервиса по созданию резюме, в 2017 году наиболее распространенным подходом оставался итеративный, состоящий в проведении нескольких этапов интервью. Самый популярный вариант содержал три интервью.

Получасовой созвон по скайпу так называемый скрининг позволяет получить общее представление о кандидате, принять решение о желании общаться дальше, как на первом свидании. Чем, как не эффективной демонстрацией софт скиллов, обусловлена договоренность о следующей встрече? Очень важно, чтобы это был именно видеозвонок, а не гадание по фотографии. Вопросы, которые будут заданы на скрининге, также должны быть направлены на формирование картины ценностей кандидата: представление о ролях в команде, эффективности взаимодействия, о своей роли, эффективности и компетенции. Нелишним будет лично задать вопрос о мотивах смены работы и найти пересечения, пробежавшись по персональному техрадару кандидата, вроде любимых сериалов.

Конечно, эпидемиологическая ситуация в мире внесла свои коррективы, и Zoom вытеснил Skype, но скрининг был и остаётся нашим бессменным первым этапом.

Забегу вперед, оставив самое вкусное на потом, и упомяну третий этап финал, до которого суждено добраться не всем. Это как знакомство с родителями, если продолжать аналогию со свиданием. Это тоже своеобразный скрининг, но с руководителем подразделения или техническим директором. На этом этапе важно донести до кандидата ценности компании, стратегию развития проекта. Проверив его ещё раз, задавая встречные вопросы о планах и ценностях, если у вас есть сомнения по поводу кандидата.

Оставим детализацию этого процесса на откуп самим руководителям, перейдём к техническому интервью главному этапу в собеседовании разработчика.

Отличное интервью это работа. Требуется время, чтобы подготовиться, провести собеседование, а затем эффективно подвести итог. Если вы не хотите делать эту работу, не берите интервью, продолжая цитировать Нила Роузмана, стоит согласиться с тем, что это действительно работа, и, вне зависимости от того, какой формат вы выберите, с кандидатом придется вести диалог. Помимо тех, кто фактически обучался найму, эйчаров, техническому интервьюеру также следует прокачать навыки хайринга.

3. О бедном эйчаре замолвите слово (про важность хорошего HR-специалиста)


В статье я рассуждаю с выигрышной позиции: крупная компания, известный бренд многие специалисты даже без уточнения технических деталей согласятся здесь работать. Но в любой компании нельзя недооценивать работу HR-специалиста. Он является своеобразным фильтром на входе в компанию. Сотрудники, которых вы нанимаете, будут так же хороши, как и команда по найму, которую вы собираете, подтверждает Роузман.

Главным активом любой компании являются люди. Хороший рекрутер даже при отсутствии бренда, в никому не известном стартапе может обеспечить специалистов с приемлемым уровнем знаний, и сделать даже нечто большее: найти единомышленников тех, кто будет радеть за продукт.

Крупная компания, известный бренд могут также осложнять работу HR-специалиста, будучи у всех на слуху. Хорошее знание проекта, процессов разработки и команды позволяют рекрутеру уже на первом этапе принять решение о том, подходит ли кандидат для вакансии. В результате до 60% кандидатов доходят до технического интервью, говорит руководитель направления подбора персонала Mail.ru Group Карина Пушкина. Однако объёмы таковы, что 60% это не 6 человек.

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

Хороший рекрутер должен всегда иметь в рукаве нескольких тузов, если вдруг на рынке не найдётся подходящего кандидата в активном поиске работы. Для этого рекрутеры регулярно занимаются поиском холодных кандидатов. Как правило, мы предлагаем людям просто нейтрально познакомиться с нами, без интервью. Далеко не все соглашаются после этого пройти техническое собеседование, и это нормально. Мы строим долгосрочные отношения в надежде на то, что если не сейчас, то через год или два нам всё-таки удастся поработать вместе.

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

Главное правило оставайтесь людьми, и помните про партнерские отношения с каждым кандидатом, невозможно сказать лучше, чем Карина Пушкина.



4. Как играть на поле кандидата, не забывая про себя (требования к интервьюерам)


К нам приходят очень разные технические специалисты из разных компаний. В продуктовых компаниях большее внимание уделяется пользовательским интерфейсам, простым и быстрым реализациям, а в софтверных гигантах сложные технические решения, нативный код, глубокое погружение в недра операционных систем превалируют над внешней составляющей и реакцией на изменяющиеся условия. Так и кандидаты, в зависимости от того, в какой компании они работали, могут специализироваться на определенных технологиях, пусть даже каждый из них претендует на одну условную вакансию iOS-разработчика. Означает ли это, что специалист, погруженный, например, в реализацию родительского контроля на iPhone, никогда не сможет перебежать на сторону продуктовых витрин, заказов и отображения локаций на карте? Как оценить претендента, если в рассказе о прошлом опыте он упоминает подходы, никак не отзывающиеся в вашей памяти?

Многообразие технологий, архитектурных приёмов и фреймворков усложняет поиск кандидатов, но говорить на общем с кандидатами языке можно и нужно. Специалист с головой на плечах сможет адаптироваться к новым для него практикам и даже привнести оригинальные решения в устоявшиеся подходы. Снятие ограничения при поиске кандидатов, скажем, на архитектурные паттерны или языки программирования в рамках одной технологической платформы, конечно, потребует от интервьюера знаний в соответствующих предметных областях. Где взять такие знания? Опыт проведения собеседований подскажет интервьюеру, в каком направлении стоит подтянуть знания, какие технологии и подходы распространены на рынке сейчас и в чем их отличие от принятых в компании. Необязательно становиться экспертом насильно. Достаточно представлять в общих чертах предметную область и вести с кандидатом диалог, позволив ему самому объяснить тонкости реализации. Такой подход позволит вам избежать чрезмерных затрат на подготовку к конкретному собесу, а кандидату продемонстрировать свои гибкие навыки, выраженные в способности объяснять свою точку зрения другому специалисту.

Это отнюдь не означает, что к собеседованию не нужно готовиться. Здесь важно соблюдать баланс. Предложите кандидату самому выбрать и объяснить задачу и её решение при помощи его любимой технологии. Попросите решить типовую задачу из вашей предметной области, но при помощи его инструментов. Так или иначе вы сможете совместно поработать над проблемой, знакомясь и с точкой зрения кандидата, и с привычной ему технологией. Типовые задачи, которые подходят для такого кейс-интервью, как правило, хорошо иллюстрируются проектированием архитектуры сервиса, модуля или экрана. Кандидат абстрагируется от конкретной реализации, а вам не нужно погружаться в техническую специфику. Поэтому открывайте draw.io или любой другой сайт для проектирования блок-схем и диаграмм, и вперёд! Этот формат отлично подходит для Zoom.

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

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

Задавать технические вопросы, как и проводить техническое собеседование, интервьюер должен структурировано. Нил Роузман предостерегает: Если вы провели собеседование и всё, что можете сказать это: Ну да, он вроде ничего, мне понравился, тогда вы потратили время зря. Структурированный подход должен стать рутиной в подготовке и проведении собесов.

5. Сценарий идеального технического собеседования


Конечно, здесь я не представлю универсальный, идеальный план проведения технического собеса. Это уникальный для каждой компании и каждого интервьюера процесс. Но я хочу предложить несколько идей, опробованных в реальной практике найма в Delivery Club, применение которых известным образом усилило нашу команду.

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

Составьте план технического собеседования. Наличие у интервьюера плана с заранее предусмотренными вопросами разной сложности, а также оценками уровня знаний по каждой теме позволит грамотно подвести итог после собеседования. Развернутый результат всегда проще сопоставлять с другими результатами и аргументировано объяснять своё решение по кандидату.

Дальнейшее применение таких оценок, в случае принятия положительного решения по кандидату, может выразится в развитии матрицы компетенций сотрудника. Таким образом, качественное техническое собеседование станет залогом для дальнейшего развития специалиста в составе вашей команды.

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

Старайтесь использовать на интервью задачи, максимально близкие к рабочим. Если задаете академический вопрос, а кандидат начинает плавать, попробуйте обрисовать практическую задачу, которая включает в себя ответ на этот вопрос. Вариант для опытных интервьюеров прислать свои вопросы кандидату заранее, и на собеседовании, основываясь на том, что вы с кандидатом обладаете одинаковыми знаниями, пообщаться и обсудить более практические и сложные кейсы.

Предложите кандидату выполнить code review, вместо того, чтоб заставлять его писать новый код. Практика code review отлично подходит для технической части ситуационного интервью. Подготовьте заранее неоптимальный код или код, в котором допущена ошибка. А если в листинге не содержится секретной информации, то лучше показать тот pull request, для которого коллегами было оставлено больше всего замечаний. Тем самым вы переосмыслите подход к коду на листочке: многие кандидаты испытывают трудности при написании кода вне предпочитаемой среды разработки или при стороннем наблюдателе. А code review на собесе позволит вам убедиться в навыках чтения и понимания кода, оценить способность командного взаимодействия по тону и содержанию комментариев кандидата.

Завершите техническое собеседование презентацией проекта, с которым предстоит работать кандидату. Важно не только выслушать кандидата, но и ответить на его вопросы. Не всегда возможно отвечать максимально подробно, но необходимо, чтобы кандидат понимал, с чем ему предстоит работать, хотя бы в общих чертах.

В моей практике с этой задачей отлично справилась презентация о развитии проекта, которую изначально готовили для технической конференции, но она смогла отразить и legacy проекта, и инновационные подходы. Благодаря такой своеобразной визитной карточке ваш проект и команда значительно вырастут в глазах кандидата.

Проведите собеседование от начала и до конца. Даже если вы понимаете через 15 минут после начала собеседования, что кандидат не подойдет, важно пройти все эти этапы, говорит Нил Роузман, Вы должны постараться провести почти полное интервью, потому что мир очень тесен, и пусть лучше человек думает, что у него только что было отличное интервью, даже если он не получил предложение о работе.

Поиск подходящего сотрудника это большой труд, и его можно облегчить, если обе стороны будут получать удовлетворение и удовольствие от этого процесса. Вы будете расти и развиваться с каждым новым собесом, с каждым новым кандидатом, и в итоге обязательно найдёте идеального.

Сценарий идеального технического интервью. Драма в пяти действиях


Далее я в игровой манере опишу план одного воображаемого технического собеседования, основанного на разных реальных интервью. После каждой части дам небольшие комментарии о том, какие выводы может сделать интревьюер и как продолжать собеседование.

Действующие лица:

  • Олег молодой, перспективный iOS-разработчик
  • Василий умудрённый опытом тех лид команды iOS

Действие первое




Никто из действующих лиц не знает, как пойдёт собеседование дальше. Олег может начать плутать в коде, скакать по наименованиям методов, тщетно предполагая, где они могут вызываться, а может ввести в поиск 1.5 и точно определить место, где задаётся пауза, так и не сумев объяснить механику работы. Тогда Василий постарается скорее завершить эту часть собеседования, поблагодарив и указав собеседнику на возможность постановки таких задач перед сотрудником только при наличии соответствующих знаний в области Objective-C.

С другой стороны, Олег может логически дойти до требуемого метода, где организована очередь, объяснить выбор параметров очереди и механизм её работы, рассказав попутно про другие виды очередей и способы реализации подобной задачи.

В обоих случаях Василий должен следить за временем, не позволяя процессу размышления и обсуждению затянуться, а также постараться оценить ответ Олега по заранее подготовленной шкале оценок по Objective-C, а возможно, ещё и многопоточности.


Действие второе




От ответа Олега зависит, как интервью будет развиваться дальше. Но даже если он не сумеет ответить на этот вопрос, беседа не прекратится, просто Василий вернётся к базовым вопросам, предварительно ответив на заданный Олегу вопрос.

Если Олега осенит после предложенной подсказки, это будет означать, что можно переходить к более сложным вопросам.


Действие третье




Василий уже понимает, что Олег может решать задачи формирования пользовательского интерфейса в соответствии с требованиями дизайна. Также создаётся впечатление, что Олег больше склонен действовать, а не объяснять, и, возможно, это проливает свет на то, что в некоторых вопросах он плавает, хотя точно знает, как всё работает.

Действие четвертое




У Василия начинает складываться вполне конкретное представление об Олеге, как о возможном будущем сотруднике. Олег разбирается в предметной области, следует требованиям, но не готов давать обратную связь и мыслить более абстрактно, чем, возможно, того требует задача.

Для понимания последнего кейса Василий предлагает Олегу архитектурную задачу.


Действие пятое




Процесс проектирования архитектуры какой-либо системы всегда творческий. Он сопровождается диалогом, постоянными правками. Он уникален и ценен сам по себе, потому что однозначно даёт понять, разбирается ли автор в том, что объясняет, а заодно умеет ли объяснять.

Но и результат этого процесса может кое-что рассказать о кандидате. Если бы Олег спроектировал архитектуру, как показано на одной из иллюстраций, у Василия сложились бы совершенно разные представления о навыках кандидата.




Что в результате нарисовал Олег, мы опустим, и предоставим читателю на основании иллюстраций самому пофантазировать о том, понимает ли Олег выбранную архитектуру.



На этом всё. Спасибо, что дочитали!
Подробнее..

MFS паттерн построения UI в iOS приложениях

26.01.2021 12:18:23 | Автор: admin

Логика развития мобильных приложений заключается в постепенном усложнении функциональной нагрузки на пользовательский интерфейс.
Что, в свою очередь, приводит к росту кодовой базы и затруднению ее обслуживания.

MFS - позволяет создавать современный дизайн приложений и при этом избежать такого явления как MassiveViewController.

Фото: 10 years of the App Store: The design evolution of the earliest apps - 9to5Mac

Причины создания паттерна

Времена когда весь интерфейс контроллера настраивался целиком в методеviewDidLoadдавно закончились.
Настройка графических компонентов и пользовательскогоlayoutстала занимать сотни строк кода.
Помимо массивной настройки и инициализации, встает второй немало важный вопрос поддержки и управления созданными активами.

О существующих трудностях знают в Купертино, на изменение требований рынка компания отреагировала выпуском перспективной технологиейSwiftUI.
Которая, к сожалению, имеет ряд серьезных ограничений, например поддержка отiOS 13 и выше.
Что на данный момент - абсолютно неприемлемо для большинства солидных приложений, которые стараются охватить максимально большую аудиторию пользователей.

MFS - же напротив ориентирован на поддержку и разгрузку уже существующих приложений.
А набор используемых технологий позволяет внедрять паттерн на самую большую аудиторию приложений.

Архитектурный паттернMFS(Managment-Frames-Styles) был разработан, для того чтобы соответствовать духу времени и его потребностям.

Кому может понадобиться MFS ?

В первую очередь паттерн рекомендуется использовать тем разработчикам, которые занимаются строительством сложных интерфейсов, в особенности если дизайн значительно различается в зависимости от ориентации устройства.

Если же ваше приложение имеет крайне простой интерфейс, то возможно более оптимальным будет использование стандартногоStoryboard иAutolayout.

Отношение к Autolayout и другим подобным технологиям

Поскольку исторически паттерн создавался именно для реализации сложных дизайнов, поэтому в стандартной своей реализацииMFS- имеет ручное вычисление всехframe, без использования каких-либо сторонних библиотек.

Подобный подход позволяет крайне гибко реализовывать любые требования дизайнера, чего к примеру не позволяет делать или же серьезно ограничиваетAutolayout.
Однако, использование технологииAutolayout не запрещено, поскольку паттерн имеет высокую декомпозированность, пользуясь которой, вы можете заменить ручной расчет координат на созданиеconstraints.

Обзор паттерна

ПаттернMFS призван равномерно распределить обязанности между категориями класса, с целью избежания возникновенияMassiveViewController.

Название категории

Обязанности

+Managment

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

+Frames

Содержит методы вычисляющие размеры и координаты subviews.

+Styles

Содержит методы графической конфигурации subviews.
Например задает цвет,закругление,шрифт, и т.д.
Как правило, для каждого property существует отдельный метод настройки.

Как стало вам понятно, категория+Managment является главенствующей, она содержит дополнительные методы, которые полностью забирают на себя все хлопоты по созданию и обслуживанию интерфейса контроллера.
Остальные же категории содержат так называемыечистые функции, которые более широко известны под названиемpure function.

Функция является "чистой" если она мутирует только те значения, которые создала самостоятельно или же приняла из собственных параметров.


То есть, если функция изменяет некие глобальные переменные, которые не были переданы ей в параметры, то "чистой" она не является.

Обратите внимание, на то, что методы категории+Frames ВСЕГДА должны быть чистыми.
Методы же категории+Stylesмогут быть чистыми по усмотрению пользователя, поскольку это не так критично.

На главный файл имплементации контроллера (ViewController.m) ложится обязанность выполнять протоколы различных представлений (напр.:UITableViewDelegate,UITableViewDataSource), а также содержать методыIBAction.


Выше были перечисленны основные обязанности, но в случае острой необходимости, вы должны самостоятельно принимать решение - размещать некий функционал в файле имплементации или же вынести его в отдельную категорию.
Также стоит заметить, что вся бизнес-логика содержится во вьюМодели вашего контроллера или же во вьюМоделях егоsubviews.

Подобная целенаправленная политика позволяет умещать такие сложные контроллеры, как профиль пользователя и его стену, всего в около~300 строчек кода наObjC и вероятно еще меньше - наSwift.

В свою очередь, такая лаконичность полностью исключает побочный эффект в виде "бесконечного скроллинга" и поиска нужного метода.

Все методы распределены строго по категориям и если нам, например, потребуется изменить размер шрифта или поменятьlayout, мы с легкостью откроем соответствующий файл и не будем бесконечно скролить основной код контроллера.

Порядок вызова методов построения UI

На схеме ниже показаны методы и порядок их вызовов для построенияUI вUIViewController.
Как мы можем увидеть процесс построенияUI начинается из методаviewDidAppear, который вызывает методprepareUI.

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

На самых простых представлениях, после переворота экрана, нам потребуется вызывать только методresizeSubviews, на более сложных, где нужно, например скрывать некоторые элементы для определенных ориентаций, там может понадобиться и вызовupdateStyles или жеbindDataFrom.

Обзор контроллера

Подобный обзор паттерна будет проводиться на примере демо приложения.
Данный контроллер входа в приложение имеет различную верстку для разных ориентаций.

В портретной ориентации кнопки находятся друг под другом, а в горизонтальной находятся на одной высоте, с разных сторон экрана.

Даже такой минималистичный дизайн является частным случаем труднореализуемого интерфейса с помощью применения стандартногоAutolayout.

Ниже представлены.h/.m контроллера.
Обратите внимание, что помимо стандартного набор проперти, наш контроллер имеет две достаточно необычных, в привычном понимании, переменных.

@interface LoginController : UIViewController// ViewModel@property (nonatomic, strong, nullable) LoginViewModel* viewModel;@property (nonatomic, strong, nullable) LoginViewModel* oldViewModel;// UI@property (nonatomic, strong, nullable) UIImageView* logoImgView;@property (nonatomic, strong, nullable) UIButton* signInButton;@property (nonatomic, strong, nullable) UIButton* signUpButton;@property (nonatomic, strong, nullable) CAGradientLayer *gradient;@property (nonatomic, assign) CGSize oldSize;#pragma mark - Actions- (void) signUpBtnAction:(UIButton*)sender;- (void) signInBtnAction:(UIButton*)sender;#pragma mark - Initialization+ (LoginController*) initWithViewModel:(nullable LoginViewModel*)viewModel;@end


Речь идет оoldViewModel иoldSize - эти проперти помогают избегать лишних перерисовок и вставок данных.
Подробней о них будет рассказано в разборах отдельных категорий.

@interface LoginController ()@end@implementation LoginController#pragma mark - Life cycle- (void) viewDidAppear:(BOOL)animated{    [super viewDidAppear:animated];    [self prepareUI];}- (void)viewWillTransitionToSize:(CGSize)size      withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator{    __weak LoginController* weak = self;    [coordinator animateAlongsideTransition:nil             completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {        [UIView animateWithDuration:0.3                               delay:0                            options:UIViewAnimationOptionCurveEaseOut  animations:^{            [weak resizeSubviews:weak.viewModel];        } completion:nil];    }];}#pragma mark - Action- (void) signUpBtnAction:(UIButton*)sender{    [self.viewModel signUpBtnAction];}- (void) signInBtnAction:(UIButton*)sender{    [self.viewModel signInBtnAction];}#pragma mark - Getters/Setters- (void)setViewModel:(LoginViewModel *)viewModel{    _viewModel = viewModel;      if ((!self.oldViewModel) &amp;&amp; (self.view)){         [self prepareUI];    } else if ((self.oldViewModel) &amp;&amp; (self.view)){        [self bindDataFrom:viewModel];        [self resizeSubviews:viewModel];    }}#pragma mark - Initialization+ (LoginController*) initWithViewModel:(nullable LoginViewModel*)viewModel{    LoginController* vc = [[LoginController alloc] init];    if (vc) {        vc.viewModel = (viewModel) ? viewModel : [LoginViewModel defaultMockup];    }    return vc;}@end

Просмотрев код расположенный выше, можно сказать, что в результате декомпозиции основной файл контроллера остается крайне лаконичным.

Обзор методов категории +Managment

Имя метода

Принимает ли вьюМодель

Предназначение

prepareUI

Главный метод построения UI, вызывает нужную последовательность методов.
Данную функцию рекомендуется вызывать из viewDidAppear.

removeSubviews

Удаляет все subviews с superView.
А также обнуляет все проперти на UI элементы.

initSubviews

Инициализирует нужные subviews.

updateStyles

Вызывает индивидуальные методы настройки для каждого subviews, куда также передает вьюМодель, на основании данных которой может быть принято решение относительно стилей.
Например, задает разный цвет для плашек сообщений в зависимости от пола пользователя.

bindDataFrom

Вставляет данные из вьюМодели в subviews.

resizeSubviews

Вызывает индивидуальные методы расчета размеров и координат для каждой subviews.

addSubviewsToSuperView

Добавляет subviews на superView если те были проинициализированы и не добавлены на родительское представление ранее.

postUIsetting

Здесь должна происходить настройкаsubviews для которых не было создано уникальных методов конфигурации по причине их ненадобности.
Например, тут будет настраиватьсяstatusBar,gestures,allowSelectation и тд.

Из выше перечисленных методов явно прослеживается виденье того, как должен строиться UI в приложении:

  1. Удаление всехsubviewsи обнуление всех проперти наUIэлементы.
    (Если того требует ситуация).

  2. Инициализация нужныхsubviews.

  3. Обновление стилейsubviews (цвет/размер шрифта итд).

  4. Вставка данных вsubviews.

  5. Расчет и установка корректныхframesдляsubviews.

  6. Добавление полностью готовыхsubviews на родительское представление.

Реализация методов категории +Managment

Для того чтобы внутриviewDidAppear не вызывать целый набор методов, был придуман метод-оберткаprepareUI,который вызывает методы категории в нужной последовательности.

Обратите внимание, на методыresizeSubviews иbindDataFrom, порядок их вызовов в некоторых ситуациях может быть прямо противоположенный.

Например, некоторые библиотеки, кэширующие изображения из интернета, возвращают картинку не в полном разрешении, а уже заранее подготовленную под размер вашегоUIImageView, тогда, если вы сначала попытаетесь вставить картинку в рамку размером0x0, у вас может произойти ошибка.

В классическом сценарии сначалаsubviews наполняются данными, а потом производится расчет размеров и координат.

/*----------------------------------------------------------------------  Основной метод построения интерфейса.   Вызывает нужную последовательность методов ----------------------------------------------------------------------*/- (void) prepareUI{    if (self.view){        [self removeSubviews];        [self initSubviews:self.viewModel];        [self updateStyles:self.viewModel];        [self bindDataFrom:self.viewModel];        [self resizeSubviews:self.viewModel];        [self addSubviewsToSuperView];        [self postUIsetting];    }}


Метод удаления всехsubviews с родительского представления.
При работе с контроллером нужен - только в исключительных случаях.

Например, если данные во viewModel могут динамически меняться и набор с расположениемsubviews зависит от вариативностиviewModel, то есть для одной вьюМодели у вас будет один наборsubviews, в определенном месте, а для вьюМодел с другим набором данных, будут иные subviews с прочимиUIэффектами.

Тогда при сменеviewModel имеет смысл вызывать неbindDataFrom иresizeSubviews, а полноценный методprepareUI, потому что он вызовет всю цепочку, которая прежде всего удалит все старые представления.

/*---------------------------------------------------------------------- Удаляем все `subviews` и обнуляем все проперти на UI элементы. ----------------------------------------------------------------------*/- (void) removeSubviews{    // removing subviews from superview    for (UIView* subview in self.view.subviews){        [subview removeFromSuperview];    }    // remove sublayers from superlayer    for (CALayer* sublayer in self.view.layer.sublayers) {        [sublayer removeFromSuperlayer];    }    self.logoImgView   = nil;    self.signInButton  = nil;    self.signUpButton  = nil;    self.gradient      = nil;}


Обратите внимание, что в этом методе происходит чистая инициализация, без каких-либо настроек.

/*---------------------------------------------------------------------- Инициализирует нужные subviews на основе данных из viewModel ----------------------------------------------------------------------*/- (void) initSubviews:(LoginViewModel*)viewModel{  if (self.view)  {   if (!self.logoImgView)  self.logoImgView  = [[UIImageView alloc] init];   if (!self.signInButton) self.signInButton = [UIButton buttonWithType:UIButtonTypeCustom];   if (!self.signUpButton) self.signUpButton = [UIButton buttonWithType:UIButtonTypeCustom];  }}


МетодupdateStyles вызывает индивидуальные методы для каждого изsubviews, с целью настроить их внешний вид.

/*----------------------------------------------------------------------  Задает стили для subviews. Цвета/размера шрифта/селекторы для кнопок ----------------------------------------------------------------------*/- (void) updateStyles:(LoginViewModel*)viewModel{    if (!viewModel) return;    if (self.logoImgView)  [self styleFor_logoImgView:self.logoImgView   vm:viewModel];    if (self.signInButton) [self styleFor_signInButton:self.signInButton vm:viewModel];    if (self.signUpButton) [self styleFor_signUpButton:self.signUpButton vm:viewModel];}


Во время декларации.hфайла контроллера, фигурировало пропертиoldViewModel.
В данном случае оно понадобилось нам для осуществления проверки на идентичность моделей.
Если вьюМодели идентичны, то биндинга данных не произойдет.

Традиционно подобная конструкция чаще используется при работе с ячейками, но в некоторых случаях может потребоваться и при работе с контроллером.

/*---------------------------------------------------------------------- Связывает данные из вьюМодели в subviews ----------------------------------------------------------------------*/- (void) bindDataFrom:(LoginViewModel*)viewModel{    // Если модели идентичны, то биндинга данных не происходит    if (([self.oldViewModel isEqualToModel:viewModel]) || (!viewModel)){        return;    }    [self.logoImgView setImage:[UIImage imageNamed:viewModel.imageName]];    [self.signInButton setTitle:viewModel.signInBtnTitle forState:UIControlStateNormal];    [self.signUpButton setTitle:viewModel.signUpBtnTitle forState:UIControlStateNormal];    self.oldViewModel = viewModel;}


МетодisEqualToModel в каждом отдельном случае имеет разную реализацию.
Например, может возникнуть ситуация, когда в ваш контроллер устанавливается новая вьюМодель, но основные данные, критически важные данные не отличаются, а были изменены только второстепенные проперти, которые не отображаются в вашемUI.

Тогда методisEqualToModel должен вернуть значениеNO, чтобы избежать повторного биндинга данных.

В нашем случае он имеет подобную реализацию:

/*---------------------------------------------------------------------- Сравнивает модели данных на индетичность. ----------------------------------------------------------------------*/- (BOOL) isEqualToModel:(LoginViewModel*)object{    BOOL isEqual = YES;    if (![object.imageName isEqualToString:self.imageName]){        return NO;    }    if (![object.signInBtnTitle isEqualToString:self.signInBtnTitle]){        return NO;    }    if (![object.signUpBtnTitle isEqualToString:self.signUpBtnTitle]){        return NO;    }    return isEqual;}

Так же как и вbindDataFrom, методresizeSubviews в самом начале имеет условие проверки, которое не позволяет повторно вычислять размеры и координаты дляsubviews, если модель данных или размер родительского представления не был изменен.

/*---------------------------------------------------------------------- Вызывает индивидуальные методы расчета размеров и координат для subviews.  После изменения ориентации или после первой инициализации. ----------------------------------------------------------------------*/- (void) resizeSubviews:(LoginViewModel*)viewModel{    // Выходим если модель данных и размеры одни и те же    if ((([self.oldViewModel isEqualToModel:self.viewModel]) &&        (CGSizeEqualToSize(self.oldSize, self.view.frame.size))) || (!viewModel)) {        return;    }    if (self.view){      if (self.logoImgView)  self.logoImgView.frame  = [LoginController rectFor_logoImgView:viewModel  parentFrame:self.view.frame];      if (self.signInButton) self.signInButton.frame = [LoginController rectFor_signInButton:viewModel parentFrame:self.view.frame];      if (self.signUpButton) self.signUpButton.frame = [LoginController rectFor_signUpButton:viewModel parentFrame:self.view.frame];      if (self.gradient)     self.gradient.frame     =  self.view.bounds;    }    self.oldSize = self.view.frame.size;}


Добавляемsubviews на родительскоеview.

/*---------------------------------------------------------------------- Добавляет subviews на superView ----------------------------------------------------------------------*/- (void) addSubviewsToSuperView{    if (self.view){        if ((self.logoImgView)  &amp;&amp; (!self.logoImgView.superview))   [self.view addSubview:self.logoImgView];        if ((self.signInButton) &amp;&amp; (!self.signInButton.superview))  [self.view addSubview:self.signInButton];        if ((self.signUpButton) &amp;&amp; (!self.signUpButton.superview))  [self.view addSubview:self.signUpButton];    }}


На этом этапе все методы категории+Managment были разобраны и остался единственный метод пост-настройки, который принадлежит категории+Styles.

В методеpostUIsetting мы настраиваемUI компоненты, для которых не создали индивидуальных методов.
Например, в нем можно добавлятьgestures, настраивать таблицу, устанавливать цвет статус бара и т.д.

- (void) postUIsetting{    UIColor* firstColor  =    [UIColor colorWithRed: 0.54 green: 0.36 blue: 0.79 alpha: 1.00];       UIColor* secondColor =    [UIColor colorWithRed: 0.41 green: 0.59 blue: 0.88 alpha: 1.00];;    self.gradient = [CAGradientLayer layer];    self.gradient.frame      = self.view.bounds;    self.gradient.startPoint = CGPointZero;    self.gradient.endPoint   = CGPointMake(1, 1);    self.gradient.colors     = [NSArray arrayWithObjects:(id)firstColor.CGColor,                           (id)secondColor.CGColor, nil];    [self.view.layer insertSublayer:self.gradient atIndex:0];}

Реализация методов категории +Styles

В отличии от категории+Managment,+Styles не имеет системных методов, а лишь содержит индивидуальные методы настройкиUI компонентов.
Ниже будет приведен один из методов.

- (void) styleFor_logoImgView:(UIImageView*)imgView   vm:(LoginViewModel*)viewModel{    if (!imgView.isStylized){        imgView.contentMode = UIViewContentModeScaleAspectFit;        imgView.backgroundColor = [UIColor clearColor];        imgView.opaque = YES;        imgView.clipsToBounds       = YES;        imgView.layer.masksToBounds = YES;        imgView.alpha      = 1.0f;        imgView.isStylized = YES;    }}


Обратите внимание на некое пропертиisStylized, оно было добавлено категорией к каждому наследнику классаUIView.

Традиция использовать данную переменную при настройкеUIэлементов пришла от опыта работы с ячейками в таблице.

Создана была для того, чтобы при переиспользовании однотипных ячеек не производилась повторная настройка (уже настроенных элементов), то есть, чтобы еще раз не добавлялись тени, блюры и т.д.

Реализация методов категории +Frames

Категория+Frames также как и+Styles не имеет системных методов, но может иметь словари класса, в которых могут быть расположены закэшированные размеры и координатыsubviews.

Подробный листинг данных методов публиковать не имеет смысла из-за их громоздкости, по сути, там не происходит ничего интересного, стандартное ручное вычисление координат и размеров.

Но стоит также обратить внимание, что по сравнению с другими категориями,+FramesсодержитИСКЛЮЧИТЕЛЬНОметоды класса (+).

Данная традиция также пришла из опыта работы с ячейками, первоначально методы класса были созданы для того, чтобы в фоне можно было заранее вычислить координаты и размеры для всехsubviewsне имея фактического объекта ячейки, а имея только лишь ее вьюМодель.

+ (CGRect) rectFor_signUpButton:(LoginViewModel*)viewModel                     parentFrame:(CGRect)parentFrame{    if (CGRectEqualToRect(CGRectZero, parentFrame)) return CGRectZero;    // Calculating...    return rect;}

Советы и рекомендации

По собственному опыту использования могу сказать, что имплементироватьMFSможно выборочно.

Например, как правило, имеет смысл создавать подобную архитектуру в тех случаях, когда мы имеем сложныйUIViewController(UIдля которого ввиду его сложности мы создаем кодом), или же когда имеем сложные ячейки таблицы.

То есть, для контроллера классаUITableViewController, за исключением особых случаев - смысла имплементировать данное решение нет.

Заключение

В данной статье вы имели возможность ознакомиться с паттерномMFS на примере работы с вьюКонтроллерами.

Во второй части статьи мы поговорим о примененииMFSпри работе с ячейками таблицы, как обеспечивать60FPSпри быстром скроллинге сложных таблиц на старых девайсах.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru