SOLID programming

General discussion for topics related to the FreeBASIC project or its community.
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming

Post by paul doe »

We can see how the 'everything is an object' mentality pervades even the concepts of otherwise pretty good object-oriented authors, like Yegor Bugayenko: How much do you objects encapsulate?

Second, a good encapsulation does not allow you to 'manipulate' its attributes (even less if this 'attributes' modify the object's behavior), simply because it doesn't need to. Consider this example:

Code: Select all

type RGBAColor as ulong

type Ball
  public:
    declare constructor()
    declare destructor()
    
    declare property size() as integer
    declare property size( byval as integer )
    declare property color() as RGBAColor
    declare property color( byval as RGBAColor )
    declare property canBounce() as boolean
    declare property canBounce( byval as boolean )
    /'
      et. al.
    '/
  private:
    m_size as integer
    m_color as RGBAColor
    m_canBounce as boolean
end type
You may have seen code like this, perhaps you even code like this yourself. What's the problem, you may ask? The problem, as it turns out, is that this doesn't encapsulates anything, really. Everybody can access the objects innards and change the way it looks/behaves simply by setting its properties. This doesn't seems right, even if you're into the 'everything is an object' mindset: you can't simply look intensely at a red ball in real life and tell it 'become green' -unless you're Chuck Norris.

Even worse, your code may depend on mutable state, which effectively not only breaks encapsulation, but also violates the Dependency Inversion principle (since you're depending on a concretion, in this case an assumption about the object state); as a consecuence, your code become coupled. This is what I'm talking about:

Code: Select all

type SomeClass
  /'
    ...
  '/
  declare function doSomething() as integer
  /'
    ...
  '/
end type

/'
  Some code in another part of the codebase
'/
var aClass = SomeClass()
dim as integer aValue = aClass.doSomething()
That's it, that's all it takes to couple some piece of code to a specific implementation of a class, since the class doesn't derive (inherits) from an interface. This is another, very common case:

Code: Select all

type SomeClass
  /'
    ...
  '/
  declare property isCoupled() as boolean
  declare property isCoupled( byval as boolean )
  /'
    ...
  '/
end type

/'
  Some code in another part of the codebase
'/
var aClass = SomeClass()

if( aClass.isCoupled ) then
  /'
    ...
  '/
end if
This is even worse, because you're coupling code with mutable state, which is essentially equivalent to coupling code to shared variables:

Code: Select all

dim shared as boolean isCoupled = true

sub doSomething()
  if( isCoupled ) then
    /'
      ...
    '/
  end if
end sub
This is a leaky abstraction; from the OO standpoint, it means that the class encapsulation is poor. The class exposes too much state, and depending on this state couples the code. To decouple it, you can refactor the class to derive from an interface, and use the interface instead of the concrete instance:

Code: Select all

type ISomeClass extends Object
  declare abstract property isCoupled() as boolean
end type

type SomeClass extends ISomeClass
  /'
    ...
  '/
  declare property isCoupled() as boolean override
  /'
    ...
  '/
end type

property SomeClass.isCoupled() as boolean
  return( false )
end property

/'
  Some code in another part of the codebase
'/
dim as ISomeClass ptr aClass = new SomeClass()

if( aClass->isCoupled ) then
  ? "This code is coupled"
  /'
    ...
  '/
end if

sleep()

delete( aClass )
This solution is somewhat better, but the abstraction still leaks. The alternative? You can design the object to expose functionality instead of properties:

Code: Select all

type ISomeClass extends Object
  declare abstract sub doSomethingIfCoupled()
end type

type SomeClass extends ISomeClass
  public:
    /'
      ...
    '/
    declare sub doSomethingIfCoupled()
    /'
      ...
    '/
  private:
    m_isCoupled as boolean = true
end type

sub SomeClass.doSomethingIfCoupled()
  if( m_isCoupled = true ) then
    ? "Doing something"
  end if
end sub

/'
  Some code in another part of the codebase
'/
dim as ISomeClass ptr aClass = new SomeClass()

aClass->doSomethingIfCoupled()

sleep()

delete( aClass )
That way, the client code doesn't have to worry about details on the state of the object, and focus simply on interacting with it to solve a problem. Of course, each pattern makes sense in different contexts, there's not a 'one-size-fits-all' kind of solution to this. I prefer the second approach, but you may prefer the first one (it's more familiar, especially if you are proficient in a pure procedural approach). Another limitations come from the language itself: FreeBasic doesn't support friend classes nor friend members, so the only way to implement cooperating classes without breaking encapsulation is through read-only properties.
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming

Post by paul doe »

There are, however, some concerns. What about shared state? What if an object needs to communicate with another, and has to let it know its state? This is a very common pattern that arises from the need to share data between methods/procedures/functions. A very common case arises in games with the rendering component. Games are notorious for being quite complex systems, since you have concerns that cross-cut virtually every layer of the system (in-game GUIs, for example).

Consider this code:

Code: Select all

type RGBAColor as ulong

type Shape extends Object
  declare virtual destructor()
  
  declare abstract property width() as integer
  declare abstract property height() as integer
  
  declare abstract sub render( _
    byval as integer, _
    byval as integer, _
    byval as RGBAColor )
end type

destructor Shape()
end destructor

type Rectangle extends Shape
  public:
    declare constructor( _
      byval as integer, _
      byval as integer )
    declare destructor() override
    
    declare property width() as integer override
    declare property height() as integer override
    
    declare sub render( _
      byval as integer, _
      byval as integer, _
      byval as RGBAColor ) override
  
  private:
    declare constructor()
    
    m_width as integer
    m_height as integer
end type

constructor Rectangle()
end constructor

constructor Rectangle( _
  byval aWidth as integer, _
  byval aHeight as integer )
  
  m_width = iif( aWidth < 1, 1, aWidth )
  m_height = iif( aHeight < 1, 1, aHeight )
end constructor

destructor Rectangle()
end destructor

property Rectangle.width() as integer
  return( m_width )
end property

property Rectangle.height() as integer
  return( m_height )
end property

sub Rectangle.render( _
  byval x as integer, _
  byval y as integer, _
  byval aColor as RGBAColor )
  
  line( x, y ) - ( x + m_width - 1, y + m_height - 1 ), aColor, b
end sub

sub renderShape( _
  byval aShape as Shape ptr, _
  byval x as integer, _
  byval y as integer, _
  byval aColor as RGBAColor )
  
  aShape->render( x, y, aColor )
end sub

/'
  Main code
'/
screenRes( 800, 600, 32 )

var aRectangle = new Rectangle( 100, 100 )

renderShape( aRectangle, 300, 200, rgba( 255, 0, 0, 255 ) )

sleep()

delete( aRectangle )
The 'renderShape()' function could be in another, completely different layer of the application (let's call it the RenderLayer), and the object that gets rendered perhaps needs more data to render itself properly than a simple rectangle. So, we can already see a couple of potential problems. While the code is (somewhat) decoupled, what happens if I want to, say, derive a 'Circle' class from the 'Shape' interface? Not a problem, but perhaps I want to parameterize the Circle class using a center point and a radius, instead of a width and a height.

Also, we can see that the interface is not segregated. Computational geometry code does not have an use for the 'render()' method. So far, we have code that violates two S.O.L.I.D. principles: the Single Responsibility principle (since the Shape interface is required to have two responsibilities: representing a shape and rendering it), and the Interface Segregation principle (since the Shape interface forces derived classes to implement unrelated functionality). So, let's see if we could refactor them, and at the same time maintain the encapsulation:

Code: Select all

type RGBAColor as ulong

type Shape extends Object
  declare virtual destructor()
  
  declare abstract property width() as integer
  declare abstract property height() as integer
end type

destructor Shape()
end destructor

type RenderableShape extends Shape
  declare virtual destructor() override
  
  declare abstract sub render( _
    byval as integer, _
    byval as integer, _
    byval as RGBAColor )
end type

destructor RenderableShape()
end destructor

type Rectangle extends Shape
  public:
    declare constructor( _
      byval as integer, _
      byval as integer )
    declare destructor() override
    
    declare property width() as integer override
    declare property height() as integer override
    
  private:
    declare constructor()
    
    m_width as integer
    m_height as integer
end type

constructor Rectangle()
end constructor

constructor Rectangle( _
  byval aWidth as integer, _
  byval aHeight as integer )
  
  m_width = iif( aWidth < 1, 1, aWidth )
  m_height = iif( aHeight < 1, 1, aHeight )
end constructor

destructor Rectangle()
end destructor

property Rectangle.width() as integer
  return( m_width )
end property

property Rectangle.height() as integer
  return( m_height )
end property

type RenderableRectangle extends RenderableShape
  public:
    declare constructor( _
      byref as Rectangle )
    declare destructor() override
    
    declare property width() as integer override
    declare property height() as integer override
    
    declare sub render( _
      byval as integer, _
      byval as integer, _
      byval as RGBAColor )
  
  private:
    declare constructor()
    
    m_rectangle as Rectangle ptr
end type

constructor RenderableRectangle()
end constructor

constructor RenderableRectangle( _
  byref aRectangle as Rectangle )
  
  m_rectangle = @aRectangle
end constructor

destructor RenderableRectangle()
end destructor

property RenderableRectangle.width() as integer
  return( m_rectangle->width )
end property

property RenderableRectangle.height() as integer
  return( m_rectangle->height )
end property

sub RenderableRectangle.render( _
  byval x as integer, _
  byval y as integer, _
  byval aColor as RGBAColor )
  
  line( x, y ) - ( x + m_rectangle->width - 1, y + m_rectangle->height - 1 ), aColor, b
end sub

sub renderShape( _
  byval aShape as RenderableShape ptr, _
  byval x as integer, _
  byval y as integer, _
  byval aColor as RGBAColor )
  
  aShape->render( x, y, aColor )
end sub

/'
  Main code
'/
screenRes( 800, 600, 32 )

var aRectangle = Rectangle( 100, 100 )
var aRenderableRectangle = new RenderableRectangle( aRectangle )

renderShape( _
  aRenderableRectangle, _
  300, 200, rgba( 255, 0, 0, 255 ) )

sleep()

delete( aRenderableRectangle )
Now we have two interfaces, and one decorator that decorates the Rectangle class with rendering functionality. The interface has been segregated into two concerns. Note that the 'renderShape()' function now accepts a RenderableShape, instead of just a Shape. The decorator encapsulates a Rectangle instance (via constructor injection), and uses the exposed properties to render the rectangle on the screen.

That's better, but somehow it doesn't feels right. As you can see, the RenderableRectangle decorator has to tunnel the access to the width and height properties of the rectangle it decorates, so it can also pose transparently as a Shape. This is indeed part of the pattern; the problem, thus, is not in the decorator, but in how we laid out the interfaces. We established what the real problem was already (all objects conforming to the Shape interface must implement the width() and height() properties, which could not make sense for all objects). So, how we can improve on this design? Let's study a possible solution next.
dafhi
Posts: 1641
Joined: Jun 04, 2005 9:51

Re: SOLID programming

Post by dafhi »

As much as I would like to write a fully-functioning path tracer, say, up to the level of Disney's Hyperion renderer, I'm not sure it's Really what I want to do. Will write more when I can
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming

Post by paul doe »

Ok, what I ideally want is to be able to separate the responsibilities of an object from his representation. This way, we can keep the Shape interface minimal, and define additional interfaces that describe the operations we want for each object. This will keep the interfaces segregated and allow us to compose functionality into our objects as requested. Sounds good, but there's a problem (a language limitation, actually): FreeBasic won't allow us to do this.

Object-oriented languages like Java allow multiple inheritance but only from interfaces, so we can do what I described above. In FreeBasic, we only have single inheritance available (we don't even have the concept of 'interface' explicitly implemented yet), so we'll have to make do with what we have.

Decorators are a great way to achieve this: we can dynamically add/remove responsibilities (even at run-time), we can compose specific instances of objects with the behaviors we want (as opposed to statically extend a class to add the required behavior), and so we can even add classes of objects to our code in a plug-in fashion (using both the Abstract Factory and Prototype patterns), to create easily extensible applications.

For now, let us consider this simple design and see where it takes us:
Image

This is a UML diagram that shows the pattern I want. The interface IShape (I'm using the traditional 'I' prefix to denote interfaces) only defines two properties: x and y. This means I'm assuming a 'shape' will have a location in 2D space. This is also the minimum common information that all shapes in this model (that is, under the assumptions I mentioned earlier) will share.

The interface IRectangleShape defines specific representational information about a shape, in this case a rectangle. It is from this interface we will be deriving the concrete class Rectangle. Adding this layer of abstraction allows us to define other representations for a rectangle (for example, a point in space and two radii), or a representation for, say, a circle. And finally, at the bottom of the hierarchy is the concrete class Rectangle, derived from IRectangleShape, and this one contains the actual implementation details of the hierarchy for this particular class.

Now, on the right side of the diagram, we see another, different class hierarchy. At the top of it is the IRenderableShape, that defines a renderable shape. It has a render() method, that will execute the functionality needed to render a specific shape.

And then implementing this interface is a decorator, RenderableRectangle. This decorator aggregates exactly one instance of a IRectangleShape, and will actually perform the rendering of the shape.

Ok, the code for this design would be:

Code: Select all

/'
  Some colors (mostly for convenience and readability )
'/
type RGBAColor as ulong

namespace colors
  const as RGBAColor red      = rgba( 255,   0,   0, 255 )
  const as RGBAColor green    = rgba(   0, 255,   0, 255 )
  const as RGBAColor blue     = rgba(   0,   0, 255, 255 )
  const as RGBAColor yellow   = rgba( 255, 255,   0, 255 )
  const as RGBAColor cyan     = rgba(   0, 255, 255, 255 )
  const as RGBAColor magenta  = rgba( 255,   0, 255, 255 )
end namespace

/'
  Interface for all shapes
  
  Here, we'll put all the generic functionality that shapes will
  need to implement. 
'/
type IShape extends Object
  declare virtual destructor()
  
  x as integer
  y as integer  
  
  protected:
    '' This makes the class behaves as an interface ie. inherit-only
    declare constructor()
end type

constructor IShape()
end constructor

destructor IShape()
end destructor

/'
  Interface for all _rectangular_ shapes
'/
type IRectangleShape extends IShape
  declare virtual destructor() override
  
  declare abstract property width() as integer
  declare abstract property height() as integer  
end type

destructor IRectangleShape()
end destructor

/'
  Interface for all _renderable_ shapes
'/
type IRenderableShape extends Object
  declare virtual destructor()
  
  declare abstract sub render()
end type

destructor IRenderableShape()
end destructor

/'
  A concrete class that represents a rectangular IShape
'/
type Rectangle extends IRectangleShape
  public:
    declare constructor( _
      byval as integer, _
      byval as integer, _
      byval as integer, _
      byval as integer )
    declare destructor() override
    
    declare property width() as integer override
    declare property height() as integer override
    
  private:
    declare constructor()
    
    m_width as integer
    m_height as integer
end type

constructor Rectangle()
end constructor

constructor Rectangle( _
  byval newX as integer, _
  byval newY as integer, _
  byval aWidth as integer, _
  byval aHeight as integer )
  
  x = newX
  y = newY
  m_width = iif( aWidth < 1, 1, aWidth )
  m_height = iif( aHeight < 1, 1, aHeight )
end constructor

destructor Rectangle()
end destructor

property Rectangle.width() as integer
  return( m_width )
end property

property Rectangle.height() as integer
  return( m_height )
end property

/'
  A decorator that provides rendering functionality for
  the IRenderableShape interface
  
  This one renders rectangular shapes.
'/
type RenderableRectangle extends IRenderableShape
  public:
    declare constructor( _
      byref as IRectangleShape, _
      byval as RGBAColor )
    declare destructor() override
    
    declare sub render() override
    declare property decorated() as IRectangleShape ptr
    
  private:
    declare constructor()
    
    m_rectangle as IRectangleShape ptr
    m_color as integer
end type

constructor RenderableRectangle()
end constructor

constructor RenderableRectangle( _
  byref aRectangle as IRectangleShape, _
  byval aColor as RGBAColor )
  
  m_rectangle = @aRectangle
  m_color = aColor
end constructor

destructor RenderableRectangle()
end destructor

property RenderableRectangle.decorated() as IRectangleShape ptr
  return( m_rectangle )
end property

sub RenderableRectangle.render()
  line( m_rectangle->x, m_rectangle->y ) - _
    ( m_rectangle->x + m_rectangle->width - 1, m_rectangle->y + m_rectangle->height - 1 ), _
    m_Color, bf
end sub

sub renderShapes( theRenderableShapes() as IRenderableShape ptr )  
  /'
    And this could represent another layer (namely, the Rendering
    layer), tasked with rendering objects.
    
    All objects get processed in a generic (polymorphic) way.
  '/
  for i as integer = 0 to ubound( theRenderableShapes )
    theRenderableShapes( i )->render()
  next
end sub

/'
  Main code
'/
screenRes( 800, 600, 32 )

/'
  These objects will be the objects we want to process. By
  themselves, they don't do anything, they just 'represent'
  a IRectangleShape.
'/
var aRectangle = Rectangle( _
  300, 200, 100, 100 )
var anotherRectangle = Rectangle( _
  100, 100, 50, 50 )
  
/'
  So let's decorate them with functionality.
'/
dim as IRenderableShape ptr renderableShapes( ... ) = { _
  new RenderableRectangle( aRectangle, colors.blue ), _
  new RenderableRectangle( anotherRectangle, colors.red ) }

/'
  And then we pass them to an appropriate procedure whose
  responsibility is to render IRenderableShapes.
'/
renderShapes( renderableShapes() )

sleep()

/'
  Dispose of the explicitly created decorators
'/
for i as integer = 0 to ubound( renderableShapes )
  delete( renderableShapes( i ) )
next
Looks like a lot of work to render 2 rectangles on the screen, no? Actually, there's more to this implementation than meets the eye. We'll discuss some implementation details now.
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming

Post by paul doe »

@dafhi: Or perhaps the implementation is clear enough so I don't have to explain it in minute detail? Do you see where I'm going with this? Tell me what you think.
dafhi
Posts: 1641
Joined: Jun 04, 2005 9:51

Re: SOLID programming

Post by dafhi »

That should do it. If I can come up with an example of where I'm having difficulty, that'd probably make things easier. Thinking I'll be quite busy for the foreseeable future. It could be weeks or months before I'm back in the programming game.
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming

Post by paul doe »

Very well, then. We'll leave it at that for now. I'll post two more examples that round up the idea and leave it at that. Best of luck in your endeavours ;)
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming

Post by paul doe »

Well, I folded the two examples into one. It's a bit long, but it's straightforward and has some comments to help understand what's going on.

Code: Select all

#include once "fbgfx.bi"
/'
  Some colors (mostly for convenience and readability )
'/
type RGBAColor as ulong

namespace colors
  const red as RGBAColor = rgba( 255, 0, 0, 255 )
  const green as RGBAColor = rgba( 0, 255, 0, 255 )
  const blue as RGBAColor = rgba( 0, 0, 255, 255 )
  const yellow as RGBAColor = rgba( 255, 255, 0, 255 )
  const cyan as RGBAColor = rgba( 0, 255, 255, 255 )
  const magenta as RGBAColor = rgba( 255, 0, 255, 255 )
end namespace

/'
  Interface for all shapes
  
  Here, we'll put all the generic functionality that shapes will
  need to implement. 
'/
type IShape extends Object
  declare virtual destructor()
  
  x as single
  y as single  
  
  protected:
    '' This makes the class behaves as an interface ie. inherit-only
    declare constructor()
end type

constructor IShape()
end constructor

destructor IShape()
end destructor

/'
  Interface for all renderable shapes
'/
type IRenderableShape extends Object
  declare virtual destructor()
  
  declare abstract sub render()
end type

destructor IRenderableShape()
end destructor

/'
  Interface for all shapes that need updating
'/
type IUpdatableShape extends Object
  declare virtual destructor()
  
  declare abstract sub update()
end type

destructor IUpdatableShape()
end destructor

/'
  Interface for all rectangular shapes
'/
type IRectangleShape extends IShape
  declare virtual destructor() override
  
  declare abstract property width() as integer
  declare abstract property height() as integer  
end type

destructor IRectangleShape()
end destructor

/'
  Interface for all circular shapes
'/
type ICircleShape extends IShape
  declare virtual destructor()
  
  declare abstract property radius() as integer
end type

destructor ICircleShape()
end destructor

/'
  A concrete class that represents a rectangular shape
'/
type Rectangle extends IRectangleShape
  public:
    declare constructor( _
      byval as integer, _
      byval as integer, _
      byval as integer, _
      byval as integer )
    declare destructor() override
    
    declare property width() as integer override
    declare property height() as integer override
    
  private:
    declare constructor()
    
    m_width as integer
    m_height as integer
end type

constructor Rectangle()
end constructor

constructor Rectangle( _
  byval newX as integer, _
  byval newY as integer, _
  byval aWidth as integer, _
  byval aHeight as integer )
  
  x = newX
  y = newY
  m_width = iif( aWidth < 1, 1, aWidth )
  m_height = iif( aHeight < 1, 1, aHeight )
end constructor

destructor Rectangle()
end destructor

property Rectangle.width() as integer
  return( m_width )
end property

property Rectangle.height() as integer
  return( m_height )
end property

/'
  A decorator that provides rendering functionality for
  the IRenderableShape interface
  
  This one renders rectangular shapes.
'/
type RenderableRectangle extends IRenderableShape
  public:
    declare constructor( _
      byref as IRectangleShape, _
      byval as RGBAColor )
    declare destructor() override
    
    declare sub render() override
    declare property decorated() as IRectangleShape ptr
    
  private:
    declare constructor()
    
    m_rectangle as IRectangleShape ptr
    m_color as RGBAColor
end type

constructor RenderableRectangle()
end constructor

constructor RenderableRectangle( _
  byref aRectangle as IRectangleShape, _
  byval aColor as RGBAColor )
  
  m_rectangle = @aRectangle
  m_color = aColor
end constructor

destructor RenderableRectangle()
end destructor

property RenderableRectangle.decorated() as IRectangleShape ptr
  return( m_rectangle )
end property

sub RenderableRectangle.render()
  line( m_rectangle->x, m_rectangle->y ) - _
    ( m_rectangle->x + m_rectangle->width - 1, m_rectangle->y + m_rectangle->height - 1 ), _
    m_Color, bf
end sub

/'
  Another decorator, for a rectangle with borders
'/
type RenderableRectangleWithBorder extends IRenderableShape
  public:
    declare constructor( _
      byval as RenderableRectangle ptr, _
      byval as integer, _
      byval as RGBAColor )
    declare destructor() override
    
    declare sub render() override
  
  private:
    declare constructor()
    
    m_renderableRectangle as RenderableRectangle ptr
    m_borderSize as integer
    m_borderColor as RGBAColor
end type

constructor RenderableRectangleWithBorder()
end constructor

constructor RenderableRectangleWithBorder( _
  byval aRenderableRectangle as RenderableRectangle ptr, _
  byval aBorderSize as integer, _
  byval aBorderColor as RGBAColor )
  
  m_renderableRectangle = aRenderableRectangle
  m_borderSize = iif( aBorderSize < 1, 1, aBorderSize )
  m_borderColor = aBorderColor
end constructor

destructor RenderableRectangleWithBorder()
  delete( m_renderableRectangle )
end destructor

sub RenderableRectangleWithBorder.render()
  dim as integer halfSize = m_borderSize / 2
  /'
    Here you call the decorated instance for doing the base
    rendering. This is not necessary, but a 'true' decorator
    always calls the method of the instance it decorates.
  '/
  m_renderableRectangle->render()
  
  line( _
    m_renderableRectangle->decorated->x - halfSize, _
    m_renderableRectangle->decorated->y - halfSize ) - ( _
    m_renderableRectangle->decorated->x + m_renderableRectangle->decorated->width - 1 + halfSize, _
    m_renderableRectangle->decorated->y + m_renderableRectangle->decorated->height - 1 + halfSize ), _
    m_borderColor, b
    
  line( _
    m_renderableRectangle->decorated->x + halfSize, _
    m_renderableRectangle->decorated->y + halfSize ) - ( _
    m_renderableRectangle->decorated->x + m_renderableRectangle->decorated->width - 1 - halfSize, _
    m_renderableRectangle->decorated->y + m_renderableRectangle->decorated->height - 1 - halfSize ), _
    m_borderColor, b
  
  paint( _
    m_renderableRectangle->decorated->x, _
    m_renderableRectangle->decorated->y ), _
    m_borderColor, m_borderColor
end sub

/'
  A concrete class that represents a circle
'/
type Circle extends ICircleShape
  public:
    declare constructor( _
      byval as integer, _
      byval as integer, _
      byval as integer )
    declare destructor() override
    
    declare property radius() as integer override
  
  private:
    declare constructor()
    
    m_radius as integer
end type

constructor Circle()
end constructor

constructor Circle( _
  byval newX as integer, _
  byval newY as integer, _
  byval aRadius as integer )
  
  x = newX
  y = newY
  m_radius = aRadius
end constructor

destructor Circle()
end destructor

property Circle.radius() as integer
  return( m_radius )
end property

/'
  This decorator adds rendering functionality for a circular
  shape.
'/
type RenderableCircle extends IRenderableShape
  public:
    declare constructor( _
      byref as ICircleShape, _
      byval as RGBAColor )
    declare destructor() override
    
    declare sub render() override
    declare property decorated() as ICircleShape ptr
    
  private:
    declare constructor()
    
    m_circle as ICircleShape ptr
    m_color as RGBAColor
end type

constructor RenderableCircle()
end constructor

constructor RenderableCircle( _
  byref aCircle as ICircleShape, _
  byval aColor as RGBAColor )
  
  m_circle = @aCircle
  m_color = aColor
end constructor

destructor RenderableCircle()
end destructor

property RenderableCircle.decorated() as ICircleShape ptr
  return( m_circle )
end property

sub RenderableCircle.render()
  circle( _
    m_circle->x, _
    m_circle->y ), m_circle->radius, m_color, , , , f
end sub

/'
  This decorator adds moving functionality to the shape; in
  this case, to move the shape with the arrow keys.
'/
type MovableWithKeyboard extends IUpdatableShape
  public:
    declare constructor( _
      byref as IShape )
    declare destructor() override
    
    declare sub update() override
  
  private:
    declare constructor()
    
    m_shape as IShape ptr
end type

constructor MovableWithKeyboard()
end constructor

constructor MovableWithKeyboard( _
  byref aShape as IShape )
  
  m_shape = @aShape
end constructor

destructor MovableWithKeyboard()
end destructor

sub MovableWithKeyboard.update()
  if( multikey( fb.SC_UP ) ) then
    m_shape->y -= 1
  end if
  
  if( multikey( fb.SC_DOWN ) ) then
    m_shape->y += 1
  end if

  if( multikey( fb.SC_LEFT ) ) then
    m_shape->x -= 1
  end if
  
  if( multikey( fb.SC_RIGHT ) ) then
    m_shape->x += 1
  end if
end sub

/'
  This decorator adds moving functionality to the shape; in
  this case, to move the shape with the mouse.
'/
type MovableWithMouse extends IUpdatableShape
  public:
    declare constructor( _
      byref as IShape )
    declare destructor() override
    
    declare sub update() override
  
  private:
    declare constructor()
    
    m_shape as IShape ptr
end type

constructor MovableWithMouse()
end constructor

constructor MovableWithMouse( _
  byref aShape as IShape )
  
  m_shape = @aShape
end constructor

destructor MovableWithMouse()
end destructor

sub MovableWithMouse.update()
  dim as integer mx, my
  
  getMouse( mx, my )
  
  m_shape->x = mx
  m_shape->y = my
end sub

/'
  This decorator moves the shape in a random direction, keeping it
  within the given bounds.
'/
type BoundedMovement extends IUpdatableShape
  public:
    declare constructor( _
      byref as IShape, _
      byval as integer, _
      byval as integer, _
      byval as integer, _
      byval as integer )
    declare destructor() override
    
    declare sub update() override
  
  private:
    declare constructor()
    
    m_shape as IShape ptr
    m_top as integer
    m_left as integer
    m_right as integer
    m_bottom as integer
    m_directionX as single
    m_directionY as single
end type

constructor BoundedMovement()
end constructor

constructor BoundedMovement( _
  byref aShape as IShape, _
  byval topLimit as integer, _
  byval leftLimit as integer, _
  byval rightLimit as integer, _
  byval bottomLimit as integer )
  
  m_shape = @aShape
  m_top = topLimit
  m_left = leftLimit
  m_right = rightLimit
  m_bottom = bottomLimit
  m_directionX = ( rnd() * 2.0 ) - 1.0
  m_directionY = ( rnd() * 2.0 ) - 1.0
end constructor

destructor BoundedMovement()
end destructor

sub BoundedMovement.update()
  m_shape->x += m_directionX
  m_shape->y += m_directionY
  
  if( m_shape->x < m_left ) then
    m_shape->x = m_left
    m_directionX = -m_directionX
  end if
  
  if( m_shape->x > m_right ) then
    m_shape->x = m_right
    m_directionX = -m_directionX
  end if

  if( m_shape->y < m_top ) then
    m_shape->y = m_top
    m_directionY = -m_directionY
  end if
  
  if( m_shape->y > m_bottom ) then
    m_shape->y = m_bottom
    m_directionY = -m_directionY
  end if
end sub

/'
  And these represent other layers of the application. They need
  not be functions, they can also be classes, each with its own
  processing model.
'/
sub renderShapes( theRenderableShapes() as IRenderableShape ptr )  
  '' Render shapes
  for i as integer = 0 to ubound( theRenderableShapes )
    theRenderableShapes( i )->render()
  next
end sub

sub updateShapes( theUpdatableShapes() as IUpdatableShape ptr )
  '' Update shapes
  for i as integer = 0 to ubound( theUpdatableShapes )
    theUpdatableShapes( i )->update()
  next
end sub

/'
  Main code
'/
screenRes( 800, 600, 32 )
randomize()

/'
  These objects will be the objects we want to process. By
  themselves, they don't do anything, they just 'represent'
  a shape. I put them into separate lists for clarity (to
  avoid downcasting), but you can also put them into a single
  list. Of course, 'list' here means just an array.
'/
dim as Rectangle rectangles( ... ) = { _
  Rectangle( 300, 200, 100, 100 ), _
  Rectangle( 100, 120, 50, 50 ), _
  Rectangle( 500, 500, 80, 50 ) }

dim as Circle circles( ... ) = { _
  Circle( 200, 50, 30 ), _
  Circle( 500, 400, 50 ), _
  Circle( 50, 500, 100 ) }
  
/'
  These two arrays hold decorated instances of objects, that are
  passed to the appropriate processing functions.
'/
dim as IRenderableShape ptr renderableShapes( ... ) = { _
  new RenderableRectangle( rectangles( 0 ), colors.blue ), _
  new RenderableRectangleWithBorder( _
    new RenderableRectangle( rectangles( 1 ), colors.red ), _
    5, colors.yellow ), _
  new RenderableRectangleWithBorder( _
    new RenderableRectangle( rectangles( 2 ), colors.cyan ), _
    2, colors.magenta ), _
  new RenderableCircle( circles( 0 ), colors.green ), _
  new RenderableCircle( circles( 1 ), colors.yellow ) }

dim as IUpdatableShape ptr updatableShapes( ... ) = { _
  new MovableWithKeyboard( rectangles( 0 ) ), _
  new MovableWithMouse( circles( 0 ) ), _
  new BoundedMovement( _
    rectangles( 1 ), _
    0, 0, 799, 599 ) }
  
/'
  Main loop
  
  Nothing fancy, just render and update shapes. However, do note that
  not all shapes get rendered, and not all shapes get updates; only
  the ones that matter (the ones we specified in the two arrays above)
  are passed to the functions that consume them.
'/
do
  updateShapes( updatableShapes() )
  
  screenLock()
    cls()
    renderShapes( renderableShapes() )
  screenUnlock()
  
  sleep( 1, 1 )
loop until( multikey( fb.SC_ESCAPE ) )

/'
  Dispose of the explicitly created decorators
'/
for i as integer = 0 to ubound( renderableShapes )
  delete( renderableShapes( i ) )
next

for i as integer = 0 to ubound( updatableShapes )
  delete( updatableShapes( i ) )
next
Use the arrow keys to move a shape and the mouse to move another. There's a shape that moves automatically, and two shapes that don't move at all.

Well, that should wrap some basic concepts about encapsulation and interfaces. Do UML Class Diagrams help anybody to better grasp the concepts? If it does, I'll post the class diagram for this snippet also.
Last edited by paul doe on Sep 28, 2018 10:59, edited 2 times in total.
dafhi
Posts: 1641
Joined: Jun 04, 2005 9:51

Re: SOLID programming

Post by dafhi »

your source is interesting enough that I'll read it before running. thanks a bunch!

[edit] m_color as int, line 156
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming

Post by paul doe »

dafhi wrote:[edit] m_color as int, line 156
Corrected, thanks.
dodicat
Posts: 7976
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: SOLID programming

Post by dodicat »

I have something like this in squares.
But I have converted it to inheritance.
No need for any other oop features.
The 3d shapes are not actually SOLID, but full of air, so I apologise if I am off topic.

Code: Select all

 

'======  globals ====
Type temp As Point Ptr 'advance notice
Dim Shared lightsource As temp
Const pi=4*Atn(1)
Dim Shared As Integer xres,yres
Screen 19,32,,64  'screen 20 or 19 or 17 
Color Rgb(200,200,200),Rgb(0,0,55)
Screeninfo xres,yres
#define farpoint type<point>(xres\2,yres\2,1000) 'eyepoint
Randomize 10
'======================

Declare Function Fmain() As Long
'run
End Fmain

'types
Type Point 
    As Single x,y,z
    Declare  Function rotate(As Point,As Point,As Point=Type<Point>(1,1,1)) As Point
    Declare  Function perspective(As Point=farpoint) As Point
    Declare  Function dot(As Point) As Single
End Type

Type plane 
    As Point p(Any)
    Declare Sub Draw(As Ulong)
    Declare Static Sub fill(() As Point,As Ulong,As Long,As Long)
End Type

Type Shape 'needs point and plane
    As plane    f(Any)      'faces
    As Point norm(Any)   'normals to faces
    As Ulong  clr(Any)    'colours
    Declare Sub setarrays(As Long,As Long,s() As Point)'set the above in constructors
    As Point centre      'centroid
    As Point aspect      'orientation in space
    As Point d           ' speed (dx,dy,dz)
    Declare Sub spin(As Point)                         'spin about centroid
    Declare Sub translate(v As Point,s As Double)      'shift and blow
    Declare Function rotate(As Point,As Point) As Shape 'roatate about a chosen point
    Declare Static Sub bsort(() As Shape )              'bubblesort (fast enough for a small number of shapes)
    Declare Sub Draw
End Type

Type cube  Extends Shape
    Declare Constructor
End Type

Type tetra Extends Shape
    Declare Constructor
End Type

Type square Extends Shape
    Declare Constructor  
End Type

'==========   methods for shape =========
Sub shape.setarrays(uboundf As Long,uboundp As Long,s() As Point)
    Redim f(1 To uboundf)
    Redim norm(1 To uboundf)
    Redim clr(1 To uboundf)
    For n As Long=1 To uboundf
        Redim (f(n).p)(1 To uboundp)'faces vertices
    Next
    For n As Long=1 To uboundf
        clr(n)=Rgb(Rnd*255,Rnd*255,Rnd*255) 'set a default colour
        For m As Long=1 To uboundp
            f(n).p(m)= s(n,m)   'set to s() 
        Next m
    Next n
End Sub

Sub Shape.spin(p As Point)
    Dim As Shape tmp=This
    For n As Long=1 To Ubound(f)
        For m As Long=1 To Ubound(f(n).p)
            tmp.f(n).p(m)=this.f(n).p(m).rotate(centre,p)
            tmp.f(n).p(m)=tmp.f(n).p(m).perspective()
        Next
        tmp.norm(n)=tmp.norm(n).rotate(centre,p)'normals spin also 
    Next
    tmp.draw
End Sub

Sub Shape.draw
    Static As Ubyte Ptr col
    For n As Long=1 To Ubound(f)-1
        For m As Long=n+1 To Ubound(f)
            If norm(n).z<norm(m).z Then
                Swap f(n),f(m)
                Swap norm(n),norm(m)
                Swap clr(n),clr(m)
            End If
        Next m
    Next n
    #define map(a,b,x,c,d) ((d)-(c))*((x)-(a))/((b)-(a))+(c)
    For n As Long=1 To Ubound(f)
        col=Cptr(Ubyte Ptr,@clr(n))
        Dim As Single cx=norm(n).x-centre.x,cy=norm(n).y-centre.y,cz=norm(n).z-centre.z
        Dim As Point k=Type<Point>(cx,cy,cz)
        Dim As Single dt=k.dot(*lightsource)
        dt=map(1,-1,dt,.3,1)
        f(n).draw(Rgba(dt*col[2],dt*col[1],dt*col[0],col[3]))
    Next n
End Sub

Sub Shape.translate(v As Point,s As Double)
    For n As Long=1 To Ubound(f)
        norm(n).x*=s
        norm(n).y*=s
        norm(n).z*=s
        For m As Long=1 To Ubound(f(n).p)
            f(n).p(m).x*=s
            f(n).p(m).y*=s
            f(n).p(m).z*=s
        Next m
    Next n
    For n As Long=1 To Ubound(f)
        norm(n).x=norm(n).x+v.x
        norm(n).y=norm(n).y+v.y
        norm(n).z=norm(n).z+v.z
        For m As Long=1 To Ubound(f(n).p)
            f(n).p(m).x= f(n).p(m).x+v.x 
            f(n).p(m).y= f(n).p(m).y+v.y
            f(n).p(m).z= f(n).p(m).z+v.z
        Next m
    Next n
    centre.x+=v.x
    centre.y+=v.y
    centre.z+=v.z
End Sub

Function Shape.rotate(c As Point,ang As Point) As Shape
    Dim As Shape tmp=This
    For n As Long=1 To Ubound(f)
        For m As Long=1 To Ubound(f(n).p)
            tmp.f(n).p(m)=this.f(n).p(m).rotate(c,ang)
        Next
        tmp.norm(n)=this.norm(n).rotate(c,ang)  
    Next
    tmp.centre=this.centre.rotate(c,ang)
    Return tmp
End Function

Sub Shape.bsort(c() As Shape)
    For n As Long=Lbound(c) To Ubound(c)-1
        For m As Long=n+1 To Ubound(c)
            If c(n).centre.z<c(m).centre.z Then Swap c(n),c(m)
        Next
    Next
End Sub

'======================  methods for point ====================
Function point.dot(v2 As Point) As Single 'dot product |v1| * |v2| *cos(angle between v1 and v2)
    Dim As Single d1=Sqr(x*x + y*y+ z*z),d2=Sqr(v2.x*v2.x + v2.y*v2.y +v2.z*v2.z)
    Dim As Single v1x=x/d1,v1y=y/d1,v1z=z/d1 'normalize
    Dim As Single v2x=v2.x/d2,v2y=v2.y/d2,v2z=v2.z/d2 'normalize
    Return (v1x*v2x+v1y*v2y+v1z*v2z) 
End Function

Function point.Rotate(c As Point,angle As Point,scale As Point) As Point
    Dim As Single sx=Sin(angle.x),sy=Sin(angle.y),sz=Sin(angle.z)
    Dim As Single cx=Cos(angle.x),cy=Cos(angle.y),cz=Cos(angle.z)
    Dim As Single dx=this.x-c.x,dy=this.y-c.y,dz=this.z-c.z
    Return Type<Point>((scale.x)*((cy*cz)*dx+(-cx*sz+sx*sy*cz)*dy+(sx*sz+cx*sy*cz)*dz)+c.x,_
    (scale.y)*((cy*sz)*dx+(cx*cz+sx*sy*sz)*dy+(-sx*cz+cx*sy*sz)*dz)+c.y,_
    (scale.z)*((-sy)*dx+(sx*cy)*dy+(cx*cy)*dz)+c.z)',p.col)
End Function

Function point.perspective(eyepoint As Point) As Point
    Dim As Single   w=1+(this.z/eyepoint.z)
    Return Type<Point>((this.x-eyepoint.x)/w+eyepoint.x,_
    (this.y-eyepoint.y)/w+eyepoint.y,_
    (this.z-eyepoint.z)/w+eyepoint.z)
End Function  

' ================    methods for plane  ===================

Sub plane.fill(a() As Point, c As Ulong,min As Long,max As Long)
    Static As Long i,j,k,dy,dx, x,y
    Static As Long NewX (1 To Ubound(a))
    Static As Single Grad(1 To Ubound(a))
    For i=1 To Ubound(a) - 1 
        dy=a(i+1).y-a(i).y
        dx=a(i+1).x-a(i).x
        If(dy=0) Then Grad(i)=1
        If(dx=0) Then Grad(i)=0
        If ((dy <> 0) And (dx <> 0)) Then
            Grad(i) = dx/dy
        End If
    Next i
    For y=min To max
        k = 1
        For i=1 To Ubound(a) - 1
            If( ((a(i).y<=y) Andalso (a(i+1).y>y)) Or ((a(i).y>y) _
            Andalso (a(i+1).y<=y))) Then
            NewX(k)= Int(a(i).x+ Grad(i)*(y-a(i).y))
            k +=1
        End If
    Next i
    For j = 1 To k-2
        For i = 1 To k-2
            If NewX(i) > NewX(i+1) Then Swap  NewX(i),NewX(i+1)
        Next i
    Next j
    For i = 1 To k - 2 Step 2
        Line (NewX(i),y)-(NewX(i+1)+1,y), c
    Next i
Next y
End Sub

Sub plane.draw(clr As Ulong )
    Static As Long miny=1e6,maxy=-1e6
    Redim As Point V1(1 To  Ubound(p)+1)
    Dim As Long n
    For n =1 To Ubound(p)
        If miny>p(n).y Then miny=p(n).y
        If maxy<p(n).y Then maxy=p(n).y
        V1(n)=p(n) 
    Next
    v1(Ubound(v1))=p(Lbound(p))
    plane.fill(v1(),clr,miny,maxy)
End Sub

'construct three shapes around origin (0,0,0)
Constructor cube
Static As Point g(1 To ...,1 To ...)= _    
{{(-1,-1,-1),(1,-1,-1),(1,1,-1),(-1,1,-1)},_'front
{(1,-1,-1),(1,-1,1),(1,1,1),(1,1,-1)},_     'right
{(-1,-1,1),(1,-1,1),(1,1,1),(-1,1,1)},_     'back
{(-1,-1,-1),(-1,-1,1),(-1,1,1),(-1,1,-1)},_ 'left
{(1,1,-1),(1,1,1),(-1,1,1),(-1,1,-1)},_     'top
{(1,-1,-1),(1,-1,1),(-1,-1,1),(-1,-1,-1)}}  'base
setarrays(6,4,g())   '6 faces,4 face vertices
norm(1)=Type(0,0,-1) 'face normals to cube
norm(2)=Type(1,0,0)
norm(3)=Type(0,0,1)
norm(4)=Type(-1,0,0)
norm(5)=Type(0,1,0)
norm(6)=Type(0,-1,0)
centre=Type(0,0,0) 
aspect=Type(Rnd*2*pi,Rnd*2*pi,Rnd*2*pi) 
For n As Long=1 To Ubound(f)
    norm(n)=norm(n).rotate(centre,aspect)
    For m As Long=1 To Ubound(f(n).p)
        f(n).p(m)=f(n).p(m).rotate(centre,aspect)
    Next
Next
'speeds
d=Type((Rnd-Rnd)/50,(Rnd-Rnd)/50,(Rnd-Rnd)/50)
End Constructor

Constructor tetra
Static As Point t(1 To ...,1 To ...)= _                                               
{{(-1,-1/Sqr(3),-1/Sqr(6)),(1,-1/Sqr(3),-1/Sqr(6)),(0,2/Sqr(3),-1/Sqr(6))}, _ 'b
{(-1,-1/Sqr(3),-1/Sqr(6)),(1,-1/Sqr(3),-1/Sqr(6)),(0,0,3/Sqr(6))},_           'f
{(1,-1/Sqr(3),-1/Sqr(6)),(0,2/Sqr(3),-1/Sqr(6)),(0,0,3/Sqr(6))}, _            'r
{(-1,-1/Sqr(3),-1/Sqr(6)),(0,2/Sqr(3),-1/Sqr(6)),(0,0,3/Sqr(6))}}             'l
setarrays(4,3,t())'4 faces,3 face vertices
norm(1)=Type(0, 0,-0.4082483)
norm(2)=Type(0,-0.3849002, 0.1360828)
norm(3)=Type(0.3333333, 0.1924501, 0.1360828)
norm(4)=Type(-0.3333333, 0.1924501, 0.1360828)
centre=Type(0,0,0)
aspect=Type(Rnd*2*pi,Rnd*2*pi,Rnd*2*pi) 
For n As Long=1 To Ubound(f)
    norm(n)=norm(n).rotate(centre,aspect)
    For m As Long=1 To Ubound(f(n).p)
        f(n).p(m)=f(n).p(m).rotate(centre,aspect)
    Next
Next
'speeds
d=Type((Rnd-Rnd)/50,(Rnd-Rnd)/50,(Rnd-Rnd)/50)
End Constructor

Constructor square
Static As Point s(1 To ...,1 To ...)= _  
{{(-1,-1,0),(1,-1,0),(1,1,0),(-1,1,0)}}
setarrays(1,4,s())'1 faces,4 face vertices
norm(1)=Type(0,0,1)
centre=Type(0,0,0)
aspect=Type(Rnd*2*pi,Rnd*2*pi,Rnd*2*pi) 
For n As Long=1 To 1
    norm(n)=norm(n).rotate(centre,aspect)
    For m As Long=1 To 4
        f(n).p(m)=f(n).p(m).rotate(centre,aspect)
    Next
Next
'speeds
d=Type((Rnd-Rnd)/50,(Rnd-Rnd)/50,(Rnd-Rnd)/50)
End Constructor

'================   end methods  ======================
'independent functions
Function Regulate(Byval MyFps As Long,Byref fps As Long) As Long
    Static As Double timervalue,lastsleeptime,t3,frames
    Var t=Timer
    frames+=1
    If (t-t3)>=1 Then t3=t:fps=frames:frames=0
    Var sleeptime=lastsleeptime+((1/myfps)-T+timervalue)*1000
    If sleeptime<1 Then sleeptime=1
    lastsleeptime=sleeptime
    timervalue=T
    Return sleeptime
End Function

Function Fmain() As Long
                lightsource=New Point(0,1,0)
Dim As Shape Ptr c(1 To 9)={New cube,New cube,New cube,New cube,New cube, _
                            New tetra,New tetra, _ 
                            New square,New square}
    c(8)->clr(1)=Rgba(255,0,0,100)'set transparent colour for this square                       
    Dim As Shape  tmp(Lbound(c) To Ubound(c))
    Dim As Single cx=xres\2,cy=yres\2
    'set the screen positions and sizes
    c(1)->translate(Type(cx-.5*cx,cy-.6*cy,0),cx/10) 'cube
    c(2)->translate(Type(cx+.5*cx,cy-.6*cy,0),cx/10) 'cube
    c(3)->translate(Type(cx+.5*cx,cy+.6*cy,0),cx/10) 'cube
    c(4)->translate(Type(cx-.5*cx,cy+.6*cy,0),cx/10) 'cube
    c(5)->translate(Type(cx,cy,0),cx/5)              'centre cube
    c(6)->translate(Type(cx,cy-.75*cy,0),cx/10)      'tetra
    c(7)->translate(Type(cx,cy+.75*cy,0),cx/10)      'tetra
    c(8)->translate(Type(cx-.75*cy,cy,0),cx/10)      'square
    c(9)->translate(Type(cx+.75*cy,cy,0),cx/10)      'square
    Dim As Double pi2=2*pi
    
    For n As Long=Lbound(c) To Ubound(c)
        *c(n)=c(n)->rotate(Type(xres\2,yres\2,0),Type(0,pi/2,0)) 'flip 90 (set up for rotation).
    Next
    
    Dim As Point a
    'start angles y and z
    a.y=-pi/9
    a.z= pi/2
    
    Dim As Long fps
    #define fmod(x,y) y*Frac(x/y)
    #define circ(n) c(n)->aspect.x=fmod(c(n)->aspect.x,pi2)
    Dim As String key
    Do
        key=Inkey 
        If key=Chr(255)+"P" Then a.y-=.05   'down
        If key=Chr(255)+"H" Then a.y+=.05   'up 
        If key=Chr(255)+"K" Then a.z-=.05   'right
        If key=Chr(255)+"M" Then a.z+=.05   'left
        If key=" " Then a.z=pi/2:a.y=-pi/9  'reset
        
        Screenlock
        Cls
        a.x+=.01:a.x=fmod(a.x,pi2) 'keep within 2*pi
        Draw String (20,20),"Cubes tetrahedrons and plates. use arrow and space keys"
        Draw String (20,50),"FPS = " &fps
        
        For n As Long=Lbound(c) To Ubound(c)
            tmp(n)=c(n)->rotate(Type(xres\2,yres\2,0),a) 'rotate shapes about screen cenre
        Next
        Shape.bsort(tmp()) 'sort by centre.z
        For n As Long=Lbound(tmp) To Ubound(tmp)'advance aspect and spin.
            c(n)->aspect.x+=c(n)->d.x:circ(n)   'keep within 2*pi
            c(n)->aspect.y+=c(n)->d.y:circ(n)
            c(n)->aspect.z+=c(n)->d.z:circ(n)
            tmp(n).spin(tmp(n).aspect)
        Next
        Screenunlock
        Sleep regulate(90,fps),1
    Loop Until key=Chr(27)
    Sleep
    For i As Long=Lbound(c) To Ubound(c)
        Delete c(i)
    Next
    Delete lightsource
    Return 0
End Function

  
dafhi
Posts: 1641
Joined: Jun 04, 2005 9:51

Re: SOLID programming

Post by dafhi »

very nice. thanks for sharing
adele
Posts: 47
Joined: Jun 13, 2015 19:33

Re: SOLID programming; Off topic: Diagrams

Post by adele »

Hi paul doe,
paul doe wrote: (a diagram)
Please pardon me. But: Could you please tell me which program you used to create the good looking diagram stored on "http://imgbox.com/5sDpyzbd" ? Thank you ! Adi [trying to be shortest]
paul doe
Moderator
Posts: 1730
Joined: Jul 25, 2017 17:22
Location: Argentina

Re: SOLID programming; Off topic: Diagrams

Post by paul doe »

adele wrote:Please pardon me. But: Could you please tell me which program you used to create the good looking diagram stored on "http://imgbox.com/5sDpyzbd" ? Thank you ! Adi
Hi, Adi

Sure. The tool in question is UMLet. Free, lightweight, easy to use. It uses markup for specifying the format, but it comes with some prefabs that you can use to learn it (documentation is, unfortunately, scarce; but with a little experimentation, you can mostly figure it out). You're welcome.
adele
Posts: 47
Joined: Jun 13, 2015 19:33

Re: SOLID programming; Off topic: Diagrams

Post by adele »

paul doe wrote: Sure. The tool in question is UMLet.
Thank you very much! Adi
Post Reply