Dart Behavioral OOP Patterns Are State Patterns
Is this not obvious that OOP behavioral patterns are state patterns?
Honestly, I do not know how to write about OOP other than to describe it and show it. And to be blunt honest I can never ever comprehend the UML diagrams anyway. I might change the Dart intro chapter to more examples instead.
But anyway, the behavioral OOP patterns are actually the patterns we use to create state behaviors in our apps.
Chan Of Responsibility
The Chain of Responsibility design pattern is one of the twenty-three well-known GoF design patterns that describe common solutions to recurring design problems when designing flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.
What problems can the Chain of Responsibility design pattern solve?
Coupling the sender of a request to its receiver should be avoided. It should be possible that more than one receiver can handle a request. Implementing a request directly within the class that sends the request is inflexible because it couples the class to a particular receiver and makes it impossible to support multiple receivers.
What solution does the Chain of Responsibility design pattern describe?
Define a chain of receiver objects having the responsibility, depending on run-time conditions, to either handle a request or forward it to the next receiver on the chain (if any). This enables us to send a request to a chain of receivers without having to know which one handles the request. The request gets passed along the chain until a receiver handles the request. The sender of a request is no longer coupled to a particular receiver.
An example is the logger, where the logger is the processing unit and the command objects are of course the individual log events. And in such a situation we can have more than one logger with each logger specifying what command events it responsd to. Here is an example in dart of basic logging:
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:chain_of_resp/console_logger.dart'; | |
import 'package:chain_of_resp/email_logger.dart'; | |
import 'package:chain_of_resp/file_logger.dart'; | |
import 'package:chain_of_resp/log_level.dart'; | |
void main() { | |
var logger = ConsoleLogger(Set.from(LogLevel.values)); | |
var eLog = EmailLogger({LogLevel.functionalMessage, LogLevel.functionalError}); | |
var fLog = FileLogger({LogLevel.warning, LogLevel.error}); | |
logger.next = eLog; | |
eLog.next = fLog; | |
logger.log(LogLevel.debug, "Some amazingly helpful debug message"); | |
logger.log(LogLevel.info, "Pretty important information"); | |
logger.log(LogLevel.warning, "This is a warning!"); | |
logger.log(LogLevel.error, "AHHHHHHHHH!"); | |
logger.log(LogLevel.functionalError, "This is not a show stopper"); | |
logger.log(LogLevel.functionalMessage, "This is basically just info"); | |
/* | |
[Console]: Some amazingly helpful debug message | |
[Console]: Pretty important information | |
[Console]: This is a warning! | |
[File]: This is a warning! | |
[Console]: AHHHHHHHHH! | |
[File]: AHHHHHHHHH! | |
[Console]: This is not a show stopper | |
[Email]: This is not a show stopper | |
[Console]: This is basically just info | |
[Email]: This is basically just info | |
*/ | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:chain_of_resp/logger.dart'; | |
class ConsoleLogger extends Logger { | |
ConsoleLogger(super.levels); | |
@override | |
void writeMessage(String msg) => print("[Console]: $msg"); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:chain_of_resp/logger.dart'; | |
class EmailLogger extends Logger { | |
EmailLogger(super.levels); | |
@override | |
void writeMessage(String msg) => print("[Email]: $msg"); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:chain_of_resp/logger.dart'; | |
class FileLogger extends Logger { | |
FileLogger(super.levels); | |
@override | |
void writeMessage(String msg) => print("[File]: $msg"); | |
} |
enum LogLevel { | |
none, | |
info, | |
debug, | |
warning, | |
error, | |
functionalMessage, | |
functionalError, | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:chain_of_resp/log_level.dart'; | |
abstract class Logger { | |
late Set<LogLevel> levels; | |
late Logger _next; | |
Logger(this.levels); | |
bool get universal => levels.containsAll(LogLevel.values); | |
set next(Logger nextLogger) => _next = nextLogger; | |
void addLevel(LogLevel level) => levels.add(level); | |
void log(LogLevel level, String msg) { | |
if (levels.contains(level) || universal) { | |
writeMessage(msg); | |
} | |
_next.log(level, msg); | |
} | |
void writeMessage(String msg); | |
} |
Command
The command design pattern is one of the twenty-three well-known GoF design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.
Using the command design pattern can solve these problems:
Coupling the invoker of a request to a particular request should be avoided. That is, hard-wired requests should be avoided.
It should be possible to configure an object (that invokes a request) with a request.
Implementing (hard-wiring) a request directly into a class is inflexible because it couples the class to a particular request at compile-time, which makes it impossible to specify a request at run-time.
Using the command design pattern describes the following solution:
Define separate (command) objects that encapsulate a request.
A class delegates a request to a command object instead of implementing a particular request directly.
This enables one to configure a class with a command object that is used to perform a request. The class is no longer coupled to a particular request and has no knowledge (is independent) of how the request is carried out.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:command/light.dart'; | |
import 'package:command/light_switch.dart'; | |
void main() { | |
var myFavoriteLamp = Light(); | |
var iotLightSwitch = LightSwitch(myFavoriteLamp); | |
iotLightSwitch.perform("on"); | |
iotLightSwitch.perform("off"); | |
iotLightSwitch.perform("blink"); | |
iotLightSwitch.perform("on"); | |
print("\r\n*** Fancy IoT Switch Logs ***\r\n${iotLightSwitch.history}"); | |
/* | |
Light on! | |
Light off! | |
Uh...wait, wut? | |
Light on! | |
*** Fancy IoT Switch Logs *** | |
[2019-06-20 08:00:38.880050] Executed Turn on | |
[2019-06-20 08:00:38.883495] Executed Turn off | |
[2019-06-20 08:00:38.883702] Executed Turn on | |
*/ | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:command/receiver.dart'; | |
abstract class Command { | |
late Receiver receiver; | |
late String name; | |
Command(this.receiver); | |
@override | |
String toString() => name; | |
void execute(); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:command/command.dart'; | |
class Invoker { | |
List<String> history = []; | |
void execute(Command cmd) { | |
cmd.execute(); | |
history.add("[${DateTime.now()}] Executed $cmd"); | |
} | |
@override | |
String toString() => history.fold("", (events, event) => "$events$event\r\n"); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:command/receiver.dart'; | |
class Light implements Receiver { | |
void turnOff() => print("Light off!"); | |
void turnOn() => print("Light on!"); | |
@override | |
Set<String> get actions => {"off", "on"}; | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:command/invoker.dart'; | |
import 'package:command/light.dart'; | |
import 'package:command/turn_off_command.dart'; | |
import 'package:command/turn_on_command.dart'; | |
class LightSwitch { | |
final Invoker _switch = Invoker(); | |
Light light; | |
LightSwitch(this.light); | |
String get history => _switch.toString(); | |
void perform(String action) { | |
if (!light.actions.contains(action)) { | |
return print("Uh...wait, wut?"); | |
} | |
switch (action) { | |
case "on": | |
return _switch.execute(TurnOnCommand(light)); | |
case "off": | |
return _switch.execute(TurnOffCommand(light)); | |
} | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
abstract class Receiver { | |
Set<String> get actions; | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: overridden_fields | |
import 'package:command/command.dart'; | |
import 'package:command/light.dart'; | |
class TurnOnCommand extends Command { | |
@override | |
String name = "Turn on"; | |
TurnOnCommand(Light super.light); | |
@override | |
void execute() { | |
(receiver as Light).turnOn(); | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: overridden_fields | |
import 'package:command/command.dart'; | |
import 'package:command/light.dart'; | |
class TurnOffCommand extends Command { | |
@override | |
String name = "Turn off"; | |
TurnOffCommand(Light super.light); | |
@override | |
void execute() { | |
(receiver as Light).turnOff(); | |
} | |
} |
Interpreter
The Interpreter design pattern is one of the twenty-three well-known GoF design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.
What problems can the Interpreter design pattern solve?
A grammar for a simple language should be defined so that sentences in the language can be interpreted.
When a problem occurs very often, it could be considered to represent it as a sentence in a simple language
(Domain Specific Languages) so that an interpreter can solve the problem by interpreting the sentence.
For example, when many different or complex search expressions must be specified. Implementing (hard-wiring) them directly into a class is inflexible because it commits the class to particular expressions and makes it impossible to specify new expressions or change existing ones independently from (without having to change) the class.
What solution does the Interpreter design pattern describe?
Define a grammar for a simple language by defining an Expression class hierarchy and implementing an interpret() operation.
Represent a sentence in the language by an abstract syntax tree (AST) made up of Expression instances.
Interpret a sentence by calling interpret() on the AST.
The expression objects are composed recursively into a composite/tree structure that is called abstract syntax tree (see Composite pattern). The Interpreter pattern doesn't describe how to build an abstract syntax tree. This can be done either manually by a client or automatically by a parser.
Uses
Specialized database query languages such as SQL.
Specialized computer languages that are often used to describe communication protocols.
Most general-purpose computer languages actually incorporate several specialized languages[citation needed].
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:interpreter/expression.dart'; | |
import 'package:interpreter/number.dart'; | |
class Add implements Expression { | |
@override | |
Number number; | |
Add(this.number); | |
@override | |
void interpret(int value) { | |
print("Adding $value..."); | |
number.value += value; | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:interpreter/number.dart'; | |
abstract class Expression { | |
late Number number; | |
void interpret(int value); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:interpreter/add.dart'; | |
import 'package:interpreter/aubtract.dart'; | |
import 'package:interpreter/number.dart'; | |
void main() { | |
var number = Number(0); | |
var adder = Add(number); | |
var subtracter = Subtract(number); | |
adder.interpret(100); | |
subtracter.interpret(60); | |
adder.interpret(2); | |
assert(number.value == 42); | |
print("And the result is ${number.value}!"); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
class Number { | |
late int value; | |
Number(this.value) { | |
print("Starting with $value..."); | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:interpreter/expression.dart'; | |
import 'package:interpreter/number.dart'; | |
class Subtract implements Expression { | |
@override | |
Number number; | |
Subtract(this.number); | |
@override | |
void interpret(int value) { | |
print("Subtracting $value..."); | |
number.value -= value; | |
} | |
} |
Iterator
The Iterator design pattern is one of the 23 well-known "Gang of Four" design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.
What problems can the Iterator design pattern solve?
The elements of an aggregate object should be accessed and traversed without exposing its representation (data structures).
New traversal operations should be defined for an aggregate object without changing its interface.
Defining access and traversal operations in the aggregate interface is inflexible because it commits the aggregate to particular access and traversal operations and makes it impossible to add new operations later without having to change the aggregate interface.
What solution does the Iterator design pattern describe?
Define a separate (iterator) object that encapsulates accessing and traversing an aggregate object.
Clients use an iterator to access and traverse an aggregate without knowing its representation (data structures).
Different iterators can be used to access and traverse an aggregate in different ways.
New access and traversal operations can be defined independently by defining new iterators.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:iterator/rainbow_iterator.dart'; | |
void main() { | |
var rainbowColors = RainbowIterator(); | |
while (rainbowColors.moveNext()) { | |
print(rainbowColors.current); | |
} | |
/* | |
Red | |
Orange | |
Yellow | |
Green | |
Blue | |
Indigo | |
Violet | |
*/ | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
class RainbowIterator implements Iterator { | |
final _colors = ["Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet"]; | |
var _index = 0; | |
@override | |
String get current => _colors[_index++]; | |
@override | |
bool moveNext() => _index < _colors.length; | |
} |
Mediator
The mediator design pattern is one of the twenty-three well-known design patterns that describe how to solve recurring design problems to design flexible and reusable object-oriented software, that is, objects that are easier to implement, change, test, and reuse.
Problems that the mediator design pattern can solve:
Tight coupling between a set of interacting objects should be avoided.
It should be possible to change the interaction between a set of objects independently.
Defining a set of interacting objects by accessing and updating each other directly is inflexible because it tightly couples the objects to each other and makes it impossible to change the interaction independently from (without having to change) the objects. And it stops the objects from being reusable and makes them hard to test.
Tightly coupled objects are hard to implement, change, test, and reuse because they refer to and know about many different objects.
Solutions described by the mediator design pattern:
Define a separate (mediator) object that encapsulates the interaction between a set of objects.
Objects delegate their interaction to a mediator object instead of interacting with each other directly.
The objects interact with each other indirectly through a mediator object that controls and coordinates the interaction.
This makes the objects loosely coupled. They only refer to and know about their mediator object and have no explicit knowledge of each other.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:mediator/stateful.dart'; | |
class Attendee extends Stateful { | |
String name; | |
Attendee(this.name); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:mediator/attendee.dart'; | |
import 'package:mediator/mediator.dart'; | |
void main() { | |
var curly = Attendee("Curly"); | |
var larry = Attendee("Larry"); | |
var moe = Attendee("I prefer not to disclose my name"); | |
var mixer = List<Attendee>.from([curly, larry, moe]); | |
var publicAnnouncementSystem = Mediator(mixer); | |
publicAnnouncementSystem.update("Do NOT eat the shrip tacos!"); | |
for (var person in mixer) { | |
print("${person.name} heard \"${person.state}\"."); | |
} | |
/* | |
Curly heard "Do NOT eat the shrip tacos!". | |
Larry heard "Do NOT eat the shrip tacos!". | |
I prefer not to disclose my name heard "Do NOT eat the shrip tacos!". | |
*/ | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:mediator/stateful.dart'; | |
class Mediator { | |
final List<Stateful> _parties; | |
Mediator(this._parties); | |
void update(String state) { | |
for (var party in _parties) { | |
party.state = state; | |
} | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: unnecessary_getters_setters | |
abstract class Stateful { | |
late String _state; | |
String get state => _state; | |
set state(String newState) => _state = newState; | |
} |
Memento
The memento pattern is implemented with three objects: the originator, a caretaker and a memento. The originator is some object that has an internal state. The caretaker is going to do something to the originator, but wants to be able to undo the change. The caretaker first asks the originator for a memento object. Then it does whatever operation (or sequence of operations) it was going to do. To roll back to the state before the operations, it returns the memento object to the originator. The memento object itself is an opaque object (one which the caretaker cannot, or should not, change). When using this pattern, care should be taken if the originator may change other objects or resources—the memento pattern operates on a single object.
Classic examples of the memento pattern include a pseudorandom number generator (each consumer of the PRNG serves as a caretaker who can initialize the PRNG (the originator) with the same seed (the memento) to produce an identical sequence of pseudorandom numbers) and the state in a finite state machine.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:memento/care_taker.dart'; | |
import 'package:memento/originator.dart'; | |
void main() { | |
var me = Originator("Returned from store"); | |
me.state = "Placing car keys down"; | |
var locationOfKeys = me.saveToMemento(); | |
var memory = CareTaker(); | |
memory.memento = locationOfKeys; | |
me.state = "Putting away groceries"; | |
me.state = "Noticed missing pickles"; | |
me.state = "Looking for car keys..."; | |
me.restoreFromMemento(memory.memento); | |
me.state = "Going back to store for pickles"; | |
/* | |
[Originator] Set state to Returned from store | |
[Originator] Set state to Placing car keys down | |
[Originator] Saving to memento... | |
[Memento] State "Placing car keys down" has been saved! | |
[Originator] Set state to Putting away groceries | |
[Originator] Set state to Noticed missing pickles | |
[Originator] Set state to Looking for car keys... | |
[Originator] Restoring from memento... | |
[Memento] Providing saved state "Placing car keys down"... | |
[Originator] Restored to Placing car keys down. | |
[Originator] Set state to Going back to store for pickles | |
*/ | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:memento/memento.dart'; | |
class CareTaker { | |
late Memento memento; | |
} |
class Memento { | |
late String _state; | |
Memento(String s) { | |
_state = s; | |
print("[Memento] State \"$s\" has been saved!"); | |
} | |
String get state { | |
print("[Memento] Providing saved state \"$_state\"..."); | |
return _state; | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:memento/memento.dart'; | |
class Originator { | |
late String _state; | |
// NOTE: This uses the state setter on init to get a handy print | |
Originator(String s) { | |
state = s; | |
} | |
String get state => _state; | |
set state(String newState) { | |
_state = newState; | |
print("[Originator] Set state to $newState"); | |
} | |
Memento saveToMemento() { | |
print("[Originator] Saving to memento..."); | |
return Memento(state); | |
} | |
void restoreFromMemento(Memento m) { | |
print("[Originator] Restoring from memento..."); | |
_state = m.state; | |
print("[Originator] Restored to $state."); | |
} | |
} |
Observer
In software design and engineering, the observer pattern is a software design pattern in which an object, named the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.
It is often used for implementing distributed event-handling systems in event-driven software. In such systems, the subject is usually named a "stream of events" or "stream source of events" while the observers are called "sinks of events." The stream nomenclature alludes to a physical setup in which the observers are physically separated and have no control over the emitted events from the subject/stream source. This pattern thus suits any process by which data arrives from some input that is not available to the CPU at startup, but instead arrives seemingly at random (HTTP requests, GPIO data, user input from peripherals and distributed databases, etc.).
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:observer/coffee_maker.dart'; | |
import 'package:observer/observer.dart'; | |
void main() { | |
var me = Observer("Tyler"); | |
var mrCoffee = CoffeeMaker(List.from([me])); | |
var myWife = Observer("Kate"); | |
mrCoffee.registerObserver(myWife); | |
mrCoffee.brew(); | |
/* | |
Brewing the coffee... | |
[2019-06-18T07:30:04.397518] Hey Tyler, coffee's done! | |
[2019-06-18T07:30:04.397518] Hey Kate, coffee's done! | |
*/ | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:observer/notification.dart'; | |
import 'package:observer/observable.dart'; | |
class CoffeeMaker extends Observable { | |
CoffeeMaker([super.observers]); | |
void brew() { | |
print("Brewing the coffee..."); | |
notifyObservers(Notification.forNow("coffee's done")); | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
class Notification { | |
late String message; | |
late DateTime timestamp; | |
Notification(this.message, this.timestamp); | |
Notification.forNow(this.message) { | |
timestamp = DateTime.now(); | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:observer/notification.dart'; | |
import 'package:observer/observer.dart'; | |
class Observable { | |
late List<Observer> _observers; | |
Observable([List<Observer>? observers]) { | |
_observers = observers ?? []; | |
} | |
void registerObserver(Observer observer) { | |
_observers.add(observer); | |
} | |
void notifyObservers(Notification notification) { | |
for (var observer in _observers) { | |
observer.notify(notification); | |
} | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:observer/notification.dart'; | |
class Observer { | |
late String name; | |
Observer(this.name); | |
void notify(Notification notification) { | |
print("[${notification.timestamp.toIso8601String()}] Hey $name, ${notification.message}!"); | |
} | |
} |
State
The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines. The state pattern can be interpreted as a strategy pattern, which is able to switch a strategy through invocations of methods defined in the pattern's interface.
The state pattern is used in computer programming to encapsulate varying behavior for the same object, based on its internal state. This can be a cleaner way for an object to change its behavior at runtime without resorting to conditional statements and thus improve maintainability.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:state/stateful.dart'; | |
import 'package:state/status_off.dart'; | |
void main() { | |
var lightSwitch = Stateful(StatusOff()); | |
print("The light switch is ${lightSwitch.state}."); | |
print("Toggling the light switch..."); | |
lightSwitch.touch(); | |
print("The light switch is ${lightSwitch.state}."); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:state/stateful.dart'; | |
abstract class State { | |
void handler(Stateful context); | |
@override | |
String toString(); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: unnecessary_getters_setters | |
import 'package:state/state.dart'; | |
class Stateful { | |
State _state; | |
Stateful(this._state); | |
State get state => _state; | |
set state(State newState) => _state = newState; | |
void touch() { | |
print(" Touching the Stateful..."); | |
_state.handler(this); | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:state/state.dart'; | |
import 'package:state/stateful.dart'; | |
import 'package:state/status_on.dart'; | |
class StatusOff implements State { | |
@override | |
handler(Stateful context) { | |
print(" Handler of StatusOff is being called!"); | |
context.state = StatusOn(); | |
} | |
@override | |
String toString() { | |
return "off"; | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:state/state.dart'; | |
import 'package:state/stateful.dart'; | |
import 'package:state/status_off.dart'; | |
class StatusOn implements State { | |
@override | |
handler(Stateful context) { | |
print(" Handler of StatusOn is being called!"); | |
context.state = StatusOff(); | |
} | |
@override | |
String toString() { | |
return "on"; | |
} | |
} |
Strategy
In computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
Strategy lets the algorithm vary independently from clients that use it. Strategy is one of the patterns included in the influential book Design Patterns by Gamma et al. that popularized the concept of using design patterns to describe how to design flexible and reusable object-oriented software. Deferring the decision about which algorithm to use until runtime allows the calling code to be more flexible and reusable.
For instance, a class that performs validation on incoming data may use the strategy pattern to select a validation algorithm depending on the type of data, the source of the data, user choice, or other discriminating factors. These factors are not known until run-time and may require radically different validation to be performed. The validation algorithms (strategies), encapsulated separately from the validating object, may be used by other validating objects in different areas of the system (or even different systems) without code duplication.
Typically, the strategy pattern stores a reference to some code in a data structure and retrieves it. This can be achieved by mechanisms such as the native function pointer, the first-class function, classes or class instances in object-oriented programming languages, or accessing the language implementation's internal storage of code via reflection.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:strategy/coffee_strategy.dart'; | |
class AmericanoStrategy implements CoffeeStrategy { | |
@override | |
String announce(String roast) => "an Americano with $roast beans"; | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:strategy/american_strategy.dart'; | |
import 'package:strategy/coffee_drinker.dart'; | |
import 'package:strategy/drip_strategy.dart'; | |
import 'package:strategy/moca_frappuccino_strategy.dart'; | |
void main() { | |
var americano = AmericanoStrategy(); | |
var drip = DripStrategy(); | |
var mocha = MochaFrappuccinoStrategy(); | |
var me = CoffeeDrinker("Tyler", drip); | |
var europeanBuddy = CoffeeDrinker("Pieter", americano); | |
var myDaughter = CoffeeDrinker("Joanie", mocha); | |
final String roastOfTheDay = "Italian"; | |
for (var person in [me, europeanBuddy, myDaughter]) { | |
print("Hey ${person.name}, whatcha drinkin' over there?"); | |
print("I'm enjoying ${person.preferredDrink.announce(roastOfTheDay)}!\r\n"); | |
} | |
/* | |
Hey Tyler, whatcha drinkin' over there? | |
I'm enjoying a drip coffee with Italian beans! | |
Hey Pieter, whatcha drinkin' over there? | |
I'm enjoying an Americano with Italian beans! | |
Hey Joanie, whatcha drinkin' over there? | |
I'm enjoying a delicious mocha frappuccion with Italian beans! | |
*/ | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:strategy/coffee_strategy.dart'; | |
class CoffeeDrinker { | |
CoffeeStrategy preferredDrink; | |
String name; | |
CoffeeDrinker(this.name, this.preferredDrink); | |
} |
abstract class CoffeeStrategy { | |
String announce(String roast); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:strategy/coffee_strategy.dart'; | |
class DripStrategy implements CoffeeStrategy { | |
@override | |
String announce(String roast) => "a drip coffee with $roast beans"; | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:strategy/coffee_strategy.dart'; | |
class MochaFrappuccinoStrategy implements CoffeeStrategy { | |
@override | |
String announce(String roast) => "a delicious mocha frappuccion with $roast beans"; | |
} |
Template Method
In object-oriented programming, the template method is one of the behavioral design patterns identified by Gamma et al.in the book Design Patterns. The template method is a method in a superclass, usually an abstract superclass, and defines the skeleton of an operation in terms of a number of high-level steps. These steps are themselves implemented by additional helper methods in the same class as the template method.
The helper methods may be either abstract methods, in which case subclasses are required to provide concrete implementations, or hook methods, which have empty bodies in the superclass. Subclasses can (but are not required to) customize the operation by overriding the hook methods. The intent of the template method is to define the overall structure of the operation, while allowing subclasses to refine, or redefine, certain steps.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
abstract class Abstract { | |
String abstractMethod(); | |
String hookMethod() => "OMG I am a hook!"; | |
void templateMethod() { | |
print(abstractMethod()); | |
print(hookMethod()); | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:template_method/abstract.dart'; | |
class ConcreteOverridingHook extends Abstract { | |
@override | |
String abstractMethod() => "So, so boring..."; | |
@override | |
String hookMethod() => "I'm an overriden hook method!"; | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:template_method/abstract.dart'; | |
class Concrete extends Abstract { | |
@override | |
String abstractMethod() => "This is a boring example..."; | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:template_method/concrete.dart'; | |
import 'package:template_method/concrete_overriding_hook.dart'; | |
void main() { | |
var con1 = Concrete(); | |
var con2 = ConcreteOverridingHook(); | |
con1.templateMethod(); | |
con2.templateMethod(); | |
/* | |
This is a boring example... | |
OMG I am a hook! | |
So, so boring... | |
I'm an overriden hook method! | |
*/ | |
} |
Visitor
A visitor pattern is a software design pattern that separates the algorithm from the object structure. Because of this separation new operations can be added to existing object structures without modifying the structures. It is one way to follow the open/closed principle in object-oriented programming and software engineering.
In essence, the visitor allows adding new virtual functions to a family of classes, without modifying the classes. Instead, a visitor class is created that implements all of the appropriate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch.
Programming languages with sum types and pattern matching obviate many of the benefits of the visitor pattern, as the visitor class is able to both easily branch on the type of the object and generate a compiler error if a new object type is defined which the visitor does not yet handle.
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:visitor/body_part.dart'; | |
import 'package:visitor/body_part_visitor.dart'; | |
import 'package:visitor/face.dart'; | |
import 'package:visitor/finger.dart'; | |
import 'package:visitor/foot.dart'; | |
class Body implements BodyPart { | |
@override | |
String name = "Body"; | |
late List<BodyPart> bodyParts; | |
Body() { | |
bodyParts = List.from( | |
[Face(), Finger("Index Finger"), Finger("Thumb"), Foot("Left Foot"), Foot("Right Foot"), Foot("Third Foot?")]); | |
} | |
@override | |
void accept(BodyPartVisitor visitor) { | |
for (var bodyPart in bodyParts) { | |
bodyPart.accept(visitor); | |
} | |
visitor.visit(this); | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:visitor/body_part_visitor.dart'; | |
abstract class BodyPart { | |
late String name; | |
void accept(BodyPartVisitor visitor) => visitor.visit(this); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:visitor/body.dart'; | |
import 'package:visitor/body_part.dart'; | |
import 'package:visitor/body_part_visitor.dart'; | |
import 'package:visitor/face.dart'; | |
import 'package:visitor/finger.dart'; | |
import 'package:visitor/foot.dart'; | |
class BodyPartProdVisitor implements BodyPartVisitor { | |
@override | |
void visit(BodyPart bodyPart) { | |
if (bodyPart is Body) { | |
print("Poking my ${bodyPart.name} with an electrode."); | |
} | |
if (bodyPart is Face) { | |
print("Prodding ${bodyPart.name}."); | |
} | |
if (bodyPart is Finger) { | |
print("Pricking ${bodyPart.name}."); | |
} | |
if (bodyPart is Foot) { | |
print("Tickling ${bodyPart.name}."); | |
} | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:visitor/body.dart'; | |
import 'package:visitor/body_part.dart'; | |
import 'package:visitor/body_part_visitor.dart'; | |
import 'package:visitor/face.dart'; | |
import 'package:visitor/finger.dart'; | |
import 'package:visitor/foot.dart'; | |
class BodyPartStitchVisitor implements BodyPartVisitor { | |
@override | |
void visit(BodyPart bodyPart) { | |
if (bodyPart is Body) { | |
print("Reanimating my ${bodyPart.name}."); | |
} | |
if (bodyPart is Face) { | |
print("Stitching together my ${bodyPart.name}."); | |
} | |
if (bodyPart is Finger) { | |
print("Stitching together my ${bodyPart.name}."); | |
} | |
if (bodyPart is Foot) { | |
print("Gluing on my ${bodyPart.name}."); | |
} | |
} | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:visitor/body_part.dart'; | |
abstract class BodyPartVisitor { | |
void visit(BodyPart bodyPart); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: overridden_fields | |
import 'package:visitor/body_part.dart'; | |
class Face extends BodyPart { | |
@override | |
String name = "Face"; | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: overridden_fields | |
import 'package:visitor/body_part.dart'; | |
class Finger extends BodyPart { | |
@override | |
String name = "Finger"; | |
Finger(this.name); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
// ignore_for_file: overridden_fields | |
import 'package:visitor/body_part.dart'; | |
class Foot extends BodyPart { | |
@override | |
String name = "Foot"; | |
Foot(this.name); | |
} |
// Copyright 2024 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:visitor/body.dart'; | |
import 'package:visitor/body_part_prod_visitor.dart'; | |
import 'package:visitor/body_part_stitch_visitor.dart'; | |
void main() { | |
var body = Body(); | |
body.accept(BodyPartStitchVisitor()); | |
body.accept(BodyPartProdVisitor()); | |
/* | |
Stitching together my Face. | |
Stitching together my Index Finger. | |
Stitching together my Thumb. | |
Gluing on my Left Foot. | |
Gluing on my Right Foot. | |
Gluing on my Third Foot?. | |
Reanimating my Body. | |
Prodding Face. | |
Pricking Index Finger. | |
Pricking Thumb. | |
Tickling Left Foot. | |
Tickling Right Foot. | |
Tickling Third Foot?. | |
Poking my Body with an electrode. | |
*/ | |
} |
Conclusion
This is like sentence diagramming in English class. It will get easier if you take the definitions and start examing different code from apps and the SDK and start asking what pattern a specific piece of code is.