Очевидный прием, для облегчения чтения кода, если в коде встречается константа, о назначении которой сложно догадаться, назовите ее и используйте замените все вхождения числа
Нужно применять всегда,особенно когда вокруг этого поля появляется логика
private int _total;
public int Total
{
get => _total;
set
{
RaiseDomainEvent(_total);
_total = value;
}
}
Например при изменении Total нужно выбросить доменное событие, инкапсуляция поля так же хороша тем, что мы соблюдаем инвариант класса, что просто необходимо в ООП Если поле - ссылочный тип, будьте осторожны, ведь его поля меняются по ссылке, возможно лучше выставить наружу методы изменения полей этого объекта релевантные для того, чтобы находиться в нашем классе
public class Salary
{
public decimal Value { get; set; }
public int Month { get; set; }
}
public class Employee
{
private Salary _salary { get; set; }
public decimal SalaryValue => _salary.Value;
public void SetSalary(decimal newSalary) => _salary.Value = newSalary;
}
В случае выше клиенты Employee не должны менять поле Month, а клиенты другого класса вполне могут это делать, поэтому оставляем setter Month публичным и определяем делегирующие методы в классе Employee
Отличие инкапсуляции поля от инкапсуляции коллекции в том что некоторые коллекции передаются по ссылке, и получив ссылку коллекцию можно изменять как угодно, например List или T[], но IEnumerable позволяет только итерироваться по коллекции => этот интерфейс можно выставлять наружу, а для внутреннего пользования можно использовать HashSet, если объекты в коллекции не должны повторяться
public class Pupil
{
}
public class Group
{
private HashSet<Pupil> _pupils { get; set; }
public void AddPupil(Pupil p)
{
//logics
_pupils.Add(p);
}
public void RemovePupil(Pupil p)
{
//...
//logics
_pupils.Remove(p);
}
public IEnumerable<Pupil> Pupils => _pupils.ToList();
Метод ToList используется для отвязывания ссылки
В C# нет рекорд типов, но есть в F#, предположим, что в начале разработки вы закодировали Pupil так:
type Pupil = {Name:string;Surname:string}
Прошло время и у Pupil появляется поведение(Мы сейчас говорим про ООП, В ФП это был бы модуль PupilActions и методы возвращающие новый(модифицированный Pupil) и в парадигме ООП появляется поведение, мы выделяет отдельный класс куда переносим все поля и добавляет методы модицикации
Рефакторинг позволяет не загромождать класс, бывает полезно, чтобы он не превратился в хранилище различной информации, в котором почти нет поведения:
public class Pupil
{
public string Name { get; }
public string Surname { get; private set; }
public string Age { get; }
public long PassportNumber { get; }
public long PassportSeries { get; }
public long INN { get; }
public Sex Sex { get; private set; }
public void ChangeSex(Sex newSex)
{
Sex = newSex;
}
public void ChangeSurname(string surname)
{
Surname = surname;
}
}
Всего пара методов, и 7 полей данных, рефакторинг превращает этот класс в :
public class PupilCard
{
public long PassportNumber { get; }
public long PassportSeries { get; }
public long INN { get; }
}
public class Pupil
{
public string Name { get; }
public string Surname { get; private set; }
public string Age { get; }
public PupilCard PupilCard { get; }
public Sex Sex { get; private set; }
public void ChangeSex(Sex newSex)
{
Sex = newSex;
}
public void ChangeSurname(string surname)
{
Surname = surname;
}
}
Более сложный случай чем Replace Type Code with Class. Код типа влияет на поведение объекта:
public enum PupilType
{
Bachelor,
Master
}
public class Pupil
{
public abstract PupilType PupilType { get; }
public string Name { get; set; }
public string Surname { get; set; }
public string FormatName()
{
switch (PupilType)
{
case PupilType.Bachelor:
if (Name.Length < 10)
return $"B {Name} {Surname}";
else return $"B {Name.First()}.{Surname}";
case PupilType.Master:
return $"M {Name} {Surname}";
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
Тут рефакторинг нужно применять, когда в будущем у этих двух сущностей может появиться различное поведение, он заключается в выделении общей части (Name,Surname), а полиморфные методы переносятся в наследников
```csharp
public abstract class Pupil
{
public abstract PupilType PupilType { get; }
public string Name { get; set; }
public string Surname { get; set; }
public abstract string FormatName();
}
public abstract class Master : Pupil
{
public override PupilType PupilType { get; } = PupilType.Master;
public override string FormatName()
{
return $"M {Name} {Surname}";
}
}
public class Bachelor : Pupil
{
public override PupilType PupilType { get; } = PupilType.Bachelor;
public override string FormatName()
{
if (Name.Length < 10)
return $"B {Name} {Surname}";
return $"B {Name.First()}.{Surname}";
}
}
Класс Pupil делаем абстрактным, и заставляем наследников определить Type, в будущем этот тип можно убрать и полностью выпилить из базового класса, чтобы по родителю ничего нельзя было сказать про потомков После такого выделения нужно пройтись по всем клиентам класса Pupil и заменить их на использования того или иного класса Если в базовом классе встречаются методы в SwitchCase, то создаем методы с таким же названием в каждом наследнике и переносим код из соответсвующих веток case туда.Пример - делаем метод FormatName также абстрактным и определяем в каждом наследнике, в методы наследников перемещаем код из соответсвующих веток case
Этот прием применяется когда объект на протяжении своей жизни может менять свое состояние, например бакалавр может стать магистром, поэтому нужно предусмотреть вариант перехода из одного состояние в другое Применять нужно когда этот тип влияет на поведение(нельзя заменить на кодирования типа классом(Replace Type Code With Data Class)), и также уже существует одна иерархия для данного объекта, поэтому нельзя заменить и на кодирование типа подклассами(Replace Type Code With Subclasses) или, как я уже написал, объект может менять свое состояние на протяжении жизни Но можно обеспечить полиморфное поведение и стратегией, это более легковесно, тогда клиент может сам выбирать стратегию для какого-то алгоритма в классе, тут важно найти баланс - можно ли клиенту самому решать какую реализация предлагать? Если да, то без проблем, если нет, то паттерн состояние подойдет лучше Рассмотрим на текущем примере:
public class Pupil
{
public abstract PupilType PupilType { get; }
public string Name { get; set; }
public string Surname { get; set; }
public string FormatName()
{
switch (PupilType)
{
case PupilType.Bachelor:
if (Name.Length < 10)
return $"B {Name} {Surname}";
else return $"B {Name.First()}.{Surname}";
case PupilType.Master:
return $"M {Name} {Surname}";
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
Теперь PupilType заменим на класс, в котором определим метод FormatName:
public abstract class PupilType
{
public abstract string FormatName(string name, string surname);
}
Определим наследников, в каждом из них реализуем метод FormatName
public class BachelorType : PupilType
{
public override string FormatName(string name, string surname)
{
if (name.Length < 10)
return $"B {name} {surname}";
return $"B {name.First()}.{surname}";
}
}
public class MasterType : PupilType
{
public override string FormatName(string name, string surname)
{
return $"M {name} {surname}";
}
}
Далее - делегируем из основого класса в класс состояния метод FormatName передав необходимую информацию, можно передать this, но в данном случае лучше будет уточнить, что именно мы передаем
public class Pupil
{
public PupilType PupilType { get; }
public string Name { get; set; }
public string Surname { get; set; }
public string FormatName() => PupilType.FormatName(Name, Surname);
}
Также в класс нужно добавить методы перехода из одного состояние в другое состояние
Часто выделение подклассов излишне, например когда состояние только два(как в нашем случае хаха), и большинство методов в обоих состояних одинаковые, тогда нужно избавиться от наследников и перейти к одному классу в который положить все методы (обратный рефакторинг, чем тот который я проделал в прошлом Replace Type Code with State or Strategy