Поля описывают данные, а методы – операции над ними. На предыдущих страницах мы уже познакомились с тем, что такое методы. Но как вы могли заметить, в прошлый раз мы к каждому методу приписывали static. Теперь мы этого делать не будем. На данный момент каждый метод будет членом объекта и в процессе выполнения он сможет изменять состояние своего объекта. Давайте вернёмся к примеру с танком.
Фрагмент 1.39
class Program
{
private static void Main(string[] args)
{
Tank tank1 = new Tank();
tank1.Speed = 10;
tank1.MoveRight();
tank1.MoveRight();
Console.WriteLine(tank1.PositionX); //20
}
}
class Tank
{
public int Speed;
public int PositionX;
public void MoveLeft()
{
PositionX -= Speed;
}
public void MoveRight()
{
PositionX += Speed;
}
}
Мы добавили в танк его позицию по X оси. А также два метода, которые двигают его вправо или влево в зависимости от его скорости. Как вы видите, при вызове этих методов они выполняются в контексте объекта. Т.е имеют доступ к позиции и скорости танка, у которого они были вызваны.
Давайте рассмотрим разницу между членом объекта, с которым он работает, и методом статическим.
Инкапсуляция
С помощью методов мы также можем достичь инкапсуляции. Давайте представим, что у объекта появился ряд специфичных правил. Так, при движении вправо или влево мы должны следить за тем, чтобы позиция по X не была меньше нуля и больше 100. Так, для того, чтобы это правило всегда соблюдалось, нам хотелось бы запретить изменять позицию танка напрямую. Т.е позиция может изменяться только через движение влево или вправо методов. Но для того, чтобы мы могли нарисовать танк в правильной позиции, нам нужно чтобы позицию могли читать.
Т.е мы закрываем позицию для записи, но оставляем открытой для чтения.
Это всё мы можем достичь с помощью методов.
Фрагмент 1.40
class Tank
{
public int Speed;
public int PositionX;
public void MoveLeft()
{
PositionX -= Speed;
if (PositionX < 0)
PositionX = 0;
}
public void MoveRight()
{
PositionX += Speed;
if (PositionX > 100)
PositionX = 100;
}
}
Мы добавили в методы движения проверку на выход позиции за границы. Но всё ещё можно изменить позицию напрямую, в обход наших методов, тем самым сломать состояние нашего объекта.
Фрагмент 1.41
class Program
{
private static void Main(string[] args)
{
Tank tank1 = new Tank();
tank1.Speed = 10;
tank1.MoveRight();
tank1.MoveRight();
Console.WriteLine(tank1.GetPositionX()); //20
}
}
class Tank
{
public int Speed;
private int _positionX;
public void MoveLeft()
{
_positionX -= Speed;
if (_positionX < 0)
_positionX = 0;
}
public void MoveRight()
{
_positionX += Speed;
if (_positionX > 100)
_positionX = 100;
}
public int GetPositionX()
{
return _positionX;
}
}
И вот мы изменили поле PositionX. Во-первых, мы изменили модификатор доступа с public на private. В результате этого, поле теперь недоступно за пределами объекта. Т.е его могут изменять методы определённые в классе. Но методы определённые в других классах, имея ссылку на объект этого типа, уже не могут обращаться к этому члену (члену определённому с модификатором private).
Во-вторых, мы, согласно нотации, приватное поле назвали с маленькой буквы и с нижним подчёркиванием. Это не обязательно, но лично я применяю именно этот стиль программирования.
Так как поле не доступно вне класса вообще, то нам надо предоставить доступ на чтение. Это мы сделали с помощью метода GetPositionX. Он возвращает текущее значение поля и мы пользуемся этим методом, когда хотим узнать текущее положение танка.
Давайте подумаем про инкапсуляцию в таком контексте. Нам захотелось убрать из танка ответственность ограничения движения. Точнее делегировать определения возможных позиций другой сущности. Например.
class Boundary
{
public bool IsAvailable(int positionX);
}
Мы не будем вдаваться в подробности содержания этого класса. Допустим он просто следит за тем, чтобы некая позиция была доступна если там нет стены и она в границах уровня. Промодифицируем класс танка.
class Tank
{
public int Speed;
private int _positionX;
private Boundary _boundary;
public Tank(Boundary boundary)
{
_boundary = boundary;
}
public void MoveLeft()
{
Move(Speed * -1);
}
public void MoveRight()
{
Move(Speed);
}
private void Move(int delta)
{
if (_boundary.IsAvailable(_positionX + delta))
_positionX += delta;
}
public int GetPositionX()
{
return _positionX;
}
}
Соблюдаем ли мы инкапсуляцию в таком случае? Допустим мы говорим: танк всегда находится в пределах уровня и не пересекается с другими объектами. Можем ли мы это гарантировать?
Можно сказать что да. Но на самом деле нет. Хоть и нельзя сменить Boundary, т.е в процессе выполнения конкретному танку заменить контекст в котором он находится чтобы его позиция стала не валидной, но можно изменить текущий контекст. Если бы класс Boundary был изменяемый то тот, кто нам ссылку предоставил, мог изменить объект как ему хочется.
Подробней эта проблема инкапсуляции будет изложена в другой главе.