[C#] 7.4 구현하기

이제는 시나리오와 시퀀스 다이어그램 등을 보면서 구체적으로 구현합시다.

시나리오를 보시면 캠퍼스 생활은 크게 초기화 부분과 사용자에 의한 동작으로 나눌 수가 있습니다. 이에 캠퍼스 생활에는 초기화하는 Init 메서드와 사용자에 의한 동작인 Run 메서드를 추가하고 프로그램 진입점에서는 캠퍼스 생활 단일체를 참조하여 Init과 Run 메서드를 호출하기로 합시다.

class Program
{
    static void Main(string[] args)
    {
        CampusLife campuslife = CampusLife.Singleton;
        campuslife.Init();
        campuslife.Run();
    }
}
class CampusLife
{
    ... 중략 ...
    internal void Init()
    {
        throw new System.NotImplementedException();
    }
    internal void Run()
    {
        throw new System.NotImplementedException();
    }
}

현 상태에서 빌드해 보면 Place가 추상 클래스로 되어 있지 않다는 오류를 확인할 수 있습니다. 여러분은 단계마다 습관적으로 빌드하여 문법적 오류가 있는 부분을 계속 수정하시기 바랍니다.

abstract class Place
{
    ... 중략 ...
}

캠퍼스 생활의 초기화 부분을 작성해 보기로 합시다. 초기화 과정에서는 각 장소를 생성하는 부분과 학생을 생성하는 부분으로 나눌 수 있습니다. 먼저, 각 장소를 생성하는 부분에서는 Campus 개체를 생성과 각 장소들을 생성하는 부분이 있습니다. 프로그램의 시나리오를 변경하여도 수정하기 쉽게 하려고 각 장소 유형을 열거형으로 정의하여 이용하기로 할게요.

enum PlaceType
{
    PT_LECTUREROOM,
    PT_DORMITORY,
    PT_LIBARY,
    MAX_PLACETYPES
}

캠퍼스 생활에서 초기화를 담당하는 Init 메서드에서는 장소를 생성하는 부분과 학생들을 생성하는 부분이 있으므로 이들을 별도의 메서드로 정의하여 Init 메서드에서는 이들을 호출하는 것으로 구현할게요.

class CampusLife
{
    ... 중략 ...
    internal void Init()
    {
        MakePlaces();
        MakeStudents();
    }
    private void MakeStudents()
    {
        throw new System.NotImplementedException();
    }
    private void MakePlaces()
    {
        throw new System.NotImplementedException();
    }
}

각 장소를 생성하는 MakePlaces를 구현해 봅시다. 여기에서는 캠퍼스 개체를 생성하는 것과 각 장소를 구현하는 것을 담당합니다. 그리고 캠퍼스 생활에서는 생성한 캠퍼스 개체와 각 장소 개체들을 이용해야 하므로 멤버 필드를 선언하기로 합시다. 이에 캠퍼스 개체는 하나만 생성할 것이므로 Campus 형식의 멤버 필드를 선언하고 각 장소는 Place 형식을 원소로 하는 배열로 선언합시다. 그리고 Init 메서드에서 Campus 개체와 각 장소 개체를 생성하여 적절한 멤버 필드에 대입하세요.

class CampusLife
{
    ... 중략 ...
    Place[] places = new Place[(int)PlaceType.MAX_PLACETYPES];
    Campus campus;

    private void MakePlaces()
    {
        campus = new Campus();
        MakePlace(PlaceType.PT_LECTUREROOM, new LectureRoom());
        MakePlace(PlaceType.PT_DORMITORY, new Dormitory());
        MakePlace(PlaceType.PT_LIBARY, new Library());
    }

    private void MakePlace(PlaceType placeType, Place place)
    {
        places[(int)placeType] = place;
   }
}

그리고 Campus 생성과 각 장소 생성자에서는 특별한 행위가 약속된 바가 없으니 예외 구문을 없애고 자신을 생성하는 것을 콘솔에 출력합시다.

class Campus
{
   ... 중략 ...
    internal Campus()
    {
        Console.WriteLine("캠퍼스 생성");
    }
}

실행하면 다음과 초기화 부분에서는 다음과 같은 내용이 화면에 출력할 것입니다.

▶ 실행 결과

캠퍼스 생성
강의실 생성
기숙사 생성
도서관 생성

각 장소를 생성하고 나면 생성할 최대 학생 수를 입력받아 학생들을 생성합니다. 여기에서 결정할 최대 학생 수는 프로그램 전체에 영향을 끼치므로 GameRule 클래스를 추가하여 공통으로 사용할 멤버를 추가할게요. 그리고 GameRule은 개체라기보다는 공통으로 관리할 데이터들의 집합체 역할을 할 것이므로 정적 클래스로 정의하겠습니다.

정적 클래스 GameRule을 프로젝트에 추가할게요. 그리고 관리할 최소 학생 수와 최대 학생 수를 const 멤버로 정의하고 사용자에 의해 결정될 최대 학생 수를 관리하는 멤버 필드를 추가하고 이에 대해 얻거나 설정할 수 있는 멤버 속성을 추가할게요. 특히, 설정하는 set 블록은 접근 한정을 private으로 지정하여 외부에서 변경할 수 없게 하겠습니다. 이럴 때 정적 생성자를 사용하면 효과적입니다.

static class GameRule //정적 클래스
{
    internal const int min_students = 5;
    internal const int max_students = 100;
    static int user_max_students;
    internal static int MaxStudents
    {
        get
        {
            return user_max_students;
        }
        private set
        {
            if (value < min_students)
            {
                value = min_students;
            }
            if (value > max_students)
            {
                value = max_students;
            }
            user_max_students = value;
        }
    }
    static GameRule()
    {
        Console.WriteLine("최대 학생 수를 입력하세요. " );
        Console.WriteLine("최소:{0} 최대:{1}", min_students, max_students);
        GameRule.MaxStudents = EHLib.GetNum();
        Console.WriteLine("최대 학생수:{0} ", GameRule.MaxStudents);
    }
}

그리고 수를 입력하는 기능을 비롯하여 일반적으로 사용할 만한 라이브러리 형태의 기능들은 EHLib 클래스를 추가하여 제공하려고 합니다. EHLib도 정적 클래스로 제공할게요.

static class EHLib
{
    static internal int GetNum()
    {
        int num = 0;
        if (int.TryParse(Console.ReadLine(), out num))
        {
            return num;
        }
        return 0;
    }
}

GameRule과 EHLib 클래스가 추가되었으므로 클래스 다이어그램을 수정하시고 학생 생성에 관한 시퀀스 다이어그램도 수정해야 할 것입니다. 이제 MakeStudents 메서드를 구현해 봅시다. 먼저, GameRule의 MaxStudents 속성을 통해 최대 관리할 학생 수를 얻어와서 해당 수만큼의 학생을 생성하여 캠퍼스 개체에게 보내주면 되겠죠. 물론, 각 학생을 생성하기 위해서는 어떠한 유형의 학생을 선택할 것인지와 이름을 입력받고 생성할 때 학생의 일련번호와 이름을 인자로 전달해야 할 것입니다.

enum StuType{ ST_CSTUDENT, ST_MSTUDENT, ST_PSTUDENT, MAX_STUTYPE }
class CampusLife
{
    ... 중략 ...
    private void MakeStudents()
    {
        int max_students = GameRule.MaxStudents;
        Student student = null;
        for (int i = 0; i < students.Length; i++)
        {
            student = MakeStudent(i + 1);
            campus.InStudent(student);
        }
    }
    private Student MakeStudent(int nth)
    {
        Console.WriteLine("{0} 번째 생성할 학생 정보를 입력하세요.", nth);
        StuType stype = SelectStuType(); 
        Console.WriteLine("학생 이름을 입력하세요.");
        string name = Console.ReadLine();

        switch (stype)
        {
            case StuType.ST_CSTUDENT: return new CStudent(nth, name);
            case StuType.ST_MSTUDENT: return new MStudent(nth, name);
            case StuType.ST_PSTUDENT: return new PStudent(nth, name);
        }
        return null;
    }

    private StuType SelectStuType()
    {   
        Console.WriteLine("생성할 학생 유형을 선택하세요. ");
        Console.WriteLine("{0}:도전적 (디폴트), {1}:보수적, {2}:수동적",
               (int)StuType.ST_CSTUDENT, (int)StuType.ST_MSTUDENT,
               (int)StuType.ST_PSTUDENT);

        switch (EHLib.GetNum())
        {
            case 0: return StuType.ST_CSTUDENT;
            case 1: return StuType.ST_MSTUDENT;
            case 2: return StuType.ST_PSTUDENT;
            default: return StuType.ST_CSTUDENT;
        }
    }
}

그리고 Student 클래스의 생성자를 구현해야 할 것입니다. 입력 인자로 전달받은 이름과 번호는 생성 시에 결정되고 필터링 과정은 필요가 없으니 매핑되는 멤버 필드 없이 속성으로 캡슐화할게요. 대신 set 블록은 private으로 접근을 지정하세요.

class Student
{
    internal string Name
    {
        get;
        private set;
    }
    internal int Num
    {
        get;
        private set;
    }
    internal Student(int snum, string name)
    {
        Num = snum;
        Name = name;
    }
    ... 중략 ...
}

그리고 CStudent, MStudent, PStudent의 생성자에서는 특별히 구현할 코드가 없으므로 비어있는 상태로 변경합시다.

class CStudent:Student
{
    internal CStudent(int snum, string name)
        : base(snum, name)
    {
    }
    ... 중략 ...
}

Campus 클래스에 InStudent 메서드를 구현하기 전에 먼저 생성자에서 학생들을 보관하기 위한 멤버를 추가합시다. 그리고 Campus 생성자에서 GameRule의 MaxStudents 멤버를 얻어와 배열을 생성합시다. 이와 같은 작업을 하였으면 InStudent에서는 빈 자를 찾아 해당 위치에 학생을 보관하면 되겠죠. 이처럼 작성하면 초기화 과정에 최대 학생 수를 결정하는 부분이 캠퍼스 생성과정에서 수행되므로 시나리오를 변경하고 시퀀스 다이어그램도 변경하세요.

class Campus
{
    Student[] students = null;
    ... 중략 ...
    internal Campus()
    {
        Console.WriteLine("캠퍼스 생성");
        students = new Student[GameRule.MaxStudents];
    }
    internal void InStudent(Student student)
    {
        int empty_index = FindEmptyIndex();
        students[empty_index] = student;
    }
    private int FindEmptyIndex()
    {
        int i;
        for (i = 0; i < students.Length; i++)
        {
            if (students[i] == null)
            {
                 break;
            }
        }
        return i; 
    }
}

이제 CampusLife의 사용자 명령에 따라 동작하는 Run 메서드를 구현해 보기로 합시다. Run에서는 메뉴를 선택하면 해당 메뉴의 기능을 선택하는 것을 반복하면 되겠죠. 먼저, CampusLife에서 사용할 키에 대한 상수를 GameRule에 멤버로 추가할게요.

static class GameRule
{
    ... 중략 ...
    internal const ConsoleKey ExitKey = ConsoleKey.Escape;
    internal const ConsoleKey MoveStuKey = ConsoleKey.F1;
    internal const ConsoleKey MoveFocusKey = ConsoleKey.F2;
    internal const ConsoleKey ViewKey = ConsoleKey.F3;
}

Run 메서드에서는 메뉴를 선택하고 해당 기능을 수행하는 것은 반복문을 사용하고 선택한 메뉴에 따라 해당 기능을 수행하는 것은 switch case 선택문이 필요하겠죠.

class CampusLife
{
    ... 중략 ...
    internal void Run()
    {
        ConsoleKey key;
        while ((key = SelectMenu()) != GameRule.ExitKey)
        {
            switch (key)
            {
                case GameRule.MoveStuKey: MoveStudent(); break;
                case GameRule.MoveFocusKey: MoveFocus(); break;
                case GameRule.ViewKey: View(); break;
                default: Console.WriteLine("잘못된 메뉴를 선택하였습니다."); break;
            }
            Console.WriteLine("아무키나 누르세요.");
            Console.ReadKey();
        }
    }
}

그리고 SelectMenu, MoveStudent, MoveFocus, View 메서드를 추가하여 하나씩 구현해 봅시다. 이들 메서드는 CampusLife 클래스 내부에서 필요한 멤버이므로 private으로 접근 지정하면 되겠죠. 앞으로 새롭게 추가하는 것들은 특별한 사유가 없는 경우에 private로 지정할 것입니다.

메뉴를 선택하는 SelectMenu 는 특이한 사항이 없으니 별다를 설명을 하지 않겠습니다.

class CampusLife
{
    ... 중략 ...
    private ConsoleKey SelectMenu()
    {
        Console.Clear();
        Console.WriteLine("캠퍼스 생활 메뉴"); 
        Console.WriteLine("{0} 학생 이동", GameRule.MoveStuKey);
        Console.WriteLine("{0} 포커스 이동", GameRule.MoveFocusKey);
        Console.WriteLine("{0} 전체보기 ", GameRule.ViewKey);
        Console.WriteLine("{0} 종료 ", GameRule.ExitKey);
        return Console.ReadKey().Key;
    }
}

이제 학생 이동에 대한 MoveStudent 메서드를 구현해 봅시다. 시퀀스 다이어그램을 보면 학생 이동 기능에서는 먼저 캠퍼스에 있는 모든 학생 정보를 보여주고 사용자가 이동할 학생 번호를 입력한 후에 장소를 선택하면 해당 장소로 보내기로 되어 있습니다. 그런데 현재 약속된 시퀀스 다이어그램에서는 선택된 학생이 캠퍼스에도 여전히 존재하게 되어 있네요. 이에 선택된 장소에 학생을 보내기 전에 캠퍼스에세 나오게 하는 메서드(OutStudent)를 Campus 클래스에 추가하기로 할게요. 여러분들은 시퀀스 다이어그램에도 수정하시기 바랍니다.

class Campus
{
    ... 중략 ...
    internal void OutStudent(Student student)
    {
        throw new NotImplementedException();
    }
}

캠퍼스 생활의 MoveStudent는 시퀀스 다이어그램를 보면서 작성해 봅시다. 먼저, 모든 학생 정보를 출력하고 이동할 학생과 장소를 선택해서 선택된 장소로 보내세요.

class CampusLife
{
    ... 중략 ...
    private void MoveStudent()
    {
        ViewStuInfoInCampus(); //모든 학생 정보 출력
        Console.WriteLine("이동할 학생 번호를 입력하세요.");
        int num = EHLib.GetNum();

        Student student = campus[num]; //입력한 번호의 학생 참조

        if (student == null) //입력한 번호의 학생이 없을 때
        {
            Console.WriteLine("잘못 선택하였습니다. ");
            return;
        }
        Place place = SelectPlace(); //장소 선택

        if (place == null) //잘못 선택하였을 때
        {
            Console.WriteLine("잘못 선택하였습니다.");
            return;
        }
        campus.OutStudent(student); //캠퍼스에서 학생이 나간다.
        place.InStudent(student); //선택된 장소로 학생이 들어간다.
    }
}

이제 캠퍼스에 있는 학생 정보를 출력하는 메서드인 ViewStuInfoInCampus를 구현해 봅시다. 여기에서는 단순히 캠퍼스에 있는 학생 수를 얻어와서 차례대로 학생 정보를 얻어와 화면에 출력하세요.

class CampusLife
{
    ... 중략 ...
    private void ViewStuInfoInCampus()
    {
        int scnt = campus.StuCount;
        for (int i = 0; i < scnt; i++)
        {
            Console.WriteLine(campus.GetStuInfo(i));
        }
    }
}

캠퍼스의 StuCount 속성은 학생들을 보관하는 배열에 보관된 요소 개수를 반환하면 될 것입니다. 그리고 캠퍼스에 i번째 요소 정보를 문자열로 반환하는 GetStuInfo에서는 학생의 ToString 메서드를 호출하여 얻은 문자열을 그대로 반환합시다.

class Campus
{
    ... 중략 ...
    internal int StuCount
    {
        get
        {
            return FindEmptyIndex();
        }
    }

    internal string GetStuInfo(int nth)
    {
        if ((nth >= 0) && (nth < FindEmptyIndex() ) )
        {
           return students[nth].ToString();
        }

        return "해당 학생 정보 없음";
    }
}

이번에는 Campus의 특정 번호의 학생을 반환하는 인덱서를 구현해 보기로 합시다. 여기에서는 차례대로 학생의 번호가 입력된 번호와 같은지 확인해서 해당 학생 개체를 반환하세요.

class Campus
{
    ... 중략 ...
    internal Student this[int snum]
    {
        get
        {
            for (int i = 0; i < students.Length; i++)
            {
                if (students[i].Num == snum)
                {
                    return students[i];
                }
            }
            return null;
        }
    }
}

그리고 Student 클래스의 ToString 메서드를 재정의하여 학생 번호와 이름을 문자열로 반환하게 작성합시다.

class Student
{
    ... 중략 ...
    public override string ToString()
    {
       return string.Format("번호:{0} 이름:{1}", Num, Name);
    }
}

장소를 선택하는 SelectPlace에서는 캠퍼스 생활의 places 배열에 있는 각 장소 정보를 화면에 출력하여 사용자에게 특정 장소를 선택하게 하여 선택한 장소를 반환하세요.

class CampusLife
{
    ... 중략 ...
    private Place SelectPlace()
    {
        for (int i = 0; i < places.Length; i++)
        {
            Console.WriteLine("{0}:{1}", i + 1, places[i].ToString());
        }

        Console.WriteLine("장소를 선택하세요.");
        int num = EHLib.GetNum();

        if ((num > 0) && (num <= places.Length))
        {
            return places[num - 1];
        }
        return null;
   }
}

그리고 각 장소의 ToString을 재정의하세요.

class Dormitory:Place
{
     ... 중략 ...
    public override string ToString()
    {
       return "기숙사";
    }
}

이번에는 Campus 클래스에서 특정 학생을 빼내는 OutStudent 메서드를 구현합시다. 먼저, 입력된 학생을 찾아야 할 것입니다. 그리고 찾은 위치에 맨 뒤에 있는 학생을 옮기고 맨 뒤를 null로 바꿔주면 되겠네요. 정확히 이해하기 위해 필요하시면 논리적인 그림을 그려보시기 바랍니다.

class Campus
{
    ... 중략 ...
    internal void OutStudent(Student student)
    {
        int index = FindStudent(student);
        if (index == -1)
        {
            return;
        }
        students[index] = students[students.Length - 1];
        students[students.Length - 1] = null;
    }
    private int FindStudent(Student student)
    {
        for (int i = 0; i < students.Length; i++)
        {
            if (students[i] == student)
            {
                return i;
            }
        }
        return -1;
    }
}

그리고 학생이 특정 장소로 이동했을 때 수행되는 Place 클래스의 InStudent 메서드를 구현해 봅시다. 먼저, 학생들을 보관하기 위한 배열을 생성하는 코드를 생성자에 추가해야겠지요. 그리고 InStudent 메서드에서는 처음으로 발견되는 빈자리를 찾아 해당 위치에 입력된 학생 개체를 보관하면 될 것입니다. 캠퍼스에서도 같은 방법으로 구현했기 때문에 크게 어렵지 않을 것입니다.

abstract class Place
{
    ... 중략 ...
    Student[] students = null;
    internal Place()
    {
        students = new Student[GameRule.MaxStudents];
    }
    internal void InStudent(Student student)
    {
        int empty_index = FindEmptyIndex();
        students[empty_index] = student;
    }
    private int FindEmptyIndex()
    {
        int i;
        for (i = 0; i < students.Length; i++)
        {
            if (students[i] == null)
            {
                break;
            }
        }
        return i; 
    }
}

이상으로 학생 이동에 관련된 MoveStudent 메서드에 대한 구현을 마쳤습니다.

이번에는 초점 이동 기능에 해당하는 MoveFocus 메서드를 구현해 보기로 합시다. 마찬가지로 초점 이동에 관한 시퀀스 다이어그램을 보면서 작성을 하시면 되겠죠.

초점 이동에서는 먼저, 어디로 초점을 이동할 것인지를 선택한 후에 해당 장소에서 수행할 수 있는 메뉴에 의해 진행이 된 후에 다시 초점이 캠퍼스 생활로 올 때 사용자가 선택한 학생들을 캠퍼스로 복귀시킵니다.

class CampusLife
{
    ... 중략 ...
    private void MoveFocus()
    {
        Console.WriteLine("초점 이동 기능을 선택하였습니다.");
        Place place = SelectPlace();
        if (place == null)
        {
            Console.WriteLine("잘못 선택하였습니다.");
            return;
        }
        MoveFocusAt(place);
        ComeBack(place);
    }
}

이미 MoveStudent 메서드를 구현하면서 장소를 선택하는 SelectPlace는 구현하였기 때문에 그대로 호출해서 사용하면 되겠죠. MoveFocusAt 메서드와 ComeBack은 새로 추가하여 만들어야 할 것입니다. 그리고 각 장소에 포커스가 왔을 때의 작업은 장소별로 별도의 시퀀스로 작성하였으니 여기에서는 메서드를 비어있는 상태로 만듭시다. 테스트하기 위해 예외처리 구문은 주석으로 처리하세요.

class CampusLife
{
    ... 중략 ...
    private void MoveFocusAt(Place place)
    {
        //throw new NotImplementedException();
    }
}

캠퍼스로 복귀하는 과정은 해당 장소에 있는 학생 정보를 보여주고 복귀시키고자 하는 학생 번호를 입력받아 해당 장소에서는 나오게 하고 해당 학생을 캠퍼스에 들어가게 하면 될 것입니다. 그런데 시퀀스 다이어그램을 보시면 해당 장소에 있는 학생을 나오게 하는 메서드에 대한 약속이 없네요. 여러분은 해당 시퀀스에서 학생을 나오는 메서드를 추가하시기 바랍니다. 저는 해당 메서드 이름을 OutStudent라고 정하였습니다. 이를 위해 캠퍼스 생활의 ComeBack 메서드는 다음과 같이 구현할 수 있을 것입니다.

class CampusLife
{
    ... 중략 ...
    private void ComeBack(Place place)
    {
        while (true)
        {
            ViewStuInfoInPlace(place);
            Console.WriteLine("복귀할 학생 번호를 입력하세요. 없을 때는 0을 입력");
            int num = EHLib.GetNum();
            if (num == 0)
            {
                return;
            }
            Student student = place[num];
            if (student == null)
            {
                Console.WriteLine("잘못 선택하였습니다.");
                return;
            }
            place.OutStudent(student);
            campus.InStudent(student);
        }
    }
}

먼저, 해당 장소에 있는 학생 중에 복귀할 학생을 선택하기 전에 해당 장소에 있는 학생들의 정보를 화면에 보여주는 ViewStuInfoInPlace 메서드를 구현해 봅시다.

class CampusLife
{
    ... 중략 ...
    private void ViewStuInfoInPlace(Place place)
    {
        int scnt = place.GetStuCount();
        for (int i = 0; i < scnt; i++)
        {
            Console.WriteLine(place.GetStuInfo(i));
        }
    }
}

Place 클래스에 GetStuCount와 GetStuInfo 메서드를 구현해야겠지요. GetStuCount 메서드는 학생을 보관하는 배열에 처음으로 빈 자리가 나올 때까지 카운팅을 해서 반환하면 되겠죠. GetStuInfo에서는 특정 인덱스에 있는 학생 정보를 문자열로 반환하세요.

abstract class Place
{
    ... 중략 ...
    internal int GetStuCount()
    {
        int cnt = 0;
        while(students[cnt] != null)
        {
            cnt++;
        }
        return cnt;
    }
    internal string GetStuInfo(int nth)
    {
        int cnt = GetStuCount();
        if ((nth >= 0) && (nth < cnt))
        {
            return students[nth].ToString();
        }
        return "해당 학생 정보 없음";
    }
}

그리고 캠퍼스 생활의 ComeBack 메서드에서 장소 개체에 있는 인덱서와 OutStudent 메서드를 구현해야겠지요. 인덱서에서는 학생을 보관하는 배열에서 입력 인자로 전달된 번화와 같은 번호를 갖는 학생을 찾아 해당 학생을 반환하면 될 것입니다. 없다면 null을 반환하면 되겠죠. OutStudent 메서드에서는 입력 인자로 전달된 학생이 있는 인덱스 위치에 맨 뒤에 있는 학생으로 바꾸면 되겠죠.

abstract class Place
{
    ... 중략 ...

    internal Student this[int snum]
    {
        get
        {
            for (int i = 0; i < students.Length; i++)
            {
                if (students[i] == null)
                {
                    return null;
                }

                if (students[i].Num == snum)
                {
                    return students[i];
                }

            }
            return null;
        }
    }


    internal void OutStudent(Student student)
    {
        int cnt = GetStuCount();

        for (int i = 0; i < cnt; i++)
        {
            if (students[i] == null)
            {
                break;
            }
            if (students[i] == student)
            {
                students[i] = students[cnt-1];
                students[cnt - 1] = null;
                return;
            }
        }
    }
}

이상으로 초점 이동에 대한 구현을 마치겠습니다.

이번에는 초점이 강의실에 왔을 때에 수행하는 판서 강의와 발표 수업에 대해 구현해 보기로 합시다. 먼저, 초점이동이 되었을 때 수행하는 MoveFocusAt 메서드를 구현합시다. 여기에서는 입력 인자로 전달된 장소가 어디인지에 따라 각각의 장소에 초점이 왔을 때 수행하는 메서드를 호출하세요.

class CampusLife
{
    ... 중략 ...
    private void MoveFocusAt(Place place)
    {
        if (place is LectureRoom)
        {
            FocusAtLectureRoom(place as LectureRoom);
        }
        if (place is Library)
        {
            FocusAtLibrary(place as Library);
        }
        if (place is Dormitory)
        {
            FocusAtDormitory(place as Dormitory);
        }
    }
    private void FocusAtDormitory(Dormitory dormitory)
    {
        throw new NotImplementedException();
    }
    private void FocusAtLibrary(Library library)
    {
        throw new NotImplementedException();
    }
    private void FocusAtLectureRoom(LectureRoom lectureRoom)
    {
        throw new NotImplementedException();
    }
}

이 중에 강의실에 초점이 왔을 때 수행하는 FocusAtLectureRoom을 구현해 봅시다. 여기에서는 다시 강의실에서 수행할 수 있는 메뉴를 화면에 출력하여 사용자가 원하는 기능을 수행할 수 있게 해야 할 것입니다.

class CampusLife
{
    ... 중략 ...
    private void FocusAtLectureRoom(LectureRoom lectureRoom)
    {
        ConsoleKey key;
        while ((key = SelectLRMenu()) != GameRule.ExitKey)
        {
            switch (key)
            {
                case GameRule.LR_Forwarding: StartForwading(lectureRoom); break;
                case GameRule.LR_Announce: StartAnnounce(lectureRoom); break;
                default: Console.WriteLine("잘못된 메뉴를 선택하였습니다."); break;
            }
            Console.WriteLine("아무키나 누르세요.");
            Console.ReadKey();
        }
    }
    private void StartAnnounce(LectureRoom lectureRoom)
    {        throw new NotImplementedException();    }
    private void StartForwading(LectureRoom lectureRoom)
    {        throw new NotImplementedException();    }
    private ConsoleKey SelectLRMenu()
    {
        Console.Clear();
        Console.WriteLine("강의실 메뉴");
        Console.WriteLine("{0} 판서 강의", GameRule.LR_Forwarding);
        Console.WriteLine("{0} 발표 수업", GameRule.LR_Announce);
        Console.WriteLine("{0} 캠퍼스 생활로 돌아가기", GameRule.ExitKey);
        return Console.ReadKey().Key;
    }
}

그리고 강의실 메뉴에서 사용하는 메뉴 키에 대한 상수를 GameRule에 정의해야겠지요. 미리 도서관과 기숙사에서 필요한 메뉴 키에 대한 상수도 정의하세요.

static class GameRule
{
    ... 중략 ...
    internal const ConsoleKey LR_Forwarding = ConsoleKey.F1;
    internal const ConsoleKey LR_Announce = ConsoleKey.F2;

    internal const ConsoleKey LI_Seminar = ConsoleKey.F1;
    internal const ConsoleKey LI_Reading = ConsoleKey.F2;

    internal const ConsoleKey DO_Sleep = ConsoleKey.F1;
    internal const ConsoleKey DO_TV = ConsoleKey.F2;
}

이제 판서 강의를 구현합시다. 판서 강의에 대한 시퀀스 다이어그램을 보면 캠퍼스 생활에서는 강의실에 판서 강의를 선택하였다는 것을 입력 인자로 전달하며 DoIt 메서드를 호출하게 약속하였죠. 이는 향후 시나리오가 변경되어 강의실에서 할 수 있는 기능을 추가할 때를 고려한 것입니다.

class CampusLife
{
    ... 중략 ...
    private void StartForwading(LectureRoom lectureRoom)
    {
        lectureRoom.DoIt(GameRule.CMD_LR_Forwarding);
    }
}

마찬가지로 각 장소에 사용자가 선택한 기능이 무엇인지에 대한 상수도 GameRule에 정의합시다.

static class GameRule
{
   ... 중략 ...
    internal const int CMD_LR_Forwarding = (int)LR_Forwarding;
    internal const int CMD_LR_Announce = (int)LR_Announce;
    internal const int CMD_LI_Seminar = (int)LI_Seminar;
    internal const int CMD_LI_Reading = (int)LI_Reading;
    internal const int CMD_DO_Sleep = (int)DO_Sleep;
    internal const int CMD_DO_TV = (int)DO_TV;
}

현재 시나리오에는 LectureRoom에 입력 인자로 cmd 하나가 오는 DoIt 메서드는 판서 강의만 있습니다. 하지만 시나리오의 변경이 있을 수 있으니 선택문으로 판서 강의를 하는 것으로 구현하겠습니다. 판서 강의에서는 강의실에 모든 학생에게 강의를 듣게 하는 것과 해당 학생이 진취적인 학생일 경우 질문을 하는 것으로 되어 있습니다. 이에 각 장소의 기반 클래스인 Place에 특정 인덱스에 해당하는 학생을 참조할 수 있는 GetStudent메서드를 제공하고 이에 대한 접근 지정을 protected로 지정하겠습니다.

abstract class Place
{
   ... 중략 ...
    protected Student GetStudent(int nth)
    {
        int cnt = GetStuCount();
        if ((nth >= 0) && (nth < cnt))
        {
            return students[nth];
        }
        return null;
    }
}
class LectureRoom:Place
{
    ... 중략 ...
    internal override void DoIt(int cmd)
    {
        switch (cmd)
        {
            case GameRule.CMD_LR_Forwarding: StartForwarding(); break;
            default: return;
        }
   }

    private void StartForwarding()
    {
        int cnt = GetStuCount();
        Student student = null;
        CStudent cstudent = null;

        for (int i = 0; i < cnt; i++)
        {
            student = GetStudent(i);
            student.ListenLecture();
            cstudent = student as CStudent;
            if (cstudent != null)
            {
                cstudent.Question();
            }
        }
    }
}

이제 Student 클래스에 ListenLecture 메서드를 구현해 봅시다. 시나리오를 보시면 강의를 들으면 학생의 아이큐가 5올라가고 HP가 4내려가고 CP가 1내려가는 것으로 되어 있습니다. 이를 반영하기 위해 Student에 멤버 필드로 iq와 hp, cp를 추가하고 이를 얻어오거나 설정하는 멤버 속성을 추가하겠습니다. 또한, 이들의 최대값과 최소값, 초기값을 약속하였는데 이는 정적 클래스 StudentValueDefine을 추가하여 상수 멤버를 이용하세요.

static class StudentValueDefine
{
    internal const int MinIq = 60;
    internal const int MaxIq = 200;
    internal const int DefIq = 80;

    internal const int MinHp = 0;
    internal const int MaxHp = 100;
    internal const int DefHp = 50;

    internal const int MinCp = 0;
    internal const int MaxCp = 100;
    internal const int DefCp = 0;
}
class Student
{
    ... 중략 ...

    int iq;
    int hp;
    int cp;

    public int Iq
    {
        get
        {
            return iq;
        }
        protected set
        {
            if (value < StudentValueDefine.MinIq) 
            {
                 value = StudentValueDefine.MinIq;
            }
            if (value > StudentValueDefine.MaxIq)
            {
                value = StudentValueDefine.MaxIq;
            }
            iq = value;
        }
    }

    public int Hp
    {
        get
        {
            return hp;
        }
        protected set
        {
            if (value < StudentValueDefine.MinHp)
            {
                 value = StudentValueDefine.MinHp;
            }
            if (value > StudentValueDefine.MaxHp)
            {
                value = StudentValueDefine.MaxHp;
           }
            hp = value;
        }
    }

    public int Cp
    {
        get
        {
            return cp;
        }
        protected set
        {
            if (value < StudentValueDefine.MinCp)
            {
                value = StudentValueDefine.MinCp;
            }
            if (value > StudentValueDefine.MaxCp)
            {
                value = StudentValueDefine.MaxCp;
            }
            cp = value;
        }
    }
    internal void ListenLecture()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}",ToString(),Iq,Hp,Cp);
        Console.WriteLine("강의를 듣다.");
        Iq += 5;
        Hp -= 4;
        Cp -= 1;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
    }
}

도전적인 학생이 질문하는 Question 메서드는 시나리오처럼 아이큐와 대화능력을 변경하세요.

class CStudent:Student
{
    ... 중략 ...
    internal void Question()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}",ToString(),Iq,Hp,Cp);
        Console.WriteLine("질문을 하다.");
        Iq += 1;
        Cp += 1;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}",ToString(),Iq,Hp,Cp);
    }
}

이번에는 강의실에서 발표 수업을 선택하였을 때 수행하는 캡퍼스 생활의 StartAnnounce 메서드를 구현합시다. 발표 수업에 대한 시퀀스 다어이그램을 보면 강의실에 있는 학생들 정보를 보여준 다음 발표할 학생 번호를 입력받아 발표 수업을 선택한 것과 학생 번호를 입력 인자로 강의실 개체에 DoIt 메서드를 호출합니다.

class CampusLife
{
    ... 중략 ...
    private void StartAnnounce(LectureRoom lectureRoom)
    {
        ViewStuInfoInPlace(lectureRoom);
        Console.WriteLine("발표할 학생 번호를 입력하세요. ");
        int num = EHLib.GetNum();
        lectureRoom.DoIt(GameRule.CMD_LR_Announce, num);
    }
}

강의실에 두 개의 입력 인자를 전달받는 DoIt 메서드를 구현해 봅시다. DoIt 메서드에서는 입력 인자로 전달받은 학생 번호에 해당하는 학생이 존재하는지를 확인하는 것이 우선일 것입니다. 그리고 발표자는 발표를 진행하고 나머지 학생들은 자유 토론을 하도록 하세요.

class LectureRoom:Place
{
    ... 중략 ...
    internal override void DoIt(int cmd, int snum)
    {
        Student student = this[snum];
        if (student == null)
        {
            Console.WriteLine("{0}번 학생은 없습니다.", snum);
            return;
        }
        switch (cmd)
        {
            case GameRule.CMD_LR_Announce: StartAnnounce(student); break;
            default: return;
        }
    }
    private void StartAnnounce(Student student)
    {
        student.Announce();
        int cnt = GetStuCount();
        Student stu = null;
        for (int i = 0; i < cnt; i++)
        {
            stu = GetStudent(i);
            if (stu != student)
            {
                stu.Discuss();
            }
        }
    }
}

이제 Student 클래스에 Announce 메서드와 Discuss 메서드를 구현합시다. 이 부분은 시나리오에 나온 것처럼 학생의 각 능력치를 변경하게 하면 될 것입니다. 참고로 자유 토론에 해당하는 Discuss에서는 시나리오에 어떠한 능력을 어떻게 변경하라는 것이 명시되어 있지 않으니 단순히 화면에 출력합시다.

class Student
{
    ... 중략 ...
    internal void Announce()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
        Console.WriteLine("발표를 하다.");
        Cp+=3;
        Hp-=2;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
    }
    internal void Discuss()
    {
        Console.WriteLine("{0} 자유 토론을 하다.", ToString());
    }
}

이번에는 초점이 도서관에 왔을 때 수행하는 세미나와 책 읽기에 대한 부분을 구현하기로 합시다. 앞에서 초점이 강의실에 왔을 때 수행하는 부분과 구현하는 방법이 크게 다르지 않겠지요.

class CampusLife
{
    ... 중략 ...
    private void FocusAtLibrary(Library library)
    {
        ConsoleKey key;
        while ((key = SelectLibMenu()) != GameRule.ExitKey)
        {
            switch (key)
            {
                case GameRule.LI_Seminar: StartSeminar(library); break;
                case GameRule.LI_Reading: StartReading(library); break;
                default: Console.WriteLine("잘못된 메뉴를 선택하였습니다."); break;
            }
            Console.WriteLine("아무키나 누르세요. ");
            Console.ReadKey();
        } 
    }

    private void StartReading(Library library)
    {
        ViewStuInfoInPlace(library);
        Console.WriteLine("책 읽기를 할 학생 번호를 입력하세요.");
        int num = EHLib.GetNum();
        library.DoIt(GameRule.CMD_LI_Reading, num);
    }

    private void StartSeminar(Library library)
    {
        library.DoIt(GameRule.CMD_LI_Seminar);
    }

    private ConsoleKey SelectLibMenu()
    {
        Console.Clear();
        Console.WriteLine("도서관 메뉴");
        Console.WriteLine("{0} 세미나", GameRule.LI_Seminar);
        Console.WriteLine("{0} 책 읽기", GameRule.LI_Reading);
        Console.WriteLine("{0} 캠퍼스 생활로 돌아가기", GameRule.ExitKey);
        return Console.ReadKey().Key;
    }
}

이제 시퀀스 다이어그램을 보면서 세미나를 구현해 보기로 합시다. Library클래스의 입력 인자가 한 개 오는 DoIt 메서드도 LectureRoom에서 구현했던 것과 큰 차이가 없겠죠.

class Library:Place
{
    ... 중략 ...
    internal override void DoIt(int cmd)
    {
        switch (cmd)
        {
            case GameRule.CMD_LI_Seminar: StartSeminar(); break;
            default: return;
        }
    }
    private void StartSeminar()
    {
        int cnt = GetStuCount();
        Student student = null;
        for (int i = 0; i < cnt; i++)
        {
            student = GetStudent(i);
            student.ListenSeminar();
        }
    }
    internal override void DoIt(int cmd, int snum)
    {
        Student student = this[snum];
        if (student == null)
        {
            Console.WriteLine("{0}번 학생은 없습니다.", snum);
            return;
        }
        switch (cmd)
        {
            case GameRule.CMD_LI_Reading: StartReading(student); break;
            default: return;
        }
    }
    private void StartReading(Student student)
    {
        student.Reading();
    }
}

이제 시나리오를 보면서 Student 클래스에 ListenSeminar 메서드를 구현합시다. 단순히 시나리오에 나와있는 것에 따라 능력치를 변경하면 될 것입니다. 그리고 Student 클래스는 추상 클래스로 하는 것이 적절하므로 abstract 키워드를 추가하겠습니다. 이처럼 어떠한 단계에서든 잘못되었다고 생각이 들면 바로 고치세요.

abstract class Student
{
    ... 중략 ...
    internal void ListenSeminar()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
        Console.WriteLine("세미나를 듣다.");
        Iq += 5;
        Hp -= 4;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
   }
    internal virtual void Reading()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
        Console.WriteLine("책을 읽다.");
        Iq += 2;
        Cp += 2;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
    }
}

특히, 책을 읽는 행위에서 도전적인 학생은 추가로 대화 능력이 1올라가야 합니다. 이를 위해 Student의 Reading 메서드는 가상 메서드로 지정을 하였습니다. 그리고 CStudent에서는 이를 재정의해 주어야 하겠죠. CStudent의 Reading 메서드에서는 기반 클래스의 Reading 메서드를 base 키워드를 이용하여 호출한 후에 추가로 대화 능력을 변경합니다.

class CStudent:Student
{
   ... 중략 ...
    internal override void Reading()
    {
        base.Reading();
        Cp += 1;
        Console.WriteLine("도전적인 학생, 추가로 대화 능력에 변화: {0}", Cp);
    }
}

이번에는 초점이 기숙사에 왔을 때 수행하는 잠자기와 TV 시청에 대한 부분을 구현하기로 합시다. 앞에서 초점이 강의실과 도서관에 왔을 때 수행하는 부분과 구현하는 방법이 크게 다르지 않겠지요.

class CampusLife
{
    ... 중략 ...

    private void FocusAtDormitory(Dormitory dormitory)
    {
        ConsoleKey key;
        while ((key = SelectDormMenu()) != GameRule.ExitKey)
        {
            switch (key)
            {
                case GameRule.DO_Sleep: StartSleep(dormitory); break;
                case GameRule.DO_TV: StartTV(dormitory); break;
                default: Console.WriteLine("잘못된 메뉴를 선택하였습니다."); break;
            }

            Console.WriteLine("아무키나 누르세요.");
            Console.ReadKey();
        } 
    }

    private void StartTV(Dormitory dormitory)
    {
        dormitory.DoIt(GameRule.CMD_DO_Sleep);
    }

    private void StartSleep(Dormitory dormitory)
    {
        dormitory.DoIt(GameRule.CMD_DO_TV);
    }

    private ConsoleKey SelectDormMenu()
    {
        Console.Clear();
        Console.WriteLine("기숙사 메뉴");
        Console.WriteLine("{0} 잠자기", GameRule.DO_Sleep);
        Console.WriteLine("{0} TV 시청", GameRule.DO_TV);
        Console.WriteLine("{0} 캠퍼스 생활로 돌아가기", GameRule.ExitKey);

        return Console.ReadKey().Key;
    }
}

이제 시퀀스 다이어그램을 보면서 잠자기와 TV 시청을 구현해 보기로 합시다. Library클래스의 입력 인자가 한 개 오는 DoIt 메서드도 LectureRoom에서 구현했던 것과 큰 차이가 없겠죠.

class Dormitory:Place
{
    ... 중략 ...

    internal override void DoIt(int cmd)
    {
        switch (cmd)
        {
            case GameRule.CMD_DO_Sleep: TurnOff(); break;
            case GameRule.CMD_DO_TV: StartTV(); break;
            default: return;
        }
    }

    private void StartTV()
    {
        int cnt = GetStuCount();
        Student student = null;
        for (int i = 0; i < cnt; i++)
        {
            student = GetStudent(i);
            student.WatchingTV();
        }
    }
    private void TurnOff()
    {
        int cnt = GetStuCount();
        Student student = null;
        PStudent pstudent = null;
        for (int i = 0; i < cnt; i++)
        {
            student = GetStudent(i);
            student.Sleep();
            pstudent = student as PStudent;
            if (pstudent != null)
            {
                pstudent.TalkingInSleep();
            }
        }
    }
}

이제 시나리오를 보면서 Student 클래스에 Sleep 메서드와 WatchingTV 메서드를 구현합시다. 단순히 시나리오대로 능력치를 변경하면 될 것입니다. WatchingTV의 경우 보수적인 학생은 구체적인 행위가 다르므로 가상 메서드로 정의해야겠지요.

abstract class Student
{
    ... 중략 ...
    internal void Sleep()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
        Console.WriteLine("잠을 자다.");
        Hp += 2;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
    }
    internal virtual void WatchingTV()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
        Console.WriteLine("TV를 시청하다.");
        Hp -= 2;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
    }
}

보수적인 학생의 경우 WatchingTV 메서드를 재정의합니다.

class MStudent:Student
{
    ... 중략 ...
    internal override void WatchingTV()
    {
        base.WatchingTV();
        Cp -= 1;
        Console.WriteLine("보수적인 학생, 추가로 대화능력에 변화: {0}", Cp);
    }
}

그리고 수동적인 학생이 잠을 잘 때 추가로 잠꼬대하는 TalkingInSleep 메서드를 구현합시다. 마찬가지로 시나리오를 보면서 능력에 변화를 주세요.

class PStudent:Student
{
    ... 중략 ...
    internal void TalkingInSleep()
    {
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
        Console.WriteLine("잠꼬대를 하다.");
        Hp -= 1;
        Iq += 1;
        Console.WriteLine("{0} 아이큐:{1} 체력:{2} 대화능력:{3}", ToString(), Iq, Hp, Cp);
    }
}

마지막으로 전체 보기 기능을 구현해 봅시다. 이 부분은 이미 다른 기능들을 구현하면서 작성된 메서드를 이용하세요.

class CampusLife
{
    ... 중략 ...
    private void View()
    {
        Console.WriteLine("캠퍼스");
        ViewStuInfoInCampus();
        foreach (Place place in places)
        {
            Console.WriteLine(place.ToString());
            ViewStuInfoInPlace(place);
        }
    }
}

이상으로 첫 번째 프로그램 실습인 캠퍼스 생활을 만드는 일련의 과정을 소개하였습니다. 이처럼 프로그래밍 언어의 문법이나 프로그래밍 기술을 학습하는 과정에서는 더 정확하게 자신의 것으로 만들기 위해 프로그램을 만들어 보시기 바랍니다. 그리고 단순히 프로그램이 동작하게 하려고만 하지 마시고 좋은 공정과 견고한 설계를 하는 것을 생략하지 마시기 바랍니다. 이 책에서 보여준 캠퍼스 생활도 취약한 구조가 존재할 것입니다. 여러분께서 보다 견고한 구조로 설계를 하시고 구현해 보시기 바랍니다.