빈약 한 도메인을 피하고 의존성 주입?


33

이것은 프로그래밍 언어에 무관심한 질문 일 수 있지만 .NET 생태계를 목표로하는 답변에 관심이 있습니다.

시나리오입니다. 공공 관리를위한 간단한 콘솔 응용 프로그램을 개발해야한다고 가정하십시오. 응용 프로그램은 차량 세금에 관한 것입니다. 그들에게는 다음과 같은 비즈니스 규칙이 있습니다.

1.a) 차량이 자동차이고 소유자가 마지막으로 세금을 납부 한 시간이 30 일 전인 경우 소유자는 다시 지불해야합니다.

1.b) 차량이 오토바이이고 소유자가 세금을 마지막으로 납부 한 시간이 60 일 전인 경우 소유자는 다시 지불해야합니다.

다시 말해, 자동차가있는 경우 30 일마다 지불해야하거나 오토바이가있는 경우 60 일마다 지불해야합니다.

시스템의 각 차량에 대해 애플리케이션은 해당 규칙을 테스트하고이를 충족하지 않는 차량 (플레이트 번호 및 소유자 정보)을 인쇄해야합니다.

내가 원하는 것은 :

2.a) SOLID 원칙 (특히 개방 / 폐쇄 원칙)을 준수합니다.

내가 원하지 않는 것은 (생각합니다)

2.b) 빈곤 영역, 따라서 비즈니스 로직은 비즈니스 엔티티 내부로 이동해야합니다.

나는 이것으로 시작했다 :

public class Person
// You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
{
    public string Name { get; set; }

    public string Surname { get; set; }
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Person Owner { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract bool HasToPay();
}

public class Car : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class Motorbike : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public class PublicAdministration
{
    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return this.GetAllVehicles().Where(vehicle => vehicle.HasToPay());
    }

    private IEnumerable<Vehicle> GetAllVehicles()
    {
        throw new NotImplementedException();
    }
}

class Program
{
    static void Main(string[] args)
    {
        PublicAdministration administration = new PublicAdministration();
        foreach (var vehicle in administration.GetVehiclesThatHaveToPay())
        {
            Console.WriteLine("Plate number: {0}\tOwner: {1}, {2}", vehicle.PlateNumber, vehicle.Owner.Surname, vehicle.Owner.Name);
        }
    }
}

2.a : 개방 / 폐쇄 원칙이 보장됩니다. 자전거 세금을 원한다면 Vehicle에서 상속 받으면 HasToPay 메서드를 재정의하면됩니다. 단순한 상속을 통해 개방 / 폐쇄 원칙이 충족되었습니다.

"문제"는 다음과 같습니다.

3.a) 왜 차량이 지불해야하는지 알아야합니까? 공공 행정 문제가 아닙니까? 정부가 자동차 세금을 변경해야하는 경우 자동차를 변경해야하는 이유는 무엇입니까? "왜 차량에 HasToPay 메서드를 넣었을까요?" 더 나은 대안이 있습니까?

3.b) 어쩌면 세금 납부는 결국 차량 문제 일 수도 있고, 또는 나의 초기 개인 차량-자동차-자전거-공공 행정 설계가 잘못되어 있거나 철학자와 더 나은 비즈니스 분석가가 필요할 수도 있습니다. 해결책은 다음과 같습니다. HasToPay 메소드를 다른 클래스로 옮기면 TaxPayment라고 할 수 있습니다. 그런 다음 TaxPayment 파생 클래스를 두 개 만듭니다. CarTaxPayment 및 오토바이 TaxPayment. 그런 다음 Vehicle 클래스에 추상 Payment 속성 (TaxPayment 유형)을 추가하고 Car 및 Motorbike 클래스에서 올바른 TaxPayment 인스턴스를 리턴합니다.

public abstract class TaxPayment
{
    public abstract bool HasToPay();
}

public class CarTaxPayment : TaxPayment
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class MotorbikeTaxPayment : TaxPayment
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Person Owner { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract TaxPayment Payment { get; }
}

public class Car : Vehicle
{
    private CarTaxPayment payment = new CarTaxPayment();
    public override TaxPayment Payment
    {
        get { return this.payment; }
    }
}

public class Motorbike : Vehicle
{
    private MotorbikeTaxPayment payment = new MotorbikeTaxPayment();
    public override TaxPayment Payment
    {
        get { return this.payment; }
    }
}

그리고 우리는 이전 프로세스를 다음과 같이 호출합니다.

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
    }

그러나 이제이 코드는 CarTaxPayment / MotorbikeTaxPayment / TaxPayment 내에 LastPaidTime 멤버가 없기 때문에 컴파일되지 않습니다. CarTaxPayment와 MotorbikeTaxPayment가 비즈니스 엔터티가 아닌 "알고리즘 구현"과 비슷해지기 시작했습니다. 어쨌든 이제 이러한 "알고리즘"에는 LastPaidTime 값이 필요합니다. 물론, 우리는 그 가치를 TaxPayment의 생성자에게 전달할 수 있지만, 그것은 차량 캡슐화 나 책임, 또는 OOP 전도자들이 부르는 것을 위반하는 것이 아닐까요?

3.c) Entity Framework ObjectContext (개인, 차량, 자동차, 오토바이, 공공 행정)를 사용하는 Entity Framework ObjectContext가 이미 있다고 가정합니다. PublicAdministration.GetAllVehicles 메소드에서 ObjectContext 참조를 가져 와서 기능을 구현하려면 어떻게해야합니까?

3.d) CarTaxPayment.HasToPay에 대해 동일한 ObjectContext 참조가 필요하지만 "주입"을 책임지는 MotorbikeTaxPayment.HasToPay에 대해 다른 ObjectContext 참조가 필요한 경우 또는 해당 참조를 지불 클래스에 전달하는 방법은 무엇입니까? 이 경우 빈혈 도메인을 피하고 서비스 객체를 피하면 재앙으로 이어지지 않습니까?

이 간단한 시나리오를위한 디자인 선택은 무엇입니까? 내가 보여준 예제는 쉬운 작업을하기에는 너무 복잡하지만 동시에 아이디어는 SOLID 원칙을 준수하고 빈혈 영역 (파괴 명령)을 방지하는 것입니다.

답변:


15

개인이나 차량 모두 지불이 필요한지 알 수 없다고 말하고 싶습니다.

문제 영역 재검토

문제 영역 "자동차를 소유 한 사람들"을 보면 : 자동차를 구매하기 위해 한 사람에서 다른 사람으로 소유권을 이전하는 계약이 있습니다. 당신의 모델은 완전히 부족합니다.

생각 실험

결혼 한 부부가 있다고 가정하면 남자는 차를 소유하고 세금을 내고 있습니다. 이제 남자는 죽고 아내는 차를 물려 받고 세금을 내야합니다. 그러나, 당신의 판은 동일하게 유지 될 것입니다 (적어도 우리 나라에서는 그렇게 될 것입니다). 여전히 같은 차입니다! 도메인에서 모델링 할 수 있어야합니다.

=> 소유권

다른 사람이 소유하지 않은 자동차는 여전히 자동차이지만 아무도 세금을 내지 않습니다. 실제로, 당신은 자동차에 대한 세금을 지불하지 않고 자동차 를 소유하는 특권에 대해 지불 합니다. 내 제안은 다음과 같습니다.

public class Person
{
    public string Name { get; set; }

    public string Surname { get; set; }

    public List<Ownership> getOwnerships();

    public List<Vehicle> getVehiclesOwned();
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Ownership currentOwnership { get; set; }

}

public class Car : Vehicle {}

public class Motorbike : Vehicle {}

public abstract class Ownership
{
    public Ownership(Vehicle vehicle, Owner owner);

    public Vehicle Vehicle { get;}

    public Owner CurrentOwner { get; set; }

    public abstract bool HasToPay();

    public DateTime LastPaidTime { get; set; }

   //Could model things like payment history or owner history here as well
}

public class CarOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class MotorbikeOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public class PublicAdministration
{
    public IEnumerable<Ownership> GetVehiclesThatHaveToPay()
    {
        return this.GetAllOwnerships().Where(HasToPay());
    }

}

이것은 완벽하지는 않지만 원격으로 올바른 C # 코드는 아니지만 아이디어를 얻길 바랍니다. 유일한 단점은 CarOwnershipa Car와 a 사이에 올바른 것을 만들기 위해 공장이나 무언가가 필요할 것입니다 .Person


본인은 차량 자체에 지불 된 세금을 기록해야하며 개인은 모든 차량에 대해 지불 한 세금을 기록해야한다고 생각합니다. 자동차에 대한 세금이 6 월 1 일에 지불되어 6 월 10 일에 판매되는 경우 새 소유자가 6 월 10 일, 7 월 1 일 또는 7 월 10 일에 세금에 대해 책임을 질 수 있도록 세금 코드를 작성할 수 있습니다. 현재 변경 될 수있는 한 가지 방법). 소유권 변경을 통해 30 일 규칙이 자동차에 대해 일정하지만 아무도 납부하지 않은 자동차는 세금을 납부 할 수없는 경우, 30 일 이상 소유하지 않은 자동차를 구매할 경우, 세금 납부 ...
supercat

... 가상 "소유하지 않은 차량 세액 공제"사람으로부터 기록되어 판매 시간 기준으로 차량 소진 기간에 관계없이 세금 책임은 0 일 또는 30 일이됩니다.
supercat

4

비즈니스 규칙이 대상 개체 외부에 존재하는 이러한 상황에서는 유형 테스트가 허용 가능한 수준 이상이라고 생각합니다.

확실히, 많은 상황에서 전략 패턴을 사용하여 객체가 물건을 직접 처리하도록해야하지만 항상 바람직한 것은 아닙니다.

실제로는 점원이 차량 유형을보고이를 기반으로 세금 데이터를 조회합니다. 소프트웨어가 동일한 비즈니스 규칙을 따라야하는 이유는 무엇입니까?


자, 당신이 저를 설득하고 GetVehiclesThatHaveToPay 방법에서 우리는 차량 유형을 검사한다고 말합니다. LastPaidTime은 누가 책임 져야합니까? LastPaidTime은 차량 문제입니까 아니면 공공 관리입니까? 그것이 Vehicle 인 경우 비즈니스 로직이 Vehicle에 없어야합니까? 질문 3.c에 대해 무엇을 말씀해 주시겠습니까?

@DavidRobertJones-이상적으로는 차량 라이센스를 기반으로 지불 내역 로그에서 마지막 지불을 조회합니다. 세금 납부는 차량 문제가 아니라 세금 문제입니다.
Erik Funkenbusch

5
@DavidRobertJones 나는 당신이 자신의 은유와 혼동하고 있다고 생각합니다. 당신이 가진 것은 차량이하는 모든 일을해야하는 실제 차량이 아닙니다. 대신, 단순화를 위해 TaxableVehicle (또는 이와 유사한 것)을 Vehicle이라고 명명했습니다. 귀하의 전체 도메인은 세금 납부입니다. 실제 차량의 기능에 대해 걱정하지 마십시오. 그리고 필요한 경우 은유를 버리고 더 적절한 것을 찾으십시오.

3

내가 원하지 않는 것은 (생각합니다)

2.b) 빈곤 영역. 이는 사업체 내부의 비즈니스 논리를 의미한다.

당신은 이것을 거꾸로 가지고 있습니다. 빈혈 도메인은 로직이 비즈니스 엔티티 외부에 있는 도메인입니다 . 로직을 구현하기 위해 추가 클래스를 사용하는 것이 좋지 않은 이유는 많습니다. 왜해야하는지에 대한 몇 가지.

따라서 빈혈이라고 불리는 이유는 클래스에 아무것도 없다는 것입니다.

내가 할 것 :

  1. 추상 차량 클래스를 인터페이스로 변경하십시오.
  2. 차량이 구현해야하는 ITaxPayment 인터페이스를 추가하십시오. HasToPay를 끄십시오.
  3. MotorbikeTaxPayment, CarTaxPayment, TaxPayment 클래스를 제거하십시오. 불필요한 합병증 / 혼란입니다.
  4. 각 차량 유형 (자동차, 자전거, 모터 홈)은 최소 차량으로 구현해야하며 세금이 부과되는 경우 ITaxPayment도 구현해야합니다. 이렇게하면 세금이 부과되지 않는 차량과 그렇지 않은 차량을 구분할 수 있습니다.이 상황이 존재한다고 가정합니다.
  5. 각 차량 유형은 클래스 자체의 경계 내에서 인터페이스에 정의 된 메소드를 구현해야합니다.
  6. 또한 ITaxPayment 인터페이스에 마지막 지불 날짜를 추가하십시오.

네가 옳아. 원래 질문은 그런 식으로 공식화되지 않았으므로 부분을 삭제하고 의미가 변경되었습니다. 이제 수정되었습니다. 감사!

2

왜 차량이 지불해야하는지 알아야합니까? 공공 행정 문제가 아닙니까?

아니요. 이해했듯이 전체 앱은 과세에 관한 것입니다. 따라서 차량에 세금을 부과해야하는지에 대한 우려는 더 이상 공공 행정의 문제가 아니라 전체 프로그램의 다른 문제입니다. 여기 외에는 어디에도 넣을 이유가 없습니다.

public class Car : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class Motorbike : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

이 두 코드 스 니펫은 상수 만 다릅니다. 전체 HasToPay () 함수를 재정의하는 대신 함수를 재정의합니다 int DaysBetweenPayments(). 상수를 사용하는 대신 호출하십시오. 이것이 실제로 다른 전부라면 수업을 중단합니다.

또한 Motorbike / Car는 하위 클래스가 아닌 차량의 범주 속성이어야한다고 제안합니다. 그것은 당신에게 더 많은 유연성을 제공합니다. 예를 들어 오토바이 나 자동차의 범주를 쉽게 변경할 수 있습니다. 물론 자전거는 실제 생활에서는 자동차로 변신하지 않을 것입니다. 그러나 차량이 잘못 입력되어 나중에 변경해야한다는 것을 알고 있습니다.

물론, 우리는 그 가치를 TaxPayment의 생성자에게 전달할 수 있지만, 그것은 차량 캡슐화 나 책임, 또는 OOP 전도자들이 부르는 것을 위반하는 것이 아닐까요?

실제로는 아닙니다. 의도적으로 데이터를 다른 개체에 매개 변수로 전달하는 개체는 큰 캡슐화 문제가 아닙니다. 실제 관심사는 다른 객체가이 객체 내부에 도달하여 데이터를 꺼내거나 객체 내부의 데이터를 조작하는 것입니다.


1

당신은 올바른 길을 가고 있습니다. 문제는 알다시피 TaxPayment 내에 LastPaidTime 멤버가 없다는 것 입니다. 공개적으로 노출 될 필요는 없지만 정확히 어디에 속해 있습니다.

public interface ITaxPayment
{
    // Your base TaxPayment class will know the 
    // next due date. But you can easily create
    // one which uses (for example) fixed dates
    bool IsPaymentDue();
}

public interface IVehicle
{
    string Plate { get; }
    Person Owner { get; }
    ITaxPayment LastTaxPayment { get; }
} 

지불을받을 때마다 ITaxPayment현재 정책에 따라 새 인스턴스를 작성합니다. 이전 인스턴스와 완전히 다른 규칙을 가질 수 있습니다.


문제는 지불 기한이 소유자가 마지막으로 지불 한 시간 (다른 차량, 기한이 다름)에 달려 있다는 것입니다. ITaxPayment가 계산을 수행하기 위해 차량 (또는 소유자)이 마지막으로 지불 한 시간을 어떻게 알 수 있습니까?

1

@sebastiangeiger대답 은 문제가 실제로 차량이 아니라 차량 소유권에 관한 것입니다. 나는 그의 대답을 사용하도록 제안하지만 약간의 변화가 있습니다. Ownership클래스를 일반 클래스로 만들 것입니다 .

public class Vehicle {}

public abstract class Ownership<T>
{
    public T Item { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract TimeSpan PaymentInterval { get; }

    public virtual bool HasToPay
    {
        get { return (DateTime.Today - this.LastPaidTime) >= this.PaymentInterval; }
    }
}

상속을 사용하여 각 차량 유형에 대한 지불 간격을 지정하십시오. 또한 TimeSpan일반 정수 대신 강력한 형식의 클래스를 사용하는 것이 좋습니다 .

public class CarOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(30, 0, 0, 0); }
    }
}

public class MotorbikeOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(60, 0, 0, 0); }
    }
}

이제 실제 코드는 하나의 클래스 만 처리하면 Ownership<Vehicle>됩니다.

public class PublicAdministration
{
    List<Ownership<Vehicle>> VehicleOwnerships { get; set; }

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return VehicleOwnerships.Where(x => x.HasToPay).Select(x => x.Item);
    }
}

0

나는 당신에게 그것을 나누는 것을 싫어하지만 빈혈 영역 , 즉 객체 외부의 비즈니스 논리가 있습니다. 이것이 당신이 가고있는 일이라면 괜찮습니다. 그러나 피해야하는 이유에 대해 읽으십시오.

문제 영역을 모델링하지 말고 솔루션을 모델링하십시오. 그렇기 때문에 병렬 상속 계층 구조가 필요하고 필요한 것보다 더 복잡해집니다.

이 예제에 필요한 것은 다음과 같습니다.

public class Vehicle
{
    public Boolean isMotorBike()
    public string PlateNumber { get; set; }
    public String Owner { get; set; }
    public DateTime LastPaidTime { get; set; }
}

public class TaxPaymentCalculator
{
    public List<Vehicle> GetVehiclesThatHaveToPay(List<Vehicle> all)
}

훌륭한 SOLID를 따르고 싶지만 Bob 아저씨가 말한 것처럼 엔지니어링 원리 일뿐 입니다. 그것들은 엄격하게 적용되도록 의도 된 것이 아니라 고려해야 할 지침입니다.


4
귀하의 솔루션이 특별히 확장 가능하지 않다고 생각합니다. 추가 한 각 차량 유형에 대해 새 부울을 추가해야합니다. 내 의견으로는 그것은 좋지 않은 선택입니다.
Erik Funkenbusch

@Garrett Hall, "도메인"에서 Person 클래스를 제거하는 것이 좋았습니다. 나는 처음에는 그 수업을 갖지 않을 것이므로 죄송합니다. 그러나 isMotorBike는 의미가 없습니다. 구현은 어느 것입니까?

1
@DavidRobertJones : Mystere에 동의합니다. isMotorBike방법을 추가하는 것은 좋은 OOP 디자인 선택이 아닙니다. 다형성을 유형 코드로 대체하는 것과 같습니다 (부울 임에도 불구하고).
Groo

@MystereMan, 하나는 쉽게 bools를 삭제하고 사용할 수 있습니다enum Vehicles
radarbob

+1. 문제 영역을 모델링하지 말고 솔루션을 모델링하십시오 . 피티 . 분명히 말했다. 그리고 원칙은 논평합니다. 나는 엄격한 문자 해석에 대한 시도가 너무 많다는 것을 안다 . 무의미한 말.
radarbob

0

지불 기간이 실제 자동차 / 오토바이의 자산이 아니라고 생각합니다. 그러나 이것은 차량 등급의 속성입니다.

객체 유형에서 if / select를 갖는 길을 갈 수는 있지만 그렇게 확장 할 수는 없습니다. 나중에 트럭과 세그웨이를 추가하고 자전거와 버스 및 비행기를 밀면 (아마도 보이지 않을 수 있지만 공공 서비스를 모르는 경우) 상태가 더 커집니다. 트럭의 규칙을 변경하려면 해당 목록에서 규칙을 찾아야합니다.

문자 그대로 애트리뷰트를 만들고 리플렉션을 사용하여 가져 오는 것이 어떻습니까? 이 같은:

[DaysBetweenPayments(30)]
public class Car : Vehicle
{
    // actual properties of the physical vehicle
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class DaysBetweenPaymentsAttribute : Attribute, IPaymentRequiredCalculator
{
    private int _days;

    public DaysBetweenPaymentAttribute(int days)
    {
        _days = days;
    }

    public bool HasToPay(Vehicle vehicle)
    {
        return vehicle.LastPaid.AddDays(_days) <= DateTime.Now;
    }
}

더 편한 경우 정적 변수를 사용할 수도 있습니다.

단점은

  • 약간 느려질 것입니다. 많은 차량을 처리해야한다고 가정합니다. 그것이 문제라면 더 실용적인 관점을 취하고 추상 속성이나 인터페이스 방식을 고수 할 수 있습니다. (하지만 유형별로 캐싱을 고려할 것입니다.)

  • 시작시 각 차량 서브 클래스에 IPaymentRequiredCalculator 속성이 있거나 기본값을 허용하는지 확인해야합니다. 1000 대의 차량을 처리하지 않고 모터 사이클에 PaymentPeriod가 없기 때문에 충돌 만하기 때문입니다.

단점은 나중에 월 또는 연간 지불금을 만나면 새로운 속성 클래스를 작성할 수 있다는 것입니다. 이것은 OCP의 정의입니다.


이 문제의 주요 문제는 차량 지불 사이의 일수를 변경하려면 다시 빌드하고 재배치해야하며 지불 빈도가 엔티티의 속성 (예 : 엔진 크기)에 따라 달라진다는 것입니다. 그에 대한 우아한 수정을 얻기가 어렵습니다.
Ed James

@ EdWoodcock : 첫 번째 문제는 대부분의 솔루션에서 사실이며 실제로 걱정하지 않을 것이라고 생각합니다. 나중에 이러한 수준의 유연성을 원한다면 DB 앱을 제공하겠습니다. 두 번째로, 나는 완전히 동의하지 않습니다. 이 시점에서, 차량을 옵션 인수로 HasToPay ()에 추가하고 필요하지 않은 곳에서는 무시하십시오.
pdr

나는 그것이 존재하지 않는 경우 DB에 꽂을 가치가 있는지 여부와 관련하여 앱에 대한 장기 계획과 클라이언트의 특정 요구 사항에 달려 있다고 생각합니다 (나는 거의 모든 소프트웨어가 DB라고 가정하는 경향이 있습니다) 내가 항상 알고있는 것은 아닙니다. 그러나 두 번째 요점에서 중요한 한정자는 우아합니다 . 나는 속성에 연결된 클래스의 인스턴스를 취하는 속성의 메소드에 추가 매개 변수를 추가하는 것이 특히 우아한 해결책이라고 말하지 않을 것입니다. 그러나 다시, 그것은 고객의 요구 사항에 달려 있습니다.
Ed James

@ Edwoodcock : 사실, 나는 이것에 대해 생각하고 있었고 lastPaid 인수는 이미 Vehicle의 속성이어야합니다. 귀하의 의견을 염두에두고 나중에이 아닌 지금 인수를 대체 할 가치가 있습니다. 편집하십시오.
pdr
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.