The significance of DDD to business support

e0c177840c997997a0affd2742accbcc.gif

In "Forrest Gump", Forrest Gump started running non-stop. After a while, there were many followers running together. Why did they run?

  • Forrest Gump: I don't know, I just want to run.

  • Follower: It feels meaningful to do so, and Forrest Gump is still leading the way.

Similarly, I didn't know what DDD was at the beginning, but when I found that everyone was mentioning DDD and learning DDD, I also involuntarily joined the movement like a runner: since a big cow proposed DDD, Since so many people are flocking to it, there must be something to be desired.

However, one day, A-Gump stopped running, he didn't want to run, and the followers encountered a problem: Do we still want to run? When we feel that we can't learn in the process of learning DDD, we may also ask: Do we still need to learn? This is indeed thought-provoking.

Based on work experience, this article tries to talk about some understanding of DDD, hoping to better explore the meaning of learning DDD.

1651e405dc62fbfeda2f7e5d4745c577.png

Focus on the value of DDD

Regardless of whether you are doing business, or doing a platform or a middle-end platform, everyone is often exhausted by intertwined and complicated business logic and obscure coupled business codes. I think everyone's pursuit of DDD is also a demand for easy support for business development, and they are exploring whether there is a suitable theory that can improve the status quo. After all, a better life is shared.

▐Status   : layered support mechanism

We choose various frameworks and carry out various organizational designs, the core of which is to improve production efficiency. However, if the business logic is implemented case by case and lacks reuse, the R&D cost will be very high and the investment cycle will be very long.

In order to increase reuse and shorten the landing time of services, many general-purpose capabilities and products are needed. In our delivery process, there are two main layers:

  1. Basic capabilities: Relatively atomic capabilities are basic (domain) capabilities, which can better support business customization. Due to the comparison of basics, the range of product capabilities expressed is also very large. However, a complete product requires a lot of basic capabilities in series, and the cost of series connection is also very high.

  2. Platform products: The versatility of basic capabilities means that there is a lack of understanding of the scene and the lack of "genes" to further improve production efficiency. Therefore, at the time of delivery, abstraction will be based on some high-frequency scenarios to form the product capabilities of the platform, and strive to achieve "out of the box". When the business is customized based on the "platform product" layer, the cost of understanding will be greatly reduced. 

0667ee003cb963ebc54551df2cc0330a.png

layered support frame

   Corruption: business logic penetration


This level looks very good: in the initial stage, due to the lack of historical burden, it can indeed improve a certain production efficiency, which is the benefit of the ability itself. However, as time goes on, with the increase of business access, businesses begin to influence each other, and the resistance to research and development is also increasing.

The important reason for the decline in R&D efficiency is that more often, we still follow the logic of "how fast and how fast the business can run" to do related work, build bridges when encountering water, open holes when encountering mountains, and then go directly to the destination to carry out information The communication, the operation of the database field.

Such a process violates our original intention of "hoping to enrich platform capabilities through business scenarios while ensuring a clean kernel". Capabilities should be abstracted based on a relatively large number of use cases and relatively complete thinking. It is a unified look at the horizontal and has a deeper understanding. However, vertical delivery allows us to deal with problems more vertically, often just "snooping" the link. Under the constraints of delivery time and business nodes, it is difficult to think more comprehensively and profoundly, and it is difficult to make a more general design.

▐Business   defense: platform framework guard

So, why focus on DDD? If DDD directly hits the core of software complexity - "problem domain", it may still be relatively abstract. Specifically, because this is indeed in line with the values ​​we pursue [enhancing long-term productivity]:

  1. Subdividing fields and cultivating professional people and things: Because the core of DDD is to understand and package each field well, disassemble and place a business requirement in its own reasonable place, through such decomposition and precipitation, ensure the domain The input can achieve long-term sustainable development and form competitiveness.

  2. Mechanism guarantee, not dependent on changeable things: DDD actually summarizes many common skills and experiences, which can make such implementation more deterministic. Whether it is the ability of the aggregate root to control domain entities, the interaction strategy of the bounded context, the abstract status of the domain core, etc., once you choose to respect and confirm, you can fall into the code structure, organizational relationship, and team documents. Consensus is formed, and it will not fluctuate violently due to changes in personnel and other factors.

The focus on DDD can be compared to our focus on "artisan spirit". The focus on DDD is also our focus on business understanding. To be close to the business, it is necessary to understand the business, not only to understand the business, but also to understand most of the business. This kind of pursuit allows us to look up a level and return to the most essential question: what problem do we want to solve? How can it be solved better?

8058c3c2988230b9752ebe6e2212cd68.png

Difficulties in learning DDD

I don't know if you are so confused: the learning process of DDD seems to be a process of "finding a needle in a haystack". Even if you can get something and use it, there will still be a feeling of "fake things", which is not very natural. Why is learning DDD so difficult?

▐Feeling   : no door

The following scene in the Analects is quite similar to our confusion:

Uncle Wu Shuyu said to the doctor in the court: "Zigong is more worthy than Zhongni." Zifu Jingbo told Zigong, and Zigong said: "For example, the wall of the palace, the wall given is also shoulder-length, and you can see the beauty of the house; The wall of the master is as high as 100,000, and the gate is not allowed to enter. The beauty of the ancestral temple and the wealth of hundreds of officials are not seen. Those who win the gate may be few, and the master's cloud is not suitable!"

Just as you want to feel the state that Confucius has achieved, you also need to have a certain accumulation of your own knowledge. If we want to feel the power of DDD, we must grow to a certain extent (such as: have experienced some successful or failed designs, have our own experience or lessons), in order to form resonance and recognition.

In the work, it is true that the best practice of DDD is rarely seen. In the face of complex business, no one has the courage to say which software structure is the ideal design:

  1. Because this is not a deterministic problem decomposition, your design will be studied under the microscope, and various counterexamples can always be found.

  2. Moreover, we know that the best practice must be "soft" enough, leaving design for expansion, and being able to iterate with business development, not a static result.

Therefore, opening the door to learning cannot be accomplished overnight with a few cases. It needs to be combined with our own work to accumulate and experience slowly.

   Difficulty: pattern divergence

We have a confusion, what exactly is DDD:

  1. An example of practice is unconvincing: When we see "DDD practice", we may ask: Is this also considered DDD? It is not just a normal server-side framework and solution, nor can it cover other scenarios or departmental systems.

  2. Abstract theory feels empty: When we see "abstract DDD definition and strategy", we may ask: Is this also DDD? Isn't it just some software design consensus, and then some noun definitions are imposed, and some strategies don't match the system we have.

No matter looking at the abstraction or the specific implementation, it is difficult to find convincing theory and practice (capable of deterministic implementation). Because this is not like the 23 design patterns, most of the patterns can be covered by N templates.

Why can't a particular pattern be generated? You can take a closer look at the following figure:

  1. Abstract theory: Like abstract interfaces, the top learning of "DDD understanding" is mainly theoretical definitions, such as: aggregate root, value object, resource library, core domain, support domain and other definitions, which are easy to understand and grasp.

  2. General practice: Like relatively specific abstract classes, the middle level of "DDD understanding" is some general principles and techniques, such as: context mapping strategy, architecture selection, etc. These factors are certain, but you need to make trade-offs and choices independently, and you need to keep pace with the times. The incremental part needs to be learned and supplemented by yourself.

  3. Specific practices: Like specific class instances, the lower levels in "DDD Understanding" are a series of specific practices, combined with their respective business scenarios, to design, choose and supplement different factors. Because there are many options involved, the final selection results are often divergent, which makes people feel "thousands of people have thousands of faces".

The difference between the two is:

  1. The hierarchical relationship in the "code abstraction level" is relatively clear and constrained.

  2. There are no clear constraints in the "domain-driven understanding level", which are the trade-offs of multiple strategies and some key suggestions.

Therefore, due to the high level of abstraction of the problem and the high uncertainty of various strategies, it is difficult to generate a refined pattern like "23 design patterns" in DDD. If there must be, it is also a series of patterns, which are relatively divergent.



eac28f9af562ca811e3a2eb8525a915b.png

Analogy of DDD Comprehension Level

Therefore, we gradually understand: DDD is oriented to the "soft" of software, covering all aspects. An in-depth understanding of DDD requires "thousands of tempering" before you can understand those in-depth and simple suggestions, and understand the mantra of "the master leads the door, and the practice depends on the individual", and you can feel that "the crowd looks for him thousands of times, but that person is there. A wonderful moment in a dimly lit place.

99331ad21c3f5d01308686f439aecbae.png

Look at DDD based on design principles

Although the practice of DDD itself may be thousands of people, but the thinking of some core topics should be focused. The understanding of these high-frequency topics can enable us to design better, and the cost-effectiveness of the discussion is also high. Next, based on the "six major design principles" (solid principles) as an introduction, we will look at some key understandings in DDD.

▐Single   Responsibility Principle: Domain Division

Single Responsibility says: A class should have only one reason to change. A single responsibility allows better cohesion, reduces coupling, and facilitates evolution.

The domain division in DDD can be thought of by analogy. For the division of domains, whether it is divided by domain entities, functional modules, or services, we actually want to ensure that the domains are as orthogonal as possible and can evolve and develop independently.

a70640c3fadbd2d482b83a209ff4e61b.png

Single Responsibility Principle: Single Responsibility Principle

How to divide the field is more appropriate? When I first entered the business platform, I learned that domain segmentation is based on the boundary of "one or more entity objects". This is indeed more reasonable, because the core responsibility of the domain is to manage domain entities. But is this an effect, or a cause? When splitting, is it because we have a judgment on the domain, so it is more appropriate for some entities to be grouped together; or because some entities have obvious boundaries, so they can form a domain? Just like the picture below:

  1. Can be used as a whole as a part.

  2. It can be divided into 2 parts according to the vertical positive and negative: 1 above (red), 2 below (yellow, green).

  3. It can be divided into 2 parts according to the positive and negative of the horizontal axis: 1 on the left (yellow) and 2 on the right (red and green).

  4. Can be cut into 3 parts (red, yellow, green).

7d89aab4961b50963fdb3ec16e8fd718.png

Clustering example

This is indeed food for thought. When segmentation is relatively easy, it is often because there are already industry standards (for example, it is more reasonable for e-commerce systems to have fields such as orders, payment, logistics, and inventory). Where do the industry's standards come from? is from evolution:

  1. At the beginning, it may be just a big transaction, for example: at the beginning of payment, it is only an agreement between the buyer and the seller, and no modeling is required.

  2. Later, when payment developed, a domain became independent. In the past, there was no need for dedicated maintenance, and a team will gradually be pulled out to undertake related research and development.

Therefore, the evolution and division of the domain is very similar to the "heuristic algorithm" (an algorithm based on intuition or experience, which gives a feasible solution for each instance of the combinatorial optimization problem to be solved at an acceptable cost):

  1. Initialization: According to the preliminary division of experience, it can also be an industry standard (when there is no industry standard, it can only rely on experience).

  2. Cost evaluation: Human cost measurement in the production and delivery process, focusing on factors such as understanding cost, development efficiency, system stability, and operation and maintenance costs.

  3. Better solution: In the process of business development, calculate the cost evaluation, analyze the "good factors" and "bad factors" that affect the evaluation, and make further adjustments.

Often in the end, we will find:

  1. The content of the adjustment: in fact, it is to match the production relationship.

  2. The principle of adjustment: pursue the cohesion of responsibilities and refine the division of labor.

  3. Reasons for constant adjustment: As business develops, cohesion standards need to keep pace with the times.

In addition, from the perspective of association, we can often see the boundaries of the field when we look at the organizational structure. The core reason is that the organizational structure also needs to adapt to the production relationship.

▐Opening   and closing principle: physical behavior

The principle of opening and closing means that objects in software (classes, modules, functions, etc.) should be open for extension, but closed for modification. In other words, the extension block must be designed, and the extended part should not affect the stability logic.

In DDD, the behavior of the entity also needs to consider the ability to expand while ensuring that it is closed to the outside world.

ae88330215cba904393a05899b11f0db.png

Open Closed Principle: Open and Closed Principle

When we first started learning DDD, we might force some logic into entities for control and convergence. But later, as the business changes, you will find it difficult to assume behavioral logic in the entity:

  1. Greater impact: It is difficult to have the courage to modify a core class frequently.

  2. Too centralized: With the increase of methods and logic, entities become more and more bloated.

  3. There are many scenarios: a lot of logic is not orthogonal, not like if or else, full of intersection and superposition.

Abandoning the get and set of POJO and moving towards the rich behavior of entities, does it make it more difficult for us to write code? In fact, our troubles come from paying too much attention to the closure of entity behaviors and ignoring the design of extensions:

  1. The essence of the original get set writing method is that many extensions are placed in business scripts. Although business scripts are riddled with holes, they are at the application layer and away from the core logic. The basic logic such as the underlying model and common components is relatively clean.

  2. When applying DDD, after sinking some behaviors to the domain layer, expansion should also be considered. If you only pay attention to the closing and not the expansion, then it is indeed "drawing the ground as a prison", "picking up the sesame seeds, but losing the watermelon".

However, in order to break through this dilemma and be able to design extensions in entity behaviors, we must actually have such a recognition: we must look up to a level, which is the expression of entity behaviors. It does not necessarily have to be completed by only one class. It can be done through strategy patterns and other methods. Routing is done by some classes in a module, as long as there is external encapsulation and control.

It is also our advanced direction to break through the limitation of one class and move towards the collaborative design of more classes.

▐Liskov   Substitution Principle: Resource Library

The Liskov substitution principle says: Wherever a base class can appear, a subclass must appear. What is important is reasonable abstraction and reuse. A thought-provoking example is: a square is a special rectangle. If a square is a subclass of a rectangle, there will be a conflict when setting the length. The length and width of a rectangle can be set independently, and the length and width of a square are constrained. Yes, it is awkward to use the inheritance relationship.

In DDD, regarding substitutability, I want to talk about resource libraries.

418e597ed1722383947dc617d8188750.png

Liskov Substitution Principle: Liskov Substitution Principle

  • Alternative Requirements for Repositories

In the original layered architecture, storage capabilities such as databases, as an underlying infrastructure, are regarded as stable underlying services. However, in the actual delivery process, different scenarios are often encountered:

  1. Local deployment: offline retail transactions are expected to have the ability to store data locally for service stability.

  2. Products on the cloud: Trading products sold to external companies have different cost requirements. It is expected to purchase different storage services on the cloud.

These scenarios have made everyone gradually believe that when facing a broader market, the infrastructure is also full of uncertainties and needs to have replaceable capabilities.

  • Reuse strategy between storage implementations

In the actual implementation process, not every field will be deployed independently, and some fields will be deployed together due to organizational and performance factors. Often the code in these areas is also in a project module. For the consideration of horizontal efficiency, a unified storage framework will be designed.

Different facilities have different storage capabilities, but the entire storage process is similar (protocol conversion, storage statement generation, execution and transaction, and return results), so it is necessary to make trade-offs in the process reuse methods of different storage capabilities (database, Tair, etc. Is it a separate abstraction or a unified abstraction):

  1. If "big unification" is the main focus, then when abstracting different storages such as relational storage and KV storage, you will be hesitant like "the problem of rectangles and squares":

    Benefits: If you maintain for a long time and understand the particularity inside, you can indeed omit some main code and improve development efficiency.

    Cost: But when most people want to expand, they will feel a lot of confusion. There are many unnecessary adaptations, which are full of confusion.

  2. If the focus is on combination, then multiple sets of templates can be used to better make independent choices. This kind of divide and conquer reduces the cost of everyone's understanding, and can also evolve independently, which is more suitable for the capabilities and characteristics of storage. However, multiple sets of understanding need to be accumulated, and human support is often lacking.

I think, "Based on different storage capabilities, design different template frameworks" should be the first choice. The unified abstraction may have a lower labor cost at the beginning, but because of the higher level of abstraction, it will be a "black hole of human cost" in understanding and maintenance. As time goes by, it will reduce the overall income, and in the long run, it will not be worth the loss. of. On the contrary, different templates can be reused to obtain better overall benefits in the end.

▐Demeter   's Law: Domain Collaboration

Demeter's law, also known as the principle of minimum knowledge, means that a software entity should interact with other entities as little as possible. Should communicate with some "key class".

In DDD, collaboration between domains also requires related planning and design. If the mutual calls between domains are not managed, the link relationship will expand to the point of incomprehensibility.

7a2eb8a55caeb08905515146528ea43a.png

Law of Demeter: Law of Demeter

In the design pattern, whether it is the intermediary pattern or the appearance pattern, it is hoped that through centralized management, the complex many-to-many relationship can be simplified into an easy-to-understand structure such as many-to-one and one-to-many. Similarly, in the process of domain collaboration and external delivery, a coordination layer can often be added to connect the interactions of various domains. This can not only reduce the cost of collaboration in each domain, but also reduce the cost of external understanding, and have a better R&D experience.

How should the coordination layer be generated? It's like attending a class: although the teacher can teach, but when the teacher is not there, you can appoint a student to attend the class. Although the students can do it, they are not proficient in teaching skills, and they themselves have the responsibility of learning, and the role is also very embarrassing. The role relationship between the coordination layer and the domain will be discussed below.

  • Can a Domain Be a Coordination Layer

What is worth discussing is the concept of "transaction domain" and "order domain" in the transaction:

  1. The "transaction domain" seems to be responsible for the entire transaction process and can coordinate various domains. Logically, it is more appropriate to be a coordinator, but it is mainly in the management of orders, and other coordination capabilities endowed, this part does not have domain entities.

  2. The "order domain" seems to be only responsible for the service of the order itself, and does not care much about other domains, but because the order contract has all the contract information (whether it is held directly or indirectly), it means that the "order domain" itself has coordination potential, but the responsibility does not seem to be single enough.

There is no entity, why there is a "transaction domain", I understand it this way: in the case of strong control over the transaction process, the API service of the transaction is regarded as a domain service (such as: placing an order), and the "transaction domain" is in Logically, there are boundaries and can be established. But the essence is still to manage orders, relying on the order domain to become a domain, and at the same time want to accumulate coordination capabilities.

  • Can the coordination layer become a domain

So, if the management of the model of the order is not given to the transaction management, this is the question I have always thought about: if there is no own database entity, only the memory model, purely relying on the understanding of downstream business activities and data flow, can it become a domain? ?

The answer is most likely no:

  1. Logically, individuals recognize purely by understanding as a domain, after all, knowledge itself is also an asset.

  2. But in fact, without a carrier, you can’t do a lot of things, including status records, data services, etc., and can only assist, without core competitiveness, it is difficult to survive.

The role of the coordinator, in order to become a relatively recognized "domain", must hold the data model by itself, or, with the help of some data models of the basic domain, and enjoy management authority.

  • The name of the coordination layer

Whether the domain wants to assume the role of coordinator or the coordinator wants to develop into a domain, it does not conform to the logic of single responsibility, but such "part-time jobs" often occur, and the core is the overlapping of developer roles.

Since the coordination layer is not suitable to be selected from a domain, nor is it suitable to be a domain, then what should be called the coordination part between business activities and capabilities of each domain? At present, words such as "commercial capability" and "solution" are more appropriate.

▐Interface   isolation principle: business activities

The interface segregation principle says: A client should not depend on interfaces it does not need. A class's dependence on another class should be based on the smallest interface.

In addition to learning domain construction in DDD, we also need to pay attention to how the application layer can better undertake business requests, and study the basis for business logic segmentation. 

a27e6b6484758c200e4e883f8ed9e059.png

Interface Segregation Principle: Interface Segregation Principle

  • Without rules, it's hard to get around

When I was doing business in the business department before, I didn’t have related concepts such as business activities and processes. I often wrote business scripts based on business needs. The amount of experience would affect the elegance of the code. But apart from experience, everyone does not have a better structural framework and principles to undertake various business logics of the application layer, so they are also full of doubts:

  1. How should the external service interface be segmented?

  2. Can processes be shared between similar services?

  3. How can the business execution process be further structured and segmented?

  4. ......

The result of no standardization is that everyone often has their own opinions. Everyone wants to establish a set of structures, and no one agrees with the set of others. Each has its own code area.

  • Stable framework of the platform

Now at work, because the platform has been thinking and governing for a long time, and there is a relatively stable consensus. The overall design, between the business entrance and the business entrance, between the business entrance and the stable logic, reserves space and expansion capabilities to undertake the scenario-based logic, and the structure is relatively definite:

  1. The entrance is segmented according to business activities: the service entrance is expanded according to business activities, the core understands and pays attention to use cases, and what roles need to do what.

  2. Process independent undertaking and orchestration: Business activities remain independent and orchestrated before reaching the multiplexing layer (such as domain services, external services, and business expansion).

  3. Arrange the execution process with the help of process files: segment the execution process into execution nodes, and the segmentation of nodes can be based on "function points", "involved domains", "involved entities", etc. The common capabilities between nodes sink into the basic capabilities.

  4. ......

  • Repeated reviews form a consensus

Divide and conquer, narrow everyone's focus, and better divide and collaborate, which is really easy to understand and accept. But to ensure reasonable segmentation, there must be a unified consensus and principles.

Often the formation of a consensus is not to agree on a consensus first, and then divide it, but to try to divide it after the initial communication, and then reach a consensus in the review, and move forward amidst twists and turns. If everyone's views and conflicts are relatively large, then the process of reaching a consensus will be relatively slow. Fortunately, this kind of segmentation does not happen often, and it is also when there is a big demand and a big refactoring. At this time, the reserved research time and development resources are also sufficient.

Dependency Inversion Principle: Hexagonal Architecture

The principle of dependency inversion is to say: the program should depend on the abstract interface, not on the concrete implementation.

The hexagonal architecture mentioned in DDD further enhances the status of the abstract core, taking domain construction as the core goal of the architecture.



b4b7692ba5c8fea6e0182cf9d5588045.png

Dependence Inversion Principle: Dependency Inversion Principle

Taking the field as the center is actually a relatively important change:

  1. In the past, the layered architecture was the main focus: pay attention to looking at the layers, try to lower the capabilities as much as possible, reuse more tools, and accumulate common components.

  2. Now focus on the field: focus on looking at the level of abstraction, try to integrate understanding into the core of the field, and carry out more "understanding" reuse, accumulating business knowledge.

Such a change allows us to consciously take "domain understanding" as the core, form industry competitiveness, and sell "knowledge" as an asset.

In order to ensure the abstraction of the domain kernel, it is necessary to define the boundaries of the domain kernel. There are two types of interfaces:

  1. Capabilities provided to the upstream: Through the interface declaration, explain the responsibilities that can be assumed, and implement support within the domain.

  2. Dependency on downstream capabilities: external services, business extension customization, and storage services can all be regarded as downstream services, and service dependencies are declared through interfaces.

It can be seen that the interface between the domain core and the outside is decoupled. For more basic services, it will be regarded as the same external port as the business entrance, which belongs to the application layer. For example, the storage service:

  1. It turns out that it is more about basic capabilities: data frame + DO, no need to understand the conversion, the conversion is completed upstream, and DO will also be used upstream as the core model. When adopting the model-following strategy, the upstream completely uses DO as the core object for circulation .

  2. Now it can be understood as a "business component": it needs to implement the storage interface of the domain, undertake protocol conversion, and convert the domain object into a data storage object DO. DO will not be directly understood by the domain, but needs to be converted into a domain object and then exposed to the outside world. The kernel defines performance.

This kind of architecture establishes the abstract core position of domain understanding, allows R&D students to pay more attention to thinking about business issues, and builds more "flesh and blood" software that is "close to the core business issues", not just the "basic skeleton", let us Getting closer to the value of customers is a more recognized direction in the process of dealing with software complexity.

7efbb759d236346247da9a7ff4981d4b.png

Summarize

The pursuit of DDD comes from our desire to solve various business problems elegantly, hoping that a set of frameworks can guide us to decompose problems and obtain stable and efficient production efficiency.

But this is like the pursuit of "perpetual motion machine", it is a process that is difficult to have a definite answer. Solutions that can solve the complexity of software must be combined with relevant scenarios and evolve continuously. Simply pursuing DDD will not get a "silver bullet".

However, just like the research on "perpetual motion machines", we can focus on the conversion process of energy, which can lead us to create more efficient energy machines. The research and pursuit of DDD can make us pay more attention to the deep understanding of the business, and can lead us to write code implementations that are easier to expand.

I think it is the existence of "continuous development of business" and "complexity of software" that makes programming full of challenges and makes everyone enthusiastic about framework research. Isn't this a wonderful thing.

2a0245be1171da4b004f860edfb6670e.png

team introduction

We are the Big Taobao Technology-Trading Platform Team. The team is mainly engaged in the delivery of transaction links. In the delivery work, abstraction and construction of horizontal product capabilities (such as: pre-sales, electronic vouchers, etc.), the team focuses on business architecture, DDD and other theories and practices, and is committed to efficient and stable business realization. Access and abstract empowerment.

¤  Extended reading  ¤

3DXR Technology  |  Terminal Technology  |  Audio and Video Technology

Server Technology  |  Technical Quality  |  Data Algorithms

Guess you like

Origin blog.csdn.net/Taobaojishu/article/details/132200409
ddd