다음을 통해 공유


Object-Oriented 프로그래밍(C#)

C#은 개체 지향 프로그래밍 언어입니다. 개체 지향 프로그래밍의 네 가지 기본 원칙은 다음과 같습니다.

  • 절취 엔터티의 관련 특성 및 상호 작용을 클래스로 모델링하여 시스템의 추상 표현을 정의합니다.
  • 캡슐화 개체의 내부 상태 및 기능을 숨기고 공용 함수 집합을 통해서만 액세스를 허용합니다.
  • 상속 기존 추상화에 따라 새 추상화 만들기 기능
  • 다형성 여러 추상화에서 여러 가지 방법으로 상속된 속성 또는 메서드를 구현하는 기능.

이전 자습서에서 클래스 소개를 통해 추상화캡슐화를 모두 살펴보았습니다. 클래스는 BankAccount 은행 계좌의 개념에 대한 추상화가 제공되었습니다. 클래스 BankAccount의 구현을 수정할 수 있으며, 이를 사용하는 코드에 영향을 주지 않습니다. BankAccount 클래스와 Transaction 클래스는 모두 코드에서 이러한 개념을 설명하는 데 필요한 구성 요소를 캡슐화합니다.

이 자습서에서는 상속다형성을 사용하여 새 기능을 추가하도록 해당 애플리케이션을 확장합니다. 또한 이전 자습서에서 학습한 BankAccount캡슐화 기술을 활용하여 클래스에 기능을 추가합니다.

다양한 유형의 계정 만들기

이 프로그램을 빌드한 후 기능을 추가하라는 요청을 받습니다. 그것은 하나의 은행 계좌 유형이있는 상황에서 잘 작동합니다. 시간이 지남에 따라 요구 사항이 변경되고 관련 계정 유형이 요청됩니다.

  • 매월 말에 이자를 누적하는 이자 수익 계정입니다.
  • 마이너스 잔액을 가질 수 있는 크레딧 라인이지만 잔액이 있을 때 매달 이자 청구가 있습니다.
  • 선불 기프트 카드 계좌는 단일 입금으로 시작하며, 사용 후에만 결제 가능합니다. 매월 초에 한 번 다시 채울 수 있습니다.

이러한 모든 다른 계정은 이전 자습서에서 정의한 BankAccount 클래스와 유사합니다. 해당 코드를 복사하고, 클래스의 이름을 바꾸고, 수정할 수 있습니다. 이 기술은 단기적으로는 작동하지만 시간이 지남에 따라 더 많은 작업이 될 것입니다. 모든 변경 내용은 영향을 받는 모든 클래스에서 복사됩니다.

대신 이전 자습서에서 만든 클래스에서 BankAccount 메서드와 데이터를 상속하는 새 은행 계좌 유형을 만들 수 있습니다. 이러한 새 클래스는 각 형식에 필요한 특정 동작으로 BankAccount 클래스를 확장할 수 있습니다.

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

이러한 각 클래스는 공유 기본 클래스인 클래스에서 공유 동작을 상속합니다BankAccount. 각 파생 클래스에서 새 기능과 다른 기능에 대한 구현을 작성합니다. 이러한 파생 클래스에는 이미 BankAccount 클래스에서 정의된 모든 동작이 있습니다.

다른 소스 파일에서 각 새 클래스를 만드는 것이 좋습니다. Visual Studio에서 프로젝트를 마우스 오른쪽 단추로 클릭하고 클래스 추가를 선택하여 새 파일에 새 클래스를 추가할 수 있습니다. Visual Studio Code에서 [파일], [새로 만들기]를 선택하여 새 원본 파일을 만듭니다. 두 도구에서 InterestEarningAccount.cs, LineOfCreditAccount.cs GiftCardAccount.cs 클래스와 일치하도록 파일 이름을 지정합니다.

앞의 샘플과 같이 클래스를 만들 때 파생 클래스가 컴파일되지 않는 것을 알 수 있습니다. 생성자는 개체 초기화를 담당합니다. 파생 클래스 생성자는 파생 클래스를 초기화하고 파생 클래스에 포함된 기본 클래스 개체를 초기화하는 방법에 대한 지침을 제공해야 합니다. 적절한 초기화는 일반적으로 추가 코드 없이 발생합니다. 클래스는 BankAccount 다음 서명을 사용하여 하나의 공용 생성자를 선언합니다.

public BankAccount(string name, decimal initialBalance)

직접 생성자를 정의할 때 컴파일러는 기본 생성자를 생성하지 않습니다. 즉, 각 파생 클래스는 이 생성자를 명시적으로 호출해야 합니다. 기본 클래스 생성자에 인수를 전달할 수 있는 생성자를 선언합니다. 다음 코드는 InterestEarningAccount의 생성자를 보여줍니다.

public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}

이 새 생성자에 대한 매개 변수는 기본 클래스 생성자의 매개 변수 형식 및 이름과 일치합니다. 구문을 사용하여 : base() 기본 클래스 생성자에 대한 호출을 나타냅니다. 일부 클래스는 여러 생성자를 정의하며, 이 구문을 사용하면 호출하는 기본 클래스 생성자를 선택할 수 있습니다. 생성자를 업데이트한 후에는 각 파생 클래스에 대한 코드를 개발할 수 있습니다. 새 클래스에 대한 요구 사항은 다음과 같이 명시할 수 있습니다.

  • 이자 수익 계정:
    • 월말 잔액에서 2% 포인트를 적립받게 됩니다.
  • 신용한도
    • 음수 잔액을 가질 수 있지만 절대값은 신용 한도보다 크지 않습니다.
    • 월말 잔액이 0이 아닌 매월 이자 청구가 발생합니다.
    • 크레딧 한도를 초과하여 인출할 때마다 수수료가 부과됩니다.
  • 기프트 카드 계정:
    • 매월 마지막 날에 지정된 금액으로 매월 한 번 다시 채울 수 있습니다.

이러한 세 가지 계정 유형 모두 매월 말에 수행되는 작업이 있음을 알 수 있습니다. 그러나 각 계정 유형은 서로 다른 작업을 수행합니다. 다형성을 사용하여 이 코드를 구현합니다. virtual 클래스에 단일 BankAccount 메서드를 만드십시오.

public virtual void PerformMonthEndTransactions() { }

위의 코드에서는 키워드를 virtual 사용하여 파생 클래스가 다른 구현을 제공할 수 있는 메서드를 기본 클래스에서 선언하는 방법을 보여 있습니다. virtual 메서드는 파생된 클래스를 다시 구현하도록 선택할 수 있는 메서드입니다. 파생 클래스는 키워드를 override 사용하여 새 구현을 정의합니다. 일반적으로 이를 "기본 클래스 구현 재정의"라고 합니다. 키워드는 virtual 파생 클래스가 동작을 재정의할 수 있도록 지정합니다. 파생 클래스가 동작을 재정의해야 하는 메서드를 선언 abstract 할 수도 있습니다. 기본 클래스는 메서드에 대한 구현을 abstract 제공하지 않습니다. 다음으로, 만든 두 개의 새 클래스에 대한 구현을 정의해야 합니다. InterestEarningAccount로 시작합니다.

public override void PerformMonthEndTransactions()
{
    if (Balance > 500m)
    {
        decimal interest = Balance * 0.02m;
        MakeDeposit(interest, DateTime.Now, "apply monthly interest");
    }
}

에 다음 코드를 추가합니다 LineOfCreditAccount. 이 코드는 계정의 잔액을 반대로 계산하여 양의 이자 청구액을 도출하고 계정에서 인출합니다.

public override void PerformMonthEndTransactions()
{
    if (Balance < 0)
    {
        // Negate the balance to get a positive interest charge:
        decimal interest = -Balance * 0.07m;
        MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest");
    }
}

GiftCardAccount 월말 기능을 구현하려면 클래스를 두 가지 변경해야 합니다. 먼저 매월 추가할 선택적 금액을 포함하도록 생성자를 수정합니다.

private readonly decimal _monthlyDeposit = 0m;

public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance)
    => _monthlyDeposit = monthlyDeposit;

생성자는 monthlyDeposit 값을 기본값으로 제공하여 호출자가 월별 예치금 0을 생략할 수 있습니다. 다음으로 생성자에서 PerformMonthEndTransactions 0이 아닌 값으로 설정된 경우 월별 보증금을 추가하는 메서드를 재정의합니다.

public override void PerformMonthEndTransactions()
{
    if (_monthlyDeposit != 0)
    {
        MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit");
    }
}

오버라이드는 생성자에 설정된 월별 입금을 적용합니다. 변경 사항을 MainGiftCardAccount에 대해 테스트하기 위해 InterestEarningAccount 메서드에 다음 코드를 추가하십시오.

var giftCard = new GiftCardAccount("gift card", 100, 50);
giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee");
giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries");
giftCard.PerformMonthEndTransactions();
// can make additional deposits:
giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending money");
Console.WriteLine(giftCard.GetAccountHistory());

var savings = new InterestEarningAccount("savings account", 10000);
savings.MakeDeposit(750, DateTime.Now, "save some money");
savings.MakeDeposit(1250, DateTime.Now, "Add more savings");
savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills");
savings.PerformMonthEndTransactions();
Console.WriteLine(savings.GetAccountHistory());

결과를 확인합니다. 이제 다음과 유사한 테스트 코드 집합을 추가합니다.LineOfCreditAccount

var lineOfCredit = new LineOfCreditAccount("line of credit", 0);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());

위의 코드를 추가하고 프로그램을 실행하면 다음과 같은 오류가 표시됩니다.

Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit must be positive (Parameter 'amount')
   at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date, String note) in BankAccount.cs:line 42
   at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance) in BankAccount.cs:line 31
   at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal initialBalance) in LineOfCreditAccount.cs:line 9
   at OOProgramming.Program.Main(String[] args) in Program.cs:line 29

비고

실제 출력에는 프로젝트와 함께 폴더의 전체 경로가 포함됩니다. 간단히 하기 위해 폴더 이름을 생략했습니다. 또한 코드 형식에 따라 줄 번호가 약간 다를 수 있습니다.

초기 잔액이 BankAccount 0보다 커야 한다고 가정하므로 이 코드가 실패합니다. 클래스에 BankAccount 구운 또 다른 가정은 잔액이 음수로 갈 수 없다는 것입니다. 대신 계좌에서 잔액을 초과하는 인출은 거부됩니다. 이러한 두 가정은 모두 변경해야 합니다. 신용 대출 계정은 0부터 시작하며 일반적으로 잔액이 음수입니다. 또한 고객이 너무 많은 돈을 빌린 경우 수수료가 발생합니다. 거래가 승인되었으며 비용이 더 듭니다. 첫 번째 규칙은 최소 잔액을 지정하는 생성자에 선택적 인수를 BankAccount 추가하여 구현할 수 있습니다. 기본값은 0입니다. 두 번째 규칙에는 파생 클래스가 기본 알고리즘을 수정할 수 있는 메커니즘이 필요합니다. 어떤 의미에서 기본 클래스는 초과 인출이 발생할 때 무엇을 할지 파생 형식에게 "묻습니다". 기본 동작은 예외를 throw하여 트랜잭션을 거부하는 것입니다.

먼저 선택적 minimumBalance 매개 변수를 포함하는 두 번째 생성자를 추가해 보겠습니다. 이 새 생성자는 기존 생성자가 수행하는 모든 작업을 수행합니다. 또한 최소 잔액 속성을 설정합니다. 기존 생성자의 본문을 복사할 수 있지만 나중에 두 위치가 변경될 수 있습니다. 대신 생성자 체인을 사용하여 한 생성자가 다른 생성자를 호출하도록 할 수 있습니다. 다음 코드는 두 생성자와 새 추가 필드를 보여줍니다.

private readonly decimal _minimumBalance;

public BankAccount(string name, decimal initialBalance) : this(name, initialBalance, 0) { }

public BankAccount(string name, decimal initialBalance, decimal minimumBalance)
{
    Number = s_accountNumberSeed.ToString();
    s_accountNumberSeed++;

    Owner = name;
    _minimumBalance = minimumBalance;
    if (initialBalance > 0)
        MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}

앞의 코드는 두 가지 새로운 기술을 보여 줍니다. 먼저 minimumBalance 필드는 readonly로 표시됩니다. 즉, 개체를 생성한 후에는 값을 변경할 수 없습니다. 만든 BankAccount 후에는 minimumBalance 변경할 수 없습니다. 둘째, 두 개의 매개 변수를 사용하는 생성자는 구현으로 사용합니다 : this(name, initialBalance, 0) { } . 식은 : this() 매개 변수가 세 개 있는 다른 생성자를 호출합니다. 이 기술을 사용하면 클라이언트 코드가 여러 생성자 중 하나를 선택할 수 있더라도 개체를 초기화하기 위한 단일 구현을 사용할 수 있습니다.

이 구현은 초기 잔액이 MakeDeposit보다 큰 경우에만 0를 호출합니다. 이는 예금이 양수이어야 한다는 규칙을 유지하면서도 신용 계좌를 0 잔액으로 개설할 수 있도록 허용합니다.

이제 클래스에 BankAccount 최소 잔액에 대한 읽기 전용 필드가 있으므로 최종 변경은 하드 코드를 0 메서드로 minimumBalance 변경하는 것입니다MakeWithdrawal.

if (Balance - amount < _minimumBalance)

BankAccount 클래스를 확장한 후, 새 기본 클래스 생성자를 호출하도록 LineOfCreditAccount 생성자를 다음 코드와 같이 수정할 수 있습니다.

public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)
{
}

LineOfCreditAccount 생성자는 creditLimit 매개 변수의 부호를 변경하여 minimumBalance 매개 변수와 의미와 일치하도록 합니다.

다른 초과 인출 규칙

추가할 마지막 기능을 사용하면 LineOfCreditAccount 거래를 거부하는 대신 크레딧 한도를 초과하여 요금을 부과할 수 있습니다.

한 가지 기술은 필요한 동작을 구현하는 가상 함수를 정의하는 것입니다. 이 클래스는 BankAccount 메서드를 MakeWithdrawal 두 개의 메서드로 리팩터합니다. 인출이 최소값 이하의 잔액을 사용할 때 새 메서드는 지정된 작업을 수행합니다. 기존 MakeWithdrawal 메서드에는 다음 코드가 있습니다.

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    if (Balance - amount < _minimumBalance)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    var withdrawal = new Transaction(-amount, date, note);
    _allTransactions.Add(withdrawal);
}

다음 코드로 바꿉다.

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance - amount < _minimumBalance);
    Transaction? withdrawal = new(-amount, date, note);
    _allTransactions.Add(withdrawal);
    if (overdraftTransaction != null)
        _allTransactions.Add(overdraftTransaction);
}

protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn)
{
    if (isOverdrawn)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    else
    {
        return default;
    }
}

추가된 메서드는 protected파생 클래스에서만 호출할 수 있음을 의미합니다. 이 선언은 다른 클라이언트가 메서드를 호출하지 못하도록 합니다. 또한 virtual 파생 클래스가 동작을 변경할 수 있도록 합니다. 반환 형식은 .입니다 Transaction?. ? 주석은 메서드가 null를 반환할 수 있음을 나타냅니다. 인출 한도를 LineOfCreditAccount 초과할 때 수수료를 부과하려면 다음 구현을 추가합니다.

protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) =>
    isOverdrawn
    ? new Transaction(-20, DateTime.Now, "Apply overdraft fee")
    : default;

오버라이드는 계정이 초과 인출되었을 때 수수료 거래를 반환합니다. 인출이 제한을 초과하지 않으면 메서드는 트랜잭션을 반환합니다 null . 이는 수수료가 없음을 나타냅니다. 다음 코드를 Main 메서드에 추가하여 Program 클래스의 변경 내용을 테스트하십시오.

var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());

프로그램을 실행하고 결과를 확인합니다.

요약

잘 알 수 없는 경우 GitHub 리포지토리에서 이 자습서의 소스를 확인할 수 있습니다.

이 자습서에서는 Object-Oriented 프로그래밍에 사용되는 많은 기술을 보여 줍니다.

  • 서로 다른 계정 유형 각각에 대한 클래스를 정의할 때 Abstraction 을 사용했습니다. 이러한 클래스는 해당 유형의 계정에 대한 동작을 설명했습니다.
  • 각 클래스에 많은 세부 정보를 보관할 때 private 사용했습니다.
  • 클래스에서 이미 만든 구현을 활용하여 코드를 저장할 때 BankAccount을 사용했습니다.
  • 계정 유형에 대한 특정 동작을 만들기 위해 파생 클래스가 재정의할 수 있는 메서드를 만들 때 다형성을 사용했습니다.