与AI对话 --- 软件层级 Q:
Q:
我们来讨论一个具体的层级关系。假设有一个DisplayObject 层级。然后我们有一个层级,专门封装了一些方法,关于如何画displayobject。 这个层级是服务于display object ,可以看作是display object的extension 部分。 还有一个层级,关于如何从display object里面 query 得到信息的。这个层,也可以看作是display object层级的扩展。 从宏观上看, 似乎display object依赖于 画层级 和 query 层级。 但是从微观上看,query 层级的输入参数,是需要display object的。似乎,query 层依赖于displayobject. 这就使得宏观得到的层级关系与微观上,产生了矛盾。 为什么这样? 哪个错了?
Q:
No, I think we make the wrong thinking. The question occur when a layer is too big and we want to seperate it. for example, in the past, the display object level may have the responsbility of draw things. Now we seperate the respobisility and make draw a thin layer that works for the dispaly object. So from macro view, the dispaly object level use the draw level things.
AI:
I understand your point. When a layer becomes too large and you decide to separate its responsibilities into smaller, more manageable layers, it can create a situation where the original layer (now split) appears to have dependencies on the new layers. This can lead to confusion about the direction of dependencies.
Clarifying the Separation of Responsibilities
In your example, the DisplayObject
layer originally had the responsibility of drawing itself. To improve modularity and maintainability, you decided to separate the drawing functionality into its own layer. This separation can indeed create a situation where the DisplayObject
layer depends on the drawing layer for rendering.
Macro and Micro View of Dependencies
-
Macro View:
- From a macro perspective, the
DisplayObject
layer uses the drawing layer to render itself. This makes it seem like theDisplayObject
layer depends on the drawing layer. - Similarly, if you have a query layer to extract information from
DisplayObject
, theDisplayObject
layer might seem to depend on the query layer for querying.
- From a macro perspective, the
-
Micro View:
- From a micro perspective, the drawing layer and query layer need instances of
DisplayObject
to perform their operations. This makes it seem like the drawing and query layers depend onDisplayObject
.
- From a micro perspective, the drawing layer and query layer need instances of
Resolving the Perceived Contradiction
To resolve this perceived contradiction, it's important to clearly define the roles and responsibilities of each layer and ensure that dependencies are managed in a way that maintains the integrity of the layered architecture.
Example: DisplayObject, Drawing Layer, and Query Layer
Let's consider the following example:
- DisplayObject: Represents the object to be drawn and queried.
- Drawing Layer: Provides methods to draw
DisplayObject
. - Query Layer: Provides methods to query information from
DisplayObject
.
Refactoring for Clarity
To maintain a clear separation of concerns and manage dependencies effectively, you can use interfaces and dependency injection. This approach ensures that each layer depends on abstractions rather than concrete implementations.
Example Code
Here is an example demonstrating how to refactor the code to maintain clear dependencies:
#include <iostream>
#include <memory>
// Define IDrawable interface
class IDrawable {
public:
virtual void draw() const = 0;
virtual ~IDrawable() = default;
};
// Define IQuery interface
class IQuery {
public:
virtual std::string query() const = 0;
virtual ~IQuery() = default;
};
// DisplayObject implements IDrawable and IQuery interfaces
class DisplayObject : public IDrawable, public IQuery {
public:
void draw() const override {
std::cout << "Drawing DisplayObject" << std::endl;
}
std::string query() const override {
return "DisplayObject information";
}
};
// Drawing layer depends on IDrawable interface
class DrawingLayer {
public:
void drawObject(const IDrawable& obj) const {
obj.draw();
}
};
// Query layer depends on IQuery interface
class QueryLayer {
public:
std::string queryObject(const IQuery& obj) const {
return obj.query();
}
};
int main() {
std::shared_ptr<DisplayObject> displayObject = std::make_shared<DisplayObject>();
DrawingLayer drawingLayer;
QueryLayer queryLayer;
drawingLayer.drawObject(*displayObject);
std::cout << queryLayer.queryObject(*displayObject) << std::endl;
return 0;
}
Summary
- Separation of Responsibilities: When separating responsibilities into smaller layers, ensure that each layer has a clear and single responsibility.
- Macro View: From a macro perspective, the
DisplayObject
layer uses the drawing and query layers to perform specific tasks. - Micro View: From a micro perspective, the drawing and query layers need instances of
DisplayObject
to perform their operations. - Managing Dependencies: Use interfaces and dependency injection to manage dependencies effectively, ensuring that each layer depends on abstractions rather than concrete implementations.
By following these principles, you can maintain a clear separation of concerns and manage dependencies effectively, even when splitting a large layer into smaller, more manageable layers.
Q:
so for the interface, Idraw and Iquery, where do you put it? in the display object level or other level? and another question is which level is the low layer?
AI:
Where to Place the Interfaces (IDrawable
and IQuery
)
The placement of interfaces like IDrawable
and IQuery
depends on the design principles and the organization of your codebase. Generally, interfaces should be placed in a location that makes them easily accessible to both the implementers and the users of the interfaces. Here are two common approaches:
-
Separate Module or Package for Interfaces:
- Create a separate module or package specifically for interfaces. This module can be imported by both the
DisplayObject
level and the drawing/query layers. - This approach promotes a clear separation of concerns and makes it easy to manage dependencies.
- Create a separate module or package specifically for interfaces. This module can be imported by both the
-
Within the
DisplayObject
Level:- Place the interfaces within the
DisplayObject
level if they are tightly coupled with theDisplayObject
's functionality. - This approach can be simpler for smaller projects but may lead to tighter coupling between layers.
- Place the interfaces within the
Example: Separate Module for Interfaces
// Interfaces.h
#ifndef INTERFACES_H
#define INTERFACES_H
class IDrawable {
public:
virtual void draw() const = 0;
virtual ~IDrawable() = default;
};
class IQuery {
public:
virtual std::string query() const = 0;
virtual ~IQuery() = default;
};
#endif // INTERFACES_H
Example: Within the DisplayObject
Level
// DisplayObject.h
#ifndef DISPLAYOBJECT_H
#define DISPLAYOBJECT_H
class IDrawable {
public:
virtual void draw() const = 0;
virtual ~IDrawable() = default;
};
class IQuery {
public:
virtual std::string query() const = 0;
virtual ~IQuery() = default;
};
class DisplayObject : public IDrawable, public IQuery {
public:
void draw() const override;
std::string query() const override;
};
#endif // DISPLAYOBJECT_H
Which Level is the Lower Layer?
In a layered architecture, the lower layer is typically the one that provides foundational services or utilities that other layers depend on. The higher layers build upon the services provided by the lower layers.
In your example:
-
DisplayObject Level:
- This level represents the core objects that need to be drawn and queried.
- It can be considered a higher layer because it relies on the drawing and query layers to perform specific tasks.
-
Drawing Layer and Query Layer:
- These layers provide specific functionalities (drawing and querying) for the
DisplayObject
. - They can be considered lower layers because they provide services that the
DisplayObject
level depends on.
- These layers provide specific functionalities (drawing and querying) for the
Summary
- Placement of Interfaces: Interfaces like
IDrawable
andIQuery
can be placed in a separate module/package for better separation of concerns or within theDisplayObject
level if they are tightly coupled with its functionality. - Lower Layer: In your example, the drawing layer and query layer are considered lower layers because they provide services that the
DisplayObject
level depends on.
By organizing your code in this way, you can maintain a clear separation of concerns and manage dependencies effectively, ensuring that each layer has a well-defined role and responsibility.