State Pattern (HepsiBurada Mülakat Sorusu – MarsRover)

Merhaba. Bu yazımda State Pattern hakkında bildiklerimi size aktarmaya çalışacağım. Örneğimi daha önce bana HepsiBurada dan gelen bir mülakat sorusu üzerinden vereceğim. Açıkcası State Pattern’ e özgün bir örnek verebilmek için çok düşündüm ve sonra aklıma bu mülakat sorusu geldi. State Pattern i uygulamak için güzel bir örnek. Eminim siz de adım adım ilerlerken fark edeceksiniz.

State Pattern, bir sınıfın belli bir değişkene bağlı olarak davranışını değiştirdiği yerlerde kullanılır. Geliştirme aşamasında kodumuzun karmaşıklaşmasını önler. Belli bir değişkene göre davranışını değiştiren sınıflarda her davranışa ait bir sınıf oluşmasını ve dolayısıyla her sınıfın bir sorumluluğu olmasını sağlar.

Şimdi tekrar örneğimize gelecek olursak. Pdf halini link den görüntüleyebilirsiniz.

Özetlemek gerekirse; Mars’ta yüzeyi tarayan rover adında bir aracımız var. Bu aracı ilk önce yüzeyde bir yere konumlandırıyoruz. Sonra komutlar aracılığıyla sağa sola döndürüyoruz ya da aracımızı ileriye doğru hareket ettiriyoruz.

Konsol uygulamasından ilk gelen input, ör; 5 5 gibi, 5 birim genişliğinde 5 birim yüksek liğinde bir yüzey yaratmamızı sağlıyor.

İkinci input rover ı konumlandırmak için; 1 2 N girersek, 1. satır 2.sütunda kuzeye (N: North) bakan bir aracımızın olması gerekiyor ilk başta.

Sonra ki komutumuz daha önce de bahsettiğim gibi aracın sağa sola dönmesi veya ileri gitmesi için vereceğimiz komut. Burda L(Left) sola dönmek için, R(Right) sağa dönmek için, M(Move Forward) ise ileri gitmek için.

Bizden yapmamızı istedikleri, 5 e 5 lik bir yüzeyde, kullanıcının belirleyeceği koordinatlarda ve kullanıcının vereceği komutlar sonrasında rover ımızın hangi koordinatlarda ve yüzü nereye dönük iken kaldığı bilgisini ekrana yazdırmamız.

Unutmadan söyleyeyim; 0:0 kordinatı yüzeyin sol alt tarafına denk geliyor. 5:5 lik bir yüzeyde 5:5 kordinatı yüzeyin sağ üstüne denk geliyor.

Şimdi bunu düz mantık iflerle yapmaya çalışalım. Bir Rover sınıfı oluşturuyorum hemen. Bu rover sınıfımızın üç metodu olsun. MoveForward (İleri git), TurnLeft(Sola Dön) ve TurnRight(Sağa Dön).

Şimi aracımız kuzeye bakıyorsa ileri gitmesi için y kordinatını bir attırmalıyız. Sola dönmesi için ve sağa dönmesi içinde bir flag tutabiliriz. Bunun için direction adında bir değişken oluşturacağım. Sola ve sağa dönünce direction değişsin ve ileri gidince de bu direction değişkeninin değerine göre x yada y değerini attırsın veya azaltsın.

   public class Rover
    {
        public string Direction { get; set; }
        public int X { get; set; }
        public int Y { get; set; }
        public Rover(int x, int y, string direction)
        {

        }
        public void MoveForward()
        {
            if(Direction == "N")
            {
                Y++;
            }
            if (Direction == "S")
            {
                Y--;
            }
            if (Direction == "W")
            {
                X--;
            }
            if (Direction == "E")
            {
                X++;
            }
        }

        public void TurnRight()
        {
            if (Direction == "N")
            {
                Direction = "E";
            }
            if (Direction == "S")
            {
                Direction = "W";
            }
            if (Direction == "W")
            {
                Direction = "N";
            }
            if (Direction == "E")
            {
                Direction = "S";
            }
        }

        public void TurnLeft()
        {
            if (Direction == "N")
            {
                Direction = "W";
            }
            if (Direction == "S")
            {
                Direction = "E";
            }
            if (Direction == "W")
            {
                Direction = "S";
            }
            if (Direction == "E")
            {
                Direction = "N";
            }
        }
    }

Gördüğünüz gibi baya bir ifimiz oldu. Şimdi gelin bunlara aradaki yönlerinde eklendiğini düşünelim. KuzeyDoğu, KuzeyBatı gibi. Bu yönlere gidince x ve y aynı anda değişsin. Yani kestirmeden gitsin kısacası. Ne olacak yine bir sürü if koymam gerekecek. Eğer Kuzey Batıysa x’i bir azalt y’ yi bir arttır, Kuzey yönüne bakıyorsa sağ yaptığında Kuzey Doğudur gibi gibi. Bu da sınıfımda bir çok değişiklik demek.

Özellikle bu eğer proda ilk çıkışım değilse, ilk versiyonu önceden göndermişsem, testcime bunların hepsini tekrar dan test et demek zorunda kalacağım.

İşte burda State Pattern in ne olduğunu biliyorsanız, ikinci versiyonda var olan yerleri bu kadar çok değiştirmenize gerek kalmayacaktır.

Hemen State Pattern i uygulayalım. İlk önce bir interface yada abstract class tanımlıyoruz. Ben her sınıfta tekrar tekrar koordinat vermemek için abstract kullandım. Önemli olan ortak bir contracta bağlı olması.

    public abstract class RoverDirectionState
    {
        public Point Coordinates { get; private set; }
        public RoverDirectionState(Point Coordinates)
        {
            this.Coordinates = Coordinates;
        }
        public abstract void MoveForward();
        public abstract RoverDirectionState TurnLeft();
        public abstract RoverDirectionState TurnRight();
    }

Sonra her bir state imizi bu abstract class ı miras alan bir sınıfa çeviriyoruz.

    class NorthDirectionState : RoverDirectionState
    {
        public NorthDirectionState(Point Coordinates) : base(Coordinates) { }

        public override void MoveForward()
        {
            Coordinates.SetY(Coordinates.Y + 1);
        }

        public override RoverDirectionState TurnLeft()
        {
            return new WestDirectionState(Coordinates);
        }

        public override RoverDirectionState TurnRight()
        {
            return new EastDirectionState(Coordinates);
        }

        public override string ToString()
        {
            return RoverConstants.Directions.North;
        }
    }
    public class EastDirectionState : RoverDirectionState
    {
        public EastDirectionState(Point Coordinates) : base(Coordinates) { }

        public override void MoveForward()
        {
            Coordinates.SetX(Coordinates.X + 1);
        }

        public override RoverDirectionState TurnLeft()
        {
            return new NorthDirectionState(Coordinates);
        }

        public override RoverDirectionState TurnRight()
        {
            return new SouthDirectionState(Coordinates);
        }

        public override string ToString()
        {
            return RoverConstants.Directions.East;
        }
    }
    class SouthDirectionState : RoverDirectionState
    {
        public SouthDirectionState(Point Coordinates) : base(Coordinates) { }

        public override void MoveForward()
        {
            Coordinates.SetY(Coordinates.Y - 1);
        }

        public override RoverDirectionState TurnLeft()
        {
            return new EastDirectionState(Coordinates);
        }

        public override RoverDirectionState TurnRight()
        {
            return new WestDirectionState(Coordinates);
        }

        public override string ToString()
        {
            return RoverConstants.Directions.South;
        }
    }
    class WestDirectionState : RoverDirectionState
    {
        public WestDirectionState(Point Coordinates) : base(Coordinates) { }

        public override void MoveForward()
        {
            Coordinates.SetX(Coordinates.X - 1);
        }

        public override RoverDirectionState TurnLeft()
        {
            return new SouthDirectionState(Coordinates);
        }

        public override RoverDirectionState TurnRight()
        {
            return new NorthDirectionState(Coordinates);
        }

        public override string ToString()
        {
            return RoverConstants.Directions.West;
        }
    }

Gördüğünüz gibi, bütün state sınıflarım, state değiştiği zaman yani, aracın yönü değiştiği zaman sadece ilgili yönün sınıfını yaratıp geri dönüyorlar. Ondan sonra artık sorumluluk yeni yön sınıfında oluyor. Yeni yön sınıfımız artık x i mi artırır y yimi artırır o onun sorumluluğunda. Her yön sınıfı kendine ait sorumluluğu yerine getiriyor.

Ayrıca aşağıda da görüldüğü üzere Rover sınıfımız daha da basitleşti. Tek görevi gelen komutları RoverState sınıf değişkenine yönlendirmek.

    public class Rover
    {
        private RoverDirectionState RoverState;

        public Rover(Point coordinates, string direction)
        {
            this.RoverState = DirectionStateFactory.Create(coordinates, direction);
        }

        public void MoveForward()
        {
            this.RoverState.MoveForward();
        }

        public void TurnLeft()
        {
            this.RoverState = this.RoverState.TurnLeft();
        }

        public void TurnRight()
        {
            this.RoverState = this.RoverState.TurnRight();
        }

        public Point GetCoordinates()
        {
            return this.RoverState.Coordinates;
        }

        public override string ToString()
        {
            return this.GetCoordinates() + " " + this.RoverState;
        }
    }

Şimdi gelin, ikinci senaryoyu düşünelim. Eğer yönlerimize ara yönler de eklenecek olursa ne olur? Bu sadece aynı abstract class’ı miras alan yeni sınıflar demek. Kuzey Doğu state i MoveForward komutunda x ve y’ yi bir arttırır. Turn Right da EastDirectionState döner. Turn Left de NorthDirectionState döner.

Tabi mevcut sınıflar sağa sola dönerken yeni state leri yaratmak zorunda kalacaktır ama test ederken burda sadece ilgili type ın dönüp dönmediği kontrolü yeterli olacaktır.

State Pattern li çözümümüz daha nesne tabanlıdır. If blokları yerine polymorphism kullanılmıştır. Ayrıca yapılan değişiklikte mevucut u bozma olasılığı daha düşüktür.

Bu makalemi HepsiBurada’ ya bana böyle bir örnek case verip State Pattern uygulamama yardım ettikleri için teşekkür ederek bitirmek istiyorum. Teşekkürler HepsiBurada 🙂

Bir yorum bırakın