Skip to content

Refactor package to use Singleton pattern#3

Open
awonak wants to merge 5 commits intomainfrom
singleton
Open

Refactor package to use Singleton pattern#3
awonak wants to merge 5 commits intomainfrom
singleton

Conversation

@awonak
Copy link
Owner

@awonak awonak commented Oct 7, 2022

Additional changes:

  • Reduce the amount of exported variables
  • Refactor away overuse of interfaces
  • Refactored DigitalReader to properly provide abstracted behavior to buttons and digital input
  • Improved package documentation through the EuroPi struct
  • provide instance of EuroPi as a Singleton via europi.GetInstance()

@awonak awonak mentioned this pull request Oct 14, 2022
analog_reader.go Outdated
// A struct for handling the reading of knob voltage and position.
type Knob struct {
type knob struct {
machine.ADC
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Careful with exposing a machine.ADC here. I'd unexport this field or switch to an interface. This will prevent from users depending on tinygo directly and make mock-tests possible.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll ditto that remark - in my fork of the project I've been fighting against the lack of testability quite often because things are so intrinsically linked to machine and other TinyGo packages.

My main personal failing until now is that I've just been building runtime testing apparatus frameworks instead of just abstracting away the TinyGo linkages.... though those don't hurt to have around for final-pass QA, I guess.

analog_reader.go Outdated
// The analogue input allows you to 'read' CV from anywhere between 0 and 12V.
type AnalogInput struct {
type analogInput struct {
machine.ADC
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as below, unexport or interface seems like a good idea.

type DigitalReader interface {
Handler(func(machine.Pin))
HandlerWithDebounce(func(machine.Pin), time.Duration)
Handler(callback)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an odd API design choice. having an unexported type in an exported interface. Do you want to define semantics on the callback type? Also be careful with how you use this interface- it's present in digitalInput and button as an exported embedded field on an unexported type which are present as an exported field on EuroPi, which means the underlying DigitalReader on the button and digitalInput is accesible to users to reference. I'd consider hiding this in button and digitalInput behind an unexported field and allowing users to set them on initialization.

b.debounceDelay = delay
b.Pin.SetInterrupt(machine.PinFalling, b.debounceWrapper)
type button struct {
DigitalReader
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexport this field maybe so that users can't access the field through EuroPi. I'd prefer something like this be set on EuroPi initialization, maybe more on that in another comment.

// Display is a wrapper around `ssd1306.Device` for drawing graphics and text to the OLED.
type Display struct {
type display struct {
ssd1306.Device
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another exported field on an unexported type which is readily accesible to users.

europi.go Outdated
}
}

func GetInstance() *EuroPi {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so when I said singletons are great... Ummm... is there a thing as taking things too far?

Hahaha, kidding aside this pattern is questionable from a configuration and mock-test standpoint. Since you only have one EuroPi ever and more importantly no ways to initialize it there's no way to mock EuroPi and also no way to configure it either. Consider returning to the New() function and letting the user say when they want to start the EuroPi.

The reason I draw the line on singletons on the EuroPi is because the EuroPi type is not a direct abstraction on hardware as is a PWM, I2C and such, but rather a type that holds the direct HW abstractions and operates on them. There is no reason for EuroPi to be a singleton really unless you really want it to be a singleton, which is OK, I guess.

}

// New will return a new EuroPi struct.
func New() *EuroPi {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you choose to return to New (see comment on singletons below), then we could get some neat function-callback-configuration parameter thing going on. This is what I'm talking about. So something like

knobNumber := 0
ep := europi.New(
    europi.WithKnob(knobNumber, mockADC), // each of these package level option functions returns *another* function!
    europi.WithButton(buttonNumber, mockDigitalReader),
    europi.WithDisplay(myHDDisplay),
)

This would be pretty neat since your API would then stay pretty much the same into the future, and if a user wanted to initialize it ez-pz they could just go ep := europi.New()

@soypat
Copy link

soypat commented Oct 15, 2022

I didn't get around to reading the whole PR, but here goes the main things I noticed I might have done differently, hope it helps!

Copy link

@soypat soypat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR is looking real good :) Let me know if you want help with anything

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants