본문 바로가기

C#/이것이 C#이다

[ 이것이 C#이다 ] Chapter 08 . 인터페이스와 추상 클래스

[ 학습 흐름 ]

[ 학습 흐름 ]

  1. 인터페이스의 선언
  2. 인터페이스를 상속하는 인터페이스
  3. 여러 인터페이스 . 한꺼번에 상속하기
  4. 추상 클래스

[ 인터페이스의 선언 ]

[ 인터페이스 ]

interface 인터페이스_이름
{
	반환형식 메소드이름1(매개변수 목록);
	반환형식 메소드이름2(매개변수 목록);
	반환형식 메소드이름3(매개변수 목록);
	반환형식 메소드이름4(매개변수 목록);
}

 

- 클래스와 비슷하지만 메소드,이벤트,인덱서,프로퍼티만을 가질 수 있다 . 그 마저도 구현부가 없다 .

- 인터페이스는 접근 제한자를 사용 할 수 없고 모든것이 public 으로 선언된다 .

- 인스턴스도 만들 수 없다 .


[ 인터페이스를 상속받는 클래스 ]

class ConsoleLogger:ILogger
{
	public void WriteLog(string message)
    {
    	//메서드
    }
}

ILogger logger = new ConsoleLogger();

- 인터페이스를 상속받는 클래스는 인스턴스를 만들 수 있다 .

- 파생 클래스는 인터페이스에 선언된 모든 메소드를 public으로 반드시 구현해야 한다 .

- 인터페이스는 인스턴스는 못 만들지만 참조는 만들 수 있다 .

(파생 클래스가 기반 클래스와 같은 형식으로 간주되는 원리가 그대로 적용)


[ 인터페이스는 약속이다 ]

[ 인터페이스 ]

- USB 포트를 다양하게 활용할 수 있는 이유는 PC와 주변기기가 USB라는 약속을 따르기 때문이다 .

-  클래스가 따라야 하는 약속이 인터페이스이다 .

- 인터페이스는 파생될 클래스가 어떤 메소드를 구현해야 할지를 정의한다 .

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace Interface;

class MainApp
{
    interface ILogger
    {
        void WriteLog(string message);
    }
    class ConsoleLogger : ILogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
        }
    }
    class FileLogger:ILogger
    {
        StreamWriter writer;
        public FileLogger(string path)
        {
            writer = File.CreateText(path);
            writer.AutoFlush = true;
        }
        public void WriteLog(string message) 
        {
            writer.WriteLine("{0} {1}", DateTime.Now.ToShortTimeString(), message);
        }
    }
    class ClimateMonitor
    {
        ILogger logger;
        public ClimateMonitor(ILogger logger)
        {
            this.logger = logger;
        }
        public void Start()
        {
            while(true)
            {
                Console.WriteLine("온도를 입력해주세요 : ");
                string temperature=Console.ReadLine();
                if (temperature == "")
                    break;
                logger.WriteLog("현재온도"+temperature);

            }
        }
    }
    static void Main(string[] args)
    {
       //ClimateMonitor monitor = new ClimateMonitor(new FileLogger("MyLog.txt"));
       ClimateMonitor monitor = new ClimateMonitor(new ConsoleLogger());

        monitor.Start();
    }
}

- 예시처럼 climateMonitor 클래스를 사용하는 사용자들이 다른 방식으로 로그를 출력하려 한다면 ,

인터페이스는 좋은 해결책이 될 수 있다 .

- ClimateMonitor 클래스는 Ilogger 형식의 참조 logger를 이용해서 사용자로부터 입력받은 온도를 기록하고 ,

logger 가 어떻게 기록할지는 ClimateMonitor 생성자의 매개변수에 달려있다 .


[ 인터페이스를 상속하는 인터페이스 ]

[ 인터페이스를 상속하는 인터페이스 ]

- 인터페이스를 상속 할 수 있는건 클래스 ,구조체 ,인터페이스 .


[ 인터페이스를 인터페이스가 상속하는 이유 ]

상속하려는 인터페이스가 소스코드가 아닌 어셈블리로만 제공되는 경우

- .Net SDK에서 제공하는 인터페이스가 그 예이다 .

- 어셈블리 안에 있어 수정이 어렵기에 해당 인터페이스 새 기능을 추가하려면 상속할 수 밖에 없다 .

 

상속하려는 인터페이스를 상속하는 다른 클래스들이 존재할때

- 클래스는 반드시 인터페이스의 모든 메소드,프로퍼티를 구현해야 한다 .

- 기존 소스코드에 영향을 미치지 않고 새로운 기능을 추가하려면 상속하는게 좋다 .

using System;


namespace DerivedInterface;

class MainApp
{
    interface ILogger
    {
        void WriteLog(string message);
    }
    interface IFormattableLogger:ILogger
    {
        void WriteLog(string format, params Object[] args);
    }
    class ConsoleLogger : IFormattableLogger
    {
        public void WriteLog(string message)
        {
            Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
        }

        public void WriteLog(string format, params object[] args)
        {
            string message = String.Format(format, args);
            Console.WriteLine("{0} {1}", DateTime.Now.ToLocalTime(), message);
        }
    }
   
    
    static void Main(string[] args)
    {
       IFormattableLogger logger = new ConsoleLogger();
        logger.WriteLog("The world is not flat");
        logger.WriteLog("{0} + {1}= {2}", 1, 1, 2);
    }
}

[ 여러 인터페이스 , 한꺼번에 상속하기 ]

[ 클래스의 다중상속 ]

죽음의 다이아몬드

- 최초의 클래스가 두개의 파생클래스로부터 상속받고 , 이 두개의 파생 클래스를 다시 하나의 클래스가 상속하는것

 

업캐스팅

- 모호성의 오류를 불러일으킴

 

위의 두 문제로 C#의 클래스는 다중 상속을 허용하지 않는다 .


[ 인터페이스의 다중상속 ]

- 인터페이스는 내용이 아닌 외형만을 강제하기에 죽음의 다이아몬드와 같은 문제가 발생하지 않음

using System;


namespace DerivedInterface;

class MainApp
{
    interface IRunnable
    {
        void Run();
    }
    interface IFlyable
    {
        void Fly();
    }
    class FlyingCar:IRunnable,IFlyable
    {
        public void Run() 
        {
            Console.WriteLine("Run!");
        }
        public void Fly() 
        {
            Console.WriteLine("Fly!");
        }
    }
   
    static void Main(string[] args)
    {
        FlyingCar car = new FlyingCar();
        car.Run();  
        car.Fly();

        IRunnable runnable = car as IRunnable;
        runnable.Run();

        IFlyable flyable=car as IFlyable;
        flyable.Fly();
        
    }
}

[ 포함 ]

MyVehicle
{
	Car car = new Car();
    Plane plane = new Plane();
    
    public void Fly(){plane.Ride()}
    public void Run(){car.Ride()}
}

- 구현을 물려받는 다중 상속은 포함기능을 통해 구현 할 수 있다 .

( 상속은 구현을 물려받기 위한 장치가 아닌 다형성 : 즉 다양한 버전의 모습을 가지게 하는게 목적임 )

- 클래스 안에 물려받고 싶은 기능을 가진 클래스를 필드로 선언하는 기법


[ 인터페이스의 기본 구현 메소드 ]

[ 기본 구현 메소드 ]

- 어떠한 인터페이스로부터 수없이 많은 파생클래스가 생겼다 가정했을때 이런 코드를 레거시라고 한다 .

- 레거시 코드는 업그레이드에 각별한 주의가 필요하다 . (추가시 파생클래스에도 구현하지 않으면 컴파일 에러 생기기에)

- 초기 레거시 설계시 놓친 새로운 매소드를 추가할때 기본구현 메소드를 갖도록 해서 위 문제를 해결 할 수 있다 .

- 인터페이스 참조로업캐스팅 했을때만 사용하기에 파생 클래스에서 인터페이스에 추가된 메소드 엉뚱하게 호출 할 일 X

using System;


namespace DerivedInterface;

interface ILogger
{
    void WriteLog(string message);
    void WriteError(string Error)
    {
        Console.WriteLine($"Error : {Error}");
    }
}
class ConsoleLogger:ILogger
{
    public void WriteLog(string message)
    {
        Console.WriteLine($"{DateTime.Now.ToLocalTime()},{ message}");
    }
}   
class MainApp
{
    static void Main(string[] args)
    {
       ILogger logger = new ConsoleLogger();
        logger.WriteLog("System Up");
        logger.WriteError("Error");

        ConsoleLogger clogger=new ConsoleLogger();
        clogger.WriteLog("System Up");
        //인터페이스에 선언된 기본 구현 인터페이스는 파생 클래스의 참조로 호출 불가
        //clogger.WriteError("Error");
    }
}

[ 추상클래스 ]

[ 추상 클래스 ]

- 추상 클래스는 구현을 가질 수 있지만 인스턴스는 가질 수 없다 .

- 추상 클래스는 한정자를 명시하지 않으면 모든 메소드가 private으로 선언된다

- 추상 메소드를 가질 수 있는데 , 파생 클래스의 구현을 강제한다 .

- 추상 메서드는 약속의 역할을 하기에 public,protected,internal,protected internal 중 하나로 수식될것을 강요한다 .

- 이로 인해 클래스의 접근성 원칙과 인터페이스의 접근성 원칙 모두 지킨다 .

- 또한 추상 클래스는 추상 클래스를 상속 할 수 있다 .

using System;


namespace AbstractClass;
abstract class AbstractBase
{
    protected void PrivateMethodA()
    {
        Console.WriteLine("Base.PrivateMethodA");
    }
    public void PublicMethodA()
    {
        Console.WriteLine("Base.PublicMethodA");
    }
    public abstract void AbstractMethod();
}

class Derived : AbstractBase
{
    public override void AbstractMethod()
    {
        Console.WriteLine("Derived.AbstractMethodA");
        PrivateMethodA();
    }
}
class MainApp
{
    static void Main(string[] args)
    {
        AbstractBase obj = new Derived();
        obj.AbstractMethod();
        obj.PublicMethodA();
    }
}

[ 추상 클래스의 목적 ]

- 추상 클래스는 일반 클래스가 가질 수 있는 구현과 더불어 추상 메소드를 가지고 있다 .

- 위 기능은 일반 메소드를 선언, 클래스에 대한 아래와 같은 메뉴얼을 작성해 배포해도 같은 효과를 낼것이다 .

ex)이 클래스는 직접 인스턴스 하지 말고 파생 클래스를 만들어 사용하고 , 메서드 A,B는 꼭 오버라이딩 해주세요

- 그러나 추상 클래스를 씀으로써 위의 내용은 암시적으로 다른 프로그래머에게 전달 가능하다 .

- 또한 추상 메소드의 구현 역시 컴파일러가 알려주기에 실수를 줄일 수 있다.