0

I am using the Flyweight pattern to cache and reuse objects of the different classes. For example, I have a Shape interface class and multiple types of Shapes implementing the methods from the interface. I also have a factory class that has the cache of object types and object as key value.

As you can see, there is a Draw method implemented in each of the derived classes but the problem is for each of my classes I want to have draw methods with different signatures.

eg for Rectangle, draw method takes length and breadth and for circle, only one argument radius is needed.

Using the same Shape interface, is there any way or design pattern to implement different Draw signatures in the derived classes.

Expected function definition

void Rectangle::Draw(int length, int breadth, int color) void Circle::Draw(int radius, int color) 

Current implementation

class Shape { public: virtual void Draw() const = 0; }; class Circle : public Shape { public: Circle() { sleep(5); std::cout<<"Creation of circle object took 5s"<<std::endl; } void Draw() const override { std::cout<<"Drawing circle of radius"<<std::endl; } }; class Rectangle : public Shape { public: Rectangle() { sleep(7); std::cout<<"Creation of Rectangle object took 7s"<<std::endl; } void Draw() const override { std::cout<<"Drawing Rectangle of length and breadth"<<std::endl; } }; enum class ShapeTypes { Circles = 1, Rectangles }; class Factory { std::map <ShapeTypes, Shape*> shapeObjectCache; public: Shape* GetObject(ShapeTypes shapeType) { if(shapeObjectCache.find(shapeType) != shapeObjectCache.end()) { std::cout<<"Object of type " << static_cast<int>(shapeType) << " found in cache: "<<std::endl; return shapeObjectCache[shapeType]; } switch(shapeType) { case ShapeTypes::Circles: shapeObjectCache.insert({ ShapeTypes::Circles, new Circle() }); break; case ShapeTypes::Rectangles: shapeObjectCache.insert({ ShapeTypes::Rectangles, new Rectangle() }); break; } return shapeObjectCache[shapeType]; } ~Factory() { for (auto& entry : shapeObjectCache) { delete entry.second; } } }; int main() { Factory factory; Shape *circle1 = factory.GetObject(ShapeTypes::Circles); circle1->Draw(); Shape *rectangle1 = factory.GetObject(ShapeTypes::Rectangles); rectangle1->Draw(); Shape *circle2 = factory.GetObject(ShapeTypes::Circles); circle2->Draw(); return 0; } 

Some suggested: why not create them as class member variable and pass the value in the constructor?

The problem with this approach is I am storing the objects in cache and since they will have some initial state set, I won't be able to reuse those objects and call the draw method and hence this will not be a flyweight pattern anymore.

4
  • This screams for a parameter object passed to Draw(), but I am too lazy to type out an actual answer.
    – xofz
    CommentedJul 23, 2023 at 7:13
  • 3
    Your example is too contrived, specifically the main method. What you finally need is a caller for your Draw methods which can pass different values, depending on the type of shape. But this caller needs to get that different data from somewhere - and where is this? Some list of ShapeData objects? These objects will finally need as much memory as the data requires, which is exactly the same amount as if you would store the data directly as members insider the Shape objects. So your whole "Flyweight" approach stops making any sense. Please clarify.
    – Doc Brown
    CommentedJul 23, 2023 at 7:14
  • In case if the constructor of concrete class is taking time and the initial state of object occupies too much memeory it wouldn't make sense to keep creating object of the concrete shape classes, hence the flyweight design pattern. Now the task of draw is just to draw some shape on the screen for the given object.CommentedJul 23, 2023 at 7:16
  • @Himanshuman: as I wrote, in your example, the state of a shape object needs exactly the amount of memory required (there isn't much to save). And your "cache" already makes sure the constructors are not run more often than necessary. Please edit your question and try turn your example into one where we can see how you intent to benefit from a flyweight, don't leave this to our imagination. (And please use @ to adress me, otherwise I don't see your responds in my inbox).
    – Doc Brown
    CommentedJul 23, 2023 at 8:50

1 Answer 1

6

I think you're running into problems here because you are combining the flyweight pattern with an inheritance hierarchy (or maybe a strategy pattern?), in a way that these two patterns negate each other's benefits.

Maybe the original description of the flyweight pattern in the Design Patterns book is to blame, since it presents the flyweight pattern in an object-oriented manner, with both the FlyweightFactory and the Flyweight featuring inheritance and virtual operations. This is unnecessary. The flyweight pattern is solely about reusing objects.

Why you might have a Shape/Circle/Rectangle class hierarchy

Before we continue to the flyweight pattern, we must discuss your hierarchy of objects.

If you want to draw stuff, you could just invoke ordinary functions:

draw_circle(canvas, 5.0); draw_rectangle(canvas, 2.3, 7.8); draw_circle(canvas, 2.1); 

The benefit of using OOP techniques here is that we can define a collection of shapes that know how to draw themselves. For example, we could iterate over shapes and draw all of them, without having to know their specific type:

void draw_all(Canvas& canvas, std::vector<const Shape*>& shapes) { for (auto shape : shapes) shape->Draw(canvas); } 

But since we don't know the shape's specific type, we cannot provide information like radius or width to the draw method – instead, the Shapes must already know all of this information.

For example, your Circle class would have to be defined as:

class Circle: public Shape { public: Circle(double radius) : radius(radius) {} void Draw(Canvas&) const override { ... } private: double radius; }; 

Selecting an appropriate flyweight key

The idea of the flyweight pattern is that we can reduce memory usage by caching created objects and reusing them. There are a couple of consequences from this:

  • Each flyweight object contains some intrinsic state (possibly empty).
  • Multiple consumers might reference the same object (reuse is the entire point), so that this intrinsic state should not change – the flyweight object should be immutable.
  • We need some way to determine whether we already have a matching flyweight instance, or whether we have to create a new one. The Design Patterns book calls this the key.

Currently, your instances do not have any state, and as the key you are using an enum that describe the type (circle or rectangle).

However, as explained in the previous section, you likely want those shapes to have all necessary info for drawing themselves, including their sizes. So the key cannot just be a type, it must also contain the size information.

Let's focus just on circles for a moment. A circle flyweight factory might look like the following:

class Factory { public: const Circle* GetCircle(double radius) { auto& entry = cache[radius]; if (!entry) entry = std::make_unique<Circle>(radius); return entry.get(); } private: std::map<double, std::unique_ptr<Circle>> cache; }; 

(Note that I've made the return type const to avoid state modifications that would break the cache.)

Similarly, we might create a GetRectangle() method, except that here the key would have to contain both the width and height.

Could we also adapt the GetShape() method? Not really. At some point, you have to know what you're trying to create or configure. You cannot make everything 100% generic and dynamic, you need per-type code somewhere. The caller of GetShape() would have to know whether they want a circle or a rectangle in order to provide the necessary parameters, so they could just call the GetCircle() or GetRectangle() method directly.

I've typed up a full example in the Godbolt compiler explorer: https://godbolt.org/z/PTGe7nTKo

You probably don't need the flyweight pattern

The main benefit of the flyweight pattern is to reduce memory pressure when you have lots of small objects that often have the same internal state, and these objects must also be heap-allocated for whatever reason.

However, C++ provides alternative mechanisms in case your requirements are only part of this.

For example, if you are concerned about the performance overhead of heap allocations, then you can overload operator new to implement an optimized allocation strategy – often possible about as fast as a stack allocation using an arena allocator.

You might not even need heap allocations. Dealing with values instead of pointers is often much faster and more memory efficient, potentially needing less memory than the flyweight pattern. Here, a value-oriented solution could be created by getting rid of the Shape interface, and instead using std::variant, e.g. defining using Shape = std::variant<Circle, Rectangle>. It would not be possible to invoke a draw method on the Shape directly, but we could std::visit each shape and invoke the proper method. This would also allow you to provide type-specific context, as the different shapes no longer require a compatible interface. However, this limits extensibility in certain directions – it would no longer be possible to just create a new Shape subclass.

Here's a sketch of the C++17 visitor/variant-based solution: https://godbolt.org/z/sEqej1Efq

5
  • How clear and concise do you need the answer to be? @amon : yesCommentedJul 23, 2023 at 9:26
  • 2
    "However, this limits extensibility in certain directions – it would no longer be possible to just create a new Shape subclass." - +1 for pointing that out. It's the good old "expression problem". To expand on it a little bit: with the visitor, the tradeoff is that it's hard to add new types of shapes, but easy to add new methods (other than draw) by creating new visitor classes. However, for that to be true, the Draw method should be implemented in the visitor itself, and the shape classes should be relatively anemic and expose shape-specific properties... 1/2CommentedJul 24, 2023 at 15:14
  • 1
    ...(so that clients can write their code abstractly in terms of the variant, while the visitor is able to "see" concrete shapes). With the design in the code snippet you posted, one would have to both create a new visitor and add new operations to the shape classes. And just for completeness and the benefit of other readers - the polymorphic OO solution with the shape interface results in a tradeoff that has opposite pros & cons - easy to add new shapes, but harder to add new operations (you have to update all the subclasses once the interface is updated). 2/2CommentedJul 24, 2023 at 15:14
  • P.S. @ amon & @Himanshuman (see comments above) - something like this (code snippet) (excuse my rusty C++, but you get the gist - same general idea as what amon posted, but with changes applied in light of what I talked about above).CommentedJul 24, 2023 at 15:50
  • 1
    Since @Himanshuman wasn't able to explain where and how they actually maintain the shape's parameters (if not inside the Shape objects themselves), and how a flyweight should help them to save memory , I am pretty sure they don't need the flyweight pattern - not because C++ has "better alternatives", but because their use case does probably not benefit from it. I am under the impression the whole idea of using the Flyweight here is misguided.
    – Doc Brown
    CommentedJul 26, 2023 at 7:48

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.