[Программирование, Проектирование и рефакторинг] Не заблудиться в трёх if'ах. Рефакторинг ветвящихся условий
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
На просторах интернета можно найти множество описаний приемов упрощения условных выражений (например, тут). В своей практике я иногда использую комбинацию замены вложенных условных операторов граничным оператором и объединения условных операторов. Обычно она дает красивый результат, когда количество независимых условий и выполняемых выражений заметно меньше количества веток, в которых они комбинируются различными способами. Код будет на C#, но действия одинаковы для любого языка, поддерживающего конструкции if/else.
оригинал
Дано
Есть интерфейс IUnit.
IUnit
SPL
public interface IUnit
{
string Description { get; }
}
И его реализации Piece и Cluster.
Piece
SPL
public class Piece : IUnit
{
public string Description { get; }
public Piece(string description) =>
Description = description;
public override bool Equals(object obj) =>
Equals(obj as Piece);
public bool Equals(Piece piece) =>
piece != null &&
piece.Description.Equals(Description);
public override int GetHashCode()
{
unchecked
{
var hash = 17;
foreach (var c in Description)
hash = 23 * hash + c.GetHashCode();
return hash;
}
}
}
Cluster
SPL
public class Cluster : IUnit
{
private readonly IReadOnlyList<Piece> pieces;
public IEnumerable<Piece> Pieces => pieces;
public string Description { get; }
public Cluster(IEnumerable<Piece> pieces)
{
if (!pieces.Any())
throw new ArgumentException();
if (pieces.Select(unit => unit.Description).Distinct().Count() > 1)
throw new ArgumentException();
this.pieces = pieces.ToArray();
Description = this.pieces[0].Description;
}
public Cluster(IEnumerable<Cluster> clusters)
: this(clusters.SelectMany(cluster => cluster.Pieces))
{
}
public override bool Equals(object obj) =>
Equals(obj as Cluster);
public bool Equals(Cluster cluster) =>
cluster != null &&
cluster.Description.Equals(Description) &&
cluster.pieces.Count == pieces.Count;
public override int GetHashCode()
{
unchecked
{
var hash = 17;
foreach (var c in Description)
hash = 23 * hash + c.GetHashCode();
hash = 23 * hash + pieces.Count.GetHashCode();
return hash;
}
}
}
Также есть класс MergeClusters, который обрабатывает коллекции IUnit и объединяет последовательности совместимых Cluster в один элемент. Поведение класса проверяется тестами.
MergeClusters
SPL
public class MergeClusters
{
private readonly List<Cluster> buffer = new List<Cluster>();
private List<IUnit> merged;
private readonly IReadOnlyList<IUnit> units;
public IEnumerable<IUnit> Result
{
get
{
if (merged != null)
return merged;
merged = new List<IUnit>();
Merge();
return merged;
}
}
public MergeClusters(IEnumerable<IUnit> units)
{
this.units = units.ToArray();
}
private void Merge()
{
Seed();
for (var i = 1; i < units.Count; i++)
MergeNeighbors(units[i - 1], units[i]);
Flush();
}
private void Seed()
{
if (units[0] is Cluster)
buffer.Add((Cluster)units[0]);
else
merged.Add(units[0]);
}
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster)
{
if (next is Cluster)
{
if (!prev.Description.Equals(next.Description))
{
Flush();
}
buffer.Add((Cluster)next);
}
else
{
Flush();
merged.Add(next);
}
}
else
{
if (next is Cluster)
{
buffer.Add((Cluster)next);
}
else
{
merged.Add(next);
}
}
}
private void Flush()
{
if (!buffer.Any())
return;
merged.Add(new Cluster(buffer));
buffer.Clear();
}
}
MergeClustersTests
SPL
[Fact]
public void Result_WhenUnitsStartWithNonclusterAndEndWithCluster_IsCorrect()
{
// Arrange
IUnit[] units = new IUnit[]
{
new Piece("some description"),
new Piece("some description"),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
};
MergeClusters sut = new MergeClusters(units);
// Act
IEnumerable<IUnit> actual = sut.Result;
// Assert
IUnit[] expected = new IUnit[]
{
new Piece("some description"),
new Piece("some description"),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
};
actual.Should().BeEquivalentTo(expected);
}
[Fact]
public void Result_WhenUnitsStartWithClusterAndEndWithCluster_IsCorrect()
{
// Arrange
IUnit[] units = new IUnit[]
{
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
};
MergeClusters sut = new MergeClusters(units);
// Act
IEnumerable<IUnit> actual = sut.Result;
// Assert
IUnit[] expected = new IUnit[]
{
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
};
actual.Should().BeEquivalentTo(expected);
}
[Fact]
public void Result_WhenUnitsStartWithClusterAndEndWithNoncluster_IsCorrect()
{
// Arrange
IUnit[] units = new IUnit[]
{
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
};
MergeClusters sut = new MergeClusters(units);
// Act
IEnumerable<IUnit> actual = sut.Result;
// Assert
IUnit[] expected = new IUnit[]
{
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
};
actual.Should().BeEquivalentTo(expected);
}
[Fact]
public void Result_WhenUnitsStartWithNonclusterAndEndWithNoncluster_IsCorrect()
{
// Arrange
IUnit[] units = new IUnit[]
{
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
};
MergeClusters sut = new MergeClusters(units);
// Act
IEnumerable<IUnit> actual = sut.Result;
// Assert
IUnit[] expected = new IUnit[]
{
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
new Piece("some description"),
}),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
new Cluster(
new Piece[]
{
new Piece("another description"),
new Piece("another description"),
new Piece("another description"),
new Piece("another description"),
}),
new Piece("another description"),
};
actual.Should().BeEquivalentTo(expected);
}
Нас интересует метод void MergeNeighbors(IUnit, IUnit) класса MergeClusters.
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster)
{
if (next is Cluster)
{
if (!prev.Description.Equals(next.Description))
{
Flush();
}
buffer.Add((Cluster)next);
}
else
{
Flush();
merged.Add(next);
}
}
else
{
if (next is Cluster)
{
buffer.Add((Cluster)next);
}
else
{
merged.Add(next);
}
}
}
С одной стороны, он работает правильно, но с другой, хотелось бы сделать его более выразительным и по возможности улучшить метрики кода. Метрики будем считать с помощью инструмента Analyze > Calculate Code Metrics, который входит в состав Visual Studio Community. Изначально они имеют значения:
Configuration: Debug
Member: MergeNeighbors(IUnit, IUnit) : void
Maintainability Index: 64
Cyclomatic Complexity: 5
Class Coupling: 4
Lines of Source code: 32
Lines of Executable code: 10
Описанный далее подход в общем случае не гарантирует красивый результат.
Бородатая шутка по случаю
SPL
#392487
Мне недавно рассказали как делают корабли в бутылках. В бутылку засыпают силикатного клея, говна и трясут. Получаются разные странные штуки, иногда корабли.
© bash.org
Рефакторинг
Шаг 1
Проверяем, что каждая цепочка условий одного уровня вложенности заканчивается блоком else, в противном случае добавляем пустой блок else.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster)
{
if (next is Cluster)
{
if (!prev.Description.Equals(next.Description))
{
Flush();
}
else
{
}
buffer.Add((Cluster)next);
}
else
{
Flush();
merged.Add(next);
}
}
else
{
if (next is Cluster)
{
buffer.Add((Cluster)next);
}
else
{
merged.Add(next);
}
}
}
Шаг 2
Если на одном уровне вложенности с условными блоками существуют выражения, переносим каждое выражение к каждый условный блок. Если выражение предшествует блоку, добавляем его в начало блока, в противном случае — в конец. Повторяем, пока на каждом уровне вложенности условные блоки не будут соседствовать только с другими условными блоками.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster)
{
if (next is Cluster)
{
if (!prev.Description.Equals(next.Description))
{
Flush();
buffer.Add((Cluster)next);
}
else
{
buffer.Add((Cluster)next);
}
}
else
{
Flush();
merged.Add(next);
}
}
else
{
if (next is Cluster)
{
buffer.Add((Cluster)next);
}
else
{
merged.Add(next);
}
}
}
Шаг 3
На каждом уровне вложенности для каждого блока if отрезаем остаток цепочки условий, создаем новый блок if с выражением, обратным выражению первого if, помещаем внутрь нового блока вырезанную цепочку и удаляем первое слово else. Повторяем, пока не останется ни одного else.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster)
{
if (next is Cluster)
{
if (!prev.Description.Equals(next.Description))
{
Flush();
buffer.Add((Cluster)next);
}
if (prev.Description.Equals(next.Description))
{
{
buffer.Add((Cluster)next);
}
}
}
if (!(next is Cluster))
{
{
Flush();
merged.Add(next);
}
}
}
if (!(prev is Cluster))
{
{
if (next is Cluster)
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster))
{
{
merged.Add(next);
}
}
}
}
}
Шаг 4
«Схлопываем» блоки.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster)
{
if (next is Cluster)
{
if (!prev.Description.Equals(next.Description))
{
Flush();
buffer.Add((Cluster)next);
}
if (prev.Description.Equals(next.Description))
{
buffer.Add((Cluster)next);
}
}
if (!(next is Cluster))
{
Flush();
merged.Add(next);
}
}
if (!(prev is Cluster))
{
if (next is Cluster)
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster))
{
merged.Add(next);
}
}
}
Шаг 5
К условиям каждого блока if, не имеющего вложенных блоков, с помощью оператора && добавляем условия всех родительский блоков if.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster)
{
if (next is Cluster)
{
if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)
{
Flush();
buffer.Add((Cluster)next);
}
if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)
{
buffer.Add((Cluster)next);
}
}
if (!(next is Cluster) && prev is Cluster)
{
Flush();
merged.Add(next);
}
}
if (!(prev is Cluster))
{
if (next is Cluster && !(prev is Cluster))
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster) && !(prev is Cluster))
{
merged.Add(next);
}
}
}
Шаг 6
Оставляем только блоки if, не имеющие вложенных блоков, сохраняя порядок их появления в коде.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)
{
Flush();
buffer.Add((Cluster)next);
}
if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster) && prev is Cluster)
{
Flush();
merged.Add(next);
}
if (next is Cluster && !(prev is Cluster))
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster) && !(prev is Cluster))
{
merged.Add(next);
}
}
Шаг 7
Для каждого уникального выражения в порядке их появления в коде выписываем содержащие их блоки. При этом другие выражения внутри блоков игнорируем.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)
{
Flush();
}
if (!(next is Cluster) && prev is Cluster)
{
Flush();
}
if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)
{
buffer.Add((Cluster)next);
}
if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster)
{
buffer.Add((Cluster)next);
}
if (next is Cluster && !(prev is Cluster))
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster) && prev is Cluster)
{
merged.Add(next);
}
if (!(next is Cluster) && !(prev is Cluster))
{
merged.Add(next);
}
}
Шаг 8
Объединяем блоки с одинаковыми выражениями, применяя к их условиям оператор ||.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster ||
!(next is Cluster) && prev is Cluster)
{
Flush();
}
if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster ||
prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster ||
next is Cluster && !(prev is Cluster))
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster) && prev is Cluster ||
!(next is Cluster) && !(prev is Cluster))
{
merged.Add(next);
}
}
Шаг 9
Упрощаем условные выражения с помощью правил булевой алгебры.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (prev is Cluster && !(next is Cluster && prev.Description.Equals(next.Description)))
{
Flush();
}
if (next is Cluster)
{
buffer.Add((Cluster)next);
}
if (!(next is Cluster))
{
merged.Add(next);
}
}
Шаг 10
Рихтуем напильником.
Результат
SPL
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (IsEndOfCompatibleClusterSequence(prev, next))
Flush();
if (next is Cluster)
buffer.Add((Cluster)next);
else
merged.Add(next);
}
private static bool IsEndOfCompatibleClusterSequence(IUnit prev, IUnit next) =>
prev is Cluster && !(next is Cluster && prev.Description.Equals(next.Description));
Итого
После рефакторинга метод выглядит так:
private void MergeNeighbors(IUnit prev, IUnit next)
{
if (IsEndOfCompatibleClusterSequence(prev, next))
Flush();
if (next is Cluster)
buffer.Add((Cluster)next);
else
merged.Add(next);
}
А метрики так:
Configuration: Debug
Member: MergeNeighbors(IUnit, IUnit) : void
Maintainability Index: 82
Cyclomatic Complexity: 3
Class Coupling: 3
Lines of Source code: 10
Lines of Executable code: 2
Метрики заметно улучшились, а код стал гораздо короче и выразительнее. Но самым замечательным в этом подходе, лично для меня, является вот что: кто-то способен сразу увидеть, что метод должен выглядеть, как в конечном варианте, а кто-то может написать только изначальную реализацию, но имея хотя бы какую-то формулировку правильного поведения, с помощью чисто механических действий (за исключением, возможно, последнего шага) ее можно привести к наиболее лаконичному и наглядному виду.
P.S. Все куски знаний, которые сложились в описанный в публикации алгоритм, были получены автором еще в школе более 15 лет назад. За что он выражает огромную благодарность учителям-энтузиастам, дающим детям основу нормального образования. Татьяна Алексеевна, Наталья Павловна, если вы вдруг это читаете, большое вам СПАСИБО!
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, Анализ и проектирование систем, Проектирование и рефакторинг, Промышленное программирование] Шаблоны GRASP: Polymorphism, Pure Fabrication, Indirection, Protected Variations
- [Программирование микроконтроллеров] Разработка измерительного прибора ИРИС
- [Управление разработкой, Agile, Тестирование IT-систем, Промышленное программирование] Монолог тимлида об использовании Agile-манифеста при промышленной разработке программных продуктов
- [Программирование, SQL, Администрирование баз данных] 10 приёмов работы с Oracle
- [Программирование, ВКонтакте API] Как создать мини-приложение: база знаний VK Mini Apps
- [Высокая производительность, Программирование, Java] Маленькие оптимизации в Java 9-16
- [JavaScript, Программирование, Разработка веб-сайтов] Drag'n'Drop API: пример использования
- [Анализ и проектирование систем, Программирование, Промышленное программирование, Системы управления версиями] О системах контроля версий
- [Программирование, Алгоритмы, Научно-популярное] Можно ли научить обезьяну программировать? Ждёт ли нас поколение специалистов-бабуинов
- [Разработка веб-сайтов, JavaScript, Программирование, VueJS] Vue 3.0 — первый взгляд
Теги для поиска: #_programmirovanie (Программирование), #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_programmirovanie (программирование), #_refaktoring (рефакторинг), #_programmirovanie (
Программирование
), #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:02
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
На просторах интернета можно найти множество описаний приемов упрощения условных выражений (например, тут). В своей практике я иногда использую комбинацию замены вложенных условных операторов граничным оператором и объединения условных операторов. Обычно она дает красивый результат, когда количество независимых условий и выполняемых выражений заметно меньше количества веток, в которых они комбинируются различными способами. Код будет на C#, но действия одинаковы для любого языка, поддерживающего конструкции if/else. оригинал Дано Есть интерфейс IUnit. IUnitSPLpublic interface IUnit
{ string Description { get; } } И его реализации Piece и Cluster. PieceSPLpublic class Piece : IUnit
{ public string Description { get; } public Piece(string description) => Description = description; public override bool Equals(object obj) => Equals(obj as Piece); public bool Equals(Piece piece) => piece != null && piece.Description.Equals(Description); public override int GetHashCode() { unchecked { var hash = 17; foreach (var c in Description) hash = 23 * hash + c.GetHashCode(); return hash; } } } ClusterSPLpublic class Cluster : IUnit
{ private readonly IReadOnlyList<Piece> pieces; public IEnumerable<Piece> Pieces => pieces; public string Description { get; } public Cluster(IEnumerable<Piece> pieces) { if (!pieces.Any()) throw new ArgumentException(); if (pieces.Select(unit => unit.Description).Distinct().Count() > 1) throw new ArgumentException(); this.pieces = pieces.ToArray(); Description = this.pieces[0].Description; } public Cluster(IEnumerable<Cluster> clusters) : this(clusters.SelectMany(cluster => cluster.Pieces)) { } public override bool Equals(object obj) => Equals(obj as Cluster); public bool Equals(Cluster cluster) => cluster != null && cluster.Description.Equals(Description) && cluster.pieces.Count == pieces.Count; public override int GetHashCode() { unchecked { var hash = 17; foreach (var c in Description) hash = 23 * hash + c.GetHashCode(); hash = 23 * hash + pieces.Count.GetHashCode(); return hash; } } } Также есть класс MergeClusters, который обрабатывает коллекции IUnit и объединяет последовательности совместимых Cluster в один элемент. Поведение класса проверяется тестами. MergeClustersSPLpublic class MergeClusters
{ private readonly List<Cluster> buffer = new List<Cluster>(); private List<IUnit> merged; private readonly IReadOnlyList<IUnit> units; public IEnumerable<IUnit> Result { get { if (merged != null) return merged; merged = new List<IUnit>(); Merge(); return merged; } } public MergeClusters(IEnumerable<IUnit> units) { this.units = units.ToArray(); } private void Merge() { Seed(); for (var i = 1; i < units.Count; i++) MergeNeighbors(units[i - 1], units[i]); Flush(); } private void Seed() { if (units[0] is Cluster) buffer.Add((Cluster)units[0]); else merged.Add(units[0]); } private void MergeNeighbors(IUnit prev, IUnit next) { if (prev is Cluster) { if (next is Cluster) { if (!prev.Description.Equals(next.Description)) { Flush(); } buffer.Add((Cluster)next); } else { Flush(); merged.Add(next); } } else { if (next is Cluster) { buffer.Add((Cluster)next); } else { merged.Add(next); } } } private void Flush() { if (!buffer.Any()) return; merged.Add(new Cluster(buffer)); buffer.Clear(); } } MergeClustersTestsSPL[Fact]
public void Result_WhenUnitsStartWithNonclusterAndEndWithCluster_IsCorrect() { // Arrange IUnit[] units = new IUnit[] { new Piece("some description"), new Piece("some description"), new Piece("another description"), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), }; MergeClusters sut = new MergeClusters(units); // Act IEnumerable<IUnit> actual = sut.Result; // Assert IUnit[] expected = new IUnit[] { new Piece("some description"), new Piece("some description"), new Piece("another description"), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), }; actual.Should().BeEquivalentTo(expected); } [Fact] public void Result_WhenUnitsStartWithClusterAndEndWithCluster_IsCorrect() { // Arrange IUnit[] units = new IUnit[] { new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), }; MergeClusters sut = new MergeClusters(units); // Act IEnumerable<IUnit> actual = sut.Result; // Assert IUnit[] expected = new IUnit[] { new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), }; actual.Should().BeEquivalentTo(expected); } [Fact] public void Result_WhenUnitsStartWithClusterAndEndWithNoncluster_IsCorrect() { // Arrange IUnit[] units = new IUnit[] { new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), }; MergeClusters sut = new MergeClusters(units); // Act IEnumerable<IUnit> actual = sut.Result; // Assert IUnit[] expected = new IUnit[] { new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), new Piece("another description"), new Piece("another description"), }), new Piece("another description"), }; actual.Should().BeEquivalentTo(expected); } [Fact] public void Result_WhenUnitsStartWithNonclusterAndEndWithNoncluster_IsCorrect() { // Arrange IUnit[] units = new IUnit[] { new Piece("another description"), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), }; MergeClusters sut = new MergeClusters(units); // Act IEnumerable<IUnit> actual = sut.Result; // Assert IUnit[] expected = new IUnit[] { new Piece("another description"), new Cluster( new Piece[] { new Piece("some description"), new Piece("some description"), new Piece("some description"), new Piece("some description"), }), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), }), new Piece("another description"), new Cluster( new Piece[] { new Piece("another description"), new Piece("another description"), new Piece("another description"), new Piece("another description"), }), new Piece("another description"), }; actual.Should().BeEquivalentTo(expected); } Нас интересует метод void MergeNeighbors(IUnit, IUnit) класса MergeClusters. private void MergeNeighbors(IUnit prev, IUnit next)
{ if (prev is Cluster) { if (next is Cluster) { if (!prev.Description.Equals(next.Description)) { Flush(); } buffer.Add((Cluster)next); } else { Flush(); merged.Add(next); } } else { if (next is Cluster) { buffer.Add((Cluster)next); } else { merged.Add(next); } } } С одной стороны, он работает правильно, но с другой, хотелось бы сделать его более выразительным и по возможности улучшить метрики кода. Метрики будем считать с помощью инструмента Analyze > Calculate Code Metrics, который входит в состав Visual Studio Community. Изначально они имеют значения: Configuration: Debug
Member: MergeNeighbors(IUnit, IUnit) : void Maintainability Index: 64 Cyclomatic Complexity: 5 Class Coupling: 4 Lines of Source code: 32 Lines of Executable code: 10 Описанный далее подход в общем случае не гарантирует красивый результат. Бородатая шутка по случаюSPL#392487
Мне недавно рассказали как делают корабли в бутылках. В бутылку засыпают силикатного клея, говна и трясут. Получаются разные странные штуки, иногда корабли. © bash.org Рефакторинг Шаг 1 Проверяем, что каждая цепочка условий одного уровня вложенности заканчивается блоком else, в противном случае добавляем пустой блок else. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (prev is Cluster) { if (next is Cluster) { if (!prev.Description.Equals(next.Description)) { Flush(); } else { } buffer.Add((Cluster)next); } else { Flush(); merged.Add(next); } } else { if (next is Cluster) { buffer.Add((Cluster)next); } else { merged.Add(next); } } } Шаг 2 Если на одном уровне вложенности с условными блоками существуют выражения, переносим каждое выражение к каждый условный блок. Если выражение предшествует блоку, добавляем его в начало блока, в противном случае — в конец. Повторяем, пока на каждом уровне вложенности условные блоки не будут соседствовать только с другими условными блоками. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (prev is Cluster) { if (next is Cluster) { if (!prev.Description.Equals(next.Description)) { Flush(); buffer.Add((Cluster)next); } else { buffer.Add((Cluster)next); } } else { Flush(); merged.Add(next); } } else { if (next is Cluster) { buffer.Add((Cluster)next); } else { merged.Add(next); } } } Шаг 3 На каждом уровне вложенности для каждого блока if отрезаем остаток цепочки условий, создаем новый блок if с выражением, обратным выражению первого if, помещаем внутрь нового блока вырезанную цепочку и удаляем первое слово else. Повторяем, пока не останется ни одного else. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (prev is Cluster) { if (next is Cluster) { if (!prev.Description.Equals(next.Description)) { Flush(); buffer.Add((Cluster)next); } if (prev.Description.Equals(next.Description)) { { buffer.Add((Cluster)next); } } } if (!(next is Cluster)) { { Flush(); merged.Add(next); } } } if (!(prev is Cluster)) { { if (next is Cluster) { buffer.Add((Cluster)next); } if (!(next is Cluster)) { { merged.Add(next); } } } } } Шаг 4 «Схлопываем» блоки. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (prev is Cluster) { if (next is Cluster) { if (!prev.Description.Equals(next.Description)) { Flush(); buffer.Add((Cluster)next); } if (prev.Description.Equals(next.Description)) { buffer.Add((Cluster)next); } } if (!(next is Cluster)) { Flush(); merged.Add(next); } } if (!(prev is Cluster)) { if (next is Cluster) { buffer.Add((Cluster)next); } if (!(next is Cluster)) { merged.Add(next); } } } Шаг 5 К условиям каждого блока if, не имеющего вложенных блоков, с помощью оператора && добавляем условия всех родительский блоков if. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (prev is Cluster) { if (next is Cluster) { if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster) { Flush(); buffer.Add((Cluster)next); } if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster) { buffer.Add((Cluster)next); } } if (!(next is Cluster) && prev is Cluster) { Flush(); merged.Add(next); } } if (!(prev is Cluster)) { if (next is Cluster && !(prev is Cluster)) { buffer.Add((Cluster)next); } if (!(next is Cluster) && !(prev is Cluster)) { merged.Add(next); } } } Шаг 6 Оставляем только блоки if, не имеющие вложенных блоков, сохраняя порядок их появления в коде. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster) { Flush(); buffer.Add((Cluster)next); } if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster) { buffer.Add((Cluster)next); } if (!(next is Cluster) && prev is Cluster) { Flush(); merged.Add(next); } if (next is Cluster && !(prev is Cluster)) { buffer.Add((Cluster)next); } if (!(next is Cluster) && !(prev is Cluster)) { merged.Add(next); } } Шаг 7 Для каждого уникального выражения в порядке их появления в коде выписываем содержащие их блоки. При этом другие выражения внутри блоков игнорируем. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster) { Flush(); } if (!(next is Cluster) && prev is Cluster) { Flush(); } if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster) { buffer.Add((Cluster)next); } if (prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster) { buffer.Add((Cluster)next); } if (next is Cluster && !(prev is Cluster)) { buffer.Add((Cluster)next); } if (!(next is Cluster) && prev is Cluster) { merged.Add(next); } if (!(next is Cluster) && !(prev is Cluster)) { merged.Add(next); } } Шаг 8 Объединяем блоки с одинаковыми выражениями, применяя к их условиям оператор ||. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster || !(next is Cluster) && prev is Cluster) { Flush(); } if (!prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster || prev.Description.Equals(next.Description) && next is Cluster && prev is Cluster || next is Cluster && !(prev is Cluster)) { buffer.Add((Cluster)next); } if (!(next is Cluster) && prev is Cluster || !(next is Cluster) && !(prev is Cluster)) { merged.Add(next); } } Шаг 9 Упрощаем условные выражения с помощью правил булевой алгебры. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (prev is Cluster && !(next is Cluster && prev.Description.Equals(next.Description))) { Flush(); } if (next is Cluster) { buffer.Add((Cluster)next); } if (!(next is Cluster)) { merged.Add(next); } } Шаг 10 Рихтуем напильником. РезультатSPLprivate void MergeNeighbors(IUnit prev, IUnit next)
{ if (IsEndOfCompatibleClusterSequence(prev, next)) Flush(); if (next is Cluster) buffer.Add((Cluster)next); else merged.Add(next); } private static bool IsEndOfCompatibleClusterSequence(IUnit prev, IUnit next) => prev is Cluster && !(next is Cluster && prev.Description.Equals(next.Description)); Итого После рефакторинга метод выглядит так: private void MergeNeighbors(IUnit prev, IUnit next)
{ if (IsEndOfCompatibleClusterSequence(prev, next)) Flush(); if (next is Cluster) buffer.Add((Cluster)next); else merged.Add(next); } А метрики так: Configuration: Debug
Member: MergeNeighbors(IUnit, IUnit) : void Maintainability Index: 82 Cyclomatic Complexity: 3 Class Coupling: 3 Lines of Source code: 10 Lines of Executable code: 2 Метрики заметно улучшились, а код стал гораздо короче и выразительнее. Но самым замечательным в этом подходе, лично для меня, является вот что: кто-то способен сразу увидеть, что метод должен выглядеть, как в конечном варианте, а кто-то может написать только изначальную реализацию, но имея хотя бы какую-то формулировку правильного поведения, с помощью чисто механических действий (за исключением, возможно, последнего шага) ее можно привести к наиболее лаконичному и наглядному виду. P.S. Все куски знаний, которые сложились в описанный в публикации алгоритм, были получены автором еще в школе более 15 лет назад. За что он выражает огромную благодарность учителям-энтузиастам, дающим детям основу нормального образования. Татьяна Алексеевна, Наталья Павловна, если вы вдруг это читаете, большое вам СПАСИБО! =========== Источник: habr.com =========== Похожие новости:
Программирование ), #_proektirovanie_i_refaktoring ( Проектирование и рефакторинг ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 19:02
Часовой пояс: UTC + 5