비동기 함수를 노출하는 인터페이스가 누출 추상입니까?


13

나는 Dependency Injection Principles, Practices and Patterns 책을 읽고 있으며,이 책 에 잘 설명되어있는 누출 추상화 개념에 대해 읽었습니다.

요즘 나는 의존성 주입을 사용하여 C # 코드베이스를 리팩토링하여 비동기 호출이 차단 호출 대신 사용되도록합니다. 그렇게하면 코드베이스에서 추상화를 나타내는 일부 인터페이스를 고려하고 비동기 호출을 사용할 수 있도록 다시 디자인해야합니다.

예를 들어, 애플리케이션 사용자의 저장소를 나타내는 다음 인터페이스를 고려하십시오.

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

이 책의 정의에 따르면, 누출 추상화 는 특정 구현을 염두에두고 설계된 추상화로, 일부 구현 세부 사항은 추상화 자체를 통해 "누설"됩니다.

내 질문은 다음과 같습니다. Leaky Abstraction의 예로 IUserRepository와 같은 비동기를 염두에두고 설계된 인터페이스를 고려할 수 있습니까?

물론 모든 가능한 구현이 비동기와 관련이있는 것은 아닙니다 : SQL 구현과 같은 프로세스 외부 구현 만 수행하지만 인 메모리 저장소에는 비동기가 필요하지 않습니다 (실제로 메모리 버전의 인터페이스를 구현하는 것이 더 많을 것입니다) 인터페이스가 비동기 메소드를 노출하는 경우 어려울 수 있습니다. 예를 들어 메소드 구현에서 Task.CompletedTask 또는 Task.FromResult (users)와 같은 것을 리턴해야합니다.

그것에 대해 어떻게 생각하십니까?


@Neil 아마도 요점을 알 것입니다. Task 또는 Task <T>를 반환하는 메서드를 노출하는 인터페이스는 누출 자체가 아니며 단순히 작업과 관련된 서명과의 계약입니다. Task 또는 Task <T>를 반환하는 메서드에는 비동기 구현이 포함되어 있지 않습니다 (예 : Task.CompletedTask를 사용하여 완료된 작업을 만드는 경우 비동기 구현을 수행하지 않음). 반대로 C #의 비동기 구현에서는 비동기 메서드의 반환 형식이 Task 또는 Task <T> 형식이어야합니다. 다른 방법으로 내 인터페이스의 유일한 "누수"측면은 이름의 비동기 접미사입니다.
Enrico Massone

@Neil은 실제로 모든 비동기 메소드의 이름이 "Async"로 끝나야한다는 이름 지정 지침이 있습니다. 그러나 이것은 Task 또는 Task <T>를 리턴하는 메소드가 비동기 호출을 사용하지 않고 구현 될 수 있으므로 비동기 접미어로 이름을 지정해야 함을 의미하지는 않습니다.
Enrico Massone

6
나는 메소드의 '비동기 성'이 a를 반환한다는 사실에 의해 주장된다고 주장한다 Task. async라는 단어를 사용하여 비동기 메서드 접미사에 대한 지침은 다른 동일한 API 호출을 구별하는 것입니다 (C #은 반환 유형에 따라 디스패치 할 수 없음). 우리 회사에서 우리는 그것을 모두 떨어 뜨 렸습니다.
richzilla

메소드의 비동기 특성이 추상화의 일부인 이유를 설명하는 많은 답변과 의견이 있습니다. 더 흥미로운 질문은 언어 또는 프로그래밍 API가 메소드의 기능을 실행 방법과 더 이상 태스크 리턴 값 또는 비동기 마커가 더 이상 필요하지 않은 지점까지 분리하는 방법입니다. 함수형 프로그래밍 사람들은 이것을 더 잘 파악한 것 같습니다. 비동기 메소드가 F # 및 기타 언어로 어떻게 정의되는지 고려하십시오.
Frank Hileman

2
:-)-> "기능 프로그래밍 사람들"ha. 비동기는 동기식보다 더 새롭지 않습니다. 기본적으로 동기화 코드를 작성하는 데 익숙하기 때문에 그렇게 보입니다. 기본적으로 모두 비동기로 코딩하면 동기 함수가 누출 된 것처럼 보일 수 있습니다.
StarTrekRedneck

답변:


8

물론 누설 추상화법칙을 부를 수는 있지만 모든 추상화가 누설된다는 점에서 특히 흥미롭지는 않습니다. 우리는 그 추측을 반대 할 수 있지만 , 추상화 에 의해 우리가 의미하는 것과 누설에 의해 의미하는 것을 이해하지 못한다면 도움이되지 않습니다 . 따라서 먼저 이러한 각 용어를 어떻게 보는지 설명하려고합니다.

추상화

내가 가장 좋아하는 추상화 정의는 Robert C. Martin의 APPP 에서 파생되었습니다 .

"추상화는 필수의 증폭과 관련성이없는 제거입니다."

따라서 인터페이스 자체는 추상화가 아닙니다 . 그들은 중요한 것을 표면에 가져오고 나머지를 숨기면 추상화 일뿐입니다.

새는

Dependency Injection Principles, Patterns and Practices 책 은 DI (Dependency Injection)의 맥락에서 누출 추상 이라는 용어를 정의합니다 . 이러한 맥락에서 다형성과 SOLID 원칙이 큰 역할을합니다.

로부터 의존 관계 역전 원칙 (DIP)가 다음 다시 APPP을 인용, 다음, 그 :

"클라이언트 [...]는 추상 인터페이스를 소유합니다"

이것이 의미하는 것은 클라이언트 (호출 코드)가 필요한 추상화를 정의한 다음 해당 추상화를 구현한다는 것입니다.

제 생각에 누설 되는 추상화는 클라이언트가 필요로 하지 않는 기능을 포함하여 DIP를 위반하는 추상화입니다 .

동기 종속성

비즈니스 로직을 구현하는 클라이언트는 일반적으로 DI를 사용하여 일반적으로 데이터베이스와 같은 특정 구현 세부 사항과 분리됩니다.

식당 예약 요청을 처리하는 도메인 객체를 고려하십시오.

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository repository)
    {
        Capacity = capacity;
        Repository = repository;
    }

    public int Capacity { get; }
    public IReservationsRepository Repository { get; }

    public int? TryAccept(Reservation reservation)
    {
        var reservations = Repository.ReadReservations(reservation.Date);
        int reservedSeats = reservations.Sum(r => r.Quantity);

        if (Capacity < reservedSeats + reservation.Quantity)
            return null;

        reservation.IsAccepted = true;
        return Repository.Create(reservation);
    }
}

여기서 IReservationsRepository종속성은 클라이언트 클래스에 의해 독점적 으로 결정 됩니다 MaîtreD.

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

이 인터페이스는 MaîtreD클래스가 비동기식 일 필요 가 없으므로 완전히 동기식 입니다.

비동기 종속성

인터페이스를 비동기식으로 쉽게 변경할 수 있습니다.

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

그러나 MaîtreD클래스 이러한 메소드가 비동기식 일 필요가 없으므로 이제 DIP가 위반됩니다. 구현 세부 사항으로 인해 클라이언트가 변경되기 때문에 이것이 누설되는 추상화라고 생각합니다. 이 TryAccept방법은 이제 비동기식이어야합니다.

public async Task<int?> TryAccept(Reservation reservation)
{
    var reservations =
        await Repository.ReadReservations(reservation.Date);
    int reservedSeats = reservations.Sum(r => r.Quantity);

    if (Capacity < reservedSeats + reservation.Quantity)
        return null;

    reservation.IsAccepted = true;
    return await Repository.Create(reservation);
}

도메인 로직이 비동기 적이라는 근본적인 근거는 없지만 구현의 비동기 성을 지원하기 위해서는 이제 이것이 필요합니다.

더 나은 옵션

NDC 시드니 2018 에서이 주제에 대해 이야기했습니다 . 또한 누출되지 않는 대안을 설명합니다. 2019 년에도 여러 회의에서이 강연을 할 예정이지만 이제 새로운 제목 인 Async injection으로 이름이 변경되었습니다 .

연설에 동봉 할 일련의 블로그 게시물도 게시 할 계획입니다. 이 기사는 이미 작성되어 기사 큐에 앉아 게시 대기 중이므로 계속 지켜봐 주시기 바랍니다.


내 마음에 이것은 의도의 문제입니다. 내 추상화가 한 가지 방식으로 작동 해야하는 것처럼 보이지만 세부 사항이나 제약으로 인해 추상화가 깨지는 경우 누출이 있습니다. 그러나이 경우에는 작업이 비동기 적으로 명시 적으로 제시합니다. 추상하려는 것은 아닙니다. 그것은 SQL 데이터베이스가 있고 여전히 연결 문자열을 노출한다는 사실을 추상화하려고 시도하는 현명한 또는 그렇지 않은 귀하의 예와는 다릅니다. 어쩌면 의미론 / 관점의 문제 일 수도 있습니다.
Ant P

따라서 추상화가 결코 "누설"되지 않는다고 말할 수 있습니다. 대신 하나의 특정 구현에 대한 세부 정보가 노출 된 멤버에서 유출되어 추상화 모양을 만족시키기 위해 소비자가 구현을 변경하도록 제한하는 경우 누출이 있습니다. .
Enrico Massone

2
흥미롭게도 설명에서 강조한 요점은 전체 의존성 주입 이야기에서 가장 오해 된 요점 중 하나입니다. 때때로 개발자는 의존성 역전 원리를 잊어 버리고 추상화를 먼저 디자인 한 다음 추상화 자체에 대처하기 위해 소비자 디자인을 조정합니다. 대신, 프로세스는 역순으로 수행되어야합니다.
Enrico Massone

11

전혀 누출이 아닙니다.

비 동기화는 함수 정의의 근본적인 변화입니다. 즉, 호출이 반환 될 때 작업이 완료되지 않았 음을 의미하지만, 프로그램 흐름은 시간이 오래 걸리지 않고 거의 즉시 계속됩니다. 동일한 작업을 수행하는 비동기 및 동기 함수는 본질적으로 다른 기능입니다. 비 동기화는 구현 세부 사항 이 아닙니다 . 함수 정의의 일부입니다.

함수가 어떻게 함수가 비동기 적으로 만들어 졌는지를 노출했다면 누출 될 것입니다. 구현 방법에 신경 쓰지 않아도됩니다.


5

async메소드 의 속성은 특정 관리 및 처리가 필요함을 나타내는 태그입니다. 따라서, 그것은 필요로 세계로 누출 할 수 있습니다. 비동기 작업은 제대로 작성하기가 매우 어려우므로 API 사용자에게 미리 알려주는 것이 중요합니다.

대신, 라이브러리가 자체의 모든 비동기 활동을 올바르게 관리 async하면 API에서 "누설" 할 수 없습니다 .

소프트웨어에는 데이터, 제어, 공간 및 시간이라는 네 가지 어려움이 있습니다. 비동기 작업은 네 차원 모두에 걸쳐 있으므로 가장주의해야합니다.


나는 당신의 감정에 동의하지만, "누수"는 나쁜 것을 의미합니다. 이것은 "누설 추상화"라는 용어의 의도입니다-추상화에서 바람직하지 않은 것. 비동기 대 동기화의 경우 누출이 없습니다.
StarTrekRedneck

2

누출 추상화는 특정 구현을 염두에두고 설계된 추상화로, 일부 구현 세부 사항은 추상화 자체를 통해 "누설"됩니다.

좀 빠지는. 추상화는보다 복잡한 구체적인 사물이나 문제의 일부 요소를 무시하는 개념적인 사물입니다 (사물 / 문제를 더 단순하고 다루기 쉽게 만들거나 다른 이점으로 인해). 따라서 실제로 실제 문제 / 문제와 반드시 달라야하므로 일부 하위 집합에서는 누출 될 것 입니다 (즉, 모든 추상화가 누출되는 유일한 질문은 어느 정도까지입니다. 의미는 추상화입니다) 우리에게 유용하며 적용 분야는 무엇입니까?

즉, 소프트웨어 추상화와 관련하여 때로는 (또는 종종 충분할까요?) 무시하기로 선택한 세부 사항은 실제로 우리에게 중요한 소프트웨어의 일부 측면 (성능, 유지 보수성, ...)에 영향을 미치기 때문에 무시할 수 없습니다. . 따라서 누출 추상화 는 추상화가 특정 세부 사항을 무시하도록 설계되었지만 (가능하고 유용하다는 가정하에), 이러한 세부 사항 중 일부는 실제로 중요합니다 (그들은 무시할 수 없으므로 "누출").

따라서 구현의 세부 사항을 노출하는 인터페이스 자체 가 누출되지 않습니다 (또는 분리 된 인터페이스는 자체적으로 누출 추상화가 아님). 대신, 누출은 인터페이스를 구현하는 코드 (실제로 인터페이스로 표현 된 추상화를 지원할 수 있는가)와 클라이언트 코드에 의해 생성 된 가정 (이로 표현 된 것을 보완하는 개념적 추상화에 달려 있음)에 의존합니다 인터페이스 자체이지만 코드 자체로 표현할 수는 없습니다 (예 : 언어의 기능이 충분히 표현되지 않아 문서 등에서 설명 할 수 있습니다).


2

다음 예를 고려하십시오.

이것은 이름을 반환하기 전에 설정하는 방법입니다.

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

이름을 설정하는 방법입니다. 호출자는 반환 된 작업이 완료 될 때까지 이름이 설정되었다고 가정 할 수 없습니다 ( IsCompleted= true).

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

이름을 설정하는 방법입니다. 호출자는 반환 된 작업이 완료 될 때까지 이름이 설정되었다고 가정 할 수 없습니다 ( IsCompleted= true).

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

Q : 어느 쪽이 다른쪽에 속하지 않습니까?

A : 비동기 방법은 독립적 인 방법이 아닙니다. 홀로있는 것은 void를 반환하는 메소드입니다.

나에게 여기서 "누수"는 async키워드 가 아니다 . 메소드가 태스크를 리턴한다는 사실입니다. 그리고 그것은 누출이 아닙니다. 프로토 타입의 일부이며 추상화의 일부입니다. 작업을 반환하는 비동기 메서드는 작업을 반환하는 동기 메서드와 정확히 동일합니다.

그래서, 나는 async양식 의 도입 이 그 자체로 누출 추상 이라고 생각하지 않습니다 . 그러나 인터페이스 (추상화)를 변경하여 "누설"하는 작업을 반환하기 위해 프로토 타입을 변경해야 할 수도 있습니다. 그리고 그것은 추상화의 일부이기 때문에 정의상 누출이 아닙니다.


0

이것은 모든 구현 클래스가 비동기 호출을 작성 하지 않으려 는 경우에만 누설되는 추상화 입니다. 예를 들어, 지원하는 각 데이터베이스 유형마다 하나씩 여러 구현을 작성할 수 있으며 프로그램 전체에서 사용되는 정확한 구현을 알 필요가 없다고 가정하면 완벽하게 괜찮습니다.

그리고 비동기 구현을 엄격하게 시행 할 수는 없지만 이름은 암시 적이어야합니다. 상황이 바뀌고 어떤 이유로 든 동기식 호출이 될 수 있다면 이름 변경을 고려해야 할 수도 있습니다. 따라서 제 생각은 이것이 아마도 미래.


0

반대 의견이 있습니다.

우리는 단지 . 대신 에을 원하기 때문에 돌아 오는 것에서 돌아 오는 Foo것이 아닙니다 . 때때로, 우리는 때때로 상호 작용 하지만 대부분의 실제 코드에서는이를 무시하고 그냥 사용합니다 .Task<Foo>TaskFooTaskFoo

또한 구현이 비동기식이거나 비동기식 일 때도 비동기 동작을 지원하기 위해 인터페이스를 정의하는 경우가 많습니다.

사실상, 리턴을 리턴하는 인터페이스 Task<Foo>는 구현 여부는 상관없이 상관없이 구현이 실제로 비동기인지 여부를 알려줍니다. 추상화가 구현에 대해 알아야 할 것 이상을 알려 주면 새는 것입니다.

구현이 비동기식이 아닌 경우 비동기식으로 변경 한 다음 추상화와이를 사용하는 모든 것을 변경해야합니다. 이는 매우 누출 된 추상화입니다.

그것은 판단이 아닙니다. 다른 사람들이 지적했듯이 모든 추상화는 누출됩니다. 코드 끝에 어딘가에 실제로 비동기 인 것이있을 있기 때문에 코드 전체에서 async / awaits의 파급 효과를 필요로하기 때문에 더 큰 영향을 미칩니다 .

그게 불만처럼 들립니까? 그건 내 의도가 아니지만 정확한 관찰이라고 생각합니다.

관련 요점은 "인터페이스는 추상화가 아니다"라는 주장이다. Mark Seeman이 간결하게 말한 내용은 조금 남용되었습니다.

"추상"의 정의는 .NET에서도 "인터페이스"가 아닙니다. 추상화는 다른 많은 형태를 취할 수 있습니다. 인터페이스는 추상화가 좋지 않거나 구현이 너무 가깝기 때문에 추상화가 거의 불가능합니다.

그러나 우리는 추상화를 만들기 위해 인터페이스를 절대적으로 사용합니다. 따라서 인터페이스에 대한 언급이없고 인터페이스가 밝아지지 않기 때문에 "인터페이스는 추상화가 아닙니다."


-2

GetAllAsync()실제로 비동기? 이름에 "비동기"가 있지만 제거 할 수 있습니다. 그래서 다시 묻습니다 ... Task<IEnumerable<User>>동 기적으로 해결되는를 반환하는 함수를 구현하는 것이 불가능 합니까?

.Net Task유형 의 세부 사항을 모르지만 함수를 동 기적으로 구현하는 것이 불가능한 경우 누출이 추상화되어 있는지 확인하십시오 (이 방법으로). 그렇지 않으면 그렇지 않습니다. 나는 않습니다 그것이이었다 있다면 알고 IObservable태스크보다는, 그것은 할 수 함수가 알고있는 외부 동기 또는 비동기 아무것도 그러니 구현 될 따라서는 특정 사실을 누출 아닙니다.


Task<T> 비동기를 의미 합니다. 작업 객체를 즉시 얻지 만 사용자 순서를 기다려야 할 수도 있습니다.
Caleth

는 비동기 반드시의 의미하지 않는다 기다려야한다. 까요 비동기 의미 기다립니다. 아마도 기본 작업이 이미 실행 된 경우 기다릴 필요가 없습니다 .
Daniel T.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.