источник:Энтерит Ningsun.com/03-08-2019/…
Code review
Использует ли кто-нибудь в этой комнате проверку кода в своей повседневной работе? [Весь зал поднимает руки, вдохновляя]. Хорошо, зачем код-ревью? [Кто-то скандирует "Блокировать плохой код"]
Если проверка кода направлена на обнаружение плохого кода, как узнать, хороший это код или плохой?
Точно так же, как вы можете сказать «эта картина прекрасна» или «эта комната прекрасна», теперь вы можете сказать «код уродлив» или «исходный код прекрасен», но это субъективно. Я ищу черты, которые объективно говорят о хорошем или плохом коде.
Bad code
Во время проверки кода вы можете столкнуться со следующими характеристиками плохого кода:
- Rigid- Является ли код жестким? Есть ли у него сильные типы или параметры, которые затрудняют изменение?
- Fragile- Является ли код хрупким? Может ли незначительное изменение нанести неизмеримый ущерб кодовой базе?
- Immobile- Сложно ли рефакторить код? Код, чтобы избежать циклического импорта, просто набрав на клавиатуре?
- Complex- Есть ли код для демонстрации, он переработан?
- Verbose- Является ли код трудоемким в использовании? Когда вы читаете, видите ли вы, что делает код?
Положительны ли эти слова? Хотели бы вы, чтобы эти слова использовались для проверки вашего кода?
Предположительно нет.
Good design
Но это шаг вперед, теперь мы можем сказать: «Мне это не нравится, потому что это слишком сложно исправить» или «Мне это не нравится, потому что я не знаю, что пытается сделать код», но как переслать?
Было бы неплохо, если бы существовал какой-то способ описать плохой дизайн, а также характеристики хорошего дизайна, и сделать это объективно?
SOLID
В 2002 году Роберт Мартин опубликовал свою книгуAgile Software Development, Principles, Patterns, and Practicesкоторый описывает пять принципов дизайна многократно используемого программного обеспечения и называет ихSOLID
(английская аббревиатура) Принципы:
- Принцип единой ответственности
- Открытый/Закрытый Принцип
- Принцип замены Лисков
- Принцип разделения интерфейса
- Принцип инверсии зависимости
Эта книга немного устарела, и языки, которые в ней обсуждаются, — это языки, которые использовались более десяти лет назад. Но может бытьSOLID
Некоторые аспекты принципов могут дать нам подсказки о том, как говорить о хорошо спроектированной программе Go.
Принцип единой ответственности
Первый принцип SOLID — принцип единой ответственности.
A class should have one, and only one, reason to change.
– Robert C Martin
Теперь Go, по-видимому, неclassses
- вместо этого у нас более сильная концепция композиции - но если вы можете оглянуться назадclass
Я думаю, что использование этого слова в настоящее время будет иметь некоторую ценность.
Почему так важно, что есть только одна причина изменения фрагмента кода? Что ж, как бы неприятно ни менялся ваш собственный код, еще более болезненно обнаружить, что код, от которого зависит ваш код, изменился прямо у вас под ногами. Когда ваш код должен измениться, он должен сделать это в ответ на немедленные стимулы, а не стать жертвой побочного ущерба.
Следовательно, модификация кода с единственной ответственностью имеет меньше всего причин.
Coupling & Cohesion
Два слова, которые описывают, насколько легко или сложно изменить часть программного обеспечения: сцепление и связность.
- Сцепление — это просто слово, описывающее две вещи, которые изменяются вместе: движение одной вызывает движение другой.
- Родственное, но отдельное понятие — сплоченность, сила, притягивающая друг друга.
В контексте программного обеспечения связность — это свойство, описывающее естественное притяжение между фрагментами кода.
Чтобы описать единицы связи и связности в программах Go, мы могли бы поговорить о функциях и методах, которые обсуждаются вSRP
распространен, но я считаю, что это началось с пакетной модели Go.
SRP: Single Responsibility Principle
Package names
В Go весь код находится в каком-то пакете, а хорошо спроектированный пакет начинается с его имени. Имя пакета одновременно является описанием его назначения и префиксом пространства имен. Несколько хороших примеров пакетов из стандартной библиотеки Go:
-
net/http
- Предоставить http клиент и сервер -
os/exec
- выполнять внешние команды -
encoding/json
- Реализует кодирование и декодирование документов JSON.
Когда вы используете символы другого пакета внутри себя, используйтеimport
объявление, которое устанавливает связь на уровне исходного кода между двумя пакетами. Теперь они знают о существовании друг друга.
Bad package names
Такое внимание к именам не является педантичным. Плохо названный пакет лишается возможности перечислить способы его использования, если у него действительно есть цель.
-
server
Что дает пакет? ..., ну, надеюсь, сервер, но какой протокол он использует? -
private
Что дает пакет? Что-то, чего я не должен видеть? Должен ли он иметь публичные символы? -
common
пакет и его компаньоныutils
Like package , часто встречается у других «партнеров».
Мы видим все пакеты такими, и это становится всевозможными дампами, которые часто меняются без причины, потому что у них так много обязанностей.
UNIX-философия Go
На мой взгляд, любое обсуждение несвязанного проектирования было бы неполным без ссылки на философию Unix Дуга Макилроя; небольшие, но четкие инструменты объединяются для решения более крупных задач, которые часто выходят за рамки воображения первоначального автора задачи.
Я думаю, что пакеты Go воплощают дух философии Unix. По сути, каждый пакет Go сам по себе представляет собой небольшую программу Go, единую единицу изменений с единой ответственностью.
Открытый/Закрытый Принцип
Второй принцип, а именно О,Bertrand Meyer
принцип открытого/закрытого, он писал в 1988 году:
Software entities should be open for extension, but closed for modification.
– Bertrand Meyer, Object-Oriented Software Construction
Как этот совет применим к языкам, написанным после 21 года?
package main
type A struct {
year int
}
func (a A) Greet() { fmt.Println("Hello GolangUK", a.year) }
type B struct {
A
}
func (b B) Greet() { fmt.Println("Welcome to GolangUK", b.year) }
func main() {
var a A
a.year = 2016
var b B
b.year = 2016
a.Greet() // Hello GolangUK 2016
b.Greet() // Welcome to GolangUK 2016
}
У нас есть тип A с полем year и методом Greet. У нас есть второй тип, B, который встраивает A, и поскольку A встроен, вызывающая программа видит, что методы B переопределяют методы A. Поскольку A встроен в B как поле, B может предоставить свой собственный метод Greet, затмевающий метод Greet A.
Но встраивание предназначено не только для методов, вы также можете получить доступ к полям встроенных типов. Как видите, поскольку и A, и B определены в одном пакете, B может получить доступ к закрытому полю года A, как если бы оно было объявлено в B.
Таким образом, встраивание — это мощный инструмент, который позволяет типам Go быть открытыми для расширений.
package main
type Cat struct {
Name string
}
func (c Cat) Legs() int { return 4 }
func (c Cat) PrintLegs() {
fmt.Printf("I have %d legs\n", c.Legs())
}
type OctoCat struct {
Cat
}
func (o OctoCat) Legs() int { return 5 }
func main() {
var octo OctoCat
fmt.Println(octo.Legs()) // 5
octo.PrintLegs() // I have 4 legs
}
В этом примере у нас есть тип Cat, и мы можем подсчитать его ноги, используя его метод Legs. Мы внедряем тип Cat в новый тип OctoCat и объявляем, что у Octocats пять ног. Однако, хотя OctoCat определяет свой собственный метод Legs, который возвращает 5, при вызове метода PrintLegs он возвращает 4.
Это связано с тем, что PrintLegs определен для типа Cat. Ему нужен Кот в качестве получателя, поэтому он отправляет в метод «Кошачьи лапки». Cat не знает, в какой тип он встроен, поэтому он не может изменить свой набор методов, пока он встроен.
Поэтому можно сказать, что типы Go открыты для расширения, но закрыты для модификации.
По сути, методы в Go — не более чем синтаксический сахар вокруг функций с заранее объявленными формальными параметрами (т.е. получателями).
func (c Cat) PrintLegs() {
fmt.Printf("I have %d legs\n", c.Legs())
}
func PrintLegs(c Cat) {
fmt.Printf("I have %d legs\n", c.Legs())
}
Получатель — это именно та функция, в которую вы его передаете, первый аргумент функции, а поскольку Go не поддерживает перегрузку функций, OctoCat не может заменить обычный Cat. Это подводит меня к следующему принципу.
Принцип замены Лисков
Принцип замещения Лисков, предложенный Барбарой Лисков, грубо утверждает, что два типа можно заменять, если они демонстрируют поведение, неразличимое для вызывающей стороны.
В языках, основанных на классах, принцип подстановки Лискова часто интерпретируется как спецификация абстрактного базового класса с различными конкретными подтипами. Но в Go нет классов или наследования, поэтому замена не может быть реализована в соответствии с иерархией абстрактных классов.
Interfaces
Вместо этого замена является областью действия интерфейса Go. В Go типам не нужно указывать, что они реализуют конкретный интерфейс, но любой тип реализует интерфейс, если у него есть метод, сигнатура которого соответствует объявлению интерфейса.
Мы говорим, что в Go интерфейсы выполняются неявно, а не явно, и это имеет серьезные последствия для того, как они используются в языке.
Хорошо спроектированные интерфейсы, скорее всего, будут небольшими; распространенная практика заключается в том, что интерфейс содержит только один метод. Логически небольшие интерфейсы упрощают реализацию, и наоборот. Таким образом формируется пакет, состоящий из простых реализаций общего поведения.
io.Reader
type Reader interface {
// Read reads up to len(buf) bytes into buf.
Read(buf []byte) (n int, err error)
}
Это легко приводит меня к моему любимому интерфейсу Go.io.Reader
.
io.Reader
Интерфейс очень простой;Read
Считайте данные в предоставленный буфер и верните все ошибки, которые были прочитаны байтами, и любые ошибки, возникшие во время чтения, вызывающей стороне. Выглядит очень просто, но очень мощно.
потому чтоio.Reader
Может обрабатывать все, что представлено в виде потока байтов, поэтому мы можем создавать что угодноReader
; константные строки, байтовые массивы, стандартный ввод, сетевые потоки, сжатые tar-файлы, стандартный вывод команд, выполняемых удаленно через ssh.
И все эти реализации взаимозаменяемы, потому что реализуют один и тот же простой контракт.
Таким образом, принцип подстановки Лисков, применимый к го, можно резюмировать изречением покойного Джима Вейриха.
Require no more, promise no less.
– Jim Weirich
Успешно переведен на четвертый принцип "SOLID".
Принцип разделения интерфейса
Четвертый принцип — принцип разделения интерфейса, который гласит:
Клиентов не следует заставлять зависеть от методов, которые они не используют. – Роберт С. Мартин
В Go применение принципа разделения интерфейса может относиться к процессу выделения поведения, необходимого функции для выполнения своей работы. В качестве конкретного примера предположим, что я выполнил задачу «написать функцию, которая сохраняет структуру документа на диск».
// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error
Я могу определить эту функцию, назовем ееSave
, который записывает данный Document в*os.File
. Но это имеет некоторые проблемы.
Подпись сохранения исключает возможность записи данных в сетевое расположение. Предполагая, что сетевое хранилище может стать требованием позже, сигнатура этой функции должна измениться, затрагивая всех ее вызывающих.
из-заSave
Напрямую манипулировать файлами на диске, поэтому неудобно тестировать. Для проверки его работы тест должен прочитать содержимое файла после записи. Кроме того, тестирование должно гарантировать, чтоf
Запишите во временное место, а затем удалите его.
*os.File
также определяет многиеSave
Несвязанные методы, такие как чтение каталога и проверка, является ли путь ссылкой на файл. еслиSave
Сигнатура функции может описывать только*os.File
Соответствующая часть будет полезна.
Как мы справляемся с этими проблемами?
// Save writes the contents of doc to the supplied ReadWriterCloser.
func Save(rwc io.ReadWriteCloser, doc *Document) error
использоватьio.ReadWriteCloser
Мы можем применить принцип разделения интерфейса, чтобы переопределить интерфейс, используя более общий тип файла.Save
.
С этим изменением любая реализация, котораяio.ReadWriteCloser
Тип интерфейса может заменить предыдущий*os.File
. сделатьSave
более широкое применение иSave
Звонивший утверждает, что*os.File
Какие методы типа связаны с операцией.
в видеSave
автор, у меня больше нет возможности позвонить*os.File
эти нерелевантные методы, потому что они скрыты вio.ReadWriteCloser
за интерфейсом. Мы можем продвинуть принцип изоляции интерфейса на шаг вперед.
Во-первых, еслиSave
Следуя принципу единой ответственности, было бы невозможно прочитать файл, который он только что написал, чтобы проверить его содержимое, что должно быть обязанностью другого фрагмента кода. Поэтому мы можем перейти кSave
Спецификация интерфейса ограничивается только записью и закрытием.
// Save writes the contents of doc to the supplied WriteCloser.
func Save(wc io.WriteCloser, doc *Document) error
Во-вторых, поSave
Предоставьте механизм для закрытия его потока, мы продолжаем этот механизм, чтобы он выглядел как тип файла, что создает проблему,wc
При каких обстоятельствах он будет закрыт.Save
можно безоговорочно назватьClose
, или призвать к успехуClose
.
это даетSave
Вызывающая сторона создает проблему, поскольку может захотеть записать дополнительные данные в поток после того, как документ был записан.
type NopCloser struct {
io.Writer
}
// Close has no effect on the underlying writer.
func (c *NopCloser) Close() error { return nil }
Грубое решение состоит в том, чтобы определить новый тип, который включает в себяio.Writer
и перезаписатьClose
метод предотвращенияSave
метод закрывает базовый поток данных.
Но это, вероятно, нарушило бы принцип подстановки Лискова, поскольку NopCloser на самом деле ничего не закрывает.
// Save writes the contents of doc to the supplied Writer.
func Save(w io.Writer, doc *Document) error
Лучшее решение - переопределитьSave
получать толькоio.Writer
, полностью снимает с него ответственность за выполнение каких-либо действий, кроме записи данных в поток.
Применив принцип сегрегации интерфейса, наша функция Save, получив функцию, наиболее специфичную с точки зрения требований — она принимает только записываемый параметр — и имеет самую общую функцию, теперь мы можем использоватьSave
сохранить наши данные в любую реализациюio.Writer
Место.
A great rule of thumb for Go is accept interfaces, return structs.
– Jack Lindamood
Сделав шаг назад, эта цитата представляет собой интересный мем, который пронизывал мышление Го в течение последних нескольких лет.
Джек не виноват, что этой версии размером с твит не хватает деталей, но я думаю, что она представляет собой первую законную традицию дизайна Go.
Принцип инверсии зависимости
Последний принцип SOLID — это принцип инверсии зависимостей, который гласит:
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
– Robert C. Martin
Но что означает инверсия зависимостей на практике для программистов Go?
Если вы применили все принципы, о которых мы говорили ранее, ваш код должен быть разбит на отдельные пакеты, каждый из которых имеет четко определенную ответственность или цель. Ваш код должен описывать свои зависимости в терминах интерфейсов, и эти интерфейсы следует рассматривать как описывающие только поведение, требуемое этими функциями. Другими словами, больше ничего делать не следует.
Итак, я думаю, что в контексте Go Мартин имеет в виду структуру графа импорта.
В Go граф импорта должен быть ациклическим. Несоблюдение этого ациклического требования приведет к сбою компиляции, но, что более серьезно, это представляет собой серьезную ошибку в проекте.
При прочих равных условиях график импорта хорошо спроектированной программы Go должен быть широким и относительно плоским, а не высоким и узким. Если у вас есть пакет, функции которого не могут быть запущены без помощи другого пакета, то это может указывать на то, что код плохо разложен по границам пакета.
Принцип инверсии зависимостей побуждает вас переносить определенные обязанности как можно выше по графу импорта, к основному пакету или обработчику верхнего уровня, оставляя код более низкого уровня для работы с абстрактными интерфейсами.
SOLID Go Design
Напомним, что применительно к Go каждый принцип SOLID представляет собой сильное заявление о дизайне, но вместе они имеют центральную тему.
- Принцип единой ответственности побуждает вас структурировать функции, типы и методы как пакеты с естественной связностью; типы принадлежат друг другу, а функции служат одной цели.
- Принцип открытости/закрытости побуждает вас использовать вложения для объединения простых типов в более сложные типы.
- Принцип подстановки Лисков, который побуждает вас выражать зависимости между пакетами в терминах интерфейсов, а не конкретных типов. Определяя небольшие интерфейсы, мы можем быть более уверены в том, что реализации будут точно выполнять свои контракты.
- Принцип разделения интерфейса продвигает эту идею на шаг вперед и побуждает вас определять функции и методы, которые зависят только от их желаемого поведения. Если вашей функции требуются только методы с параметрами одного типа интерфейса, функция, скорее всего, будет нести только одну ответственность.
- Принцип инверсии зависимостей побуждает вас передавать знания, от которых зависит пакет, от времени компиляции до времени выполнения. В Go это видно по уменьшению количества операторов импорта, используемых конкретным пакетом.
Если бы я подытожил разговор, это могло бы выглядеть так:interfaces let you apply the SOLID principles to Go programs
.
Потому что интерфейсы позволяют программистам Go описывать, что предоставляет их пакет, а не то, как он это делает. Другими словами, «расцепление», что на самом деле является целью, потому что чем более слабо связано программное обеспечение, тем легче его модифицировать.
Как говорит Санди Мец:
Design is the art of arranging code that needs to work today, and to be easy to change forever.
– Sandi Metz
Потому что, если Go хочет стать языком, который компании инвестируют в долгосрочной перспективе, удобство сопровождения программ Go, которые легче изменить, будет ключевым фактором в их решении.
конец
Наконец, давайте вернемся к вопросу, с которого я начал это выступление: сколько в мире программистов на языке Go? Вот мое предположение:
By 2020, there will be 500,000 Go developers.
- me
На что бы потратили свое время полмиллиона программистов на Go? Ну, очевидно, они напишут много кода на Go, и, честно говоря, не все из них будут хорошим кодом, некоторые будут ужасны.
Пожалуйста, поймите, что я не грублю, говоря это, но каждый в этой комнате, у кого есть опыт разработки других языков — языков, с которых вы пришли, приходите в Go — знает по собственному опыту, что один Дело в том, что это пророчество реально.
Within C++, there is a much smaller and cleaner language struggling to get out.
– Bjarne Stroustrup, The Design and Evolution of C++
У всех программистов есть шанс сделать наш язык успешным, полагаясь на нашу коллективную способность не связываться с людьми, которые начинают говорить о Go так, как сегодня шутят о C++.
Повествование, высмеивающее другие языки, чрезмерно многословно, многословно и сверхсложно, и когда-нибудь превратится в GO, а я не хочу этого видеть, поэтому у меня есть просьба.
Go-программистам нужно меньше говорить о фреймворках и больше о дизайне. Нам нужно перестать сосредотачиваться на производительности любой ценой и сделать все возможное для повторного использования.
Я хочу, чтобы люди говорили о том, как использовать язык, который мы используем сегодня, независимо от его выбора и ограничений, для разработки решений и решения реальных проблем.
Что я хочу услышать, так это то, что люди говорят о том, как проектировать программы Go таким образом, чтобы они были хорошо спроектированными, несвязанными, повторно используемыми и, что наиболее важно, реагирующими на изменения.
… еще кое-что
Здорово слышать здесь сегодня многих спикеров, но правда в том, что независимо от того, насколько велика эта конференция, мы — ничтожная часть от числа людей, использующих Go на протяжении всей его жизни.
Поэтому нам нужно рассказать остальному миру, как писать хорошие программы. Отличное программное обеспечение, компонуемое программное обеспечение, легко изменяемое программное обеспечение и покажите им, как изменить его с помощью Go. Это начинается с вас.
Я хочу, чтобы вы начали говорить о дизайне, возможно, используя некоторые идеи, которые я представил здесь, и, надеюсь, вы сможете провести собственное исследование и применить эти идеи в своих проектах. Тогда я хочу, чтобы ты:
- Напишите сообщение в блоге о дизайне.
- Проведите мастер-класс по дизайну.
- Напишите книгу о том, что вы узнали.
- Приходите на эту встречу в следующем году и расскажите о своих достижениях.
Потому что, делая эти вещи, мы можем создать культуру разработчиков Go, которые заботятся о программах, разработанных на века.
Благодарю.
оригинал:SOLID Go Design