- PVSM.RU - https://www.pvsm.ru -
«Божественный» код — громкий термин, который может показаться желтым заголовком, но всё же именно о таком коде будет идти речь: из каких частей он состоит и как его писать. Это история о моих стараниях сделать так, чтобы задачи не возвращались с code review с пометкой: «Всё хе*ня — переделать».
У меня нет профильного образования, и учиться программированию приходилось на практике, через ошибки, ссадины и ушибы. Непрерывно работая над улучшением качества написанного кода, я вырабатывал некоторые правила, которым он должен соответствовать. Хочу ими поделиться.
GOD’S code — акроним из акронимов — код, написанный в соответствии с принципами Grasp, Object calisthenics, Demeter’s law и Solid. Кому-то они знакомы все, кто-то встречался лишь с некоторыми, но мы рассмотрим каждую составляющую акронима. Я не ставлю своей целью детально погрузиться в каждую группу правил, так как на просторах интернета они уже много раз освещались. Вместо этого предлагаю выжимку из собственного опыта.
Девять шаблонов для назначения ответственностей классам и объектам. Для удобства запоминания я разделяю их на две подгруппы:
Подробнее можно почитать здесь [1].
Набор правил оформления кода, очень похожих на свод законов codeStyle. Их также девять. Я расскажу о трёх, которые стараюсь соблюдать в повседневной работе (немного видоизменённых), об остальных можно почитать в первоисточнике [2].
public function processItems(array items)
{
// 0
foreach (items as item) {
// 1
for (i = 0; i < 5; i++) {
// 2
… process item 5 times …
}
}
}
В этом примере пятикратную обработку item
уместно вынести в отдельный метод.
public function processItems(array items)
{
// 0
foreach (items as item) {
// 1
this.processItem(item);
}
}
public function processItem(Item item)
{
// 0
for (i = 0; i < 5; i++) {
// 1
… process item 5 times …
}
}
Опять-таки, цель — иметь возможность понять, что делает метод, кинув на него один взгляд и не компилируя код в голове.
else
там, где он не нужен.
public function processSomeDto(SomeDtoClass dto)
{
if (predicat) {
throw new Exception(‘predicat is failed’);
} else {
return this.dtoProcessor.process(dto);
}
}
И брюки превращаются:
public function processSomeDto(SomeDtoClass dto)
{
if (predicat) {
throw new Exception(‘predicat is failed’);
}
return this.dtoProcessor.process(dto);
}
В результате приходится читать меньше кода, к тому же выпрямляем поток выполнения.
Жёсткая версия слабой связанности из GRASP’a. Накладывает ограничения на то, с кем может взаимодействовать текущий модуль.
Возьмём три объекта: А содержит B, В содержит С. Рассмотрим объект А. Метод А объекта А может иметь доступ только к методам и свойствам:
И всё. Проще говоря, объект А взаимодействует только с непосредственными соседями. Взаимодействие типа this.objectB.objectC.getSomeStuff()
является нарушением Закона Деметры, потому что из объекта А мы обращаемся к методу объекта С, который не является непосредственным соседом объекта А.
Есть пара интересных следствий. Во-первых, использование фабрик приводит к нарушению Закона Деметры. Посудите сами:
public function methodA()
{
spawnedObject = this.factory.spawn();
spawnedObject.performSomeStuff();
}
Равносильно:
public function methodA()
{
this.factory.spawn().performSomeStuff();
}
Для решения этой проблемы можно выделить некую обёртку, единственным назначением которой будет создание объекта с помощью фабрики и передача его на обработку куда-либо дальше.
public function methodA()
{
this.processor.process(this.factory.spawn());
}
Второе интересное следствие: DTO/Entity. Многим довольно часто приходится собирать или использовать контейнеры данных.
public function methodA(SomeDtoClass dto)
{
dto.getAddress().getCity();
}
Если следовать букве Закона Деметры, это будет нарушением, так как мы обратились к соседу соседа. Но на практике с контейнером данных допускают послабление, считая его как единое целое, и, соответственно, обращение к методу getCity DTO Address
в нашем случае считается обращением к части контейнера dto
.
SRP, OCP, LSP, ISP, DIP — я лишь вкратце коснусь их, потому как в Википедии и на Хабре они описаны довольно подробно.
SRP — принцип единственной ответственности. Один программный модуль — одна задача, одна причина для изменения. Перекликается с High Cohesion из GRASP’a.
Пример: у нас есть контроллер, задача которого — быть связующим звеном между бизнес-логикой и представлением (MVC). Если мы засунем какую-либо часть бизнес-логики в контроллер, то автоматически нарушим SRP.
public function indexAction(RequestInterface request): ResponseInterface
{
requestDto = this.requestTransformer.transform(request);
responseDto = this.requestProcessor.process(requestDto);
return this.responseTransformer.transform(responseDto);
}
В данном примере контроллер не выполняет никакой бизнес-логики или логики преобразования, а делегирует её другим модулям, выполняя лишь одну функцию — быть связующим звеном. Нарушение данного принципа приведёт к тому, что при необходимости внесения изменений в одной функциональности, мы увеличиваем риски затронуть другую функциональность, расположенную по соседству, но которую менять мы совершенно не планировали.
OCP — принцип открытости-закрытости. Пишем код так, чтобы не приходилось его изменять, а лишь расширять.
Если в выполнении кода нужно сделать какую-либо развилку, то обычно используется if/switch. А если нужно добавить ещё одну ветку, то текущий код изменяется. И это нехорошо. Работает — не трогай. Добавляй новое и тестируй новое, а старое пусть себе работает.
Для решения подобной задачи имеет смысл создать некоторый resolver, который настраивается набором ветвей и выбирает нужную.
final сlass Resolver implements ResolverInterface
{
private mapping;
public function Resolver(array mapping)
{
this.mapping = mapping;
}
public function resolve(Item item)
{
return this.mapping[item.getType()].perform(item);
}
}
Не забудьте про обработку ошибок, её я опустил. Дам совет: если принять за правило, что все классы могут быть либо final, либо abstract, то следовать этому принципу будет гораздо проще.
LSP — принцип подстановки Барбары Лисков. Корнями уходит в контрактное программирование и говорит о том, как нам правильно строить наследование.
Постулаты таковы:
class ParentClass
{
public function someMethod(string param1)
{
// some logic
}
}
class ChildClass extends ParentClass
{
public function someMethod(string param1, string param2)
{
if (param1 == '') {
throw new ExtraException();
}
// some logic
}
}
В данном примере someMethod
класса ChildClass
требует большего от клиентов (param2
), и к тому же предъявляет определённые требования к param1
, которых не было в родительском классе. То есть нарушаются правила наследования, в результате мы не сможем без дополнительных ухищрений заменять использование ParentClass
на ChildClass
.
ISP — принцип разделения интерфейсов. Говорит о том, что не стоит засовывать все возможные методы в один интерфейс. Иными словами, если мы реализуем в классе интерфейс, и при этом часть методов обязательны к реализации, но нам они не нужны, — значит, стоит разделить этот интерфейс.
interface DuckInterface
{
public function swim(): void;
public function fly(): void;
}
class MallardDuck implements DuckInterface
{
public function swim(): void
{
// some logic
}
public function fly(): void
{
// some logic
}
}
class RubberDuck implements DuckInterface
{
public function swim(): void
{
// some logic
}
public function fly(): void
{
// can't fly :(
}
}
RubberDuck
реализует интерфейс DuckInterface
. Но, насколько мне известно, резиновые утки не летают, поэтому интерфейс DuckInterface
имеет смысл разделить на два интерфейса FlyableInterface
и SwimableInterface
, и реализовывать уже их.
DIP — принцип инверсии зависимостей. Суть его в том, чтобы вынести все внешние зависимости за пределы модуля и завязаться на абстракции, а не подробности (то бишь на интерфейсы и абстрактные классы, а не конкретные классы). Одним из признаков нарушения этого принципа является наличие ключевого слова new
в классе.
class DIPViolation
{
public function badMethod()
{
someService = new SomeService(445, 'second params');
// operations with someService
}
}
Когда мы инстанцируем какой-либо сервис непосредственно внутри нашего класса, то в будущем будем иметь большие проблемы с покрытием этого кода модульными тестами. Также мы увеличиваем связанность между классами. Пример выше следует отрефакторить следующим образом:
class DIP
{
private $service;
public function DIP(SomeServiceInterface $someService)
{
$this->someService = $someService;
}
public function goodMethod()
{
// operations with someService
}
}
Итак, в данной статье, я постарался собрать воедино правила, которыми руководствуюсь, чтобы писать более удобочитаемый и «ремонтопригодный» код. Надеюсь, вам понравилось это маленькое приключение в принципы хорошего кода. И когда в следующий раз вы услышите, что код, который вы написали, просто «божественен», вы будете знать, что именно это значит :)
Автор: Дмитрий Порожняков
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/284115
Ссылки в тексте:
[1] почитать здесь: https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)
[2] первоисточнике: https://williamdurand.fr/2013/06/03/object-calisthenics/
[3] Источник: https://habr.com/post/414201/?utm_source=habrahabr&utm_medium=rss&utm_campaign=414201
Нажмите здесь для печати.