OOP 어플리케이션의 파라미터 관리


15

OOP 원칙을 연습하는 방법으로 C ++로 중간 크기의 OOP 응용 프로그램을 작성하고 있습니다.

프로젝트에 여러 클래스가 있으며 그 중 일부는 런타임 구성 매개 변수에 액세스해야합니다. 이 매개 변수는 응용 프로그램 시작 중 여러 소스에서 읽습니다. 일부는 사용자 home-dir의 구성 파일에서 읽고 일부는 명령 행 인수 (argv)입니다.

그래서 나는 수업을 만들었습니다 ConfigBlock. 이 클래스는 모든 매개 변수 소스를 읽고 적절한 데이터 구조에 저장합니다. 예는 구성 파일 또는 --verbose CLI 플래그에서 사용자가 변경할 수있는 경로 및 파일 이름입니다. 그런 다음 ConfigBlock.GetVerboseLevel()이 특정 매개 변수를 읽기 위해 호출 할 수 있습니다 .

내 질문 : 모든 런타임 구성 데이터를 한 클래스로 수집하는 것이 좋습니다?

그런 다음 수업에서이 모든 매개 변수에 액세스해야합니다. 나는 이것을 달성하기 위해 여러 가지 방법을 생각할 수 있지만 어느 것을 취할 지 잘 모르겠습니다. 클래스의 생성자는 내 ConfigBlock에 대한 참조를 제공받을 수 있습니다.

public:
    MyGreatClass(ConfigBlock &config);

또는 내 CodingBlock 정의가 포함 된 "CodingBlock.h"헤더 만 포함합니다.

extern CodingBlock MyCodingBlock;

그런 다음 클래스 .cpp 파일 만 ConfigBlock 항목을 포함하고 사용해야합니다.
.h 파일은이 인터페이스를 클래스 사용자에게 소개하지 않습니다. 그러나 ConfigBlock에 대한 인터페이스는 여전히 존재하지만 .h 파일에서 숨겨져 있습니다.

이런 식으로 숨기는 것이 좋습니까?

인터페이스가 가능한 한 작기를 원하지만 결국에는 구성 매개 변수가 필요한 모든 클래스가 내 ConfigBlock에 연결되어 있어야합니다. 그러나이 연결은 어떻게 생겼습니까?

답변:


10

나는 실용 주의자이지만, 여기서 나의 주요 관심사는 ConfigBlock인터페이스 디자인이 나쁜 방식으로 지배 할 수 있다는 것입니다. 다음과 같은 것이있을 때 :

explicit MyGreatClass(const ConfigBlock& config);

...보다 적절한 인터페이스는 다음과 같습니다.

MyGreatClass(int foo, float bar, const string& baz);

...이 foo/bar/baz분야에서 체리를 따는 것과는 반대로 ConfigBlock.

게으른 인터페이스 디자인

더하기 측면에서 디자인의이 종류는 새로운 무언가를 필요로 끝날 경우, 당신은 단지에 그것을로드 할 수 있기 때문에 쉽게, 예를 들면 생성자, 안정된 인터페이스를 디자인 할 수 있습니다 ConfigBlock(아마도 코드를 변경하지 않고) 다음 cherry- 인터페이스를 변경하지 않고 필요한 새로운 것을 선택하십시오. 구현 구현 만 변경하십시오 MyGreatClass.

따라서 실제로 필요한 입력 만 받아들이는보다 신중하게 생각할 수있는 인터페이스를 설계 할 수있는 장점이 있습니다. "정확한 매개 변수는이 인터페이스가 작동 하는 데 필요한 것" 과는 반대로 "이 방대한 양의 데이터를 제공하고 필요한 데이터를 선택하겠습니다" 라는 사고 방식을 적용합니다 .

따라서 여기에는 분명히 몇 가지 장점이 있지만 단점으로 인해 훨씬 ​​더 중요 할 수 있습니다.

커플 링

이 시나리오에서 ConfigBlock인스턴스 에서 생성되는 모든 클래스는 종속성이 다음과 같이 표시됩니다.

여기에 이미지 설명을 입력하십시오

예를 들어이 Class2다이어그램에서 단위 테스트 를 개별적으로 수행하려는 경우 PITA가 될 수 있습니다 . 다양한 조건에서 테스트 할 수 있도록 ConfigBlock관련 필드 Class2가 포함 된 다양한 입력 을 표면적으로 시뮬레이션해야 할 수도 있습니다 .

모든 종류의 새로운 맥락에서 (단위 테스트 또는 완전히 새로운 프로젝트이든), 그러한 클래스는 우리가 항상 ConfigBlock타기를 위해 가져 와서 설정해야하므로 결국 재사용에 대한 부담이 더 커질 수 있습니다. 따라서.

재사용 성 / 배포성 / 시험 성

대신 이러한 인터페이스를 적절하게 디자인하면 다음과 같은 인터페이스를 분리 할 수 ​​있습니다 ConfigBlock.

여기에 이미지 설명을 입력하십시오

이 다이어그램에서 알 수 있듯이 모든 클래스는 독립적이됩니다 (구심 / 발신 커플 링은 1 씩 줄어 듭니다).

이로 인해 ConfigBlock새로운 시나리오 / 프로젝트에서 (재) 사용 / 테스트가 훨씬 쉬워 질 수 있는 훨씬 더 독립적 인 클래스 (적어도 독립적 )가 생깁니다 .

이제이 Client코드는 모든 것에 의존하고 함께 모아야하는 코드가됩니다. 부담은이 클라이언트 코드로 전송되어 a에서 적절한 필드를 읽고 ConfigBlock이를 매개 변수로 적절한 클래스에 전달합니다. 그러나 이러한 클라이언트 코드는 일반적으로 특정 상황에 맞게 좁게 설계되었으며 재사용 가능성은 일반적으로 어지럽거나 닫힙니다 (응용 프로그램의 main진입 점 기능 또는 이와 유사한 것일 수 있음).

따라서 재사용 성과 테스트 관점에서 이러한 클래스를보다 독립적으로 만드는 데 도움이 될 수 있습니다. 클래스를 사용하는 사람들을위한 인터페이스 관점에서 ConfigBlock모든 것에 필요한 전체 데이터 필드 세계를 모델링하는 하나의 방대한 매개 변수 대신 필요한 매개 변수를 명시 적으로 설명하는 데 도움이 될 수 있습니다 .

결론

일반적으로, 필요한 모든 것을 갖는 단일체에 의존하는 이러한 종류의 클래스 지향 디자인은 이러한 종류의 특성을 갖는 경향이있다. 결과적으로 적용 가능성, 배포 가능성, 재사용 가능성, 테스트 가능성 등이 크게 저하 될 수 있습니다. 그러나 긍정적 인 스핀을 시도하면 인터페이스 디자인을 단순화 할 수 있습니다. 이러한 장단점을 측정하고 타협점의 가치가 있는지 결정하는 것은 귀하의 몫입니다. 일반적으로 더 일반적이고 널리 적용되는 디자인을 모델링하려는 클래스의 모놀리스에서 체리 피킹하는 경우 이러한 종류의 디자인에 대해 실수하는 것이 훨씬 안전합니다.

마지막으로 :

extern CodingBlock MyCodingBlock;

...이 클래스는 클래스뿐만 아니라 클래스 ConfigBlocks특정 인스턴스 에 직접 연결되기 때문에 종속성 주입 방식보다 위에서 설명한 특성면에서 훨씬 더 나빠질 수 있습니다. 이는 적용 가능성 / 배포 가능성 / 테스트 가능성을 더욱 떨어 뜨립니다.

필자의 일반적인 조언은 적어도 당신이 디자인하는 가장 일반적으로 적용 가능한 클래스에 대한 매개 변수를 제공하기 위해 이러한 종류의 모 놀리 식에 의존하지 않는 인터페이스 디자인 측면에서 실수하는 것입니다. 피할 수없는 매우 강력하고 자신감있는 이유가 없다면 가능하지 않으면 의존성 주입없이 전역적인 접근 방식을 피하십시오.


1

일반적으로 응용 프로그램 구성은 기본적으로 팩토리 객체에서 사용됩니다. 구성에 의존하는 모든 객체는 해당 팩토리 객체 중 하나에서 생성해야합니다. 당신은 활용할 수있는 추상 팩토리 패턴 전체에 걸리는 하나 개의 클래스 구현하는 ConfigBlock객체. 이 클래스는 공용 메소드를 노출시켜 다른 팩토리 오브젝트를 리턴하고 ConfigBlock해당 팩토리 오브젝트와 관련된 부분 만 전달 합니다. 이렇게하면 구성 설정이 ConfigBlock객체에서 구성원으로, 팩토리 팩토리에서 팩토리로 "중첩"됩니다 .

언어를 더 잘 알고 있기 때문에 C #을 사용하지만 C ++로 쉽게 옮길 수 있습니다.

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

그런 다음 기본 절차에서 인스턴스화되는 "응용 프로그램"역할을하는 일종의 클래스가 필요합니다.

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

마지막으로 .NET에서 "제공자 개체"라고합니다. .NET의 공급자 객체는 구성 데이터를 팩토리 객체와 결합시키는 것 같습니다.

초보자를위한 제공자 패턴 도 참조하십시오 . 다시 말하지만 이것은 .NET 개발에 맞춰져 있지만 C #과 C ++가 모두 객체 지향 언어이므로 패턴은 주로 두 언어 사이에서 전송 될 수 있어야합니다.

이 패턴과 관련된 또 다른 좋은 읽기 : 제공자 모델 .

마지막으로,이 패턴에 대한 비판 : 제공자는 패턴이 아닙니다


공급자 모델에 대한 링크를 제외한 모든 것이 좋습니다. 리플렉션은 C ++에서 지원되지 않으므로 작동하지 않습니다.
BЈовић

@ BЈовић : 맞습니다. 클래스 리플렉션은 존재하지 않지만 수동 해결 방법으로 빌드 할 수 있습니다. 수동 해결 방법은 기본적으로 구성 파일에서 읽은 값에 대해 switch명령문 또는 if명령문 테스트로 진행됩니다.
Greg Burghardt

0

첫 번째 질문 : 모든 런타임 구성 데이터를 하나의 클래스로 수집하는 것이 좋습니다?

예. 런타임 상수 및 값과이를 읽는 코드를 중앙 집중화하는 것이 좋습니다.

클래스의 생성자는 내 ConfigBlock에 대한 참조가 주어질 수 있습니다.

이것은 나쁘다 : 대부분의 생성자는 대부분의 값을 필요로하지 않을 것이다. 대신, 구성하기 쉽지 않은 모든 것에 대한 인터페이스를 작성하십시오.

이전 코드 (귀하의 제안) :

MyGreatClass(ConfigBlock &config);

새로운 코드 :

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

MyGreatClass를 인스턴스화하십시오.

auto x = MyGreatClass{ current_config_block.great_class_values() };

여기, 클래스 current_config_block의 인스턴스 ConfigBlock(모든 값을 포함 MyGreatClass하는 GreatClassData인스턴스 )가 있고 클래스는 인스턴스를 받습니다 . 즉, 필요한 데이터 만 생성자에게 전달하고 ConfigBlock해당 데이터를 작성하는 기능을 추가하십시오 .

또는 내 CodingBlock 정의가 포함 된 "CodingBlock.h"헤더 만 포함합니다.

 extern CodingBlock MyCodingBlock;

그런 다음 클래스 .cpp 파일 만 ConfigBlock 항목을 포함하고 사용해야합니다. .h 파일은이 인터페이스를 클래스 사용자에게 소개하지 않습니다. 그러나 ConfigBlock에 대한 인터페이스는 여전히 존재하지만 .h 파일에서 숨겨져 있습니다. 이런 식으로 숨기는 것이 좋습니까?

이 코드는 전역 CodingBlock 인스턴스가 있음을 나타냅니다. 그렇게하지 마십시오 : 일반적으로 응용 프로그램이 사용하는 진입 점 (main 함수, DllMain 등)에서 전역으로 인스턴스를 선언하고 필요한 곳 ​​어디에서나 인수로 전달해야합니다 (그러나 위에서 설명한 것처럼 전달해서는 안됩니다) 전체 클래스 주변에서 데이터 주변의 인터페이스를 노출하고 전달하십시오.)

또한 클라이언트 클래스 (your MyGreatClass)를 CodingBlock; 형식으로 묶지 마십시오 . 즉, MyGreatClass문자열과 5 개의 정수를 사용하는 경우을 전달하는 것보다 해당 문자열과 정수를 전달하는 것이 CodingBlock좋습니다.


팩토리를 구성에서 분리하는 것이 좋습니다. 구성 구현에서 구성 요소를 인스턴스화하는 방법을 알아야한다는 것은 불만족 스럽습니다. 이전에는 단방향 종속성 만 존재했던 경우 양방향 종속성이 필요하기 때문입니다. 이는 인터페이스가 중요한 공유 라이브러리를 사용할 때 코드를 확장 할 때 큰 영향을 미칩니다.
Joel Cornett

0

짧은 답변:

당신은 하지 않는 코드에서 모듈 / 클래스 각각에 대한 모든 설정을해야합니다. 그렇다면 개체 지향 디자인에 문제가있는 것입니다. 특히 단위 테스트의 경우 필요하지 않은 모든 변수를 설정하고 해당 객체를 전달하면 읽거나 유지 관리하는 데 도움이되지 않습니다.


이렇게하면 하나의 중앙 위치에서 파서 코드 (구문 명령 줄 및 구성 파일)를 수집 할 수 있습니다. 그런 다음 각 클래스는 관련 매개 변수를 거기에서 선택할 수 있습니다. 당신의 의견으로는 좋은 디자인은 무엇입니까?
lugge86

어쩌면 방금 잘못 썼습니다-구성 파일 / 환경 변수에서 얻은 모든 설정으로 ConfigBlock클래스를 추상화 할 수있는 일반적인 추상화를해야한다는 것을 의미합니다 . 여기서 중요한 점은이 경우 시스템 상태의 컨텍스트, 특히 필요한 특정 값을 제공하지 않는 것입니다.
Dawid Pura
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.