DDD流程編排:上下文Context對象的定義與思索 一、業務背景 二、上下文數據傳遞 3. 框架中如何從Context(Map實現)中映射子節點需要的參數 推薦閱讀

一、業務背景

DDD戰術落地—聚合的編排一定要在應用層嗎?(領域服務與領域對象的區別)文中指出,若領域層只是單純劃分聚合根,實現數據與行爲的一致,會導致一些領域能力外泄到應用層,即調用者需要有一定的領域知識進行編排,其實並不符合DDD領域驅動設計的思路。故採用了領域服務組合一個領域中的多個聚合根實體來對外輸出領域能力。

而在落地實踐中,我們發現,簡單的領域可以僅僅使用領域對象來提供能力,複雜的領域必須藉助與領域服務來提供能力。且隨着業務的逐漸複雜。會引發領域能力的不清晰。所以需要規範化領域能力的提供方式。所以需要在領域層上搭建一層節點層(Node層)。應用層實現對Node節點的編排,最終系統中複雜的鏈路會以一個單向無環圖的方式呈現出來。

二、上下文數據傳遞

在進行能力抽象時,會有一些提供通用且基礎能力的節點,在不同的鏈路中會被複用。那麼這些通用能力的節點的上下文如何定義呢?

2.1 繼承實現(標準化Context來進行數據傳遞)

美團外賣廣告平臺化的探索與實踐中,是這樣描述的:

使用的是典型的多類繼承的方式,公共的Node節點使用的是父Context上下文。

優點:參數存取指向性明確,使用時無需定義魔法值,節點與圖解耦(實現節點在多鏈路複用

缺點:擴展不便;context會日漸膨脹,維護成本高;

2.2 Map實現

以流程編排開源框架liteflow爲例:

優點:存取方便,不會導致context膨脹,節點與圖解耦。
缺點:存取時需要用戶定義“魔法值”。

LiteFlow官網地址

2.3 接口多繼承與組合

  1. 需要被複用的上下文應聲明爲接口,以便多繼承

假設有兩個節點需要被複用:

public interface ContextA {
    int getA();

    void setA(int a);
}
public class ContextAImpl implements ContextA {

    @Getter
    @Setter
    private int a;

    public ContextAImpl(int a) {
        this.a = a;
    }
}
public interface ContextB {
    int getB();
    void setB(int b);
}
public class ContextBImpl implements ContextB {
    @Getter
    @Setter
    private int b;

    public ContextBImpl(int b) {
        this.b = b;
    }
}

業務相關的上下文定義:

使用的是多繼承,於是ContextC了ContextA、ContextB是子類,可以通過多態的方式傳遞參數。

public interface ContextC extends ContextA, ContextB {
}

實現:使用了lombok的@Delegate實現了父接口定義的方法。

public class ContextCImpl implements ContextC {
    @Delegate(types = ContextA.class)
    private final ContextA a;
    @Delegate(types = ContextB.class)
    private final ContextB b;

    /**
     * 業務特有字段
     */
    private String c;

    public ContextCImpl(ContextA a, ContextB b) {
        this.a = a;
        this.b = b;
    }

    public String getC() {
        return c;
    }

    public void setC(String c) {
        this.c = c;
    }
}

定義節點:

public interface Node<T, R> {
    R execute(T param);
}
public class ContextNodeC implements Node<ContextC, String> {
    @Override
    public String execute(ContextC param) {
        int a = param.getA();
        int b = param.getB();
        return a + " : " + b;
    }
}

測試使用:

public class Test {

    public static void main(String[] args) {
        ContextNodeC contextNodeC = new ContextNodeC();

        ContextC contextC = new ContextCImpl(new ContextAImpl(1), new ContextBImpl(2));

        String execute = contextNodeC.execute(contextC);
        System.out.println(execute);
    }
}

2.4 Map+SerializedLambda方式

思路:
使用SerializedLambda獲取到方法引用的方法名
反射工具類Generics:獲取到泛型對象的泛型類型

當傳入的是Lambda表達式實現了SerializedLambda接口,可以通過反射的方式來獲取到參數類型與name。這樣可以無需進行魔法值的轉換而獲取到轉化後的類型。

  1. 定義lambda接口:
import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.util.function.Function;


import lombok.SneakyThrows;
import sun.reflect.generics.parser.SignatureParser;
import sun.reflect.generics.tree.ClassTypeSignature;
import sun.reflect.generics.tree.MethodTypeSignature;

public interface NamedFunction<T, R> extends Function<T, R>, TypedName<R>, Serializable {
    R apply(T t);


    @SneakyThrows
    @Override
    default Class<R> type() {
        //調用writeReplace()方法,返回一個SerializedLambda對象
        SerializedLambda lambda = this.lambda();
        SignatureParser parser = SignatureParser.make();
        MethodTypeSignature methodSig = parser.parseMethodSig(lambda.getImplMethodSignature());
        ClassTypeSignature signature = (ClassTypeSignature) methodSig.getReturnType();
        return (Class<R>) Class.forName(signature.getPath().get(0).getName());
    }

    @SneakyThrows
    default String name() {
        SerializedLambda lambda = lambda();
        return lambda.getImplMethodName();
    }

    @SneakyThrows
    default SerializedLambda lambda() {
        Method method = this.getClass().getDeclaredMethod("writeReplace");
        method.setAccessible(Boolean.TRUE);
        //調用writeReplace()方法,返回一個SerializedLambda對象
        return (SerializedLambda) method.invoke(this);
    }
}
public interface TypedName<T> {
    /**
     * 對象類型
     */
    default Class<T> type() {
        return Generics.find(this.getClass(), TypedName.class, 0);
    }

    /**
     * 對象名稱
     */
    String name();
}
  1. 定義Context上下文
public interface DefaultContext {

    //定義
    <T, R> Store<R> with(NamedFunction<T, R> name);

    Map<String, Object> getContextMap();

    @Slf4j
    class Store<T> {

        protected NamedFunction function;

        protected DefaultContext data;

        public Store(NamedFunction function, DefaultContext data) {
            this.function = function;
            this.data = data;
        }

        public T get() {
            Object value = data.getContextMap().get(this.getKey());
            if (value == null) {
                return null;
            } else if (value.getClass() != function.type()) {
                log.error("key:{} value:{}, 存儲類型{}與聲明類型{}不一致!",
                        getKey(), value, value.getClass(), function.type());
            }
            return (T)value;
        }

        void set(Object value) {
            data.getContextMap().put(getKey(), value);
        }

        public String getKey() {
            return this.function.name();
        }
    }
}

實現類:

@Slf4j
public class LocalContext implements DefaultContext {

    //定義存儲結構
    private Map<String, Object> contextMap = Maps.newConcurrentMap();


    @Override
    public <T, R> Store<R> with(NamedFunction<T, R> name) {
        return new Store<>(name, this);
    }

    @Override
    public Map<String, Object> getContextMap() {
        return contextMap;
    }
}

定義Context的存儲類型:

/**
 * 基於Context的存儲結構
 */
public interface NameDef {

    Long uid();

    User user();

    Person person();
}

測試方式:無需類型強轉。

public class Test {
    public static void main(String[] args) {
        LocalContext localContext = new LocalContext();
        localContext.with(NameDef::user).set(new User(1001L, "http://baidu.com", Lists.newArrayList()));
        localContext.with(NameDef::uid).set(1001L);

        Long uid = localContext.with(NameDef::uid).get();
        User user = localContext.with(NameDef::user).get();

        System.out.println(uid);
        System.out.println(user);
    }
}

3. 框架中如何從Context(Map實現)中映射子節點需要的參數

例如:定義了Node節點的類型。在訂單鏈路中上下文Context爲DefaultContext。需要調用基礎Node(上下文爲BaseContext)節點。如何從DefaultContext中拿到BaseContext所需要的參數?

public interface Node<T, R> {
    R execute(T param);

    default Class<T> paramType() {
        return Generics.find(this.getClass(), Node.class, 1);
    }
}

最簡單的方式:

  1. DefaultContext維護的結構:class:子context
  2. 找到基礎Node中聲明的泛型類型class
  3. 從OrderContext中通過class找到param傳遞給下一個節點。

但是BaseContext的上下文,需要OrderContext上下文進行組裝纔可以拿到,並不簡單的維護好結構。於是我們可以這樣做:

  //轉化下一個子節點需要的參數類型
    @SneakyThrows
    private static <T, R> T parseParam(Node<T, R> nodeAction, LocalContext data) {
        //獲取到 子節點聲明類型
        Class<P> type = nodeAction.paramType();
        //如果nodeAction的入參是LocalContext,直接賦值
        if (type == LocalContext.class) {
            return (T) data;
        }
        //如果nodeAction的入參是ContextConstructor的子類,說明定義了convert方法
        if (ContextConstructor.class.isAssignableFrom(type)) {
            //調用convert方法
            ContextConstructor r = (ContextConstructor) type.newInstance();
            return (P) r.convert(data);
        }
        log.error("聲明的入參沒有繼承ContextConstructor");
        return null;
    }

我們需要BaseNode定義的上下文需要繼承於ContextConstructor接口:

public interface ContextConstructor<P> {
    P convert(LocalContext data);
}

實現類:

@Data
public class TestReq implements ContextConstructor<TestReq> {

    private People people;

    @Override
    public TestReq convert(LocalContext data) {

        this.people =  data.with(Name::people).get();
        return this;
    }
}

推薦閱讀

lombok 實驗性註解之 @Delegate

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章