понедельник, 21 января 2013 г.

Action как замена копипаста

Даже начинающие программисты знают, что если один и тот-же код встречается в двух разных местах, то стоит задуматься над тем, чтобы вынести этот код в отдельную функцию дабы не плодить копи-паст.
  Но что делать, когда код почти полностью совпадает, но где-нибудь в середине отличается одной-двумя строчками от которых ну никак нельзя избавиться?

 Вот, например, есть у нас список игроков в MMO игре:

List<Player> players;

Теперь представим, где-нибудь в координатах x,y произошло некоторое событие, например, взорвалась граната и теперь надо пересчитать хиты игроков, находящихся в зоне поражения и послать сообщения об их полученом дамаге игрокам.

private void ExplodeGrenade(float x, float y, float range)
{
  //сохраним константу в локальную переменную, в реальности здесь может быть 
  //гораздо более сложный код
  float damage=Grenade.Damage;

  foreach(Player player in players)
  {
    //если игрок попадает в квадрат поражения, то пересчитаем ему
//уровень урона, чем дальше, тем меньше
if (player.X<x+range && player.X>x-range && player.Y<y+range && player.Y> y+ range) { //фунция Distance считает расстояние между двумя точками float playerDamage=damage/(Distance(player.X,player.Y,x,y)+1); //пошлем игроку сообщение о том, что он получил урон player.SendMessage(Message.TakeDamage,playerDamage); } } }
Теперь представим другую операцию, например игрок переместился на карте и нужно об этом сообщить другим игрокам, находящимся в зоне видимости:

private void MovePlayer(Player movingPlayer,float x, float y, float range)
{
  //Установим игроку новые координаты
  movingPlayer.X=x;
  movingPlayer.Y=y;

  foreach(Player player in players)
  {
    //сообщим всем игрокам в области видимости, что игрок меперестился
    if (player.X<x+range && player.X>x-range 
        && player.Y<y+range && player.Y> y+ range)
    {
      player.SendMessage(Message.Move,movingPlayer);
    }
  }
}

Как видно, две этих функции очень похожи, отличаются по сути только телом цикла внутри условия, а каждый раз писать длинные, совершенно одинаковые  условия проверки очень не хочется. И ведь это еще весьма упрощенный вариант, приведенный для примера, а в реальном многопоточном сервере должны быть блокировки, вместо линейного списка игроков употребляться более эффективные структуры хранения данных и множество других  дополнительных наворотов.
  Как решить данную задачу не копипастя постоянно один и тот же код? Делается это довольно просто используя класс дотнета Action<T>.

Общий код (цикл и условие) выносим в отдельную функцию

private void PlayerActionInRange(float x,float y,float range, 
                                 Action<Player> playerAction)
Как видно, в функции присутсвует параметр с типом Action<Player>. Класс Action является оберткой для делегата или просто говоря функции, которая будет вызываться из функции PlayerActionInRange. Параметром данной функции будет являться объект типа Player.



private void PlayerActionInRange(float x,float y,float range, 
              Action<Player> playerAction)
{
  foreach(Player player in players)
  {
    //для всех игроков в области видимости вызываем функцию 
    if (player.X<x+range && player.X>x-range 
        && player.Y<y+range && player.Y> y+ range)
    {
       //Параметром функции передаем текущего игрока в цикле
       playerAction(player); 
    }
  }
}
Теперь модифицируем функцию ExplodeGrenade

private void ExplodeGrenade(float x, float y, float range)
{
  //сохраним константу в локальную переменную, в реальности 
  //здесь может быть гораздо более сложный код
  float damage=Grenade.Damage;

  //В качестве параметра функции передаем ссылку на анонимный 
  //делегат, код которого будет находиться тут же
  PlayerActionInRange(x,y,range,player =>
  {
    //фунция Distance считает расстояние между двумя точками
    float playerDamage=damage/(Distance(player.X,player.Y,x,y)+1);
    //пошлем игроку сообщение о том, что он получил урон
    player.SendMessage(Message.TakeDamage,playerDamage);
  });
}
Теперь функция ExplodeGrenade стала гораздо более ясной для понимания. А представьте, если бы для организации выборки в реальном проекте пришлось бы писать не две, а с хотя бы десяток строчек кода, насколько упростился бы вид этой функции!

Функцию MovePlayer можно сделать еще проще, воспользовавшись лямбда-выражениями:
private void MovePlayer(Player movingPlayer, 
                        float x, float y, float range)
{
  //Установим игроку новые координаты:
  movingPlayer.X=x;
  movingPlayer.Y=y;

  //всего лишь одна строчка кода
  PlayerActionInRange(x,y,range,
                     p=>p.SendMessage(Message.Move,movingPlayer));
}
Вот таким нехитрым способом можно существенно упростить читаемость кода и избежать ошибок при копировании одинакового текста из одной фунции в другую

Комментариев нет:

Отправить комментарий