C#의 팩토리 패턴:객체 인스턴스가 팩토리 클래스에서만 생성될 수 있도록 하는 방법은 무엇입니까?
-
21-08-2019 - |
문제
최근에 나는 내 코드의 일부를 보호하는 것에 대해 생각하고 있습니다.객체를 직접 생성할 수 없고 팩토리 클래스의 일부 메서드를 통해서만 생성할 수 있는지 어떻게 확인할 수 있는지 궁금합니다."비즈니스 개체" 클래스가 있고 이 클래스의 모든 인스턴스가 유효한 내부 상태를 갖는지 확인하고 싶다고 가정해 보겠습니다.이를 달성하려면 객체를 생성하기 전에 생성자에서 몇 가지 검사를 수행해야 합니다.이 수표를 비즈니스 로직의 일부로 만들기로 결정하기 전까지는 괜찮습니다.그렇다면 비즈니스 로직 클래스의 일부 메소드를 통해서만 비즈니스 객체를 생성할 수 있고 직접적으로는 생성할 수 없도록 하려면 어떻게 해야 합니까?C++의 오래된 "친구" 키워드를 사용하려는 첫 번째 자연스러운 욕구는 C#에서는 부족할 것입니다.그래서 다른 옵션이 필요합니다...
몇 가지 예를 들어보겠습니다:
public MyBusinessObjectClass
{
public string MyProperty { get; private set; }
public MyBusinessObjectClass (string myProperty)
{
MyProperty = myProperty;
}
}
public MyBusinessLogicClass
{
public MyBusinessObjectClass CreateBusinessObject (string myProperty)
{
// Perform some check on myProperty
if (true /* check is okay */)
return new MyBusinessObjectClass (myProperty);
return null;
}
}
입력을 확인하지 않고도 MyBusinessObjectClass 인스턴스를 직접 생성할 수 있다는 점을 기억하기 전까지는 아무 문제가 없습니다.나는 그러한 기술적 가능성을 완전히 배제하고 싶습니다.
그렇다면 커뮤니티에서는 이에 대해 어떻게 생각하나요?
해결책
객체를 만들기 전에 비즈니스 로직을 실행하고 싶을 것 같습니다. 왜 더러운 "MyProperty"확인 작업을 수행하는 "BusinessClass"내부에 정적 메소드를 만들고 생성자를 비공개로 만드는 이유는 무엇입니까?
public BusinessClass
{
public string MyProperty { get; private set; }
private BusinessClass()
{
}
private BusinessClass(string myProperty)
{
MyProperty = myProperty;
}
public static BusinessClass CreateObject(string myProperty)
{
// Perform some check on myProperty
if (/* all ok */)
return new BusinessClass(myProperty);
return null;
}
}
그것을 부르는 것은 매우 간단 할 것입니다.
BusinessClass objBusiness = BusinessClass.CreateObject(someProperty);
다른 팁
생성자를 비공개로 만들 수 있고 공장을 중첩 된 유형으로 만들 수 있습니다.
public class BusinessObject
{
private BusinessObject(string property)
{
}
public class Factory
{
public static BusinessObject CreateBusinessObject(string property)
{
return new BusinessObject(property);
}
}
}
중첩 된 유형은 동봉 유형의 개인 구성원에게 액세스 할 수 있기 때문에 작동합니다. 조금 제한적이라는 것을 알고 있지만 도움이되기를 바랍니다.
또는, 당신이 정말로 화려하게 가고 싶다면, 반전 통제 : 클래스가 공장을 반환하고, 수업을 만들 수있는 대의원과 함께 공장을 기기로 반환하도록하십시오.
public class BusinessObject
{
public static BusinessObjectFactory GetFactory()
{
return new BusinessObjectFactory (p => new BusinessObject (p));
}
private BusinessObject(string property)
{
}
}
public class BusinessObjectFactory
{
private Func<string, BusinessObject> _ctorCaller;
public BusinessObjectFactory (Func<string, BusinessObject> ctorCaller)
{
_ctorCaller = ctorCaller;
}
public BusinessObject CreateBusinessObject(string myProperty)
{
if (...)
return _ctorCaller (myProperty);
else
return null;
}
}
:)
MyBusinessObjectClass 클래스 내부에서 생성자를 만들어서 공장과 공장을 자신의 조립으로 옮길 수 있습니다. 이제 공장만이 클래스의 인스턴스를 구성 할 수 있어야합니다.
Jon이 제안한 것 외에도 공장 방법 (수표 포함)을 처음부터 정적 인 BusinessObject의 정적 방법이 될 수도 있습니다. 그런 다음 생성자를 비공개로두면 다른 모든 사람들이 정적 방법을 사용해야합니다.
public class BusinessObject
{
public static Create (string myProperty)
{
if (...)
return new BusinessObject (myProperty);
else
return null;
}
}
그러나 실제 질문은 - 왜이 요구 사항이 있습니까? 공장이나 공장 방법을 클래스로 옮기는 것이 허용됩니까?
또 다른 (가벼운) 옵션은 BusinessObject 클래스에서 정적 공장 메소드를 만들고 생성자를 비공개로 유지하는 것입니다.
public class BusinessObject
{
public static BusinessObject NewBusinessObject(string property)
{
return new BusinessObject();
}
private BusinessObject()
{
}
}
오랜 세월이 흐른 후 이 질문을 받게 되었고, 제가 본 모든 답변은 불행히도 직접적인 답변을 제공하는 대신 코드를 어떻게 수행해야 하는지 알려주는 것이었습니다.당신이 찾고 있던 실제 대답은 클래스에 비공개 생성자를 사용하고 공개 인스턴스화기를 사용하는 것입니다. 즉, 다른 기존 인스턴스에서만 새 인스턴스를 만들 수 있다는 의미입니다.공장에서만 사용할 수 있는 것:
수업을 위한 인터페이스:
public interface FactoryObject
{
FactoryObject Instantiate();
}
귀하의 수업:
public class YourClass : FactoryObject
{
static YourClass()
{
Factory.RegisterType(new YourClass());
}
private YourClass() {}
FactoryObject FactoryObject.Instantiate()
{
return new YourClass();
}
}
그리고 마지막으로 공장은 다음과 같습니다.
public static class Factory
{
private static List<FactoryObject> knownObjects = new List<FactoryObject>();
public static void RegisterType(FactoryObject obj)
{
knownObjects.Add(obj);
}
public static T Instantiate<T>() where T : FactoryObject
{
var knownObject = knownObjects.Where(x => x.GetType() == typeof(T));
return (T)knownObject.Instantiate();
}
}
그런 다음 인스턴스화를 위해 추가 매개변수가 필요하거나 생성한 인스턴스를 전처리하기 위해 이 코드를 쉽게 수정할 수 있습니다.그리고 이 코드를 사용하면 클래스 생성자가 비공개이므로 팩토리를 통해 인스턴스화를 강제로 수행할 수 있습니다.
따라서 내가 원하는 것은 "순수한"방식으로 할 수없는 것처럼 보입니다. 그것은 항상 논리 클래스에 어떤 종류의 "호출"입니다.
어쩌면 간단한 방식으로 할 수 있습니다. 객체 클래스에서 대조적 메소드를 작성하여 먼저 로직 클래스를 호출하여 입력을 확인하십시오.
public MyBusinessObjectClass
{
public string MyProperty { get; private set; }
private MyBusinessObjectClass (string myProperty)
{
MyProperty = myProperty;
}
pubilc static MyBusinessObjectClass CreateInstance (string myProperty)
{
if (MyBusinessLogicClass.ValidateBusinessObject (myProperty)) return new MyBusinessObjectClass (myProperty);
return null;
}
}
public MyBusinessLogicClass
{
public static bool ValidateBusinessObject (string myProperty)
{
// Perform some check on myProperty
return CheckResult;
}
}
이런 식으로 비즈니스 객체는 직접 생성되지 않으며 비즈니스 로직의 공개 점검 방법은 해를 끼치 지 않습니다.
인터페이스와 구현 사이의 좋은 분리의 경우
보호 된-건설자-공개-이니티얼 라이저 패턴은 매우 깔끔한 솔루션을 허용합니다.
비즈니스 개체가 주어진 :
public interface IBusinessObject { }
class BusinessObject : IBusinessObject
{
public static IBusinessObject New()
{
return new BusinessObject();
}
protected BusinessObject()
{ ... }
}
그리고 비즈니스 공장 :
public interface IBusinessFactory { }
class BusinessFactory : IBusinessFactory
{
public static IBusinessFactory New()
{
return new BusinessFactory();
}
protected BusinessFactory()
{ ... }
}
다음과 같은 변경 BusinessObject.New()
이니셜 라이저는 솔루션을 제공합니다.
class BusinessObject : IBusinessObject
{
public static IBusinessObject New(BusinessFactory factory)
{ ... }
...
}
여기서는 콘크리트 비즈니스 공장에 대한 참조가 BusinessObject.New()
이니셜 라이저. 그러나 필요한 참조를 가진 유일한 사람은 비즈니스 공장 자체입니다.
우리는 우리가 원하는 것을 얻었습니다 : BusinessObject
~이다 BusinessFactory
.
public class HandlerFactory: Handler
{
public IHandler GetHandler()
{
return base.CreateMe();
}
}
public interface IHandler
{
void DoWork();
}
public class Handler : IHandler
{
public void DoWork()
{
Console.WriteLine("hander doing work");
}
protected IHandler CreateMe()
{
return new Handler();
}
protected Handler(){}
}
public static void Main(string[] args)
{
// Handler handler = new Handler(); - this will error out!
var factory = new HandlerFactory();
var handler = factory.GetHandler();
handler.DoWork(); // this works!
}
공장을 도메인 클래스와 같은 어셈블리에 넣고 도메인 클래스의 생성자 내부를 표시합니다. 이런 식으로 도메인의 모든 클래스는 인스턴스를 생성 할 수 있지만 자신을 신뢰하지 않습니까? 도메인 레이어 외부에 코드를 작성하는 사람은 공장을 사용해야합니다.
public class Person
{
internal Person()
{
}
}
public class PersonFactory
{
public Person Create()
{
return new Person();
}
}
그러나 나는 당신의 접근 방식에 의문을 제기해야합니다 :-)
나는 당신이 당신의 사람 클래스가 생성시 유효하기를 원한다면, 당신은 코드를 생성자에 넣어야한다고 생각합니다.
public class Person
{
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
Validate();
}
}
이 솔루션은 기반입니다 munificents 생성자에 토큰을 사용한다는 아이디어. 이 답변에서 완료되었습니다 공장에서만 생성 된 개체 (C#)
public class BusinessObject
{
public BusinessObject(object instantiator)
{
if (instantiator.GetType() != typeof(Factory))
throw new ArgumentException("Instantiator class must be Factory");
}
}
public class Factory
{
public BusinessObject CreateBusinessObject()
{
return new BusinessObject(this);
}
}
다른 트레이드 오프가있는 여러 접근법이 언급되었습니다.
- 개인 구성 클래스에서 공장 수업을 중첩하면 공장이 1 클래스를 구성 할 수 있습니다. 그 시점에서 당신은 a로 더 좋습니다
Create
방법 및 개인 CTOR. - 상속 및 보호 된 CTOR를 사용하면 동일한 문제가 있습니다.
공개 생성자가있는 개인 중첩 클래스가 포함 된 부분 클래스로 공장을 제안하고 싶습니다. 당신은 당신의 공장이 구성하고있는 객체를 100% 숨기고 있고 하나 또는 여러 인터페이스를 통해 선택한 것을 노출시키는 것만을 숨기고 있습니다.
내가 들었던 유스 케이스는 공장에서 인스턴스의 100%를 추적하고 싶을 때입니다. 이 설계는 아무도 보장하지 않지만 공장은 "공장"에 정의 된 "화학 물질"인스턴스를 만들 수 있으며이를 달성하기 위해 별도의 어셈블리가 필요하지 않습니다.
== ChemicalFactory.cs ==
partial class ChemicalFactory {
private ChemicalFactory() {}
public interface IChemical {
int AtomicNumber { get; }
}
public static IChemical CreateOxygen() {
return new Oxygen();
}
}
== Oxygen.cs ==
partial class ChemicalFactory {
private class Oxygen : IChemical {
public Oxygen() {
AtomicNumber = 8;
}
public int AtomicNumber { get; }
}
}
== Program.cs ==
class Program {
static void Main(string[] args) {
var ox = ChemicalFactory.CreateOxygen();
Console.WriteLine(ox.AtomicNumber);
}
}
왜 "비즈니스 로직"을 "비즈니스 객체"와 분리하려는 이유를 이해하지 못합니다. 이것은 객체 방향의 왜곡처럼 들리며, 그 접근 방식을 취함으로써 매듭으로 자신을 묶게됩니다.
나는 문제보다 더 나쁘지 않은 솔루션이 있다고 생각하지 않습니다. 위의 모든 사람들은 IMHO가 더 나쁜 문제이며 사람들이 공장에 전화를 걸어 객체를 사용하는 것을 막지 않는 공개 정적 공장을 요구합니다. 아무것도 숨기지 않습니다. 어셈블리가 신뢰할 수있는 코드이기 때문에 최상의 보호 장치 인 경우 인터페이스를 노출하거나 생성자를 내부로 유지하는 것이 가장 좋습니다.
한 가지 옵션은 IOC 컨테이너와 같은 어딘가에 공장을 등록하는 정적 생성자가있는 것입니다.
다음은 "당신이해야한다는 의미는 아니기 때문에"라는 정맥의 또 다른 해결책입니다.
비즈니스 객체 생성자를 개인을 비공개로 유지하고 다른 클래스에 공장 논리를 넣는 요구 사항을 충족합니다. 그 후 약간 스케치됩니다.
공장 클래스에는 비즈니스 객체를 만드는 정적 방법이 있습니다. 개인 생성자를 호출하는 정적 보호 구조 방법에 액세스하기 위해 비즈니스 객체 클래스에서 파생됩니다.
공장은 추상적이므로 실제로 인스턴스를 만들 수 없기 때문에 (비즈니스 객체 가기 때문에 이상 할 수 있기 때문에) 개인 생성자가 있으므로 클라이언트 코드가이를 도출 할 수 없습니다.
예방되지 않은 것은 클라이언트 코드입니다 또한 비즈니스 객체 클래스에서 파생되고 보호 된 (그러나 검증되지 않은) 정적 구성 방법을 호출합니다. 또는 더 나쁜 것은 보호 된 기본 생성자를 호출하여 공장 클래스를 처음에 컴파일하기 위해 추가해야했습니다. (우연히 공장 클래스를 비즈니스 객체 클래스와 분리하는 모든 패턴에 문제가 될 수 있습니다.)
나는 올바른 마음을 가진 사람이 이런 일을해야한다고 제안하려고하지는 않지만 흥미로운 운동이었습니다. FWIW, 내가 선호하는 솔루션은 내부 생성자와 어셈블리 경계를 가드로 사용하는 것입니다.
using System;
public class MyBusinessObjectClass
{
public string MyProperty { get; private set; }
private MyBusinessObjectClass(string myProperty)
{
MyProperty = myProperty;
}
// Need accesible default constructor, or else MyBusinessObjectFactory declaration will generate:
// error CS0122: 'MyBusinessObjectClass.MyBusinessObjectClass(string)' is inaccessible due to its protection level
protected MyBusinessObjectClass()
{
}
protected static MyBusinessObjectClass Construct(string myProperty)
{
return new MyBusinessObjectClass(myProperty);
}
}
public abstract class MyBusinessObjectFactory : MyBusinessObjectClass
{
public static MyBusinessObjectClass CreateBusinessObject(string myProperty)
{
// Perform some check on myProperty
if (true /* check is okay */)
return Construct(myProperty);
return null;
}
private MyBusinessObjectFactory()
{
}
}
이 솔루션에 대한 몇 가지 생각을 듣는 것에 감사 할 것입니다. 'MyClassPrivilegekey'를 만들 수있는 유일한 사람은 공장입니다. 그리고 'myclass'는 생성자에 그것을 요구합니다. 따라서 공장에 대한 개인 계약자 / "등록"에 대한 반영을 피합니다.
public static class Runnable
{
public static void Run()
{
MyClass myClass = MyClassPrivilegeKey.MyClassFactory.GetInstance();
}
}
public abstract class MyClass
{
public MyClass(MyClassPrivilegeKey key) { }
}
public class MyClassA : MyClass
{
public MyClassA(MyClassPrivilegeKey key) : base(key) { }
}
public class MyClassB : MyClass
{
public MyClassB(MyClassPrivilegeKey key) : base(key) { }
}
public class MyClassPrivilegeKey
{
private MyClassPrivilegeKey()
{
}
public static class MyClassFactory
{
private static MyClassPrivilegeKey key = new MyClassPrivilegeKey();
public static MyClass GetInstance()
{
if (/* some things == */true)
{
return new MyClassA(key);
}
else
{
return new MyClassB(key);
}
}
}
}