diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c282350 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.dart_tool/ +.idea/ +build/ +windows/ + +.packages +pubspec.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..967cdfb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,165 @@ +## 0.38.0 +- Add conceptual facade pattern. + +## 0.37.0 +- Add iterator pattern: Word Iterator. + +## 0.36.0 +- Add iterator pattern: Github Commit. + +## 0.35.0 +- Add conceptual command pattern. + +## 0.34.0 +- Add conceptual singleton pattern. + +## 0.33.0 +- Add conceptual builder pattern. + +## 0.32.0 +- Add conceptual interpreter pattern. + +## 0.31.0 +- Add conceptual proxy pattern. + +## 0.30.0 +- Add strategy pattern: View Strategy. + +## 0.29.0 +- Add conceptual visitor pattern. + +## 0.28.0 +- Add conceptual flyweight pattern. + +## 0.27.0 +- Add template method pattern: Data Miner. + +## 0.26.0 +- Add conceptual mediator pattern. + +## 0.25.0 +- Add conceptual state pattern: Three State. + +## 0.24.1 +- Add flutter icon to project links. + +## 0.24.0 +- Add state pattern: State Manipulator. + +## 0.23.14 +- Replace web renderer html to canvakit (deploy_flutter_demo.dart). + +## 0.23.13 +- Update demos splash screen logo (Ukraine). + +## 0.23.12 +- Add properties bar to "Tool Panel Factory". + +## 0.23.0 +- Add "Tool Panel Factory" flutter example + +## 0.22.0 +- Add visitor pattern: "Shape Xml Export". + +## 0.21.0 +- Add strategy pattern: "Reservation cargo spaces". + +## 0.20.0 +- Add "Conceptual Dialog Factory" example. + +## 0.19.0 +- Add "Conceptual Gui Factory" example. + +## 0.18.0 +- Add Memento Editor. + +## 0.17.16 - refactoring +- Simplifying the ternary construction. +- Remove multiline comment from main README. +- Replace repository urls to relative. +- Fix relative urls. +- Fix name on diagram "Builder", rename JsonFormat to JsonConverter. +- Select pattern "Command" at the diagram. +- Add client code to "Bridge" pattern. +- Fix url position & add example name to ProductsAndBoxes example. +- Add example name to Decorator pattern. +- Format app_observer.dart. +- Connect Application to events on the AppObserver diagram. +- Split into separate files the "Memento" project. +- Remover unnecessary import. +- Move hash generator to application. +- Update SubscriberWidget diagram. +- Fix url position in FlutterAdapter README.md. + +## 0.17.0 +Add "Adapter" pattern: adapt a non-reactive classic type application for Flutter. + +## 0.16.5 +Add deploy_flutter_demos script. + +## 0.16.0 +- Add complex example of an Observer pattern, connected to a Flutter application. +- Add new branch "web-demos" for online examples. + +## 0.15.0 +- Add second "Observer" example. This example was created to be used in a more complex example. + +## 0.14.0 +- Add "Memento" conceptual pattern + +## 0.13.0 +- Add "Observer" pattern from official book, rewritten from Java example + +## 0.12.19 +- Refactoring: reformatting and minor changes + +## 0.12.5 +- Put "Shapes" prototype pattern to "Shapes" folder +- The list of patterns has been updated. Added links to projects + +## 0.12.0 +- Add "Command" pattern from official book, rewritten from Java example + +## 0.11.0 +- Add "Chain of Responsibility" pattern from official book, rewritten from Java example + +## 0.10.0 +- Add composite template from official book, rewritten from Java example + +## 0.9.0 +- Add decorator pattern. Translate from official book java example + +## 0.8.5 +- Add composite pattern "products_and_boxes" +- Created a Canvas helper tool for visual presentation of patterns + +## 0.7.8 +- Update diagram for text_graphics, clock, remote_control, color_text_format +- Code match with diagram in clock + +## 0.7.0 +- Add bridge pattern. Clock example + +## 0.6.5 +- Add bridge pattern. Device remote control + +## 0.5.5 +- Add example "graphics engine" and "square round conflict" for adapter patter +- Add description to prototype pattern +- Fix class diagram for text graph +- Add description Builder pattern, text formats +- Add description to car builder pattern + +## 0.3.0 +- Add an example graphics engine for the adapter pattern + +## 0.2.0 +- Add the Builder pattern using file format converters as an example + +## 0.1.0 +- Add pattern Prototype + +## 0.0.1 + +- Initial project structure +- Add README diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..29b1f4d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,51 @@ +## Contributor's Guide +We appreciate any help, whether it's a simple fix of a typo or a whole new example. +Just [make a fork](https://help.github.com/articles/fork-a-repo/), +do your change and submit a [pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). + +### Step-by-step instruction +1. Just make a fork. +2. Clone the forked repository to your local machine. +3. Create a new branch and name it, for example: fix-issue-32. +4. Make changes. +5. Create commits and push them to your forked Github repository. +6. Submit a pull request to the master branch. +7. Wait for review. + +### Style guide +Here's a style guide which might help you to keep your changes consistent with our code: + +1. All code should meet the [Effective Dart: Style](https://dart.dev/guides/language/effective-dart/style). + +2. Use [Dart Format](https://dart.dev/tools/dart-format) or auto format shortcut `Ctrl + Alt + L` in your ide. + +3. Try to hard wrap the code at 80th's character. It helps to list the code on the website without scrollbars. + +4. File names should match following convention: `some_class_name.dart` + +5. Comments may or may not have language tags in them, such as this: + ```dart + // EN: All products families have the same varieties (MacOS/Windows). + // This is a MacOS variant of a button. + + // RU: Все семейства продуктов имеют одни и те же вариации (MacOS/Windows). + // Это вариант кнопки под MacOS. + ``` + Don't be scared and ignore the non-English part of such comments. If you want to change + something in a comment like this, then do it. Even if you do it wrong, we'll tell you how + to fix it during the Pull Request. + + +### Build Flutter examples +```batch +cd root directory +flutter build web -t bin\main.dart +``` + +### Deploy flutter demos +1. Fork this repo: `https://github.com/RefactoringGuru/design-patterns-dart` +2. Apply your changes. +3. Run the script `dart bin\deploy_flutter_demos.dart`. +This script will build a web platform flutter app and push the changes to your **web-demos** branch on github. +4. You can now make a pull request on the **web-demos** branch. +5. Once approved for the merge, the web app will be available at https://refactoringguru.github.io/design-patterns-dart . diff --git a/README.md b/README.md new file mode 100644 index 0000000..36dde59 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# Design Patterns in Dart +This repository is part of the [**Refactoring.Guru**](https://refactoring.guru/design-patterns) project. +It contains **Dart** examples for all classic **GoF** design patterns. + +# Implementation checklist: +- [ ] **Creation** + - [x] **Abstract Factory** [[Conceptual Gui Factory](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/abstract_factory/conceptual_gui_factory)] [[![16x16](https://user-images.githubusercontent.com/8049534/171852337-57db0faf-1f5e-489a-a79a-22ed4f47b4ed.png) Tool Panel Factory](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/abstract_factory/tool_panel_factory)] + - [x] **Builder** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/builder/conceptual)] [[Color Text Format](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/builder/color_text_format)] + - [x] **Factory Method** [[Conceptual Platform Dialog](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/factory_method/conceptual_platform_dialog)] + - [x] **Prototype** - [[Shapes](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/prototype/shapes)] + - [x] **Singleton** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/singleton/conceptual)] +- [ ] **Behavioral** + - [x] **Chain of Responsibility** - [[Server Middleware](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/chain_of_responsibility/server_middleware)] + - [x] **Command** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/command/conceptual)] [[Text Editor](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/command/text_editor)] + - [x] **Interpreter** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/interpreter/conceptual)] + - [x] **Iterator** - [[Word Iterator](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/iterator/word_iterator)] [[Github Commit](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/iterator/github_commit)] + - [x] **Mediator** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/mediator/conceptual)] + - [x] **Memento** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/memento/conceptual)] [[![16x16](https://user-images.githubusercontent.com/8049534/171852337-57db0faf-1f5e-489a-a79a-22ed4f47b4ed.png) Memento Editor](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/memento/memento_editor)] + - [x] **Observer** - [[Open-Close Editor Events](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/open_close_editor_events)] [[AppObserver](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/app_observer)] [[![16x16](https://user-images.githubusercontent.com/8049534/171852337-57db0faf-1f5e-489a-a79a-22ed4f47b4ed.png) Subscriber Flutter Widget](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/subscriber_flutter_widget)] + - [x] **State** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/state/three_state)] [[![16x16](https://user-images.githubusercontent.com/8049534/171852337-57db0faf-1f5e-489a-a79a-22ed4f47b4ed.png) State Manipulator](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/state/manipulator_state)] + - [x] **Template Method** - [[Data Miner](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/template_method/data_miner)] + - [X] **Visitor** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/visitor/conceptual)] [[Shape XML Exporter](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/visitor/shapes_exporter)] + - [X] **Strategy** [[View Strategy](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/strategy/view_strategy)] [[Reservation Cargo Spaces](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/strategy/reservation_cargo_spaces)] +- [ ] **Structural** + - [x] **Adapter** - [[Text Graphics](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/adapter/text_graphics)] [[Square Round conflict](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/adapter/square_round_conflict)] [[![16x16](https://user-images.githubusercontent.com/8049534/171852337-57db0faf-1f5e-489a-a79a-22ed4f47b4ed.png) Flutter Adapter](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/adapter/flutter_adapter)] + - [x] **Bridge** - [[Remote Device Control](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/bridge/devices_remote_control)] [[Clock](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/bridge/clock)] + - [x] **Composite** - [[Image Editor](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/composite/image_editor)] [[Products and Boxes](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/composite/products_and_boxes)] + - [x] **Decorator** - [[Data Source Decoder](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/decorator/data_source_decoder)] + - [x] **Facade** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/facade/conceptual)] + - [x] **Flyweight** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/flyweight/conceptual)] + - [x] **Proxy** - [[Conceptual](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/proxy/conceptual)] + +## Requirements +The examples were written in **Dart 2.17**. +Some complex examples require **Flutter 3.0.0**. + +## Contributor's Guide +We appreciate any help, whether it's a simple fix of a typo or a whole new example. +Just [make a fork](https://help.github.com/articles/fork-a-repo/), +do your change and submit a [pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/). + +### Step-by-step instruction +1. Just make a fork. +2. Clone the forked repository to your local machine. +3. Create a new branch and name it, for example: fix-issue-32. +4. Make changes. +5. Create commits and push them to your forked Github repository. +6. Submit a pull request to the master branch. +7. Wait for review. + +### Style guide +Here's a style guide which might help you to keep your changes consistent with our code: + +1. All code should meet the [Effective Dart: Style](https://dart.dev/guides/language/effective-dart/style). + +2. Use [Dart Format](https://dart.dev/tools/dart-format) or auto format shortcut `Ctrl + Alt + L` in your ide. + +3. Try to hard wrap the code at 80th's character. It helps to list the code on the website without scrollbars. + +4. File names should match following convention: `some_class_name.dart` + +5. Comments may or may not have language tags in them, such as this: + ```dart + // EN: All products families have the same varieties (MacOS/Windows). + // This is a MacOS variant of a button. + + // RU: Все семейства продуктов имеют одни и те же вариации (MacOS/Windows). + // Это вариант кнопки под MacOS. + ``` + Don't be scared and ignore the non-English part of such comments. If you want to change + something in a comment like this, then do it. Even if you do it wrong, we'll tell you how + to fix it during the Pull Request. + + +### Build Flutter examples +```batch +cd root directory +flutter build web -t bin\main.dart +``` + +### Deploy flutter demos +1. Fork this repo: `https://github.com/RefactoringGuru/design-patterns-dart` +2. Apply your changes. +3. Run the script `dart bin\deploy_flutter_demos.dart`. +This script will build a web platform flutter app and push the changes to your **web-demos** branch on github. +4. You can now make a pull request on the **web-demos** branch. +5. Once approved for the merge, the web app will be available at https://refactoringguru.github.io/design-patterns-dart . + +## License +This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License. + +Creative Commons License + + +## Credits +Authors: Alexander Shvets ([@neochief](https://github.com/neochief)), ilopX ([@ilopX](https://github.com/ilopX)) diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..39e5146 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +linter: + rules: + library_private_types_in_public_api: false +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/bin/deploy_flutter_demos.dart b/bin/deploy_flutter_demos.dart new file mode 100644 index 0000000..18b34db --- /dev/null +++ b/bin/deploy_flutter_demos.dart @@ -0,0 +1,191 @@ +import 'dart:io'; + +void main() async { + await init(); + + final prepareRepository = Future.microtask(() async { + await cloneOriginRepository(); + await fetchUpstream(); + }); + + final buildProject = Future.microtask(() async { + await buildWebProject(); + }); + + await Future.wait([prepareRepository, buildProject]); + + prepareIndexHtmlForRemoteHost(); + copyFiles(); + await pushToOrigin(); + clear(); +} + + final tmpDir = Directory.systemTemp.createTempSync(); + final projectDir = thisPath(r'..\'); + final webBuildDir = Directory(projectDir.uri.toFilePath() + r'build\web'); +late final String originUrl; + +Future init() async { + print('Use temp: $tmpDir'); + originUrl = await repositoryOriginUrl(projectDir); +} + +Future buildWebProject() async { + final flutterTargetFile = '${projectDir.path}bin\\main.dart'; + print('Build web app: $flutterTargetFile'); + await cmd('flutter build web -t $flutterTargetFile'); +} + +Future cloneOriginRepository() async { + print('Cloning origin: [web-demos] $originUrl'); + await cmd( + 'git clone -b web-demos --single-branch $originUrl .', + workingDirectory: tmpDir, + ); +} + +Future fetchUpstream() async { + final shvets = r'https://github.com/RefactoringGuru/design-patterns-dart.git'; + print('Fetch upstream: [web-demos] $shvets'); + await cmd('git remote add upstream $shvets', workingDirectory: tmpDir); + await cmd('git fetch upstream', workingDirectory: tmpDir); +} + +void prepareIndexHtmlForRemoteHost() { + print('Prepare "index.html" for remote host.'); + + final indexHtmlFile = File(webBuildDir.path + r'\index.html'); + final indexContent = indexHtmlFile.readAsStringSync(); + final fixedIndexContent = indexContent.replaceFirst( + r'', + r'', + ); + + if (indexContent == fixedIndexContent) { + throw 'Base url not found. It should be '; + } + + indexHtmlFile.writeAsStringSync(fixedIndexContent); +} + +void copyFiles() { + print('Copy files:'); + copyDirectory(webBuildDir, tmpDir); +} + +Future pushToOrigin() async { + await cmd('git add .', workingDirectory: tmpDir); + await cmd( + 'git commit -m ${await lastProjectCommit()}', + workingDirectory: tmpDir, + showOut: true, + ); + + print('Push to origin: [web-demos] $originUrl'); + await cmd( + 'git push origin web-demos', + workingDirectory: tmpDir, + showOut: true, + ); +} + +void clear() { + print('Clear: $tmpDir'); + tmpDir.deleteSync(recursive: true); +} + +Future cmd( + String command, { + Directory? workingDirectory, + bool showOut = false, +}) async { + var process = await Process.run( + command, + [], + workingDirectory: workingDirectory?.path, + runInShell: true, + ); + + if (showOut) { + print(process.stdout); + } + + if (process.exitCode != 0) { + print(command); + print(process.stderr); + clear(); + exit(process.exitCode); + } + + return process.stdout; +} + +Future repositoryOriginUrl(Directory workingDir) async { + final raw = await cmd( + 'git remote show origin', + workingDirectory: workingDir, + ); + final url = RegExp('Push URL: (.+)\n').firstMatch(raw)?.group(1); + + if (url == null) { + throw Exception('Empty Remote repository'); + } + + return url; +} + +Future lastProjectCommit() async { + final rawCommit = + await cmd('git log -1 --pretty=%B', workingDirectory: projectDir); + final formatCommit = rawCommit.replaceAll(' ', '_').replaceAll('&', ''); + return 'auto_commit:_$formatCommit'; +} + +Directory thisPath(String name) { + final dir = Platform.script.pathSegments + .sublist(0, Platform.script.pathSegments.length - 1) + .join(Platform.pathSeparator) + + Platform.pathSeparator + + name; + + return Directory(Uri.directory(dir).normalizePath().toFilePath()); +} + +void copyDirectory(Directory source, Directory target) { + if (!target.existsSync()) { + target.createSync(); + } + + for (final entry in source.listSync()) { + final newPath = updatePath(entry, source, target); + + if (entry is File) { + copyFile(entry, newPath); + } else if (entry is Directory) { + copyDirectory(entry, Directory(newPath)); + } else { + throw Exception( + 'FileSystemEntity is not recognized. It must be a file or a directory', + ); + } + } +} + +void copyFile(File entry, String newFileName) { + print('\t${removerBuildPath(entry)}'); + entry.copySync(newFileName); +} + +String updatePath( + FileSystemEntity entry, + Directory source, + Directory target, +) { + return entry.path + .replaceFirst(source.path, target.path + Platform.pathSeparator) + .replaceAll(r'\\', r'\'); +} + +String removerBuildPath(FileSystemEntity target) { + return target.path.replaceFirst(webBuildDir.path, '').substring(1); +} diff --git a/bin/main.dart b/bin/main.dart new file mode 100644 index 0000000..5200232 --- /dev/null +++ b/bin/main.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import '../patterns/abstract_factory/tool_panel_factory/main.dart'; +import '../patterns/observer/subscriber_flutter_widget/main.dart'; +import '../patterns/adapter/flutter_adapter/main.dart'; +import '../patterns/memento/memento_editor/main.dart'; +import '../patterns/state/manipulator_state/main.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Refactoring Guru: Flutter launcher', + theme: ThemeData( + primarySwatch: Colors.pink, + iconTheme: IconThemeData( + size: 32, + color: Colors.white, + ), + ), + initialRoute: '/state/manipulator_state', + routes: { + '/observer/subscriber_flutter_widget': (_) => SubscriberFlutterApp(), + '/adapter/flutter_adapter': (_) => FlutterAdapterApp(), + '/memento/flutter_memento_editor': (_) => FlutterMementoEditorApp(), + '/abstract_factory/tool_panel_factory': (_) => ToolPanelFactoryApp(), + '/state/manipulator_state': (_) => ManipulatorStateApp(), + }, + ); + } +} diff --git a/lib/text_canvas.dart b/lib/text_canvas.dart new file mode 100644 index 0000000..0acce3d --- /dev/null +++ b/lib/text_canvas.dart @@ -0,0 +1,4 @@ +library design_pttern_dart; + +export 'text_canvas/canvas.dart'; +export 'text_canvas/primitives.dart'; diff --git a/lib/text_canvas/canvas.dart b/lib/text_canvas/canvas.dart new file mode 100644 index 0000000..f9cc52d --- /dev/null +++ b/lib/text_canvas/canvas.dart @@ -0,0 +1,182 @@ +import 'dart:math'; + +import 'primitives.dart'; + +class Canvas { + final int width; + final int height; + late List>> _pixel; + final int lineStretch; + + Canvas( + this.width, + this.height, { + this.lineStretch = 3, + Color? fillColor, + }) { + final realWidth = width * lineStretch; + + fillColor ??= Color.light; + if (fillColor == Color.transparent) { + fillColor = Color.white; + } + + _pixel = [ + for (var i = 0; i < height; i++) + List.from( + List.filled(realWidth, fillColor.units), + ), + ]; + } + + var translate = Point(0, 0); + var penColor = Color.black; + + void setPixel(int x, int y) { + x += translate.x; + y += translate.y; + + if (x < 0 || x > width - 1 || y < 0 || y > height - 1) { + return; + } + + if (penColor == Color.transparent) { + return; + } + + final realY = y; + final realX = (x * lineStretch) ~/ 1; + for (var i = 0; i < lineStretch; i++) { + _pixel[realY][realX + i] = penColor.units; + } + } + + void char(int x, int y, Color char) { + x += translate.x; + y += translate.y; + + if (x < 0 || x > width * lineStretch - 1 || y < 0 || y > height - 1) { + return; + } + + if (char == Color.transparent) { + return; + } + + _pixel[y][x] = char.units; + } + + var _currPos = Point(0, 0); + + void moveTo(int x, int y) => _currPos = Point(x, y); + + /// Bresenham's algorithm is taken from here + /// https://gist.github.com/bert/1085538#file-plot_line-c + void lineTo(final int x, final int y) { + final x1 = _currPos.x, y1 = _currPos.y; + + var x0 = x, y0 = y; + + final dx = (x1 - x0).abs(), + sx = x0 < x1 ? 1 : -1, + dy = -(y1 - y0).abs(), + sy = y0 < y1 ? 1 : -1; + + var err = dx + dy; + late int e2; + + while (true) { + setPixel(x0, y0); + if (x0 == x1 && y0 == y1) break; + e2 = 2 * err; + if (e2 >= dy) { + err += dy; + x0 += sx; + } + if (e2 <= dx) { + err += dx; + y0 += sy; + } + } + moveTo(x, y); + } + + void rectangle(int width, int height) { + final x = _currPos.x; + final y = _currPos.y; + lineTo(width + x, y); + lineTo(width + x, height + y); + lineTo(x, height + y); + lineTo(x, y); + } + + /// Bresenham's algorithm is taken from here + /// https://gist.github.com/bert/1085538#file-plot_circle-c + void circle(int radius) { + final xm = _currPos.x; + final ym = _currPos.y; + int x = -radius, y = 0, err = 2 - 2 * radius; + do { + setPixel(xm - x, ym + y); + setPixel(xm - y, ym - x); + setPixel(xm + x, ym - y); + setPixel(xm + y, ym + x); + radius = err; + if (radius > x) err += ++x * 2 + 1; + if (radius <= y) err += ++y * 2 + 1; + } while (x < 0); + } + + void border(int width, int height, BorderStyle borderStyle) { + if (borderStyle == BorderStyle.empty) { + return; + } + + assert(width >= 2); + assert(height >= 2); + + char(0, 0, borderStyle.topLeft); + char(width - 1, 0, borderStyle.topRight); + char(width - 1, height - 1, borderStyle.bottomRight); + char(0, height - 1, borderStyle.bottomLeft); + + for (var x = 1; x < width - 1; x++) { + char(x, 0, borderStyle.top); + } + + for (var y = 1; y < height - 1; y++) { + char(width - 1, y, borderStyle.right); + } + + for (var y = 1; y < height - 1; y++) { + char(0, y, borderStyle.right); + } + + for (var x = 1; x < width - 1; x++) { + char(x, height - 1, borderStyle.bottom); + } + } + + void text(String text, {int widthCenter = -1, int heightCenter = -1}) { + widthCenter = widthCenter < 0 ? text.length : widthCenter; + heightCenter = heightCenter < 0 ? 1 : heightCenter; + + var x = (widthCenter - text.length) ~/ 2; + var y = heightCenter ~/ 2; + + for (final c in text.split('')) { + char(x++, y, Color(c)); + } + } + + @override + String toString() { + return _pixel + .map((e) => e + .map( + (e) => String.fromCharCodes(e), + ) + .join('')) + .join('\n'); + } +} diff --git a/lib/text_canvas/primitives.dart b/lib/text_canvas/primitives.dart new file mode 100644 index 0000000..4ce4c73 --- /dev/null +++ b/lib/text_canvas/primitives.dart @@ -0,0 +1,101 @@ +import 'package:collection/collection.dart'; + +class Color { + final List units; + + Color(String symbol) + : assert(symbol.length == 1), + units = symbol.codeUnits; + + static final Color black = Color('█'); + static final Color dark = Color('▓'); + static final Color grey = Color('▒'); + static final Color light = Color('░'); + static final Color white = Color(' '); + static final Color point = Color('■'); + static final Color transparent = Color(' '); + + @override + bool operator ==(Object other) { + return other is Color && ListEquality().equals(other.units, units); + } + + @override + int get hashCode => ListEquality().hash(units); + + @override + String toString() { + final symbol = String.fromCharCodes(units); + return '$runtimeType(symbol: "$symbol", unit: $units)'; + } +} + +class BorderStyle { + static final BorderStyle bold = BorderStyle.fromBorderText( + '▄▄▄▄' + '█ █' + '▀▀█▀', + ); + + static final BorderStyle double = BorderStyle.fromBorderText( + '╔══╗' + '║ ║' + '╚═╦╝', + ); + + static final BorderStyle single = BorderStyle.fromBorderText( + '┌──┐' + '│ │' + '└─┬┘', + ); + + static final BorderStyle round = BorderStyle.fromBorderText( + '╭━━╮' + '   ' + '    ', + ); + + static final BorderStyle empty = BorderStyle.fromBorderText( + '    ' + '   ' + '    ', + ); + final Color topLeft; + final Color topRight; + final Color bottomRight; + final Color bottomLeft; + + final Color top; + final Color bottom; + + final Color left; + final Color right; + + final Color bottomConnect; + + const BorderStyle._( + this.topLeft, + this.topRight, + this.bottomRight, + this.bottomLeft, + this.top, + this.bottom, + this.left, + this.right, + this.bottomConnect, + ); + + factory BorderStyle.fromBorderText(String text) { + return BorderStyle._( + Color(text[0]), + Color(text[3]), + Color(text[11]), + Color(text[8]), + Color(text[1]), + Color(text[9]), + Color(text[4]), + Color(text[7]), + Color(text[10]), + ); + } +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/README.md b/patterns/abstract_factory/conceptual_gui_factory/README.md new file mode 100644 index 0000000..9454266 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/README.md @@ -0,0 +1,38 @@ +# Abstract Factory pattern +Abstract Factory is a creational design pattern that lets you produce families of related objects +without specifying their concrete classes. + +Tutorial: [here](https://refactoring.guru/design-patterns/abstract-factory). + +### About example. +This the very conceptual example rewrite from original source code [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/abstract_factory/example) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/165987890-e64db9a3-4865-411c-a5c0-16da21043159.png) + +### Client code: +```dart +void main() { + final guiFactory = GUIFactory(); + final app = Application(guiFactory); + app.paint(); +} + +abstract class GUIFactory { + factory GUIFactory() { + if (Platform.isMacOS) { + return MacOSFactory(); + } else { + return WindowsFactory(); + } + } + + /*...*/ +} +``` + +### Output: +``` +You have created WindowsButton. +You have created WindowsCheckbox. +``` diff --git a/patterns/abstract_factory/conceptual_gui_factory/app/application.dart b/patterns/abstract_factory/conceptual_gui_factory/app/application.dart new file mode 100644 index 0000000..a14e494 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/app/application.dart @@ -0,0 +1,18 @@ +import '../button/button.dart'; +import '../checkbox/checkbox.dart'; +import '../factories/gui_factory.dart'; + +class Application { + late Button _button; + late Checkbox _checkbox; + + Application(GUIFactory factory) { + _button = factory.createButton(); + _checkbox = factory.createCheckbox(); + } + + void paint() { + _button.paint(); + _checkbox.paint(); + } +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/button/button.dart b/patterns/abstract_factory/conceptual_gui_factory/button/button.dart new file mode 100644 index 0000000..ccf3c96 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/button/button.dart @@ -0,0 +1,3 @@ +abstract class Button { + void paint(); +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/button/mac_os_button.dart b/patterns/abstract_factory/conceptual_gui_factory/button/mac_os_button.dart new file mode 100644 index 0000000..d84c7a5 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/button/mac_os_button.dart @@ -0,0 +1,8 @@ +import 'button.dart'; + +class MacOSButton implements Button { + @override + void paint() { + print('You have created MacOSButton.'); + } +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/button/windows_button.dart b/patterns/abstract_factory/conceptual_gui_factory/button/windows_button.dart new file mode 100644 index 0000000..790345a --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/button/windows_button.dart @@ -0,0 +1,8 @@ +import 'button.dart'; + +class WindowsButton implements Button { + @override + void paint() { + print('You have created WindowsButton.'); + } +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/checkbox/checkbox.dart b/patterns/abstract_factory/conceptual_gui_factory/checkbox/checkbox.dart new file mode 100644 index 0000000..caaf517 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/checkbox/checkbox.dart @@ -0,0 +1,3 @@ +abstract class Checkbox { + void paint(); +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/checkbox/mac_os_checkbox.dart b/patterns/abstract_factory/conceptual_gui_factory/checkbox/mac_os_checkbox.dart new file mode 100644 index 0000000..26cacfd --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/checkbox/mac_os_checkbox.dart @@ -0,0 +1,8 @@ +import 'checkbox.dart'; + +class MacOSCheckbox implements Checkbox { + @override + void paint() { + print('You have created MacOSCheckbox.'); + } +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/checkbox/windows_checkbox.dart b/patterns/abstract_factory/conceptual_gui_factory/checkbox/windows_checkbox.dart new file mode 100644 index 0000000..0fa6755 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/checkbox/windows_checkbox.dart @@ -0,0 +1,8 @@ +import 'checkbox.dart'; + +class WindowsCheckbox implements Checkbox { + @override + void paint() { + print('You have created WindowsCheckbox.'); + } +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/factories/gui_factory.dart b/patterns/abstract_factory/conceptual_gui_factory/factories/gui_factory.dart new file mode 100644 index 0000000..fca44e2 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/factories/gui_factory.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import '../button/button.dart'; +import '../checkbox/checkbox.dart'; +import 'mac_os_factory.dart'; +import 'window_factory.dart'; + +abstract class GUIFactory { + factory GUIFactory() { + if (Platform.isMacOS) { + return MacOSFactory(); + } else { + return WindowsFactory(); + } + } + + Button createButton(); + + Checkbox createCheckbox(); +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/factories/mac_os_factory.dart b/patterns/abstract_factory/conceptual_gui_factory/factories/mac_os_factory.dart new file mode 100644 index 0000000..d49f919 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/factories/mac_os_factory.dart @@ -0,0 +1,13 @@ +import '../button/button.dart'; +import '../checkbox/checkbox.dart'; +import '../button/mac_os_button.dart'; +import '../checkbox/mac_os_checkbox.dart'; +import 'gui_factory.dart'; + +class MacOSFactory implements GUIFactory { + @override + Button createButton() => MacOSButton(); + + @override + Checkbox createCheckbox() => MacOSCheckbox(); +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/factories/window_factory.dart b/patterns/abstract_factory/conceptual_gui_factory/factories/window_factory.dart new file mode 100644 index 0000000..7592f86 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/factories/window_factory.dart @@ -0,0 +1,13 @@ +import '../button/button.dart'; +import '../checkbox/checkbox.dart'; +import '../button/windows_button.dart'; +import '../checkbox/windows_checkbox.dart'; +import 'gui_factory.dart'; + +class WindowsFactory implements GUIFactory { + @override + Button createButton() => WindowsButton(); + + @override + Checkbox createCheckbox() => WindowsCheckbox(); +} diff --git a/patterns/abstract_factory/conceptual_gui_factory/main.dart b/patterns/abstract_factory/conceptual_gui_factory/main.dart new file mode 100644 index 0000000..b946056 --- /dev/null +++ b/patterns/abstract_factory/conceptual_gui_factory/main.dart @@ -0,0 +1,8 @@ +import 'app/application.dart'; +import 'factories/gui_factory.dart'; + +void main() { + final guiFactory = GUIFactory(); + final app = Application(guiFactory); + app.paint(); +} diff --git a/patterns/abstract_factory/tool_panel_factory/README.md b/patterns/abstract_factory/tool_panel_factory/README.md new file mode 100644 index 0000000..8dc0de4 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/README.md @@ -0,0 +1,40 @@ +# Abstract Factory Pattern +Abstract Factory is a creational design pattern that lets you produce families of related objects +without specifying their concrete classes. + +Tutorial: [here](https://refactoring.guru/design-patterns/abstract-factory). + +### Online demo: +Click on the picture to see the [demo](https://RefactoringGuru.github.io/design-patterns-dart/#/abstract_factory/tool_panel_factory). + +[![image](https://user-images.githubusercontent.com/8049534/169530318-0ce7ee6a-3538-4398-a2ab-e6e85f2132b5.png)](https://refactoringguru.github.io/design-patterns-dart/#/abstract_factory/tool_panel_factory) + +### About +![image](https://user-images.githubusercontent.com/8049534/169521422-052cc59a-7b3d-4889-8d76-5664a75b271a.png) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/169528461-d7c5279d-7e7f-4ce7-b8d2-87388ec2d32f.png) + +### Client code: +```dart +class App { + void addShape(double x, double y) { + final newShape = activeToolFactory.createShape(x, y, activeColor); + shapes.add(newShape); + } +} + + +class PropertyPanel extends StatelessWidget { + final PropertyWidgetFactories factories; + + @override + Widget build(BuildContext context) { + return Row( + children: propertyWidgetFactories + .createListWidgetsFrom(activeToolFactory.properties) + .toList(), + ); + } +} +``` diff --git a/patterns/abstract_factory/tool_panel_factory/app/app.dart b/patterns/abstract_factory/tool_panel_factory/app/app.dart new file mode 100644 index 0000000..062ecb7 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/app/app.dart @@ -0,0 +1,21 @@ +import '../app/tools.dart'; +import 'shapes.dart'; + +class App { + final Tools tools; + final Shapes shapes; + + App({ + required this.tools, + required this.shapes, + }); + + void addShape(double x, double y) { + final activeColor = tools.activeColor.value; + final activeFactory = tools.activeFactory.value; + + final newShape = activeFactory.createShape(x, y, activeColor); + newShape.centerToFit(); + shapes.add(newShape); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/app/shapes.dart b/patterns/abstract_factory/tool_panel_factory/app/shapes.dart new file mode 100644 index 0000000..da28a47 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/app/shapes.dart @@ -0,0 +1,25 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; + +import '../pattern/shape.dart'; + +class Shapes with IterableMixin { + final List _shapes; + + Shapes(this._shapes); + + void add(Shape shape) { + _shapes.add(shape); + onAddShapeEvent._emit(); + } + + @override + Iterator get iterator => _shapes.iterator; + + final onAddShapeEvent = Event(); +} + +class Event extends ChangeNotifier { + void _emit() => notifyListeners(); +} diff --git a/patterns/abstract_factory/tool_panel_factory/app/tools.dart b/patterns/abstract_factory/tool_panel_factory/app/tools.dart new file mode 100644 index 0000000..60708d2 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/app/tools.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../mixin/icon_box_mixin.dart'; +import '../pattern/tool_factory.dart'; + +class Tools { + final List factories; + final List colors; + + late final ValueNotifier activeFactory; + + late final ValueNotifier activeColor; + + Future get iconsReady => _iconsInitCompleter.future; + + Tools({required this.factories, required this.colors}) + : assert(factories.isNotEmpty), + assert(colors.isNotEmpty) { + activeFactory = ValueNotifier(factories.first); + activeColor = ValueNotifier(colors.first); + _initIconsFromShapes(); + } + + final _iconsInitCompleter = Completer(); + + void _initIconsFromShapes() async { + await Future.wait([ + for (final factory in factories) + (factory as IconBoxMixin).updateIcon(activeColor.value), + ]); + _iconsInitCompleter.complete(Future.value(true)); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/factories/circle_factory.dart b/patterns/abstract_factory/tool_panel_factory/factories/circle_factory.dart new file mode 100644 index 0000000..18f7b0c --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/factories/circle_factory.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import '../mixin/icon_box_mixin.dart'; +import '../pattern/property.dart'; +import '../pattern/tool_factory.dart'; +import '../shapes/circle_shape.dart'; +import '../pattern/shape.dart'; + +class CircleFactory extends ToolFactory with IconBoxMixin { + var _radius = 50.0; + var _isFilled = false; + + @override + Shape createShape(double x, double y, Color color) { + return CircleShape( + radius: _radius, + isFilled: _isFilled, + x: x, + y: y, + color: color, + ); + } + + @override + Iterable get properties { + return [ + Property( + name: 'radius', + value: () => _radius, + onChange: (val) { + _radius = val; + }, + ), + Property( + name: 'filled', + value: () => _isFilled, + onChange: (val) { + _isFilled = val; + }, + ), + ]; + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/factories/line_factory.dart b/patterns/abstract_factory/tool_panel_factory/factories/line_factory.dart new file mode 100644 index 0000000..5b60be1 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/factories/line_factory.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import '../mixin/icon_box_mixin.dart'; +import '../pattern/property.dart'; +import '../pattern/tool_factory.dart'; +import '../pattern/shape.dart'; +import '../shapes/line_shape.dart'; + +class LineFactory extends ToolFactory with IconBoxMixin { + var _isMirror = true; + var _length = 100.0; + + @override + Shape createShape(double x, double y, Color color) { + return LineShape( + length: _length, + isMirror: _isMirror, + x: x, + y: y, + color: color, + ); + } + + @override + Iterable get properties { + return [ + Property( + name: 'mirror', + value: () => _isMirror, + onChange: (val) { + _isMirror = val; + }, + ), + Property( + name: 'length', + value: () => _length, + onChange: (val) { + _length = val; + }, + ), + ]; + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/factories/star_factory.dart b/patterns/abstract_factory/tool_panel_factory/factories/star_factory.dart new file mode 100644 index 0000000..cd77849 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/factories/star_factory.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import '../mixin/icon_box_mixin.dart'; +import '../pattern/property.dart'; +import '../pattern/tool_factory.dart'; +import '../pattern/shape.dart'; +import '../shapes/star_shape.dart'; + +class StarFactory extends ToolFactory with IconBoxMixin { + var _radius = 80.0; + var _isFilled = false; + + @override + Shape createShape(double x, double y, Color color) { + return StarShape( + radius: _radius, + isFilled: _isFilled, + x: x, + y: y, + color: color, + ); + } + + @override + Iterable get properties { + return [ + Property( + name: 'radius', + value: () => _radius, + onChange: (val) { + _radius = val; + }, + ), + Property( + name: 'filled', + value: () => _isFilled, + onChange: (val) { + _isFilled = val; + }, + ), + ]; + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/factories/text_factory.dart b/patterns/abstract_factory/tool_panel_factory/factories/text_factory.dart new file mode 100644 index 0000000..4831554 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/factories/text_factory.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import '../mixin/icon_box_mixin.dart'; +import '../pattern/property.dart'; +import '../pattern/tool_factory.dart'; +import '../shapes/text_shape.dart'; +import '../pattern/shape.dart'; + +class TextFactory extends ToolFactory with IconBoxMixin { + var _text = 'Text'; + var _fontSize = 50.0; + + @override + Shape createShape(double x, double y, Color color) { + return TextShape( + text: _text, + fontSize: _fontSize, + x: x, + y: y, + color: color, + ); + } + + @override + Iterable get properties { + return [ + Property( + name: 'text', + value: () => _text, + onChange: (value) { + _text = value; + }, + ), + Property( + name: 'fontSize', + value: () => _fontSize, + onChange: (value) { + _fontSize = value; + }, + ), + ]; + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/factories/triangle_factory.dart b/patterns/abstract_factory/tool_panel_factory/factories/triangle_factory.dart new file mode 100644 index 0000000..9e8b477 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/factories/triangle_factory.dart @@ -0,0 +1,41 @@ +import 'dart:ui'; + +import '../mixin/icon_box_mixin.dart'; +import '../pattern/property.dart'; +import '../pattern/tool_factory.dart'; +import '../pattern/shape.dart'; +import '../shapes/triangle_shape.dart'; + +class TriangleFactory extends ToolFactory with IconBoxMixin { + var _isFilled = false; + var _sideLength = 120.0; + + @override + Shape createShape(double x, double y, Color color) { + return TriangleShape( + sideLength: _sideLength, + isFilled: _isFilled, + x: x, + y: y, + color: color, + ); + } + + @override + Iterable get properties => [ + Property( + name: 'sideLength', + value: () => _sideLength, + onChange: (val) { + _sideLength = val; + }, + ), + Property( + name: 'filled', + value: () => _isFilled, + onChange: (val) { + _isFilled = val; + }, + ), + ]; +} diff --git a/patterns/abstract_factory/tool_panel_factory/main.dart b/patterns/abstract_factory/tool_panel_factory/main.dart new file mode 100644 index 0000000..61d45f9 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/main.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import 'app/app.dart'; +import 'app/shapes.dart'; +import 'app/tools.dart'; +import 'factories/circle_factory.dart'; +import 'factories/line_factory.dart'; +import 'factories/star_factory.dart'; +import 'factories/triangle_factory.dart'; +import 'factories/text_factory.dart'; +import 'widgets/drawing_board.dart'; +import 'widgets/property_bar.dart'; +import 'widgets/property_widgets/bool_property_widget.dart'; +import 'widgets/property_widgets/double_property_widget.dart'; +import 'widgets/property_widgets/factories/property_widget_factories.dart'; +import 'widgets/property_widgets/string_property_widget.dart'; +import 'widgets/tool_panel.dart'; + +class ToolPanelFactoryApp extends StatefulWidget { + const ToolPanelFactoryApp({Key? key}) : super(key: key); + + @override + _ToolPanelFactoryAppState createState() => _ToolPanelFactoryAppState(); +} + +class _ToolPanelFactoryAppState extends State { + final app = App( + shapes: Shapes([]), + tools: Tools( + factories: [ + LineFactory(), + CircleFactory(), + TriangleFactory(), + StarFactory(), + TextFactory(), + ], + colors: [ + Colors.white, + Colors.green, + Colors.blue, + Colors.yellow, + ], + ), + ); + + final propertyWidgetFactories = PropertyWidgetFactories( + factories: [ + StringPropertyWidgetFactory(), + DoublePropertyWidgetFactory(), + BoolPropertyWidgetFactory(), + ], + ); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + DrawingBoard( + shapes: app.shapes, + onClick: app.addShape, + ), + ToolPanel( + tools: app.tools, + ), + PropertyPanel( + tools: app.tools, + factories: propertyWidgetFactories, + ), + ], + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/mixin/icon_box_mixin.dart b/patterns/abstract_factory/tool_panel_factory/mixin/icon_box_mixin.dart new file mode 100644 index 0000000..11016a1 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/mixin/icon_box_mixin.dart @@ -0,0 +1,71 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import '../pattern/tool_factory.dart'; +import '../pattern/shape.dart'; + +mixin IconBoxMixin implements ToolFactory { + Image? _icon; + + @override + Image get icon => _icon!; + + Future updateIcon(Color color) async { + final shape = createShape(0, 0, color); + final pngBytes = await _pngImageFromShape(shape); + _icon = Image.memory( + pngBytes, + fit: BoxFit.none, + ); + } + + Future _pngImageFromShape(Shape shape) async { + final iconSize = 32.0; + + final rec = PictureRecorder(); + final can = Canvas( + rec, + Rect.fromLTWH(0, 0, iconSize, iconSize), + ); + + _scaleTo(can, shape, iconSize); + shape.paint(can); + + final image = await rec.endRecording().toImage( + iconSize.toInt(), + iconSize.toInt(), + ); + final bytes = await image.toByteData(format: ImageByteFormat.png); + + if (bytes == null) { + throw 'Bytes is empty.'; + } + + return bytes.buffer.asUint8List(); + } + + void _scaleTo(Canvas can, Shape shape, double iconSize) { + var xMove = 0.0; + var yMove = 0.0; + late double w; + late double h; + + if (shape.width >= shape.height) { + yMove = (shape.width - shape.height); + w = iconSize / shape.width; + h = iconSize / (shape.height + yMove); + yMove /= 2; + } else { + xMove = (shape.height - shape.width); + w = iconSize / (shape.width + xMove); + h = iconSize / shape.height; + xMove /= 2; + } + + can.scale(w, h); + can.translate(xMove, yMove); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/pattern/property.dart b/patterns/abstract_factory/tool_panel_factory/pattern/property.dart new file mode 100644 index 0000000..640f5b9 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/pattern/property.dart @@ -0,0 +1,11 @@ +class Property { + final String name; + final T Function() value; + final void Function(T) onChange; + + Property({ + required this.name, + required this.value, + required this.onChange, + }); +} diff --git a/patterns/abstract_factory/tool_panel_factory/pattern/shape.dart b/patterns/abstract_factory/tool_panel_factory/pattern/shape.dart new file mode 100644 index 0000000..6f68e63 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/pattern/shape.dart @@ -0,0 +1,17 @@ +import 'dart:ui'; + +abstract class Shape { + double get x; + + double get y; + + double get width; + + double get height; + + Color get color; + + void paint(Canvas can); + + void centerToFit(); +} diff --git a/patterns/abstract_factory/tool_panel_factory/pattern/tool_factory.dart b/patterns/abstract_factory/tool_panel_factory/pattern/tool_factory.dart new file mode 100644 index 0000000..60323b6 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/pattern/tool_factory.dart @@ -0,0 +1,12 @@ +import 'package:flutter/widgets.dart'; + +import 'property.dart'; +import 'shape.dart'; + +abstract class ToolFactory { + Image get icon; + + Shape createShape(double x, double y, Color color); + + Iterable get properties; +} diff --git a/patterns/abstract_factory/tool_panel_factory/shapes/base_shape.dart b/patterns/abstract_factory/tool_panel_factory/shapes/base_shape.dart new file mode 100644 index 0000000..50c04bd --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/shapes/base_shape.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../pattern/shape.dart'; + +abstract class BaseShape implements Shape { + @override + double get x => _x; + + @override + double get y => _y; + + @override + final Color color; + + BaseShape({ + required double x, + required double y, + required this.color, + }) : _x = x, + _y = y; + + @override + void centerToFit() { + _x -= width / 2; + _y -= height / 2; + } + + double _x; + double _y; +} diff --git a/patterns/abstract_factory/tool_panel_factory/shapes/circle_shape.dart b/patterns/abstract_factory/tool_panel_factory/shapes/circle_shape.dart new file mode 100644 index 0000000..1bffdec --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/shapes/circle_shape.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +import 'base_shape.dart'; + +class CircleShape extends BaseShape { + final double radius; + final bool isFilled; + + CircleShape({ + required this.radius, + required this.isFilled, + required double x, + required double y, + required Color color, + }) : super( + x: x, + y: y, + color: color, + ); + + @override + void paint(Canvas can) { + final pos = width / 2; + can.drawCircle( + Offset(pos, pos), + radius - 2, + Paint() + ..style = isFilled ? PaintingStyle.fill : PaintingStyle.stroke + ..color = color, + ); + } + + @override + double get width => radius * 2; + + @override + double get height => width; +} diff --git a/patterns/abstract_factory/tool_panel_factory/shapes/line_shape.dart b/patterns/abstract_factory/tool_panel_factory/shapes/line_shape.dart new file mode 100644 index 0000000..4191230 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/shapes/line_shape.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'base_shape.dart'; + +class LineShape extends BaseShape { + final double length; + + LineShape({ + required bool isMirror, + required this.length, + required double x, + required double y, + required Color color, + }) : super( + x: x, + y: y, + color: color, + ) { + if (isMirror) { + point1 = Offset(0, length); + point2 = Offset(length, 0); + } else { + point1 = Offset(0, 0); + point2 = Offset(length, length); + } + } + + late final Offset point1; + late final Offset point2; + + @override + void paint(Canvas can) { + can.drawLine(point1, point2, Paint()..color = color); + } + + @override + double get width => length; + + @override + double get height => length; +} diff --git a/patterns/abstract_factory/tool_panel_factory/shapes/star_shape.dart b/patterns/abstract_factory/tool_panel_factory/shapes/star_shape.dart new file mode 100644 index 0000000..f0d41b1 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/shapes/star_shape.dart @@ -0,0 +1,62 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'base_shape.dart'; + +class StarShape extends BaseShape { + final double radius; + final bool isFilled; + + StarShape({ + required this.radius, + required this.isFilled, + required double x, + required double y, + required Color color, + }) : super( + x: x, + y: y, + color: color, + ) { + _starPath = Path()..addPolygon(_createStar(), true); + } + + late final Path _starPath; + + @override + void paint(Canvas can) { + can.drawPath( + _starPath, + Paint() + ..style = isFilled ? PaintingStyle.fill : PaintingStyle.stroke + ..color = color, + ); + } + + @override + double get width => radius * 2; + + @override + double get height => radius * 2; + + List _createStar() { + const alpha = (2 * pi) / 10; + + final starXY = radius; + + final points = []; + + for (var i = 11; i != 0; i--) { + var r = radius * (i % 2 + 1) / 2; + var omega = alpha * i; + points.add(Offset( + (r * sin(omega)) + starXY, + (r * cos(omega)) + starXY, + )); + } + + return points; + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/shapes/text_shape.dart b/patterns/abstract_factory/tool_panel_factory/shapes/text_shape.dart new file mode 100644 index 0000000..5b35823 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/shapes/text_shape.dart @@ -0,0 +1,51 @@ +import 'dart:ui'; + +import 'base_shape.dart'; + +class TextShape extends BaseShape { + final String text; + final double fontSize; + + TextShape({ + required this.text, + required this.fontSize, + required double x, + required double y, + required Color color, + }) : super( + x: x, + y: y, + color: color, + ) { + _initTextParagraph(); + } + + @override + void paint(Canvas can) { + can.drawParagraph(_paragraph, Offset.zero); + } + + @override + double get width => _paragraph.maxIntrinsicWidth; + + @override + double get height => _paragraph.height; + + late final Paragraph _paragraph; + + void _initTextParagraph() { + final style = ParagraphStyle( + textDirection: TextDirection.ltr, + ); + final tStyle = TextStyle( + fontFamily: 'Arial', + color: color, + fontSize: fontSize, + ); + _paragraph = (ParagraphBuilder(style) + ..pushStyle(tStyle) + ..addText(text)) + .build(); + _paragraph.layout(ParagraphConstraints(width: double.infinity)); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/shapes/triangle_shape.dart b/patterns/abstract_factory/tool_panel_factory/shapes/triangle_shape.dart new file mode 100644 index 0000000..e94073f --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/shapes/triangle_shape.dart @@ -0,0 +1,46 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'base_shape.dart'; + +class TriangleShape extends BaseShape { + final double sideLength; + final bool isFilled; + + TriangleShape({ + required this.sideLength, + required this.isFilled, + required double x, + required double y, + required Color color, + }) : super(x: x, y: y, color: color) { + _trianglePath = Path() + ..addPolygon( + [ + Offset(0, height), + Offset(width / 2, 0), + Offset(width, height), + ], + true, + ); + } + + late final Path _trianglePath; + + @override + void paint(Canvas can) { + can.drawPath( + _trianglePath, + Paint() + ..style = isFilled ? PaintingStyle.fill : PaintingStyle.stroke + ..color = color); + } + + @override + double get width => sideLength; + + @override + double get height => sideLength * sqrt(3) / 2; +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/colors_tool_bar.dart b/patterns/abstract_factory/tool_panel_factory/widgets/colors_tool_bar.dart new file mode 100644 index 0000000..9b30eac --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/colors_tool_bar.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +import '../app/tools.dart'; +import 'independent/tool_bar.dart'; +import 'independent/tool_button.dart'; + +class ColorsToolBar extends StatelessWidget { + final Tools tools; + + const ColorsToolBar({Key? key, required this.tools}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ToolBar( + title: 'colors', + child: ValueListenableBuilder( + valueListenable: tools.activeColor, + builder: (_, activeColor, __) { + return Column( + children: [ + for (final color in tools.colors) + ToolButton( + icon: Icon(Icons.circle, color: color), + active: color == activeColor, + onTap: () { + tools.activeColor.value = color; + }, + ), + ], + ); + }, + ), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/drawing_board.dart b/patterns/abstract_factory/tool_panel_factory/widgets/drawing_board.dart new file mode 100644 index 0000000..590f94e --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/drawing_board.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import '../app/shapes.dart'; +import 'independent/event_listenable_builder.dart'; + +class DrawingBoard extends StatelessWidget { + final Shapes shapes; + final Function(double x, double y) onClick; + + const DrawingBoard({ + Key? key, + required this.shapes, + required this.onClick, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapDown: (e) => addShape(e.localPosition), + child: Container( + color: Color(0xff1f1f1f), + child: EventListenableBuilder( + event: shapes.onAddShapeEvent, + builder: (_) { + return LayoutBuilder( + builder: (_, constraints) { + return CustomPaint( + size: Size(constraints.maxWidth, constraints.maxHeight), + painter: ShapesPainter(shapes), + ); + }, + ); + }, + ), + ), + ); + } + + void addShape(Offset position) { + onClick(position.dx, position.dy); + } +} + +class ShapesPainter extends CustomPainter { + final Shapes shapes; + + ShapesPainter(this.shapes); + + @override + void paint(Canvas canvas, Size _) { + for (final shape in shapes) { + canvas.save(); + canvas.translate(shape.x, shape.y); + shape.paint(canvas); + canvas.restore(); + } + } + + @override + bool shouldRepaint(covariant CustomPainter _) { + return false; + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/factories_tool_bar.dart b/patterns/abstract_factory/tool_panel_factory/widgets/factories_tool_bar.dart new file mode 100644 index 0000000..02ad8c4 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/factories_tool_bar.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +import '../app/tools.dart'; +import 'independent/tool_bar.dart'; +import 'independent/tool_button.dart'; + +class FactoriesToolBar extends StatelessWidget { + final Tools tools; + + const FactoriesToolBar({Key? key, required this.tools}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ToolBar( + title: 'factories', + child: FutureBuilder( + future: tools.iconsReady, + builder: (_, snapshot) { + return snapshot.hasData + ? _buildToolButtons() + : Padding( + padding: EdgeInsets.all(10), + child: CircularProgressIndicator(), + ); + }, + ), + ); + } + + Widget _buildToolButtons() { + return ValueListenableBuilder( + valueListenable: tools.activeFactory, + builder: (_, activeFactory, __) { + return Column( + children: [ + for (final factory in tools.factories) + ToolButton( + icon: factory.icon, + active: factory == activeFactory, + onTap: () => tools.activeFactory.value = factory, + ), + ], + ); + }, + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart b/patterns/abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart new file mode 100644 index 0000000..c1d02d2 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class EventListenableBuilder extends StatefulWidget { + final T event; + final Widget Function(BuildContext context) builder; + + const EventListenableBuilder({ + Key? key, + required this.event, + required this.builder, + }) : super(key: key); + + @override + _EventListenableBuilderState createState() => + _EventListenableBuilderState(event); +} + +class _EventListenableBuilderState + extends State> { + final T event; + + _EventListenableBuilderState(this.event); + + @override + void initState() { + event.addListener(_update); + super.initState(); + } + + @override + void dispose() { + event.removeListener(_update); + super.dispose(); + } + + void _update() { + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/independent/hove.dart b/patterns/abstract_factory/tool_panel_factory/widgets/independent/hove.dart new file mode 100644 index 0000000..b3bb973 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/independent/hove.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class Hover extends StatefulWidget { + final Function(double hoverForce) builder; + + const Hover({Key? key, required this.builder}) : super(key: key); + + @override + _HoverState createState() => _HoverState(); +} + +class _HoverState extends State with SingleTickerProviderStateMixin { + late final AnimationController _animation; + + @override + void initState() { + _animation = AnimationController( + duration: Duration(milliseconds: 200), + value: 0.0, + vsync: this, + )..addListener( + () { + setState(() {}); + }, + ); + super.initState(); + } + + @override + void dispose() { + _animation.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) { + _animation.forward(from: _animation.value); + }, + onExit: (_) { + _animation.reverse(); + }, + child: widget.builder(_animation.value), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/independent/panel.dart b/patterns/abstract_factory/tool_panel_factory/widgets/independent/panel.dart new file mode 100644 index 0000000..c123fa8 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/independent/panel.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class Panel extends StatelessWidget { + static const thicknessWidth = 64.0; + final double thicknessHeight; + + final Axis direction; + final Widget child; + + const Panel({ + Key? key, + required this.direction, + required this.child, + this.thicknessHeight = 48.0, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + height: direction == Axis.horizontal ? thicknessHeight : null, + width: direction == Axis.vertical ? thicknessWidth : null, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Color(0xFF3B3B3B), + borderRadius: BorderRadius.all(Radius.circular(3)), + boxShadow: [ + BoxShadow( + color: Color(0x7C000000), + blurRadius: 6, + offset: Offset(2, 2), + ), + ], + ), + child: child, + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_bar.dart b/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_bar.dart new file mode 100644 index 0000000..ec554b2 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_bar.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'panel.dart'; + +class ToolBar extends StatelessWidget { + final String title; + final Widget child; + + const ToolBar({ + Key? key, + required this.title, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Panel( + direction: Axis.vertical, + child: Column( + children: [ + _buildTitle(title), + child, + ], + ), + ); + } + + Widget _buildTitle(String title) { + return Container( + height: 20, + color: Colors.white10, + width: double.infinity, + alignment: Alignment.center, + child: Text( + title, + style: TextStyle( + color: Colors.white70, + fontSize: 13, + fontFamily: 'Arial', + decoration: TextDecoration.none, + fontWeight: FontWeight.normal, + ), + ), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart b/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart new file mode 100644 index 0000000..441154a --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +import 'hove.dart'; + +class ToolButton extends StatelessWidget { + final Function() onTap; + final bool active; + final Widget icon; + + const ToolButton({ + Key? key, + required this.onTap, + this.active = false, + required this.icon, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Hover( + builder: (hoverForce) { + return Container( + width: 64, + height: 64, + color: color(hoverForce), + child: Padding( + padding: EdgeInsets.all(16), + child: icon, + ), + ); + }, + ), + ); + } + + Color color(double hoverForce) => active + ? Color.lerp(Colors.white10, Colors.white12, hoverForce)! + : Color.lerp(Colors.transparent, Colors.white10, hoverForce)!; +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_bar.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_bar.dart new file mode 100644 index 0000000..ac1483f --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_bar.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../app/tools.dart'; +import '../pattern/tool_factory.dart'; +import 'independent/panel.dart'; +import 'property_widgets/factories/property_widget_factories.dart'; +import 'property_widgets/primitive/theme_property.dart'; + +class PropertyPanel extends StatelessWidget { + final Tools tools; + final PropertyWidgetFactories factories; + + const PropertyPanel({ + Key? key, + required this.tools, + required this.factories, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + top: 12, + left: 12 + 64 + 8 + 12, + child: ThemeProperty( + child: Panel( + direction: Axis.horizontal, + child: ValueListenableBuilder( + valueListenable: tools.activeFactory, + builder: (_, activeToolFactory, __) { + return Row( + children: factories + .createListWidgetsFrom(activeToolFactory.properties) + .toList(), + ); + }, + ), + ), + ), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/bool_property_widget.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/bool_property_widget.dart new file mode 100644 index 0000000..b3e1976 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/bool_property_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import '../../pattern/property.dart'; +import 'factories/property_widget_factory.dart'; +import 'primitive/filed_label.dart'; + +class BoolPropertyWidgetFactory implements PropertyWidgetFactory { + @override + Widget createWidget(Property property) { + return BoolPropertyWidget(property: property); + } + + @override + bool isPropertyCompatible(Property property) => property.value() is bool; +} + +class BoolPropertyWidget extends StatefulWidget { + final Property property; + + const BoolPropertyWidget({Key? key, required this.property}) + : super(key: key); + + @override + _BoolPropertyWidgetState createState() => _BoolPropertyWidgetState(); +} + +class _BoolPropertyWidgetState extends State { + @override + Widget build(BuildContext context) { + return FieldLabel( + text: widget.property.name, + child: Row( + children: [ + Checkbox( + value: widget.property.value() as bool, + onChanged: (val) { + setState(() { + widget.property.onChange(val); + }); + }, + ) + ], + ), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/double_property_widget.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/double_property_widget.dart new file mode 100644 index 0000000..741f54e --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/double_property_widget.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import '../../pattern/property.dart'; +import 'factories/property_widget_factory.dart'; +import 'primitive/filed_label.dart'; + +class DoublePropertyWidgetFactory implements PropertyWidgetFactory { + @override + Widget createWidget(Property property) { + return DoublePropertyWidget(property: property); + } + + @override + bool isPropertyCompatible(Property property) => property.value() is double; +} + +class DoublePropertyWidget extends StatefulWidget { + final Property property; + + const DoublePropertyWidget({Key? key, required this.property}) + : super(key: key); + + @override + _DoublePropertyWidgetState createState() => _DoublePropertyWidgetState(); +} + +class _DoublePropertyWidgetState extends State { + @override + Widget build(BuildContext context) { + return FieldLabel( + text: widget.property.name, + child: Row( + children: [ + SizedBox( + width: 200, + child: Slider.adaptive( + min: 0.0, + max: 300, + value: widget.property.value() as double, + onChanged: (val) { + setState(() { + widget.property.onChange(val); + }); + }, + ), + ), + SizedBox( + width: 32, + child: Text( + (widget.property.value() as double).toStringAsFixed(0), + textAlign: TextAlign.right, + ), + ) + ], + ), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/factories/property_widget_factories.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/factories/property_widget_factories.dart new file mode 100644 index 0000000..760e4b2 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/factories/property_widget_factories.dart @@ -0,0 +1,31 @@ +import 'package:flutter/widgets.dart'; + +import '../../../pattern/property.dart'; + +import 'property_widget_factory.dart'; + +class PropertyWidgetFactories { + final List _factories; + + PropertyWidgetFactories({ + required List factories, + }) : _factories = factories; + + Widget createPropertyWidgetFrom(Property property) { + for (final factory in _factories) { + if (factory.isPropertyCompatible(property)) { + return factory.createWidget(property); + } + } + + throw 'Value(${property.value()}) property is not support.'; + } + + Iterable createListWidgetsFrom( + Iterable properties, + ) sync* { + for (final property in properties) { + yield createPropertyWidgetFrom(property); + } + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/factories/property_widget_factory.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/factories/property_widget_factory.dart new file mode 100644 index 0000000..66c4820 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/factories/property_widget_factory.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +import '../../../pattern/property.dart'; + +abstract class PropertyWidgetFactory { + Widget createWidget(Property property); + + bool isPropertyCompatible(Property value); +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/filed_label.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/filed_label.dart new file mode 100644 index 0000000..7bc950d --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/filed_label.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +class FieldLabel extends StatelessWidget { + final String text; + final Widget child; + + const FieldLabel({ + Key? key, + required this.text, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox(width: 10), + Text('$text:'), + SizedBox(width: 10), + child, + SizedBox(width: 20), + ], + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/theme_property.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/theme_property.dart new file mode 100644 index 0000000..af62c06 --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/primitive/theme_property.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class ThemeProperty extends StatelessWidget { + final Widget child; + + const ThemeProperty({ + Key? key, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + textStyle: TextStyle( + color: Colors.white, + fontSize: 16, + fontFamily: 'Arial', + ), + child: Theme( + data: ThemeData( + primarySwatch: Colors.pink, + unselectedWidgetColor: Colors.grey, // Your color + ), + child: child, + ), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/string_property_widget.dart b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/string_property_widget.dart new file mode 100644 index 0000000..e64fefb --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/property_widgets/string_property_widget.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import '../../pattern/property.dart'; +import 'factories/property_widget_factory.dart'; +import 'primitive/filed_label.dart'; + +class StringPropertyWidgetFactory implements PropertyWidgetFactory { + @override + Widget createWidget(Property property) { + return StringPropertyWidget(property: property); + } + + @override + bool isPropertyCompatible(Property property) => property.value() is String; +} + +class StringPropertyWidget extends StatelessWidget { + final Property property; + + const StringPropertyWidget({Key? key, required this.property}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return FieldLabel( + text: property.name, + child: Container( + color: Colors.white12, + width: 120, + height: 32, + child: TextFormField( + style: TextStyle(fontSize: 18, color: Colors.white70), + decoration: InputDecoration( + contentPadding: const EdgeInsets.only(left: 5.0, bottom: 14.0), + border: UnderlineInputBorder(), + ), + initialValue: property.value(), + onChanged: property.onChange, + ), + ), + ); + } +} diff --git a/patterns/abstract_factory/tool_panel_factory/widgets/tool_panel.dart b/patterns/abstract_factory/tool_panel_factory/widgets/tool_panel.dart new file mode 100644 index 0000000..6e0729d --- /dev/null +++ b/patterns/abstract_factory/tool_panel_factory/widgets/tool_panel.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +import '../app/tools.dart'; +import 'colors_tool_bar.dart'; +import 'factories_tool_bar.dart'; + +class ToolPanel extends StatelessWidget { + final Tools tools; + const ToolPanel({Key? key, required this.tools}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + left: 12, + top: 12, + child: Column( + children: [ + FactoriesToolBar(tools: tools), + SizedBox(height: 24), + ColorsToolBar(tools: tools), + ], + ), + ); + } +} diff --git a/patterns/adapter/flutter_adapter/README.md b/patterns/adapter/flutter_adapter/README.md new file mode 100644 index 0000000..aff519c --- /dev/null +++ b/patterns/adapter/flutter_adapter/README.md @@ -0,0 +1,60 @@ + +# Adapter pattern +Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate. + +Tutorial: [here](https://refactoring.guru/design-patterns/observer). + +## Example: Flutter classic application adapter widget +This example shows how to adapt a non-reactive classic type application for Flutter. + +### Dependency +This complex example includes these implementations: +- [[AppObserver](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/app_observer)] +- [[SubscriberWidget](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/subscriber_flutter_widget)] + +### Online demo: +Click on the picture to see a [demo](https://refactoringguru.github.io/design-patterns-dart/#/adapter/flutter_adapter). + +[![image](https://user-images.githubusercontent.com/8049534/152689272-d4bed484-e216-4eda-8833-928ada7d4051.png)](https://refactoringguru.github.io/design-patterns-dart/#/adapter/flutter_adapter) + +### Client code: +```dart +// classic application +class App extends ClassicApp { + @override + void onPointerWheel(double deltaX, double deltaY) { + // ... + } + + @override + void onMouseDown() { + // ... + repaint(); + } + + @override + void onPaint(Canvas canvas, Size canvasSize) { + //... + } +} + +void main() { + // adapting a classic application to a flutter widget tree + ClassicAppAdapterWidget( + classicApp: app, + ); +} +``` + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/152753162-1b9006ad-a633-4132-91b6-bb348559adec.png) + +### Sequence [Classic application -> Change Text color] +When user clicked to "Flutter Adapter" text. + +![image](https://user-images.githubusercontent.com/8049534/152753714-84af5abd-85c0-4845-af2d-616f512ef633.png) + +### Sequence [Flutter Widget -> Change Text color] +When the user has selected a color in the color bar. + +![image](https://user-images.githubusercontent.com/8049534/152753870-edeab3ae-8e79-4e9d-9049-7cd5a2100afa.png) diff --git a/patterns/adapter/flutter_adapter/adapter/classic_app_adapter_widget.dart b/patterns/adapter/flutter_adapter/adapter/classic_app_adapter_widget.dart new file mode 100644 index 0000000..421bf23 --- /dev/null +++ b/patterns/adapter/flutter_adapter/adapter/classic_app_adapter_widget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/widgets.dart'; + + +import '../classic_app/classic_app.dart'; +import 'classic_app_render_object.dart'; + +class ClassicAppAdapterWidget extends LeafRenderObjectWidget { + final ClassicApp classicApp; + + ClassicAppAdapterWidget({required this.classicApp}); + + @override + RenderObject createRenderObject(BuildContext context) { + return ClassicAppRenderObject(classicApp); + } + + @override + void updateRenderObject( + BuildContext context, + covariant ClassicAppRenderObject renderObject, + ) { + renderObject.classicApp = classicApp; + } +} diff --git a/patterns/adapter/flutter_adapter/adapter/classic_app_render_object.dart b/patterns/adapter/flutter_adapter/adapter/classic_app_render_object.dart new file mode 100644 index 0000000..0811cda --- /dev/null +++ b/patterns/adapter/flutter_adapter/adapter/classic_app_render_object.dart @@ -0,0 +1,87 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; + +import '../classic_app/classic_app.dart'; +import '../classic_app/repaint_event.dart'; + +class ClassicAppRenderObject extends RenderBox { + ClassicAppRenderObject(ClassicApp classicApp) { + _classicApp = classicApp; + _classicApp.events.subscribe(_clientAppRepaint); + _isSubscribe = true; + } + + @override + bool get isRepaintBoundary => true; + + @override + void performLayout() { + size = Size( + constraints.maxWidth == double.infinity ? 0 : constraints.maxWidth, + constraints.maxHeight == double.infinity ? 0 : constraints.maxHeight, + ); + } + + @override + void paint(PaintingContext context, Offset offset) { + context.canvas.translate(offset.dx, offset.dy); + context.canvas.clipRect(offset & size); + _classicApp.onPaint(context.canvas, size); + } + + @override + void dispose() { + if (_isSubscribe) { + _classicApp.events.unsubscribe(_clientAppRepaint); + _isSubscribe = false; + } + super.dispose(); + } + + late ClassicApp _classicApp; + var _isSubscribe = false; + + ClassicApp get classicApp => _classicApp; + + set classicApp(ClassicApp newClassicApp) { + if (newClassicApp == _classicApp) { + return; + } + + if (_isSubscribe) { + _classicApp.events.unsubscribe(_clientAppRepaint); + _isSubscribe = false; + } + + _classicApp = newClassicApp; + _classicApp.events.subscribe(_clientAppRepaint); + _isSubscribe = true; + } + + void _clientAppRepaint(RepaintEvent e) => markNeedsPaint(); + + @override + void handleEvent(PointerEvent event, covariant BoxHitTestEntry entry) { + if (event is PointerHoverEvent || event is PointerMoveEvent) { + _classicApp.onMouseMove(event.position.dx, event.position.dy); + } else if (event is PointerScrollEvent) { + _classicApp.onPointerWheel(event.scrollDelta.dx, event.scrollDelta.dy); + } else if (event is PointerDownEvent) { + if (event.buttons == kPrimaryMouseButton) { + _classicApp.onMouseDown(event.position.dx, event.position.dy); + } else if (event.buttons == kSecondaryMouseButton) { + } else if (event.buttons == kMiddleMouseButton) {} + } else if (event is PointerUpEvent) { + _classicApp.onMouseUp(); + } + } + + @override + bool hitTest(BoxHitTestResult result, {required Offset position}) { + if (size.contains(position)) { + result.add(BoxHitTestEntry(this, position)); + return true; + } + return false; + } +} diff --git a/patterns/adapter/flutter_adapter/classic_app/classic_app.dart b/patterns/adapter/flutter_adapter/classic_app/classic_app.dart new file mode 100644 index 0000000..7e56111 --- /dev/null +++ b/patterns/adapter/flutter_adapter/classic_app/classic_app.dart @@ -0,0 +1,24 @@ +import 'dart:ui'; + +import '../../../observer/app_observer/observer/app_observer.dart'; +import 'repaint_event.dart'; +import 'repaint_compatible.dart'; + +abstract class ClassicApp implements RepaintCompatible { + final events = AppObserver(); + + void onMouseDown(double x, double y) {} + + void onMouseUp() {} + + void onMouseMove(double x, double y) {} + + void onPointerWheel(double deltaX, double deltaY) {} + + @override + void repaint() { + events.notify(RepaintEvent()); + } + + void onPaint(Canvas canvas, Size canvasSize); +} diff --git a/patterns/adapter/flutter_adapter/classic_app/repaint_compatible.dart b/patterns/adapter/flutter_adapter/classic_app/repaint_compatible.dart new file mode 100644 index 0000000..e8d6804 --- /dev/null +++ b/patterns/adapter/flutter_adapter/classic_app/repaint_compatible.dart @@ -0,0 +1,3 @@ +abstract class RepaintCompatible { + void repaint(); +} diff --git a/patterns/adapter/flutter_adapter/classic_app/repaint_event.dart b/patterns/adapter/flutter_adapter/classic_app/repaint_event.dart new file mode 100644 index 0000000..bece697 --- /dev/null +++ b/patterns/adapter/flutter_adapter/classic_app/repaint_event.dart @@ -0,0 +1,3 @@ +import '../../../observer/app_observer/observer/event.dart'; + +class RepaintEvent extends Event {} diff --git a/patterns/adapter/flutter_adapter/client_app/app.dart b/patterns/adapter/flutter_adapter/client_app/app.dart new file mode 100644 index 0000000..7a4f5a7 --- /dev/null +++ b/patterns/adapter/flutter_adapter/client_app/app.dart @@ -0,0 +1,79 @@ +import 'dart:ui'; + +import '../classic_app/classic_app.dart'; +import 'business_rules/color_rules.dart'; +import 'business_rules/text_coloring.dart'; + +class App extends ClassicApp { + late final TextColoring textColoring; + late final ColorRules colorRules; + + App() { + textColoring = TextColoring(this); + colorRules = ColorRules(); + } + + @override + void onPointerWheel(double deltaX, double deltaY) { + textColoring.size += deltaY ~/ 10; + } + + @override + void onMouseDown(_, __) { + textColoring.color = colorRules.nextColor(textColoring.color); + } + + @override + void onPaint(Canvas canvas, Size canvasSize) { + paintText( + canvas, + 'Flutter Adapter', + canvasSize, + textColoring.size.toDouble(), + textColoring.color, + ); + + paintText( + canvas, + 'Click on the text to change the text color.\n' + 'Scroll the mouse wheel to change the text size.', + canvasSize, + 16, + Color(0xff848484), + Offset(0, canvasSize.height - 50), + ); + } +} + +void paintText( + Canvas canvas, + String text, + Size boxSize, + double textSize, + Color color, [ + Offset? pos, +]) { + final builder = ParagraphBuilder( + ParagraphStyle( + textAlign: TextAlign.center, + fontSize: textSize, + ), + ) + ..pushStyle( + TextStyle( + fontFamily: 'Arial', + color: color, + ), + ) + ..addText(text); + + final paragraph = builder.build() + ..layout( + ParagraphConstraints( + width: boxSize.width, + ), + ); + + pos ??= Offset(0, (boxSize.height - paragraph.height) / 2); + canvas.drawParagraph(paragraph, pos); +} diff --git a/patterns/adapter/flutter_adapter/client_app/business_rules/color_rules.dart b/patterns/adapter/flutter_adapter/client_app/business_rules/color_rules.dart new file mode 100644 index 0000000..4613d76 --- /dev/null +++ b/patterns/adapter/flutter_adapter/client_app/business_rules/color_rules.dart @@ -0,0 +1,21 @@ +import 'dart:ui'; + +class ColorRules { + final colors = [ + Color(0xFF000000), + Color(0xFFD81B60), + Color(0xFF5E35B1), + Color(0xFF1E88E5), + Color(0xFF43A047), + ]; + + Color nextColor(Color currentColor) { + var nextIndex = colors.indexOf(currentColor) + 1; + + if (nextIndex >= colors.length || nextIndex < 0) { + nextIndex = 0; + } + + return colors[nextIndex]; + } +} diff --git a/patterns/adapter/flutter_adapter/client_app/business_rules/text_coloring.dart b/patterns/adapter/flutter_adapter/client_app/business_rules/text_coloring.dart new file mode 100644 index 0000000..af1e04e --- /dev/null +++ b/patterns/adapter/flutter_adapter/client_app/business_rules/text_coloring.dart @@ -0,0 +1,44 @@ +import 'dart:ui'; + +import '../../classic_app/repaint_compatible.dart'; + +class TextColoring { + final RepaintCompatible _repaintContext; + + TextColoring(this._repaintContext); + + final maxTextSize = 400; + + var _size = 50; + + int get size => _size; + + set size(int newVal) { + if (newVal == _size) { + return; + } + + if (newVal > maxTextSize) { + _size = maxTextSize; + } else if (newVal < 1) { + _size = 1; + } else { + _size = newVal; + } + + _repaintContext.repaint(); + } + + var _color = Color(0xffd81b60); + + Color get color => _color; + + set color(Color newColor) { + if (_color == newColor) { + return; + } + + _color = newColor; + _repaintContext.repaint(); + } +} diff --git a/patterns/adapter/flutter_adapter/client_app/widgets/color_buttons_widget.dart b/patterns/adapter/flutter_adapter/client_app/widgets/color_buttons_widget.dart new file mode 100644 index 0000000..d87faf7 --- /dev/null +++ b/patterns/adapter/flutter_adapter/client_app/widgets/color_buttons_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class ColorButtonsWidget extends StatelessWidget { + final Color currentColor; + final List colors; + final void Function(Color color) onColorSelect; + + const ColorButtonsWidget({ + Key? key, + required this.currentColor, + required this.colors, + required this.onColorSelect, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: colors.map(_buildColorButton).toList(), + ); + } + + Widget _buildColorButton(Color color) { + final isColorSelect = (color == currentColor); + return GestureDetector( + onTap: () { + onColorSelect(color); + }, + child: Container( + width: 20, + height: 20, + color: color, + child: isColorSelect ? _buildSelectColorIcon() : null, + ), + ); + } + + Widget _buildSelectColorIcon() { + return Center( + child: Container( + width: 4, + height: 4, + color: Colors.white.withOpacity(0.8), + ), + ); + } +} diff --git a/patterns/adapter/flutter_adapter/client_app/widgets/slider_widget.dart b/patterns/adapter/flutter_adapter/client_app/widgets/slider_widget.dart new file mode 100644 index 0000000..f28abe7 --- /dev/null +++ b/patterns/adapter/flutter_adapter/client_app/widgets/slider_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class SliderWidget extends StatelessWidget { + final int current; + final int max; + final void Function(int newSize) onChange; + + const SliderWidget({ + Key? key, + required this.current, + required this.max, + required this.onChange, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + width: 31, + child: Text( + '$current', + textAlign: TextAlign.right, + ), + ), + SizedBox( + width: 200, + child: Slider( + value: current.toDouble(), + max: max.toDouble(), + min: 1, + onChanged: (newVal) { + onChange(newVal.toInt()); + }, + ), + ), + ], + ); + } +} diff --git a/patterns/adapter/flutter_adapter/client_app/widgets/text_property_widget.dart b/patterns/adapter/flutter_adapter/client_app/widgets/text_property_widget.dart new file mode 100644 index 0000000..3f96ccf --- /dev/null +++ b/patterns/adapter/flutter_adapter/client_app/widgets/text_property_widget.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import '../../../../observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart'; +import '../../classic_app/repaint_event.dart'; +import '../app.dart'; +import 'color_buttons_widget.dart'; +import 'slider_widget.dart'; + +class TextPropertyWidget extends StatelessWidget { + final App classicApp; + + const TextPropertyWidget({ + Key? key, + required this.classicApp, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final app = classicApp; + return SubscriberWidget( + observer: app.events, + builder: (context, event) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SliderWidget( + current: app.textColoring.size, + max: app.textColoring.maxTextSize, + onChange: (newVal) { + app.textColoring.size = newVal; + }, + ), + ColorButtonsWidget( + currentColor: app.textColoring.color, + colors: app.colorRules.colors, + onColorSelect: (color) { + app.textColoring.color = color; + }, + ), + ], + ); + }, + ); + } +} diff --git a/patterns/adapter/flutter_adapter/main.dart b/patterns/adapter/flutter_adapter/main.dart new file mode 100644 index 0000000..60624f5 --- /dev/null +++ b/patterns/adapter/flutter_adapter/main.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'adapter/classic_app_adapter_widget.dart' as adapter; +import 'client_app/app.dart'; +import 'client_app/widgets/text_property_widget.dart'; + +class FlutterAdapterApp extends StatefulWidget { + @override + State createState() => _FlutterAdapterAppState(); +} + +class _FlutterAdapterAppState extends State { + final app = App(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Column( + children: [ + TextPropertyWidget( + classicApp: app, + ), + Expanded( + child: adapter.ClassicAppAdapterWidget( + classicApp: app, + ), + ), + ], + ), + ); + } +} diff --git a/patterns/adapter/square_round_conflict/README.md b/patterns/adapter/square_round_conflict/README.md new file mode 100644 index 0000000..e6aa8e6 --- /dev/null +++ b/patterns/adapter/square_round_conflict/README.md @@ -0,0 +1,39 @@ +# Adapter pattern +Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate. + +## Square and Round conflict example. +**Description:** https://refactoring.guru/design-patterns/adapter?#pseudocode + +### Class Diagram: +![example](https://user-images.githubusercontent.com/8049534/147594536-66627fa1-f4eb-42ba-b648-8757f9e5bf20.png) + +### Client code: +```dart +void main() { + final hole = RoundHole(5); + final peg = RoundPeg(5); + if (hole.fits(peg)) { + print("Round peg r5 fits round hole r5."); + } + + final smallSqPeg = SquarePeg(2); + final largeSqPeg = SquarePeg(20); + + final smallSqPegAdapter = SquarePegAdapter(smallSqPeg); + final largeSqPegAdapter = SquarePegAdapter(largeSqPeg); + + if (hole.fits(smallSqPegAdapter)) { + print("Square peg w2 fits round hole r5."); + } + if (!hole.fits(largeSqPegAdapter)) { + print("Square peg w20 does not fit into round hole r5."); + } +} +``` + +**Output:** +``` +Round peg r5 fits round hole r5. +Square peg w2 fits round hole r5. +Square peg w20 does not fit into round hole r5. +``` diff --git a/patterns/adapter/square_round_conflict/main.dart b/patterns/adapter/square_round_conflict/main.dart new file mode 100644 index 0000000..aed8fb3 --- /dev/null +++ b/patterns/adapter/square_round_conflict/main.dart @@ -0,0 +1,63 @@ +/// EN: Somewhere in client code... +/// +/// RU: Где-то в клиентском коде... +void main() { + // EN: Round fits round, no surprise. + // + // RU: Круглое к круглому — всё работает. + final hole = RoundHole(5); + final peg = RoundPeg(5); + + if (hole.fits(peg)) { + print("Round peg r5 fits round hole r5."); + } + + final smallSqPeg = SquarePeg(2); + final largeSqPeg = SquarePeg(20); + // EN: hole.fits(smallSqPeg); // Won't compile. + // + // RU: hole.fits(smallSqPeg); // Не скомпилируется. + + // EN: Adapter solves the problem. + // + // RU: Адаптер решит проблему. + final smallSqPegAdapter = SquarePegAdapter(smallSqPeg); + final largeSqPegAdapter = SquarePegAdapter(largeSqPeg); + + if (hole.fits(smallSqPegAdapter)) { + print("Square peg w2 fits round hole r5."); + } + + if (!hole.fits(largeSqPegAdapter)) { + print("Square peg w20 does not fit into round hole r5."); + } +} + +class RoundPeg { + final int radius; + + RoundPeg(this.radius); +} + +class SquarePegAdapter implements RoundPeg { + final SquarePeg squarePeg; + + SquarePegAdapter(this.squarePeg); + + @override + int get radius => squarePeg.width; +} + +class SquarePeg { + final int width; + + SquarePeg(this.width); +} + +class RoundHole { + final int radius; + + RoundHole(this.radius); + + bool fits(RoundPeg roundPeg) => radius >= roundPeg.radius; +} diff --git a/patterns/adapter/text_graphics/README.md b/patterns/adapter/text_graphics/README.md new file mode 100644 index 0000000..4ee42df --- /dev/null +++ b/patterns/adapter/text_graphics/README.md @@ -0,0 +1,92 @@ +# Adapter pattern +Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate. + +**General description of the pattern:** +https://refactoring.guru/design-patterns/adapter + +## Example: +Adding shapes from a third-party library. + +### Class Diagram: +![image](https://user-images.githubusercontent.com/8049534/165749605-5361d5e2-e4a1-41ed-9d27-1c4965fa30d2.png) + +### Client code: +```dart +final renderContent = ShapeEngine( + width: 42, + height: 32, + shapes: [ + // EN: Standard graphics primitives + Rectangle( + x: 1, + y: 1, + width: 12, + height: 6, + color: Color.black, + ), + Rectangle( + x: 4, + y: 3, + width: 6, + height: 9, + color: Color.white, + ), + Circle( + x: 12, + y: 12, + radius: 7, + color: Color.grey, + ), + // EN: Graphics primitives from a third party library + GraphElementAdapter( + x: 11, + y: 20, + color: Color.dark, + graphElement: third_party.Wave(height: 5, length: 25), + ), + GraphElementAdapter( + x: 23, + y: 1, + color: Color.black, + graphElement: third_party.Star(size: 20), + ), + ], + ).render(); + print(renderContent); +``` + +**Output:** +``` +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░███████████████████████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░███░░░░░░ ░░░░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░███░░░░░░░░░░░░░░░░░░░░░░░░ +░░░███░░░░░░ ░░░░░░░░░░░░░░░ ░░░░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░███░░░░░░░░░░░░░░░░░░░░░░░░ +░░░███░░░░░░ ░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░███░░░░░░░░░░░░░░░░░░░░░░░░ +░░░███░░░░░░ ░░░░░░░░░▒▒▒▒▒▒ ░░░░░░███░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░ +░░░█████████ ██████▒▒▒██████ █████████░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░███████████████████████████████████████████████████░░░ +░░░░░░░░░░░░ ░░░▒▒▒░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░███░░░░░░░░░░░░███░░░░░░░░░███░░░░░░░░░░░░███░░░░░░ +░░░░░░░░░░░░ ░░░▒▒▒░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░███░░░░░░███░░░░░░░░░░░░░░░███░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░ ▒▒▒░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░█████████░░░░░░░░░░░░░░░█████████░░░░░░░░░░░░ +░░░░░░░░░░░░ ▒▒▒░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░ ▒▒▒ ░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░███░░░███░░░░░░░░░███░░░███░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░███░░░░░░███░░░███░░░░░░███░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░███░░░░░░░░░███░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░███░░░███░░░░░░░░░███░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░███░░░██████░░░░░░░░░██████░░░███░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░██████░░░░░░░░░░░░░░░░░░░░░░░░░░░██████░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +``` diff --git a/patterns/adapter/text_graphics/engine/shape_engine.dart b/patterns/adapter/text_graphics/engine/shape_engine.dart new file mode 100644 index 0000000..0b281de --- /dev/null +++ b/patterns/adapter/text_graphics/engine/shape_engine.dart @@ -0,0 +1,28 @@ +import 'dart:math'; + +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../shapes/shape.dart'; + +class ShapeEngine { + final List shapes; + final int width; + final int height; + + ShapeEngine({ + required this.width, + required this.height, + required this.shapes, + }); + + String render() { + final can = Canvas(width, height, lineStretch: 3); + for (final Shape shape in shapes) { + can + ..translate = Point(shape.x, shape.y) + ..penColor = shape.color; + shape.draw(can); + } + return can.toString(); + } +} diff --git a/patterns/adapter/text_graphics/main.dart b/patterns/adapter/text_graphics/main.dart new file mode 100644 index 0000000..28adfca --- /dev/null +++ b/patterns/adapter/text_graphics/main.dart @@ -0,0 +1,51 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import 'engine/shape_engine.dart'; +import 'shapes/shape.dart'; +import 'shapes/graph_element_adapter.dart'; +import 'third_party_graphics_lib/star.dart' as third_party; +import 'third_party_graphics_lib/wave.dart' as third_party; + +void main() { + final renderContent = ShapeEngine( + width: 42, + height: 32, + shapes: [ + // EN: Standard graphics primitives + Rectangle( + x: 1, + y: 1, + width: 12, + height: 6, + color: Color.black, + ), + Rectangle( + x: 4, + y: 3, + width: 6, + height: 9, + color: Color.white, + ), + Circle( + x: 12, + y: 12, + radius: 7, + color: Color.grey, + ), + // EN: Graphics primitives from a third party library + GraphElementAdapter( + x: 11, + y: 20, + color: Color.dark, + graphElement: third_party.Wave(height: 5, length: 25), + ), + GraphElementAdapter( + x: 23, + y: 1, + color: Color.black, + graphElement: third_party.Star(size: 20), + ), + ], + ); + print(renderContent.render()); +} diff --git a/patterns/adapter/text_graphics/shapes/graph_element_adapter.dart b/patterns/adapter/text_graphics/shapes/graph_element_adapter.dart new file mode 100644 index 0000000..ee50eaf --- /dev/null +++ b/patterns/adapter/text_graphics/shapes/graph_element_adapter.dart @@ -0,0 +1,24 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../third_party_graphics_lib/graph_element.dart' as third_party; +import 'shape.dart'; + +class GraphElementAdapter extends Shape { + final third_party.GraphElement graphElement; + + GraphElementAdapter({ + required int x, + required int y, + required Color color, + required this.graphElement, + }) : super(x, y, color); + + @override + void draw(Canvas can) { + final points = graphElement.points; + can.moveTo(points[0], points[1]); + for (var i = 0; i < points.length; i += 2) { + can.lineTo(points[i], points[i + 1]); + } + } +} diff --git a/patterns/adapter/text_graphics/shapes/shape.dart b/patterns/adapter/text_graphics/shapes/shape.dart new file mode 100644 index 0000000..c38c3d9 --- /dev/null +++ b/patterns/adapter/text_graphics/shapes/shape.dart @@ -0,0 +1,41 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +abstract class Shape { + final int x; + final int y; + final Color color; + + Shape(this.x, this.y, this.color); + + void draw(Canvas can); +} + +class Rectangle extends Shape { + final int width; + final int height; + + Rectangle({ + required int x, + required int y, + required this.width, + required this.height, + required Color color, + }) : super(x, y, color); + + @override + void draw(Canvas can) => can.rectangle(width, height); +} + +class Circle extends Shape { + final int radius; + + Circle({ + required int x, + required int y, + required this.radius, + required Color color, + }) : super(x, y, color); + + @override + void draw(Canvas can) => can.circle(radius); +} diff --git a/patterns/adapter/text_graphics/third_party_graphics_lib/graph_element.dart b/patterns/adapter/text_graphics/third_party_graphics_lib/graph_element.dart new file mode 100644 index 0000000..b011227 --- /dev/null +++ b/patterns/adapter/text_graphics/third_party_graphics_lib/graph_element.dart @@ -0,0 +1,5 @@ +import 'dart:typed_data'; + +abstract class GraphElement { + Int32List get points; +} diff --git a/patterns/adapter/text_graphics/third_party_graphics_lib/star.dart b/patterns/adapter/text_graphics/third_party_graphics_lib/star.dart new file mode 100644 index 0000000..f08133f --- /dev/null +++ b/patterns/adapter/text_graphics/third_party_graphics_lib/star.dart @@ -0,0 +1,27 @@ +import 'dart:typed_data'; + +import 'graph_element.dart'; + +class Star extends GraphElement { + final int size; + + Star({required this.size}) { + final list = []; + final p1 = (size * .1).toInt(), + p3 = (size * .3).toInt(), + p4 = (size * .4).toInt(), + p7 = (size * .7).toInt(), + p8 = (size * .8).toInt(), + p9 = (size * .9).toInt(); + list.addAll([p1, p9]); + list.addAll([p4, 0]); + list.addAll([p7, p9]); + list.addAll([0, p3]); + list.addAll([p8, p3]); + list.addAll([p1, p9]); + points = Int32List.fromList(list); + } + + @override + late Int32List points; +} diff --git a/patterns/adapter/text_graphics/third_party_graphics_lib/wave.dart b/patterns/adapter/text_graphics/third_party_graphics_lib/wave.dart new file mode 100644 index 0000000..e4f9d6d --- /dev/null +++ b/patterns/adapter/text_graphics/third_party_graphics_lib/wave.dart @@ -0,0 +1,22 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'graph_element.dart'; + +class Wave extends GraphElement { + Wave({ + required final int height, + final int length = 50, + final double waveStep = .8, + }) { + final list = []; + for (var x = 0; x < length; x++) { + final y = (height + cos(x / pi / waveStep) * height).toInt(); + list.addAll([x, y]); + } + points = Int32List.fromList(list); + } + + @override + late Int32List points; +} diff --git a/patterns/bridge/clock/README.md b/patterns/bridge/clock/README.md new file mode 100644 index 0000000..51878dc --- /dev/null +++ b/patterns/bridge/clock/README.md @@ -0,0 +1,33 @@ +# Bridge pattern +Bridge is a structural design pattern that lets you split a large class or a set of closely related +classes into two separate hierarchies—abstraction and implementation—which can be developed independently of each other. + +## Example +The idea for the bridge pattern example is taken from [here](https://habr.com/ru/post/85137/). + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/145851578-f6e95355-e2b3-4f94-bda2-c2d1d0de8935.png) + +### Client code: +```dart +void main() { + final melody = Melody(); + final signal = Signal(); + startClocks([ + Once(seconds: 1, bell: melody), + Once(seconds: 2, bell: signal), + Interval(seconds: 5, bell: melody), + Interval(seconds: 3, bell: signal), + ]); +} + +void startClocks(List clocks) { + for (final clock in clocks) { + clock.start(); + } +} +``` + +**Output:** + +https://user-images.githubusercontent.com/8049534/145850512-27a5e9ea-4f76-4d52-b784-9bc88aee4de8.mp4 diff --git a/patterns/bridge/clock/bells/bell.dart b/patterns/bridge/clock/bells/bell.dart new file mode 100644 index 0000000..50e3cbc --- /dev/null +++ b/patterns/bridge/clock/bells/bell.dart @@ -0,0 +1,5 @@ +abstract class Bell { + void ring(); + + void notify(String message); +} diff --git a/patterns/bridge/clock/bells/melody.dart b/patterns/bridge/clock/bells/melody.dart new file mode 100644 index 0000000..f33c7cf --- /dev/null +++ b/patterns/bridge/clock/bells/melody.dart @@ -0,0 +1,11 @@ +import 'dart:io'; + +import 'bell.dart'; + +class Melody extends Bell { + @override + void notify(String message) => stdout.write('\x1b[32m$message\x1B[0m'); + + @override + void ring() => stdout.write('\x1b[32m🎵\x1B[0m'); +} diff --git a/patterns/bridge/clock/bells/signal.dart b/patterns/bridge/clock/bells/signal.dart new file mode 100644 index 0000000..bf8b733 --- /dev/null +++ b/patterns/bridge/clock/bells/signal.dart @@ -0,0 +1,11 @@ +import 'dart:io'; + +import 'bell.dart'; + +class Signal extends Bell { + @override + void notify(String message) => stdout.write('\x1b[34m$message\x1B[0m'); + + @override + void ring() => stdout.write('\x1b[34m🔉\x1B[0m'); +} diff --git a/patterns/bridge/clock/clocks/clock.dart b/patterns/bridge/clock/clocks/clock.dart new file mode 100644 index 0000000..87c72cb --- /dev/null +++ b/patterns/bridge/clock/clocks/clock.dart @@ -0,0 +1,18 @@ +import '../bells/bell.dart'; + +abstract class Clock { + final int seconds; + final Bell bell; + + Clock({ + required this.seconds, + required this.bell, + }); + + void start(); + + void showBell(String message) { + bell.notify('$message(sec: $seconds): '); + bell.ring(); + } +} diff --git a/patterns/bridge/clock/clocks/interval.dart b/patterns/bridge/clock/clocks/interval.dart new file mode 100644 index 0000000..a935489 --- /dev/null +++ b/patterns/bridge/clock/clocks/interval.dart @@ -0,0 +1,19 @@ +import '../bells/bell.dart'; +import 'clock.dart'; + +class Interval extends Clock { + Interval({required int seconds, required Bell bell}) + : super(bell: bell, seconds: seconds); + + @override + void start() { + Future.delayed(Duration(seconds: seconds), () async { + showBell('Interval'); + for (var i = 0; i < 3 - 1; i++) { + await Future.delayed(Duration(milliseconds: 500)); + bell.ring(); + } + print(''); + }); + } +} diff --git a/patterns/bridge/clock/clocks/once.dart b/patterns/bridge/clock/clocks/once.dart new file mode 100644 index 0000000..02b8733 --- /dev/null +++ b/patterns/bridge/clock/clocks/once.dart @@ -0,0 +1,15 @@ +import '../bells/bell.dart'; +import 'clock.dart'; + +class Once extends Clock { + Once({required int seconds, required Bell bell}) + : super(seconds: seconds, bell: bell); + + @override + void start() { + Future.delayed(Duration(seconds: seconds), () { + showBell('Once'); + print(''); + }); + } +} diff --git a/patterns/bridge/clock/main.dart b/patterns/bridge/clock/main.dart new file mode 100644 index 0000000..a2d80d9 --- /dev/null +++ b/patterns/bridge/clock/main.dart @@ -0,0 +1,22 @@ +import 'bells/melody.dart'; +import 'bells/signal.dart'; +import 'clocks/once.dart'; +import 'clocks/clock.dart'; +import 'clocks/interval.dart'; + +void main() { + final melody = Melody(); + final signal = Signal(); + startClocks([ + Once(seconds: 1, bell: melody), + Once(seconds: 2, bell: signal), + Interval(seconds: 5, bell: melody), + Interval(seconds: 3, bell: signal), + ]); +} + +void startClocks(List clocks) { + for (final clock in clocks) { + clock.start(); + } +} diff --git a/patterns/bridge/devices_remote_control/README.md b/patterns/bridge/devices_remote_control/README.md new file mode 100644 index 0000000..c731ed1 --- /dev/null +++ b/patterns/bridge/devices_remote_control/README.md @@ -0,0 +1,96 @@ +# Bridge pattern +This example rewrite from [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/bridge/example) +But removed the ability to use null for devices. Instead, the EmptyDevice class is used. + +### Remote device example + +Example describe this: https://refactoring.guru/design-patterns/bridge?#pseudocode + +### Diagram: + +![image](https://user-images.githubusercontent.com/8049534/145878324-3cbc52f5-51f4-4642-921d-69fbe2886f8c.png) + +### Client code: +```dart +void main() { + testDevice(Tv()); + testDevice(Radio()); + testDevice(EmptyDevice()); +} + +void testDevice(Device device) { + print(''.padRight(36, '=')); + print(device.runtimeType); + print("Tests with basic remote."); + final basicRemote = BasicRemote.fromDevice(device); + basicRemote.power(); + device.printStatus(); + + print("Tests with advanced remote."); + final advancedRemote = AdvancedRemote.fromDevice(device); + advancedRemote.power(); + advancedRemote.mute(); + device.printStatus(); +} +``` + +**Output:** + +``` +==================================== +Tv +Tests with basic remote. +Remote: power toggle +------------------------------------ +| I'm TV set. +| I'm enabled +| Current volume is 30% +| Current channel is 1 +------------------------------------ + +Tests with advanced remote. +Remote: power toggle +Remote: mute +------------------------------------ +| I'm TV set. +| I'm disabled +| Current volume is 30% +| Current channel is 1 +------------------------------------ + +==================================== +Radio +Tests with basic remote. +Remote: power toggle +------------------------------------ +| I'm radio. +| I'm enabled +| Current volume is 30% +| Current channel is 1 +------------------------------------ + +Tests with advanced remote. +Remote: power toggle +Remote: mute +------------------------------------ +| I'm radio. +| I'm disabled +| Current volume is 30% +| Current channel is 1 +------------------------------------ + +==================================== +EmptyDevice +Tests with basic remote. +Remote: power(device not found) +------------------------------------ +| Device is Empty +------------------------------------ + +Tests with advanced remote. +Remote: power(device not found) +Remote: mute(device not found) +------------------------------------ +| Device is Empty +------------------------------------ +``` diff --git a/patterns/bridge/devices_remote_control/devices/device.dart b/patterns/bridge/devices_remote_control/devices/device.dart new file mode 100644 index 0000000..6dc18e6 --- /dev/null +++ b/patterns/bridge/devices_remote_control/devices/device.dart @@ -0,0 +1,15 @@ +abstract class Device { + bool get isEnabled; + + set isEnabled(bool enabled); + + int get volume; + + set volume(int percent); + + int get channel; + + set channel(int channel); + + void printStatus(); +} diff --git a/patterns/bridge/devices_remote_control/devices/empty_device.dart b/patterns/bridge/devices_remote_control/devices/empty_device.dart new file mode 100644 index 0000000..04142cc --- /dev/null +++ b/patterns/bridge/devices_remote_control/devices/empty_device.dart @@ -0,0 +1,28 @@ +import 'device.dart'; + +class EmptyDevice implements Device { + @override + int get channel => throw UnimplementedError('EmptyDevice'); + + @override + bool get isEnabled => throw UnimplementedError('EmptyDevice'); + + @override + int get volume => throw UnimplementedError('EmptyDevice'); + + @override + void printStatus() { + print('------------------------------------'); + print('| Device is Empty'); + print('------------------------------------\n'); + } + + @override + set channel(int channel) => throw UnimplementedError('EmptyDevice'); + + @override + set isEnabled(bool enabled) => throw UnimplementedError('EmptyDevice'); + + @override + set volume(int percent) => throw UnimplementedError('EmptyDevice'); +} diff --git a/patterns/bridge/devices_remote_control/devices/radio.dart b/patterns/bridge/devices_remote_control/devices/radio.dart new file mode 100644 index 0000000..7137bd1 --- /dev/null +++ b/patterns/bridge/devices_remote_control/devices/radio.dart @@ -0,0 +1,43 @@ +import 'device.dart'; + +class Radio implements Device { + bool _on = false; + int _volume = 30; + int _channel = 1; + + @override + bool get isEnabled => _on; + + @override + set isEnabled(bool enabled) => _on = enabled; + + @override + int get volume => _volume; + + @override + set volume(int percent) { + if (volume > 100) { + _volume = 100; + } else if (volume < 0) { + _volume = 0; + } else { + _volume = volume; + } + } + + @override + int get channel => _channel; + + @override + set channel(int channel) => _channel = channel; + + @override + void printStatus() { + print('------------------------------------'); + print('| I\'m radio.'); + print('| I\'m ${_on ? 'enabled' : 'disabled'}'); + print('| Current volume is $_volume%'); + print('| Current channel is $_channel'); + print('------------------------------------\n'); + } +} diff --git a/patterns/bridge/devices_remote_control/devices/tv.dart b/patterns/bridge/devices_remote_control/devices/tv.dart new file mode 100644 index 0000000..0fc0cd1 --- /dev/null +++ b/patterns/bridge/devices_remote_control/devices/tv.dart @@ -0,0 +1,43 @@ +import 'device.dart'; + +class Tv implements Device { + bool _on = false; + int _volume = 30; + int _channel = 1; + + @override + bool get isEnabled => _on; + + @override + set isEnabled(bool enabled) => _on = enabled; + + @override + int get volume => _volume; + + @override + set volume(int percent) { + if (volume > 100) { + _volume = 100; + } else if (volume < 0) { + _volume = 0; + } else { + _volume = volume; + } + } + + @override + int get channel => _channel; + + @override + set channel(int channel) => _channel = channel; + + @override + void printStatus() { + print('------------------------------------'); + print('| I\'m TV set.'); + print('| I\'m ${_on ? 'enabled' : 'disabled'}'); + print('| Current volume is $_volume%'); + print('| Current channel is $_channel'); + print('------------------------------------\n'); + } +} diff --git a/patterns/bridge/devices_remote_control/main.dart b/patterns/bridge/devices_remote_control/main.dart new file mode 100644 index 0000000..f35c767 --- /dev/null +++ b/patterns/bridge/devices_remote_control/main.dart @@ -0,0 +1,26 @@ +import 'devices/device.dart'; +import 'devices/empty_device.dart'; +import 'devices/radio.dart'; +import 'devices/tv.dart'; +import 'remotes/basic_advance_remote.dart'; + +void main() { + testDevice(Tv()); + testDevice(Radio()); + testDevice(EmptyDevice()); +} + +void testDevice(Device device) { + print(''.padRight(36, '=')); + print(device.runtimeType); + print("Tests with basic remote."); + final basicRemote = BasicRemote.fromDevice(device); + basicRemote.power(); + device.printStatus(); + + print("Tests with advanced remote."); + final advancedRemote = AdvancedRemote.fromDevice(device); + advancedRemote.power(); + advancedRemote.mute(); + device.printStatus(); +} diff --git a/patterns/bridge/devices_remote_control/remotes/basic_advance_remote.dart b/patterns/bridge/devices_remote_control/remotes/basic_advance_remote.dart new file mode 100644 index 0000000..6c86de5 --- /dev/null +++ b/patterns/bridge/devices_remote_control/remotes/basic_advance_remote.dart @@ -0,0 +1,76 @@ +import '../devices/device.dart'; +import '../devices/empty_device.dart'; +import 'remote.dart'; + +class BasicRemote implements Remote { + final Device _device; + + BasicRemote() : _device = EmptyDevice(); + + BasicRemote.fromDevice(this._device); + + @override + void power() { + if (_device is EmptyDevice) { + print('Remote: power(device not found)'); + return; + } + print('Remote: power toggle'); + _device.isEnabled = !_device.isEnabled; + } + + @override + void volumeDown() { + if (_device is EmptyDevice) { + print('Remote: volumeDown(device not found)'); + return; + } + print("Remote: volume down"); + _device.volume -= 10; + } + + @override + void volumeUp() { + if (_device is EmptyDevice) { + print('Remote: volumeDown(device not found)'); + return; + } + print("Remote: volume up"); + _device.volume += 10; + } + + @override + void channelDown() { + if (_device is EmptyDevice) { + print('Remote: channelDown(device not found)'); + return; + } + print("Remote: channel down"); + _device.channel -= 1; + } + + @override + void channelUp() { + if (_device is EmptyDevice) { + print('Remote: channelUp(device not found)'); + return; + } + print("Remote: channel up"); + _device.channel += 1; + } +} + +class AdvancedRemote extends BasicRemote { + AdvancedRemote() : super(); + + AdvancedRemote.fromDevice(Device device) : super.fromDevice(device); + + void mute() { + if (_device is EmptyDevice) { + print('Remote: mute(device not found)'); + return; + } + print("Remote: mute"); + _device.volume = 0; + } +} diff --git a/patterns/bridge/devices_remote_control/remotes/remote.dart b/patterns/bridge/devices_remote_control/remotes/remote.dart new file mode 100644 index 0000000..6f1ff2e --- /dev/null +++ b/patterns/bridge/devices_remote_control/remotes/remote.dart @@ -0,0 +1,11 @@ +abstract class Remote { + void power(); + + void volumeDown(); + + void volumeUp(); + + void channelDown(); + + void channelUp(); +} diff --git a/patterns/builder/cars/README.md b/patterns/builder/cars/README.md new file mode 100644 index 0000000..61edc41 --- /dev/null +++ b/patterns/builder/cars/README.md @@ -0,0 +1,19 @@ +# Builder pattern + +**Description:** +https://refactoring.guru/design-patterns/builder?#pseudocode + +**Output:** + +``` +Car built: +CarType.sportCar + +Car manual built: +Type of car: CarType.sportCar +Count of seats: 2 +Engine: volume - 3.0; mileage - 0.0 +Transmission: Transmission.semiAutomatic +Trip Computer: Functional +GPS Navigator: Functional +``` diff --git a/patterns/builder/cars/builders/builder.dart b/patterns/builder/cars/builders/builder.dart new file mode 100644 index 0000000..3360805 --- /dev/null +++ b/patterns/builder/cars/builders/builder.dart @@ -0,0 +1,23 @@ +import '../cars/car_type.dart'; +import '../components/engine.dart'; +import '../director/gps_navigation.dart'; +import '../director/transmission.dart'; +import '../director/trip_computer.dart'; + +/// EN: Builder interface defines all possible ways to configure a product. +/// +/// RU: Интерфейс Строителя объявляет все возможные этапы и шаги конфигурации +/// продукта. +abstract class Builder { + set carType(CarType type); + + set seats(int seats); + + set engine(Engine engine); + + set transmission(Transmission transmission); + + set tripComputer(TripComputer tripComputer); + + set gpsNavigator(GPSNavigator gpsNavigator); +} diff --git a/patterns/builder/cars/builders/car_builder.dart b/patterns/builder/cars/builders/car_builder.dart new file mode 100644 index 0000000..19868ed --- /dev/null +++ b/patterns/builder/cars/builders/car_builder.dart @@ -0,0 +1,35 @@ +import '../cars/car.dart'; +import '../cars/car_type.dart'; +import '../components/engine.dart'; +import '../director/gps_navigation.dart'; +import '../director/transmission.dart'; +import '../director/trip_computer.dart'; +import 'builder.dart'; + +/// EN: Concrete builders implement steps defined in the common interface. +/// +/// RU: Конкретные строители реализуют шаги, объявленные в общем интерфейсе. +class CarBuilder implements Builder { + @override + CarType? carType; + + @override + int? seats; + + @override + Engine? engine; + + @override + Transmission? transmission; + + @override + TripComputer? tripComputer; + + @override + GPSNavigator? gpsNavigator; + + Car getResult() { + return Car( + carType!, seats!, engine!, transmission!, tripComputer!, gpsNavigator!); + } +} diff --git a/patterns/builder/cars/builders/car_manual_builder.dart b/patterns/builder/cars/builders/car_manual_builder.dart new file mode 100644 index 0000000..a8fdd62 --- /dev/null +++ b/patterns/builder/cars/builders/car_manual_builder.dart @@ -0,0 +1,45 @@ +import '../cars/car_manual.dart'; +import '../cars/car_type.dart'; +import '../components/engine.dart'; +import '../director/gps_navigation.dart'; +import '../director/transmission.dart'; +import '../director/trip_computer.dart'; +import 'builder.dart'; + +/// EN: Unlike other creational patterns, Builder can construct unrelated +/// products, which don't have the common interface. +/// +/// In this case we build a user manual for a car, using the same steps as we +/// built a car. This allows to produce manuals for specific car models, +/// configured with different features. +/// +/// RU: В отличие от других создающих паттернов, Строители могут создавать +/// совершенно разные продукты, не имеющие общего интерфейса. +/// +/// В данном случае мы производим руководство пользователя автомобиля с помощью +/// тех же шагов, что и сами автомобили. Это устройство позволит создавать +/// руководства под конкретные модели автомобилей, содержащие те или иные фичи. +class CarManualBuilder implements Builder { + @override + CarType? carType; + + @override + int? seats; + + @override + Engine? engine; + + @override + Transmission? transmission; + + @override + TripComputer? tripComputer; + + @override + GPSNavigator? gpsNavigator; + + Manual getResult() { + return Manual( + carType!, seats!, engine!, transmission!, tripComputer!, gpsNavigator!); + } +} diff --git a/patterns/builder/cars/cars/car.dart b/patterns/builder/cars/cars/car.dart new file mode 100644 index 0000000..7740b80 --- /dev/null +++ b/patterns/builder/cars/cars/car.dart @@ -0,0 +1,35 @@ +import '../components/engine.dart'; +import '../director/gps_navigation.dart'; +import '../director/transmission.dart'; +import '../director/trip_computer.dart'; +import 'car_type.dart'; + +/// EN: Car is a product class. +/// +/// RU: Автомобиль — это класс продукта. +class Car { + final CarType carType; + + final int seats; + + final Engine engine; + + final Transmission transmission; + + final TripComputer tripComputer; + + final GPSNavigator gpsNavigator; + + final double fuel = 0.0; + + Car( + this.carType, + this.seats, + this.engine, + this.transmission, + this.tripComputer, + this.gpsNavigator, + ) { + tripComputer.car = this; + } +} diff --git a/patterns/builder/cars/cars/car_manual.dart b/patterns/builder/cars/cars/car_manual.dart new file mode 100644 index 0000000..ec04cca --- /dev/null +++ b/patterns/builder/cars/cars/car_manual.dart @@ -0,0 +1,53 @@ +import '../components/engine.dart'; +import '../director/gps_navigation.dart'; +import '../director/transmission.dart'; +import '../director/trip_computer.dart'; +import 'car_type.dart'; + +/// EN: Car manual is another product. Note that it does not have the same +/// ancestor as a Car. They are not related. +/// +/// RU: Руководство автомобиля — это второй продукт. Заметьте, что руководство и +/// сам автомобиль не имеют общего родительского класса. По сути, они независимы. + +class Manual { + CarType carType; + + final int seats; + + final Engine engine; + + final Transmission transmission; + + final TripComputer? tripComputer; + + final GPSNavigator? gpsNavigator; + + Manual( + this.carType, + this.seats, + this.engine, + this.transmission, [ + this.tripComputer, + this.gpsNavigator, + ]); + + String print() { + var info = ''; + info += 'Type of car: $carType\n'; + info += 'Count of seats: $seats\n'; + info += 'Engine: volume - ${engine.volume}; mileage - ${engine.mileage}\n'; + info += 'Transmission: $transmission\n'; + if (tripComputer != null) { + info += 'Trip Computer: Functional\n'; + } else { + info += 'Trip Computer: N/A\n'; + } + if (gpsNavigator != null) { + info += 'GPS Navigator: Functional\n'; + } else { + info += 'GPS Navigator: N/A\n'; + } + return info; + } +} diff --git a/patterns/builder/cars/cars/car_type.dart b/patterns/builder/cars/cars/car_type.dart new file mode 100644 index 0000000..39a8c44 --- /dev/null +++ b/patterns/builder/cars/cars/car_type.dart @@ -0,0 +1,5 @@ +enum CarType { + cityCar, + sportCar, + suv, +} diff --git a/patterns/builder/cars/components/engine.dart b/patterns/builder/cars/components/engine.dart new file mode 100644 index 0000000..bd7f242 --- /dev/null +++ b/patterns/builder/cars/components/engine.dart @@ -0,0 +1,28 @@ +/// EN: Just another feature of a car. +/// +/// RU: Одна из фишек автомобиля. +class Engine { + final double volume; + + double _mileage; + + double get mileage => _mileage; + + var _started = false; + + Engine(this.volume, this._mileage); + + void on() => _started = true; + + void off() => _started = false; + + bool get isStarted => _started; + + void go(double mileage) { + if (_started) { + _mileage += mileage; + } else { + print('Cannot go(), you must start engine first!'); + } + } +} diff --git a/patterns/builder/cars/director/director.dart b/patterns/builder/cars/director/director.dart new file mode 100644 index 0000000..1588bf2 --- /dev/null +++ b/patterns/builder/cars/director/director.dart @@ -0,0 +1,44 @@ +import '../builders/builder.dart'; +import '../cars/car_type.dart'; +import '../components/engine.dart'; +import 'gps_navigation.dart'; +import 'transmission.dart'; +import 'trip_computer.dart'; + +/// EN: Director defines the order of building steps. It works with a builder +/// object through common Builder interface. Therefore it may not know what +/// product is being built. +/// +/// RU: Директор знает в какой последовательности заставлять работать строителя. +/// Он работает с ним через общий интерфейс Строителя. Из-за этого, он может не +/// знать какой конкретно продукт сейчас строится. +class Director { + void constructSportsCar(Builder builder) { + builder + ..carType = CarType.sportCar + ..seats = 2 + ..engine = Engine(3.0, 0) + ..transmission = Transmission.semiAutomatic + ..tripComputer = TripComputer() + ..gpsNavigator = GPSNavigator(); + } + + void constructCityCar(Builder builder) { + builder + ..carType = CarType.cityCar + ..seats = 2 + ..engine = Engine(1.2, 0) + ..transmission = Transmission.automatic + ..tripComputer = TripComputer() + ..gpsNavigator = GPSNavigator(); + } + + void constructSUV(Builder builder) { + builder + ..carType = CarType.suv + ..seats = 4 + ..engine = Engine(2.5, 0) + ..transmission = Transmission.manual + ..gpsNavigator = GPSNavigator(); + } +} diff --git a/patterns/builder/cars/director/gps_navigation.dart b/patterns/builder/cars/director/gps_navigation.dart new file mode 100644 index 0000000..144860a --- /dev/null +++ b/patterns/builder/cars/director/gps_navigation.dart @@ -0,0 +1,16 @@ +/// EN: Just another feature of a car. +/// +/// RU: Одна из фишек автомобиля. +class GPSNavigator { + late String _route; + + GPSNavigator() + : _route = "221b, Baker Street, London " + "to Scotland Yard, 8-10 Broadway, London"; + + GPSNavigator.fromRout(String manualRoute) { + _route = manualRoute; + } + + String get root => _route; +} diff --git a/patterns/builder/cars/director/transmission.dart b/patterns/builder/cars/director/transmission.dart new file mode 100644 index 0000000..311bc5a --- /dev/null +++ b/patterns/builder/cars/director/transmission.dart @@ -0,0 +1,9 @@ +/// EN: Just another feature of a car. +/// +/// RU: Одна из фишек автомобиля. +enum Transmission { + singleSpeed, + manual, + automatic, + semiAutomatic, +} diff --git a/patterns/builder/cars/director/trip_computer.dart b/patterns/builder/cars/director/trip_computer.dart new file mode 100644 index 0000000..341e318 --- /dev/null +++ b/patterns/builder/cars/director/trip_computer.dart @@ -0,0 +1,21 @@ +import '../cars/car.dart'; + +/// EN: Just another feature of a car. +/// +/// RU: Одна из фишек автомобиля. +class TripComputer { + Car? car; + + void showFuelLevel() { + print("Fuel level: ${car?.fuel}"); + } + + void showStatus() { + final engine = car?.engine; + if (engine != null && engine.isStarted) { + print("Car is started"); + } else { + print("Car isn't started"); + } + } +} diff --git a/patterns/builder/cars/main.dart b/patterns/builder/cars/main.dart new file mode 100644 index 0000000..e1c55c5 --- /dev/null +++ b/patterns/builder/cars/main.dart @@ -0,0 +1,38 @@ +import 'builders/car_builder.dart'; +import 'builders/car_manual_builder.dart'; +import 'director/director.dart'; + +/// EN: Use-case. Everything comes together here. +/// +/// RU: Пример использования. Здесь всё сводится воедино. +void main() { + final director = Director(); + + // EN: Director gets the concrete builder object from the client + // (application code). That's because application knows better which + // builder to use to get a specific product. + // + // RU: Директор получает объект конкретного строителя от клиента + // (приложения). Приложение само знает какой строитель использовать, + // чтобы получить нужный продукт. + final builder = CarBuilder(); + director.constructSportsCar(builder); + + // EN: The final product is often retrieved from a builder object, since + // Director is not aware and not dependent on concrete builders and + // products. + // + // RU: Готовый продукт возвращает строитель, так как Директор чаще всего + // не знает и не зависит от конкретных классов строителей и продуктов. + final car = builder.getResult(); + print('Car built:\n${car.carType}\n'); + + final manualBuilder = CarManualBuilder(); + + // EN: Director may know several building recipes. + // + // RU: Директор может знать больше одного рецепта строительства + director.constructSportsCar(manualBuilder); + final carManual = manualBuilder.getResult(); + print("Car manual built:\n${carManual.print()}"); +} diff --git a/patterns/builder/color_text_format/README.md b/patterns/builder/color_text_format/README.md new file mode 100644 index 0000000..c2a5136 --- /dev/null +++ b/patterns/builder/color_text_format/README.md @@ -0,0 +1,79 @@ +# Builder pattern +Builder is a creational design pattern that lets you construct complex objects step by step. + +**General description of the pattern:** +https://refactoring.guru/design-patterns/builder + +## Example +Using different text formats. + +### Class diagram +![image](https://user-images.githubusercontent.com/8049534/165747411-75d587d4-aa16-4a14-a16d-52f3fa461a11.png) + +### Client code: +```dart +main() { + final reader = ColorTextReader( + text: 'I love looking at the blue sky, eating red apples and sitting on the green grass.', + ); + + final html = reader.convert(HtmlConverter()); + final json = reader.convert(JsonConverter()); + final console = reader.convert(ConsoleConverter()); + + print( + '$html,n\n' + '$json,\n\n' + '$console,\n\n', + ); +} +``` + +**Output:** +```html + + + + Color text + + + +

+ I love looking at the + blue sky, eating + red apples, sitting on the + green grass.

+ + +``` +```json +[ + { + "text": "I love looking at the " + }, + { + "color": "blue" + }, + { + "text": "sky, eating " + }, + { + "color": "red" + }, + { + "text": "apples, sitting on the " + }, + { + "color": "green" + }, + { + "text": "grass. " + } +] +``` +![image](https://user-images.githubusercontent.com/8049534/150763802-9d21f8b7-011d-4ff5-bb53-5d8a3d6d1d4c.png) + diff --git a/patterns/builder/color_text_format/color_reader/color_text_reader.dart b/patterns/builder/color_text_format/color_reader/color_text_reader.dart new file mode 100644 index 0000000..adae573 --- /dev/null +++ b/patterns/builder/color_text_format/color_reader/color_text_reader.dart @@ -0,0 +1,22 @@ +import '../converters/converter.dart'; +import '../formats/text_format.dart'; + +/// Director +class ColorTextReader { + final String text; + + ColorTextReader({required this.text}); + + T convert(Converter converter) { + for (final word in text.split(' ')) { + if (supportedColors.contains(word)) { + converter.writeColor(word); + } else { + converter.writeWord(word); + } + } + return converter.result; + } + + final supportedColors = Set.unmodifiable(['red', 'green', 'blue']); +} diff --git a/patterns/builder/color_text_format/converters/console_converter.dart b/patterns/builder/color_text_format/converters/console_converter.dart new file mode 100644 index 0000000..972f51f --- /dev/null +++ b/patterns/builder/color_text_format/converters/console_converter.dart @@ -0,0 +1,17 @@ +import '../formats/console_format.dart'; +import 'converter.dart'; + +class ConsoleConverter extends Converter { + @override + final result = ConsoleFormat(); + + @override + void writeColor(String color) { + result.color = color; + result.write(color); + result.color = 'black'; + } + + @override + void writeWord(String text) => result.write(text); +} diff --git a/patterns/builder/color_text_format/converters/converter.dart b/patterns/builder/color_text_format/converters/converter.dart new file mode 100644 index 0000000..6f98ca3 --- /dev/null +++ b/patterns/builder/color_text_format/converters/converter.dart @@ -0,0 +1,10 @@ +import '../formats/text_format.dart'; + +/// Builder +abstract class Converter { + void writeWord(String text); + + void writeColor(String color); + + T get result; +} diff --git a/patterns/builder/color_text_format/converters/html_converter.dart b/patterns/builder/color_text_format/converters/html_converter.dart new file mode 100644 index 0000000..4f2c73b --- /dev/null +++ b/patterns/builder/color_text_format/converters/html_converter.dart @@ -0,0 +1,24 @@ +import '../formats/html_format.dart'; +import 'converter.dart'; + +/// Builder +class HtmlConverter extends Converter { + @override + HtmlFormat get result => _html..closeAllTags(); + + @override + void writeColor(String color) { + _html + .addStyle(name: color, color: color) + .openTagSpan(styleName: color) + .writeText(color) + .closeLastTag(); + } + + @override + void writeWord(String text) { + _html.writeText('$text '); + } + + final _html = HtmlFormat()..openTagP(); +} diff --git a/patterns/builder/color_text_format/converters/json_converter.dart b/patterns/builder/color_text_format/converters/json_converter.dart new file mode 100644 index 0000000..f87a096 --- /dev/null +++ b/patterns/builder/color_text_format/converters/json_converter.dart @@ -0,0 +1,34 @@ +import '../formats/json_format.dart'; +import 'converter.dart'; + +class JsonConverter extends Converter { + @override + JsonFormat get result { + _closeTextBuffer(); + return _json; + } + + final _json = JsonFormat(); + + @override + void writeColor(String color) { + _closeTextBuffer(); + _json.add({ + 'color': color, + }); + } + + @override + void writeWord(String text) => _textBuffer.write('$text '); + + final _textBuffer = StringBuffer(); + + void _closeTextBuffer() { + if (_textBuffer.isNotEmpty) { + _json.add({ + 'text': _textBuffer.toString(), + }); + _textBuffer.clear(); + } + } +} diff --git a/patterns/builder/color_text_format/formats/console_format.dart b/patterns/builder/color_text_format/formats/console_format.dart new file mode 100644 index 0000000..65451ca --- /dev/null +++ b/patterns/builder/color_text_format/formats/console_format.dart @@ -0,0 +1,24 @@ +import 'text_format.dart'; + +class ConsoleFormat extends TextFormat { + final _buff = [' ']; + + static const colors = { + 'red': '\x1b[31m', + 'green': '\x1b[32m', + 'blue': '\x1b[34m', + }; + + var _fgColor = ''; + var _end = ''; + + set color(String colorName) { + _fgColor = colors[colorName] ?? ''; + _end = _fgColor == '' ? '' : '\x1B[0m'; + } + + void write(String text) => _buff.add('$_fgColor$text$_end'); + + @override + String get content => _buff.join(' '); +} diff --git a/patterns/builder/color_text_format/formats/html_format.dart b/patterns/builder/color_text_format/formats/html_format.dart new file mode 100644 index 0000000..1848f3d --- /dev/null +++ b/patterns/builder/color_text_format/formats/html_format.dart @@ -0,0 +1,81 @@ +import 'dart:collection'; +import 'text_format.dart'; + +// product +class HtmlFormat extends TextFormat { + final _openTag = Queue(); + final _content = StringBuffer(); + + HtmlFormat openTagP() { + final tag = Tag('p'); + _openTag.add(tag); + _content.write(' $tag\n '); + return this; + } + + final _styles = {}; + + HtmlFormat addStyle({required String name, required String color}) { + _styles[name] = color; + return this; + } + + HtmlFormat openTagSpan({String? styleName}) { + final tag = Tag('span', styleName); + _openTag.add(tag); + _content.write('\n $tag'); + return this; + } + + HtmlFormat writeText(String text) { + _content.write(text); + return this; + } + + HtmlFormat closeLastTag() { + final tagName = _openTag.removeLast().name; + _content.write(' '); + return this; + } + + HtmlFormat closeAllTags() { + while (_openTag.isNotEmpty) { + closeLastTag(); + } + return this; + } + + @override + String get content { + final styleContent = _styles.entries + .map((e) => '.${e.key} { color: ${e.value}; }') + .join('\n '); + + return ''' + + + Color text + + + +$_content + +'''; + } +} + +class Tag { + final String name; + final String? className; + final text = StringBuffer(); + + Tag(this.name, [this.className]); + + @override + String toString() { + final cls = className != null ? ' class="$className"' : ''; + return '<$name$cls>$text'; + } +} diff --git a/patterns/builder/color_text_format/formats/json_format.dart b/patterns/builder/color_text_format/formats/json_format.dart new file mode 100644 index 0000000..1dc3d1d --- /dev/null +++ b/patterns/builder/color_text_format/formats/json_format.dart @@ -0,0 +1,13 @@ +import 'dart:convert'; +import 'text_format.dart'; + +class JsonFormat extends TextFormat { + final _list = >[]; + + void add(Map item) { + _list.add(item); + } + + @override + String get content => JsonEncoder.withIndent(' ').convert(_list); +} diff --git a/patterns/builder/color_text_format/formats/text_format.dart b/patterns/builder/color_text_format/formats/text_format.dart new file mode 100644 index 0000000..0b16113 --- /dev/null +++ b/patterns/builder/color_text_format/formats/text_format.dart @@ -0,0 +1,9 @@ +/// Product +abstract class TextFormat { + String get content; + + @override + String toString() => '$runtimeType(\n' + '$content' + '\n)'; +} diff --git a/patterns/builder/color_text_format/main.dart b/patterns/builder/color_text_format/main.dart new file mode 100644 index 0000000..f74347f --- /dev/null +++ b/patterns/builder/color_text_format/main.dart @@ -0,0 +1,22 @@ +import 'color_reader/color_text_reader.dart'; +import 'converters/html_converter.dart'; +import 'converters/json_converter.dart'; +import 'converters/console_converter.dart'; + +void main() { + final reader = ColorTextReader( + text: 'I love looking at the blue sky, ' + 'eating red apples, ' + 'sitting on the green grass.', + ); + + final html = reader.convert(HtmlConverter()); + final json = reader.convert(JsonConverter()); + final console = reader.convert(ConsoleConverter()); + + print( + '$html,\n\n' + '$json,\n\n' + '$console,\n\n', + ); +} diff --git a/patterns/builder/conceptual/README.md b/patterns/builder/conceptual/README.md new file mode 100644 index 0000000..f07e92a --- /dev/null +++ b/patterns/builder/conceptual/README.md @@ -0,0 +1,36 @@ +# Builder Pattern +Builder is a creational design pattern that lets you construct complex objects step by step. The +pattern allows you to produce different types and representations of an object using the same +construction code. + +## Diagram: + +![image](https://user-images.githubusercontent.com/8049534/182850365-52969fc7-d743-430b-acc7-da400eae26aa.png) + +## Client code: + +```dart +void main() { + final director = Director(); + + final product1 = director.construct(ConcreteBuilder1()); + print(product1); + + final product2 = director.construct(ConcreteBuilder2()); + print(product2); +} +``` + +### Output: + +``` +ConcreteBuilder1 +001: one +002: two +003: three + +ConcreteBuilder2 +1️⃣: first +2️⃣: second +3️⃣: third +``` diff --git a/patterns/builder/conceptual/concrete_builder/concrete_builder_1.dart b/patterns/builder/conceptual/concrete_builder/concrete_builder_1.dart new file mode 100644 index 0000000..bb231f6 --- /dev/null +++ b/patterns/builder/conceptual/concrete_builder/concrete_builder_1.dart @@ -0,0 +1,25 @@ +import '../pattern/builder.dart'; +import '../pattern/product.dart'; +import '../product/concrete_product_1.dart'; + +class ConcreteBuilder1 implements Builder { + @override + void buildPart1() { + _product.addLine('one'); + } + + @override + void buildPart2() { + _product.addLine('two'); + } + + @override + void buildPart3() { + _product.addLine('three'); + } + + @override + Product get result => _product; + + final _product = ConcreteProduct1('ConcreteBuilder1'); +} diff --git a/patterns/builder/conceptual/concrete_builder/concrete_builder_2.dart b/patterns/builder/conceptual/concrete_builder/concrete_builder_2.dart new file mode 100644 index 0000000..edac9f8 --- /dev/null +++ b/patterns/builder/conceptual/concrete_builder/concrete_builder_2.dart @@ -0,0 +1,25 @@ +import '../pattern/builder.dart'; +import '../pattern/product.dart'; +import '../product/concrete_product_2.dart'; + +class ConcreteBuilder2 implements Builder { + @override + void buildPart1() { + _product.addLine('first'); + } + + @override + void buildPart2() { + _product.addLine('second'); + } + + @override + void buildPart3() { + _product.addLine('third'); + } + + @override + Product get result => _product; + + final _product = ConcreteProduct2('ConcreteBuilder2'); +} diff --git a/patterns/builder/conceptual/main.dart b/patterns/builder/conceptual/main.dart new file mode 100644 index 0000000..7bebb4d --- /dev/null +++ b/patterns/builder/conceptual/main.dart @@ -0,0 +1,13 @@ +import 'concrete_builder/concrete_builder_1.dart'; +import 'concrete_builder/concrete_builder_2.dart'; +import 'pattern/director.dart'; + +void main() { + final director = Director(); + + final product1 = director.construct(ConcreteBuilder1()); + print(product1); + + final product2 = director.construct(ConcreteBuilder2()); + print(product2); +} diff --git a/patterns/builder/conceptual/pattern/builder.dart b/patterns/builder/conceptual/pattern/builder.dart new file mode 100644 index 0000000..6a6d458 --- /dev/null +++ b/patterns/builder/conceptual/pattern/builder.dart @@ -0,0 +1,9 @@ +import 'product.dart'; + +abstract class Builder { + void buildPart1(); + void buildPart2(); + void buildPart3(); + + Product get result; +} diff --git a/patterns/builder/conceptual/pattern/director.dart b/patterns/builder/conceptual/pattern/director.dart new file mode 100644 index 0000000..70837c6 --- /dev/null +++ b/patterns/builder/conceptual/pattern/director.dart @@ -0,0 +1,12 @@ +import 'builder.dart'; +import 'product.dart'; + +class Director { + Product construct(Builder builder) { + builder.buildPart1(); + builder.buildPart2(); + builder.buildPart3(); + + return builder.result; + } +} diff --git a/patterns/builder/conceptual/pattern/product.dart b/patterns/builder/conceptual/pattern/product.dart new file mode 100644 index 0000000..54bf081 --- /dev/null +++ b/patterns/builder/conceptual/pattern/product.dart @@ -0,0 +1 @@ +abstract class Product {} diff --git a/patterns/builder/conceptual/product/concrete_product_1.dart b/patterns/builder/conceptual/product/concrete_product_1.dart new file mode 100644 index 0000000..2bcd905 --- /dev/null +++ b/patterns/builder/conceptual/product/concrete_product_1.dart @@ -0,0 +1,19 @@ +import '../pattern/product.dart'; + +class ConcreteProduct1 implements Product { + ConcreteProduct1(String name) { + _buff.add(name); + } + + void addLine(String name) { + final index = _buff.length.toString().padLeft(3, '0'); + _buff.add('$index: $name'); + } + + @override + String toString() { + return '${_buff.join('\n')}\n'; + } + + final _buff = []; +} diff --git a/patterns/builder/conceptual/product/concrete_product_2.dart b/patterns/builder/conceptual/product/concrete_product_2.dart new file mode 100644 index 0000000..a29f9cf --- /dev/null +++ b/patterns/builder/conceptual/product/concrete_product_2.dart @@ -0,0 +1,19 @@ +import '../pattern/product.dart'; + +class ConcreteProduct2 implements Product { + ConcreteProduct2(String name) { + _buff.add(name); + } + + void addLine(String name) { + final num = ['1️⃣', '2️⃣', '3️⃣'][_buff.length - 1]; + _buff.add('$num: $name'); + } + + @override + String toString() { + return '${_buff.join('\n')}\n'; + } + + final _buff = []; +} diff --git a/patterns/chain_of_responsibility/server_middleware/README.md b/patterns/chain_of_responsibility/server_middleware/README.md new file mode 100644 index 0000000..69aa509 --- /dev/null +++ b/patterns/chain_of_responsibility/server_middleware/README.md @@ -0,0 +1,77 @@ +# Chain of Responsibility pattern +Chain of Responsibility is a behavioral design pattern that lets you pass requests along a chain of handlers. + +## Server middleware example +![image](https://user-images.githubusercontent.com/8049534/149480179-ba06640c-0858-4ff9-8957-f2c4aa22ccc4.png) + +### Problem description: +https://refactoring.guru/design-patterns/chain-of-responsibility?#problem + +### Origin source code: +This example rewrite from [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/chain_of_responsibility/example) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/149488654-7ff5f659-4086-4d1a-ae44-326c71fb880a.png) + +### Client code: +```dart +void main() { + final server = Server( + users: { + 'admin@example.com': 'admin_pass', + 'user@example.com': 'user_pass', + }, + // EN: All checks are linked. Client can build various chains using the + // same components. + // + // RU: Проверки связаны в одну цепь. Клиент может строить различные + // цепи, используя одни и те же компоненты. + middleware: ThrottlingMiddleware( + requestPerMinute: 4, + next: UserExistsMiddleware( + next: RoleCheckMiddleware(), + ), + ), + ); + + for (var value in [ + ['admin@example.com', 'admin_pass'], + ['user@example.com', 'user_pass'], + ['not exist user', 'pass'], + ['user@example.com', 'fail password'], + ['user@example.com', 'user_pass'], // limited request + ]) { + userLogin(server, email: value[0], password: value[1]); + } +} +``` + +**Output:** +``` +Start middleware: +Sever: login User("admin@example.com", "admin_pass") +UserExistsMiddleware: user has been validated +RoleCheckMiddleware: role defined as "admin" +Authorization successful + +Start middleware: +Sever: login User("user@example.com", "user_pass") +UserExistsMiddleware: user has been validated +RoleCheckMiddleware: role defined as "user" +Authorization successful + +Start middleware: +Sever: login User("not exist user", "pass") +UserExistsMiddleware: this email(not exist user) is not registered! +User is not authorized + +Start middleware: +Sever: login User("user@example.com", "fail password") +UserExistsMiddleware: wrong password! +User is not authorized + +Start middleware: +Sever: login User("user@example.com", "user_pass") +ThrottlingMiddleware: Request limit exceeded! Requests=5, limit=4 per minute +User is not authorized +``` diff --git a/patterns/chain_of_responsibility/server_middleware/main.dart b/patterns/chain_of_responsibility/server_middleware/main.dart new file mode 100644 index 0000000..d369e1d --- /dev/null +++ b/patterns/chain_of_responsibility/server_middleware/main.dart @@ -0,0 +1,51 @@ +import 'middleware/role_check_middleware.dart'; +import 'middleware/throttling_middleware.dart'; +import 'middleware/user_exists_middleware.dart'; +import 'server/server.dart'; + +void main() { + final server = Server( + users: { + 'admin@example.com': 'admin_pass', + 'user@example.com': 'user_pass', + }, + // EN: All checks are linked. Client can build various chains using the + // same components. + // + // RU: Проверки связаны в одну цепь. Клиент может строить различные + // цепи, используя одни и те же компоненты. + middleware: ThrottlingMiddleware( + requestPerMinute: 4, + next: UserExistsMiddleware( + next: RoleCheckMiddleware(), + ), + ), + ); + + for (var value in [ + ['admin@example.com', 'admin_pass'], + ['user@example.com', 'user_pass'], + ['not exist user', 'pass'], + ['user@example.com', 'fail password'], + ['user@example.com', 'user_pass'], // limited request + ]) { + userLogin(server, email: value[0], password: value[1]); + } +} + +void userLogin( + Server server, { + required String email, + required String password, +}) { + print('Start middleware:'); + final success = server.logIn(email, password); + + if (success) { + print('Authorization successful'); + } else { + print('User is not authorized'); + } + + print(''); +} diff --git a/patterns/chain_of_responsibility/server_middleware/middleware/role_check_middleware.dart b/patterns/chain_of_responsibility/server_middleware/middleware/role_check_middleware.dart new file mode 100644 index 0000000..1f764b6 --- /dev/null +++ b/patterns/chain_of_responsibility/server_middleware/middleware/role_check_middleware.dart @@ -0,0 +1,16 @@ +import '../server/server.dart'; + +class RoleCheckMiddleware extends Middleware { + RoleCheckMiddleware({Middleware? next}) : super(next: next); + + @override + bool check(String email, String password) { + if (email == 'admin@example.com') { + print('RoleCheckMiddleware: role defined as "admin"'); + return true; + } + + print('RoleCheckMiddleware: role defined as "user"'); + return checkNext(email, password); + } +} diff --git a/patterns/chain_of_responsibility/server_middleware/middleware/throttling_middleware.dart b/patterns/chain_of_responsibility/server_middleware/middleware/throttling_middleware.dart new file mode 100644 index 0000000..c4cc2d9 --- /dev/null +++ b/patterns/chain_of_responsibility/server_middleware/middleware/throttling_middleware.dart @@ -0,0 +1,49 @@ +import '../server/server.dart'; + +/// EN: ConcreteHandler. Checks whether there are too many failed login requests. +/// +/// RU: Конкретный элемент цепи обрабатывает запрос по-своему. +class ThrottlingMiddleware extends Middleware { + final int requestPerMinute; + var _request = 0; + var _startTime = DateTime.now(); + + ThrottlingMiddleware({ + required this.requestPerMinute, + Middleware? next, + }) : super(next: next); + + /// EN: Please, not that checkNext() call can be inserted both in the + /// beginning of this method and in the end. + /// + /// This gives much more flexibility than a simple loop over all middleware + /// objects. For instance, an element of a chain can change the order of + /// checks by running its check after all other checks. + /// + /// RU: Обратите внимание, вызов checkNext() можно вставить как в начале + /// этого метода, так и в середине или в конце. + /// + /// Это даёт еще один уровень гибкости по сравнению с проверками в цикле. + /// Например, элемент цепи может пропустить все остальные проверки вперёд и + /// запустить свою проверку в конце. + @override + bool check(String email, String password) { + const waiting = Duration(minutes: 2); + final hasExpired = DateTime.now().isAfter(_startTime.add(waiting)); + + if (hasExpired) { + _request = 0; + _startTime = DateTime.now(); + } + + _request++; + + if (_request > requestPerMinute) { + print('ThrottlingMiddleware: Request limit exceeded! ' + 'Requests=$_request, limit=$requestPerMinute per minute'); + return false; + } + + return checkNext(email, password); + } +} diff --git a/patterns/chain_of_responsibility/server_middleware/middleware/user_exists_middleware.dart b/patterns/chain_of_responsibility/server_middleware/middleware/user_exists_middleware.dart new file mode 100644 index 0000000..3a0549c --- /dev/null +++ b/patterns/chain_of_responsibility/server_middleware/middleware/user_exists_middleware.dart @@ -0,0 +1,26 @@ +import '../server/server.dart'; + +class UserExistsMiddleware extends Middleware { + UserExistsMiddleware({Middleware? next}) : super(next: next); + + @override + bool check(String email, String password) { + if (server == null) { + return false; + } + + if (!server!.hasEmail(email)) { + print('UserExistsMiddleware: this email($email) is not registered!'); + return false; + } + + if (!server!.isValidPassword(email, password)) { + print('UserExistsMiddleware: wrong password!'); + return false; + } + + print('UserExistsMiddleware: user has been validated'); + + return checkNext(email, password); + } +} diff --git a/patterns/chain_of_responsibility/server_middleware/server/middleware.dart b/patterns/chain_of_responsibility/server_middleware/server/middleware.dart new file mode 100644 index 0000000..c9ccc0c --- /dev/null +++ b/patterns/chain_of_responsibility/server_middleware/server/middleware.dart @@ -0,0 +1,19 @@ +part of server; + +abstract class Middleware { + final Middleware? next; + + Middleware({ + this.next, + }); + + bool check(String email, String password); + + bool checkNext(String email, String password) { + return next?.check(email, password) ?? true; + } + + Server? _server; + + Server? get server => _server; +} diff --git a/patterns/chain_of_responsibility/server_middleware/server/server.dart b/patterns/chain_of_responsibility/server_middleware/server/server.dart new file mode 100644 index 0000000..a2a534c --- /dev/null +++ b/patterns/chain_of_responsibility/server_middleware/server/server.dart @@ -0,0 +1,34 @@ +library server; + +part 'middleware.dart'; + +class Server { + final Map _users; + final Middleware middleware; + + Server({ + required Map users, + required this.middleware, + }) : _users = Map.of(users) { + _applyServerToAllMiddleware(); + } + + bool logIn(String email, String password) { + print('Sever: login User("$email", "$password")'); + return middleware.check(email, password); + } + + void _applyServerToAllMiddleware() { + Middleware? middleware = this.middleware; + while (middleware != null) { + middleware._server = this; + middleware = middleware.next; + } + } + + bool hasEmail(String email) => _users.containsKey(email); + + bool isValidPassword(String email, String password) { + return _users[email] == password; + } +} diff --git a/patterns/command/conceptual/README.md b/patterns/command/conceptual/README.md new file mode 100644 index 0000000..4ea88e9 --- /dev/null +++ b/patterns/command/conceptual/README.md @@ -0,0 +1,47 @@ +# Command Pattern +Command is a behavioral design pattern that turns a request into a stand-alone object that contains +all information about the request. + +Tutorial: [here](https://refactoring.guru/design-patterns/command). + +## Diagram: +![image](https://user-images.githubusercontent.com/8049534/183062798-c2e9207d-850c-47b6-bbba-3d669299d69f.png) + +## Client dode: +```dart +void main() { + final mutStr = MutStr(); + + final input1 = AddTextCommand('One', mutStr); + final input2 = AddTextCommand('Three', mutStr); + final input3 = InsertTextCommand(' Two ', mutStr, pos: 2); + + input1.execute(); + print('text = $mutStr'); // mutStr = "One" + + input2.execute(); + print('text = $mutStr'); // mutStr = "OneThree" + + input3.execute(); + print('text = $mutStr'); // mutStr = "One Two Three" + + input3.undo(); + print('text = $mutStr'); // mutStr = "OneThree" + + input2.undo(); + print('text = $mutStr'); // mutStr = "One " + + input1.undo(); + print('text = $mutStr'); // mutStr = "" +} +``` + +### Output: +``` +text = One +text = OneThree +text = One Two Three +text = OneThree +text = One +text = +``` diff --git a/patterns/command/conceptual/command/add_text_command.dart b/patterns/command/conceptual/command/add_text_command.dart new file mode 100644 index 0000000..c7f9996 --- /dev/null +++ b/patterns/command/conceptual/command/add_text_command.dart @@ -0,0 +1,26 @@ +import '../pattern/command.dart'; +import '../mut_str/mut_str.dart'; + +class AddTextCommand implements Command { + final String addedText; + final MutStr mutStr; + + AddTextCommand(this.addedText, this.mutStr); + + @override + void execute() { + additionPosition = mutStr.len; + mutStr.push(addedText); + } + + @override + void undo() { + if (additionPosition == null) { + return; + } + + mutStr.delete(additionPosition!, additionPosition! + addedText.length); + } + + int? additionPosition; +} diff --git a/patterns/command/conceptual/command/insert_text_command.dart b/patterns/command/conceptual/command/insert_text_command.dart new file mode 100644 index 0000000..58bcc64 --- /dev/null +++ b/patterns/command/conceptual/command/insert_text_command.dart @@ -0,0 +1,27 @@ +import '../pattern/command.dart'; +import '../mut_str/mut_str.dart'; + +class InsertTextCommand extends Command { + final int pos; + final String insertText; + final MutStr mutStr; + + InsertTextCommand(this.insertText, this.mutStr, {required this.pos}); + + @override + void execute() { + _isNotExecute = false; + mutStr.insert(pos + 1, insertText); + } + + @override + void undo() { + if (_isNotExecute) { + return; + } + + mutStr.delete(pos + 1, insertText.length - 1); + } + + bool _isNotExecute = true; +} diff --git a/patterns/command/conceptual/main.dart b/patterns/command/conceptual/main.dart new file mode 100644 index 0000000..d4640d3 --- /dev/null +++ b/patterns/command/conceptual/main.dart @@ -0,0 +1,29 @@ +import 'mut_str/mut_str.dart'; +import 'command/add_text_command.dart'; +import 'command/insert_text_command.dart'; + +void main() { + final mutStr = MutStr(); + + final input1 = AddTextCommand('One', mutStr); + final input2 = AddTextCommand('Three', mutStr); + final input3 = InsertTextCommand(' Two ', mutStr, pos: 2); + + input1.execute(); + print('text = $mutStr'); // mutStr = "One" + + input2.execute(); + print('text = $mutStr'); // mutStr = "OneThree" + + input3.execute(); + print('text = $mutStr'); // mutStr = "One Two Three" + + input3.undo(); + print('text = $mutStr'); // mutStr = "OneThree" + + input2.undo(); + print('text = $mutStr'); // mutStr = "One " + + input1.undo(); + print('text = $mutStr'); // mutStr = "" +} diff --git a/patterns/command/conceptual/mut_str/mut_str.dart b/patterns/command/conceptual/mut_str/mut_str.dart new file mode 100644 index 0000000..1d57b20 --- /dev/null +++ b/patterns/command/conceptual/mut_str/mut_str.dart @@ -0,0 +1,22 @@ +class MutStr { + void push(String str) { + _buff.addAll(str.split('')); + } + + void insert(int pos, String str) { + _buff.insert(pos, str); + } + + void delete(int startPos, int len) { + _buff.removeRange(startPos, len); + } + + int get len => _buff.length; + + @override + String toString() { + return _buff.join(''); + } + + final _buff = []; +} diff --git a/patterns/command/conceptual/pattern/command.dart b/patterns/command/conceptual/pattern/command.dart new file mode 100644 index 0000000..520d5db --- /dev/null +++ b/patterns/command/conceptual/pattern/command.dart @@ -0,0 +1,4 @@ +abstract class Command { + void execute(); + void undo(); +} diff --git a/patterns/command/text_editor/README.md b/patterns/command/text_editor/README.md new file mode 100644 index 0000000..1622f96 --- /dev/null +++ b/patterns/command/text_editor/README.md @@ -0,0 +1,69 @@ +# Command pattern +Command is a behavioral design pattern that turns a request into a stand-alone object that contains +all information about the request. + +## Text Editor example +![image](https://user-images.githubusercontent.com/8049534/149916565-626f74bb-f922-4b10-acaa-87666cdbacb5.png) + +In this example, the Command pattern helps to track the history of executed operations and makes it +possible to revert an operation if needed. + +More detailed explanation on [RefactoringGuru](https://refactoring.guru/design-patterns/command?#pseudocode). + +## Origin source code: +This example rewrite from [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/command/example). +But slightly changed, see the class diagram. + +## Diagram: +![image](https://user-images.githubusercontent.com/8049534/149918792-fccae912-2e67-4068-88d1-5cf824f0df2c.png) + +### Client code: +```dart +void main() { + final app = Application(); + app.addListenerExecuteCommand(log); + app + ..keyboardInput('Hello world') + ..moveCursor(position: 6) + ..keyboardInput('Refactring GuBu ') + ..selectText(start: 17, end: 19) + ..ctrlX() + ..selectText(start: 17, end: 19) + ..keyboardInput('Guru') + ..selectText(start: 4, end: 5) + ..ctrlC() + ..moveCursor(position: 12) + ..ctrlV() + ..moveCursor(position: 28); + + print('\nUndo steps\n'); + + while (app.isHistoryNotEmpty) { + app.ctrlZ(); + } +} +``` + +**Output:** +``` +[➕] Input( cursorPosition: 0, addText: "Hello world" )__________________"Hello world|" +[➕] Move( from: 11, to: 6 )_____________________________________________"Hello |world" +[➕] Input( cursorPosition: 6, addText: "Refactring GuBu " )_____________"Hello Refactring GuBu |world" +[➕] Select( start: 17, end: 19 )________________________________________"Hello Refactring [Gu]Bu world" +[➕] Cut( cursorPosition: 17, cutText: "Gu" )____________________________"Hello Refactring |Bu world" +[➕] Select( start: 17, end: 19 )________________________________________"Hello Refactring [Bu] world" +[➕] Input( cursorPosition: 17, addText: "Guru" )________________________"Hello Refactring Guru| world" +[➕] Select( start: 4, end: 5 )__________________________________________"Hell[o] Refactring Guru world" +[➕] Copy( text: "o" )___________________________________________________"Hell[o] Refactring Guru world" +[➕] Move( from: 5, to: 12 )_____________________________________________"Hello Refact|ring Guru world" +[➕] Past( cursorPosition: 13, text: "o", pevRestore: "" )_______________"Hello Refacto|ring Guru world" +[➕] Move( from: 13, to: 28 )____________________________________________"Hello Refactoring Guru world|" + +Undo steps + +Undo_Past( cursorPosition: 13, text: "o", pevRestore: "" )______________"Hello Refact|ring Guru world" +Undo_Input( cursorPosition: 17, addText: "Guru" )_______________________"Hello Refactring Bu| world" +Undo_Cut( cursorPosition: 17, cutText: "Gu" )___________________________"Hello Refactring Gu|Bu world" +Undo_Input( cursorPosition: 6, addText: "Refactring GuBu " )____________"Hello |world" +Undo_Input( cursorPosition: 0, addText: "Hello world" )_________________"|" +``` diff --git a/patterns/command/text_editor/application/application.dart b/patterns/command/text_editor/application/application.dart new file mode 100644 index 0000000..b4b35cb --- /dev/null +++ b/patterns/command/text_editor/application/application.dart @@ -0,0 +1,92 @@ +import '../commands/command.dart'; +import '../commands/copy_command.dart'; +import '../commands/input_command.dart'; +import '../commands/move_command.dart'; +import '../commands/past_command.dart'; +import '../commands/select_command.dart'; +import 'command_history.dart'; +import 'editor.dart'; + +typedef ExecuteCommandListener = Function({ + required Command command, + required bool isUndo, + required String editorText, +}); + +class Application { + final editor = Editor(); + final _history = CommandHistory(); + + String clipboard = ''; + + void keyboardInput(String text) { + _executeAndPushHistory( + InputCommand(this, text), + ); + } + + void moveCursor({required int position}) { + _executeAndPushHistory( + MoveCommand(this, position), + ); + } + + void selectText({required int start, required int end}) { + _executeAndPushHistory( + SelectCommand(this, start, end), + ); + } + + void ctrlC() { + _executeAndPushHistory( + CopyCommand(this), + ); + } + + void ctrlX() { + _executeAndPushHistory( + CutCommand(this), + ); + } + + void ctrlV() { + _executeAndPushHistory( + PastCommand(this), + ); + } + + void ctrlZ() { + if (_history.isNotEmpty) { + final command = _history.pop(); + command.undo(); + _executeListener?.call( + command: command, + isUndo: true, + editorText: editor.toString(), + ); + } + } + + bool get isHistoryNotEmpty => _history.isNotEmpty; + + void _executeAndPushHistory(Command command) { + command.execute(); + final textAfterExecute = editor.toString(); + _executeListener?.call( + command: command, + isUndo: false, + editorText: textAfterExecute, + ); + + if (command.isSaveHistory) { + _history.push(command); + } + } + + // ignore: prefer_function_declarations_over_variables + ExecuteCommandListener? _executeListener; + + void addListenerExecuteCommand(ExecuteCommandListener listener) { + _executeListener = listener; + } +} diff --git a/patterns/command/text_editor/application/command_history.dart b/patterns/command/text_editor/application/command_history.dart new file mode 100644 index 0000000..6541985 --- /dev/null +++ b/patterns/command/text_editor/application/command_history.dart @@ -0,0 +1,11 @@ +import '../commands/command.dart'; + +class CommandHistory { + final _stack = []; + + bool get isNotEmpty => _stack.isNotEmpty; + + void push(Command command) => _stack.add(command); + + Command pop() => _stack.removeLast(); +} diff --git a/patterns/command/text_editor/application/editor.dart b/patterns/command/text_editor/application/editor.dart new file mode 100644 index 0000000..5b77c9f --- /dev/null +++ b/patterns/command/text_editor/application/editor.dart @@ -0,0 +1,71 @@ +import 'text_cursor.dart'; + +class Editor { + var _text = ''; + + String get text => _text; + + void inputText(String addText) { + if (cursor.isTextSelected) { + _replaceSelection(addText); + } else { + _insertText(addText); + } + } + + void selectText(int start, int end) { + assert(end <= _text.length, ' end: $end, textLength: ${_text.length}'); + assert(start < end, 'start: $start, end: $end'); + _cursor = TextCursor.fromSelection(start, end); + } + + void removeSelected() { + if (cursor.isTextSelected) { + _replaceSelection(''); + } + } + + String get selectedText { + return _text.substring( + cursor.startSelection, + cursor.endSelection, + ); + } + + var _cursor = TextCursor.fromPosition(0); + + TextCursor get cursor => _cursor; + + set cursorPosition(int newPosition) { + _cursor = TextCursor.fromPosition(newPosition); + } + + void _replaceSelection(String replaceText) { + _text = _text.replaceRange( + cursor.startSelection, + cursor.endSelection, + replaceText, + ); + _cursor = TextCursor.fromPosition( + cursor.startSelection + replaceText.length, + ); + } + + void _insertText(String insertText) { + _text = _text.replaceRange( + cursor.position, + cursor.position, + insertText, + ); + cursorPosition = cursor.position + insertText.length; + } + + @override + String toString() { + return _text.replaceRange( + cursor.startSelection, + cursor.endSelection, + cursor.isTextSelected ? '[$selectedText]' : '|', + ); + } +} diff --git a/patterns/command/text_editor/application/text_cursor.dart b/patterns/command/text_editor/application/text_cursor.dart new file mode 100644 index 0000000..3a756b4 --- /dev/null +++ b/patterns/command/text_editor/application/text_cursor.dart @@ -0,0 +1,17 @@ +class TextCursor { + int get position => _position; + + int get startSelection => _startSelection ?? _position; + + int get endSelection => position; + + bool get isTextSelected => _startSelection != null; + + final int _position; + + final int? _startSelection; + + TextCursor.fromPosition(this._position) : _startSelection = null; + + TextCursor.fromSelection(this._startSelection, this._position); +} diff --git a/patterns/command/text_editor/commands/command.dart b/patterns/command/text_editor/commands/command.dart new file mode 100644 index 0000000..8894441 --- /dev/null +++ b/patterns/command/text_editor/commands/command.dart @@ -0,0 +1,13 @@ +import '../application/application.dart'; + +abstract class Command { + final Application app; + + Command(this.app); + + bool get isSaveHistory; + + void execute(); + + void undo(); +} diff --git a/patterns/command/text_editor/commands/copy_command.dart b/patterns/command/text_editor/commands/copy_command.dart new file mode 100644 index 0000000..b381254 --- /dev/null +++ b/patterns/command/text_editor/commands/copy_command.dart @@ -0,0 +1,34 @@ +library copy_past; + +import '../application/application.dart'; +import 'command.dart'; + +part 'cut_command.dart'; + +class CopyCommand extends Command { + String _copyText = ''; + + CopyCommand(Application app) : super(app); + + @override + bool get isSaveHistory => false; + + @override + void execute() { + if (!app.editor.cursor.isTextSelected) { + return; + } + + _copyText = app.editor.selectedText; + app.clipboard = _copyText; + } + + @override + void undo() {} + + @override + String toString() { + return 'Copy( ' + 'text: "$_copyText" )'; + } +} diff --git a/patterns/command/text_editor/commands/cut_command.dart b/patterns/command/text_editor/commands/cut_command.dart new file mode 100644 index 0000000..d940a4c --- /dev/null +++ b/patterns/command/text_editor/commands/cut_command.dart @@ -0,0 +1,39 @@ +part of copy_past; + +class CutCommand extends CopyCommand { + int? _cursorPosition; + + CutCommand(Application app) : super(app); + + @override + bool get isSaveHistory => _copyText.isNotEmpty; + + @override + void execute() { + if (!app.editor.cursor.isTextSelected) { + return; + } + + super.execute(); + app.editor.removeSelected(); + _cursorPosition = app.editor.cursor.position; + } + + @override + void undo() { + if (_copyText.isEmpty) { + return; + } + + app.editor + ..cursorPosition = _cursorPosition! + ..inputText(_copyText); + } + + @override + String toString() { + return 'Cut( ' + 'cursorPosition: $_cursorPosition, ' + 'cutText: "$_copyText" )'; + } +} diff --git a/patterns/command/text_editor/commands/input_command.dart b/patterns/command/text_editor/commands/input_command.dart new file mode 100644 index 0000000..e246a60 --- /dev/null +++ b/patterns/command/text_editor/commands/input_command.dart @@ -0,0 +1,34 @@ +import '../application/application.dart'; +import 'command.dart'; + +class InputCommand extends Command { + final String _addText; + int? _startPosition; + String _prevSelectText = ''; + + InputCommand(Application app, this._addText) : super(app); + + @override + bool get isSaveHistory => _addText.isNotEmpty; + + @override + void execute() { + _prevSelectText = app.editor.selectedText; + _startPosition = app.editor.cursor.startSelection; + app.editor.inputText(_addText); + } + + @override + void undo() { + app.editor + ..selectText(_startPosition!, _startPosition! + _addText.length) + ..inputText(_prevSelectText); + } + + @override + String toString() { + return 'Input( ' + 'cursorPosition: $_startPosition, ' + 'addText: "$_addText" )'; + } +} diff --git a/patterns/command/text_editor/commands/move_command.dart b/patterns/command/text_editor/commands/move_command.dart new file mode 100644 index 0000000..e543fd2 --- /dev/null +++ b/patterns/command/text_editor/commands/move_command.dart @@ -0,0 +1,34 @@ +import '../application/application.dart'; +import 'command.dart'; + +class MoveCommand extends Command { + final int _positionTo; + int? _positionFrom; + + MoveCommand(Application app, this._positionTo) : super(app); + + @override + bool get isSaveHistory => false; + + @override + void execute() { + _positionFrom = app.editor.cursor.position; + app.editor.cursorPosition = _positionTo; + } + + @override + void undo() { + if (_positionFrom == null) { + return; + } + + app.editor.cursorPosition = _positionFrom!; + } + + @override + String toString() { + return 'Move( ' + 'from: $_positionFrom, ' + 'to: $_positionTo )'; + } +} diff --git a/patterns/command/text_editor/commands/past_command.dart b/patterns/command/text_editor/commands/past_command.dart new file mode 100644 index 0000000..58aba6f --- /dev/null +++ b/patterns/command/text_editor/commands/past_command.dart @@ -0,0 +1,47 @@ +import '../application/application.dart'; +import 'command.dart'; + +class PastCommand extends Command { + String _text = ''; + String _selectText = ''; + int? _cursorPosition; + + PastCommand(Application app) : super(app); + + @override + bool get isSaveHistory => _text.isNotEmpty; + + @override + void execute() { + if (app.clipboard.isEmpty) { + return; + } + + _selectText = app.editor.selectedText; + _text = app.clipboard; + app.editor.inputText(_text); + _cursorPosition = app.editor.cursor.position; + } + + @override + void undo() { + if (_text.isEmpty) { + return; + } + + app.editor + ..selectText( + _cursorPosition! - _text.length, + _cursorPosition!, + ) + ..inputText(_selectText); + } + + @override + String toString() { + return 'Past( ' + 'cursorPosition: $_cursorPosition, ' + 'text: "$_text", ' + 'pevRestore: "$_selectText" )'; + } +} diff --git a/patterns/command/text_editor/commands/select_command.dart b/patterns/command/text_editor/commands/select_command.dart new file mode 100644 index 0000000..81bfac2 --- /dev/null +++ b/patterns/command/text_editor/commands/select_command.dart @@ -0,0 +1,40 @@ +import '../application/application.dart'; +import '../application/text_cursor.dart'; +import 'command.dart'; + +class SelectCommand extends Command { + final int _start; + final int _end; + + TextCursor? _previousCursor; + + SelectCommand(Application app, this._start, this._end) : super(app); + + @override + bool get isSaveHistory => false; + + @override + void execute() { + _previousCursor = app.editor.cursor; + app.editor.selectText(_start, _end); + } + + @override + void undo() { + if (_previousCursor == null) { + return; + } + + app.editor.selectText( + _previousCursor!.startSelection, + _previousCursor!.endSelection, + ); + } + + @override + String toString() { + return 'Select( ' + 'start: $_start, ' + 'end: $_end )'; + } +} diff --git a/patterns/command/text_editor/main.dart b/patterns/command/text_editor/main.dart new file mode 100644 index 0000000..2d1b217 --- /dev/null +++ b/patterns/command/text_editor/main.dart @@ -0,0 +1,36 @@ +import 'application/application.dart'; +import 'commands/command.dart'; + +void main() { + final app = Application(); + app.addListenerExecuteCommand(log); + app + ..keyboardInput('Hello world') + ..moveCursor(position: 6) + ..keyboardInput('Refactring GuBu ') + ..selectText(start: 17, end: 19) + ..ctrlX() + ..selectText(start: 17, end: 19) + ..keyboardInput('Guru') + ..selectText(start: 4, end: 5) + ..ctrlC() + ..moveCursor(position: 12) + ..ctrlV() + ..moveCursor(position: 28); + + print('\nUndo steps\n'); + + while (app.isHistoryNotEmpty) { + app.ctrlZ(); + } +} + +void log({ + required Command command, + required bool isUndo, + required String editorText, +}) { + final addOrUndo = isUndo ? 'Undo_' : '[➕] '; + final description = '$addOrUndo$command'; + print('${description.padRight(72, '_')}"$editorText"'); +} diff --git a/patterns/composite/image_editor/README.md b/patterns/composite/image_editor/README.md new file mode 100644 index 0000000..dc61e2b --- /dev/null +++ b/patterns/composite/image_editor/README.md @@ -0,0 +1,80 @@ +# Composite pattern +Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects. + +## Problem description: +https://refactoring.guru/design-patterns/composite?#pseudocode + +## Origin source code: +This example rewrite from [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/composite/example) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/149174388-25ca21b1-d762-40b5-a853-528abe18b66c.png) + +### Client code: +```dart +void main() { + final editor = ImageEditor(); + editor.loadShapes([ + Circle(1, 1, 4, Color.grey), + CompoundShape([ + Circle(12, 12, 6, Color.dark), + Dot(12 + 6, 12 + 6, Color.dark), + ])..select(), + CompoundShape([ + Rectangle(31, 31, 10, 10, Color.black), + Dot(28, 28, Color.grey), + Dot(40, 28, Color.grey), + Dot(40, 41, Color.grey), + Dot(28, 41, Color.grey), + ]), + ]); +} +``` + +**Output:** +``` +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░▒▒▒▒▒▒░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░▒▒▒░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░▒▒▒░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░▒▒▒▒▒▒░░░░░░░░░▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┌───────────────────────────────────────────┐░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░ ░░░░░░░░░░░░░░░ ░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░ ░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░ ░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░ ░░░░░░░░░░░░░░░ ░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░└───────────────────────────────────────────┘░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███████████████████████████░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░░░░░░░░░░░░░███░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░███████████████████████████░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ + +``` diff --git a/patterns/composite/image_editor/editor/image_editor.dart b/patterns/composite/image_editor/editor/image_editor.dart new file mode 100644 index 0000000..802b27e --- /dev/null +++ b/patterns/composite/image_editor/editor/image_editor.dart @@ -0,0 +1,22 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../shapes/compound_shape.dart'; +import '../shapes/shape.dart'; + +typedef Graphics = Canvas; + +class ImageEditor { + final _allShapes = CompoundShape(); + + void loadShapes(List shapes) { + _allShapes + ..clear() + ..addAll(shapes); + } + + String render() { + final graphics = Graphics(_allShapes.width + 2, _allShapes.height + 2); + _allShapes.paint(graphics); + return graphics.toString(); + } +} diff --git a/patterns/composite/image_editor/main.dart b/patterns/composite/image_editor/main.dart new file mode 100644 index 0000000..e9e4bdd --- /dev/null +++ b/patterns/composite/image_editor/main.dart @@ -0,0 +1,27 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import 'editor/image_editor.dart'; +import 'shapes/circle.dart'; +import 'shapes/compound_shape.dart'; +import 'shapes/dot.dart'; +import 'shapes/rectangle.dart'; + +void main() { + final editor = ImageEditor(); + editor.loadShapes([ + Circle(1, 1, 4, Color.grey), + CompoundShape([ + Circle(12, 12, 6, Color.dark), + Dot(12 + 6, 12 + 6, Color.dark), + ]) + ..select(), + CompoundShape([ + Rectangle(31, 31, 10, 10, Color.black), + Dot(28, 28, Color.grey), + Dot(40, 28, Color.grey), + Dot(40, 41, Color.grey), + Dot(28, 41, Color.grey), + ]), + ]); + print(editor.render()); +} diff --git a/patterns/composite/image_editor/shapes/base_shape.dart b/patterns/composite/image_editor/shapes/base_shape.dart new file mode 100644 index 0000000..b6c9dea --- /dev/null +++ b/patterns/composite/image_editor/shapes/base_shape.dart @@ -0,0 +1,52 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../editor/image_editor.dart'; +import 'shape.dart'; + +abstract class BaseShape implements Shape { + int _x; + int _y; + final Color color; + + bool _selected = false; + + BaseShape(this._x, this._y, this.color); + + @override + int get x => _x; + + @override + int get y => _y; + + @override + void move(int x, int y) { + _x += x; + _y += y; + } + + @override + void select() => _selected = true; + + @override + void unSelect() => _selected = false; + + @override + bool get isSelected => _selected; + + void enableSelectionStyle(Graphics graphics) { + graphics.penColor = Color.white; + } + + void disableSelectionStyle(Graphics graphics) { + graphics.penColor = color; + } + + @override + void paint(Graphics graphics) { + if (isSelected) { + enableSelectionStyle(graphics); + } else { + disableSelectionStyle(graphics); + } + } +} diff --git a/patterns/composite/image_editor/shapes/circle.dart b/patterns/composite/image_editor/shapes/circle.dart new file mode 100644 index 0000000..1411511 --- /dev/null +++ b/patterns/composite/image_editor/shapes/circle.dart @@ -0,0 +1,26 @@ +import 'dart:math'; + +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../editor/image_editor.dart'; +import 'base_shape.dart'; + +class Circle extends BaseShape { + final int radius; + + Circle(int x, int y, this.radius, Color color) : super(x, y, color); + + @override + int get width => radius * 2; + + @override + int get height => radius * 2; + + @override + void paint(Graphics graphics) { + super.paint(graphics); + graphics + ..translate = Point(x + radius, y + radius) + ..circle(radius); + } +} diff --git a/patterns/composite/image_editor/shapes/compound_shape.dart b/patterns/composite/image_editor/shapes/compound_shape.dart new file mode 100644 index 0000000..751cb78 --- /dev/null +++ b/patterns/composite/image_editor/shapes/compound_shape.dart @@ -0,0 +1,131 @@ +import 'dart:math'; + +import 'package:design_patterns_dart/text_canvas.dart'; +import 'package:design_patterns_dart/text_canvas/primitives.dart'; + +import '../editor/image_editor.dart'; +import 'base_shape.dart'; +import 'shape.dart'; + +class CompoundShape extends BaseShape { + final _children = []; + + CompoundShape([List children = const []]) : super(0, 0, Color.black) { + _children.addAll(children); + } + + void add(Shape component) => _children.add(component); + + void addAll(List children) => _children.addAll(children); + + void remove(Shape child) => _children.remove(child); + + void removeList(List children) { + for (final item in children) { + _children.remove(item); + } + } + + void clear() => _children.clear(); + + @override + int get x { + if (_children.isEmpty) { + return 0; + } + + var minX = _children.fold( + _children.first.x, + (x, Shape element) => element.x > x ? x : element.x, + ); + + return minX; + } + + @override + int get y { + if (_children.isEmpty) { + return 0; + } + + var minY = _children.fold( + _children.first.y, + (y, Shape element) => element.y > y ? y : element.y, + ); + + return minY; + } + + @override + int get width { + int maxWidth = 0; + final y = this.y; + for (final child in _children) { + int childWidth = child.x + child.width - y; + if (childWidth > maxWidth) { + maxWidth = childWidth; + } + } + + return maxWidth; + } + + @override + int get height { + int maxHeight = 0; + final y = this.y; + for (final child in _children) { + final childHeight = child.y + child.height - y; + if (childHeight > maxHeight) { + maxHeight = childHeight; + } + } + + return maxHeight; + } + + @override + void move(int x, int y) { + for (Shape child in _children) { + child.move(x, y); + } + } + + @override + void unSelect() { + super.unSelect(); + for (Shape child in _children) { + child.unSelect(); + } + } + + @override + void select() { + super.select(); + for (Shape child in _children) { + child.select(); + } + } + + @override + void paint(Graphics graphics) { + if (isSelected) { + const padding = 2; + graphics + ..translate = Point( + x * graphics.lineStretch - graphics.lineStretch, + y - 2, + ) + ..border( + (width + 2 + 1) * graphics.lineStretch, + height + padding * 2 + 1, + BorderStyle.single, + ); + } + + for (Shape child in _children) { + graphics.translate = Point(0, 0); + child.paint(graphics); + } + } +} diff --git a/patterns/composite/image_editor/shapes/dot.dart b/patterns/composite/image_editor/shapes/dot.dart new file mode 100644 index 0000000..d52644d --- /dev/null +++ b/patterns/composite/image_editor/shapes/dot.dart @@ -0,0 +1,20 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../editor/image_editor.dart'; +import 'base_shape.dart'; + +class Dot extends BaseShape { + Dot(int x, int y, Color color) : super(x, y, color); + + @override + int get width => 1; + + @override + int get height => 1; + + @override + void paint(Graphics graphics) { + super.paint(graphics); + graphics.setPixel(x, y); + } +} diff --git a/patterns/composite/image_editor/shapes/rectangle.dart b/patterns/composite/image_editor/shapes/rectangle.dart new file mode 100644 index 0000000..a15fd4c --- /dev/null +++ b/patterns/composite/image_editor/shapes/rectangle.dart @@ -0,0 +1,25 @@ +import 'dart:math'; + +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../editor/image_editor.dart'; +import 'base_shape.dart'; + +class Rectangle extends BaseShape { + @override + final int width; + + @override + final int height; + + Rectangle(int x, int y, this.width, this.height, Color color) + : super(x, y, color); + + @override + void paint(Graphics graphics) { + super.paint(graphics); + graphics + ..translate = Point(x - 1, y - 1) + ..rectangle(width - 2, height - 1); + } +} diff --git a/patterns/composite/image_editor/shapes/shape.dart b/patterns/composite/image_editor/shapes/shape.dart new file mode 100644 index 0000000..010f45c --- /dev/null +++ b/patterns/composite/image_editor/shapes/shape.dart @@ -0,0 +1,21 @@ +import '../editor/image_editor.dart'; + +abstract class Shape { + int get x; + + int get y; + + int get width; + + int get height; + + void move(int x, int y); + + void select(); + + void unSelect(); + + bool get isSelected; + + void paint(Graphics graphics); +} diff --git a/patterns/composite/products_and_boxes/README.md b/patterns/composite/products_and_boxes/README.md new file mode 100644 index 0000000..c85d59d --- /dev/null +++ b/patterns/composite/products_and_boxes/README.md @@ -0,0 +1,75 @@ +# Composite pattern +Composite is a structural design pattern that lets you compose objects into tree structures and then +work with these structures as if they were individual objects. + +## Example: Products and boxes +![problem-en](https://user-images.githubusercontent.com/8049534/147579298-0c60c4a7-6acb-4ab3-a973-e06524c5a061.png) + +For example, imagine that you have two types of objects: Products and Boxes. A Box can contain +several Products as well as a number of smaller Boxes. These little Boxes can also hold some +Products or even smaller Boxes, and so on. + +Full description can be found [here](https://refactoring.guru/design-patterns/composite?#problem) + +### Folder description: +- `/products` - represent product and box (composite pattern) +- `/diagram` - convert products to render elements +- `/render_elements` - classes for visualization (real-world composite pattern) + +### Diagram: + +![image](https://user-images.githubusercontent.com/8049534/147579175-f5ce6191-a76a-4f1f-8ac9-fae1a26f87bb.png) + +### Client code: +```dart +main() { + Box( + children: [ + Box.single( + ProductLeaf('Hammer', 9), + ), + Box( + children: [ + Box( + children: [ + Box.single( + ProductLeaf('Phone', 450), + ), + Box.single( + ProductLeaf('Headphones', 30), + ), + ], + ), + Box.single( + ProductLeaf('Charger', 25), + ), + ], + ), + ProductLeaf('Receipt', 0), + ], + ); +} +``` + +**Output:** +``` + ┌─────────────────────────────┐ + │ Box(places: 5, price: 514$) │ + └──────────────┬──────────────┘ + ┌─────────────────────────────────────┼─────────────────────────────────────┐ + ┌─────┴───┐ ┌───────────────┴─────────────┐ ┴ + │ Box(9$) │ │ Box(places: 3, price: 505$) │ Receipt(0$) + └────┬────┘ └──────────────┬──────────────┘ + │ ┌────────┴────────────────────┐ + ┴ ┌──────────────┴──────────────┐ ┌─────┴────┐ + Hammer(9$) │ Box(places: 2, price: 480$) │ │ Box(25$) │ + └──────────────┬──────────────┘ └─────┬────┘ + ┌──────────┴────────┐ │ + ┌─────┴─────┐ ┌─────┴────┐ ┴ + │ Box(450$) │ │ Box(30$) │ Charger(25$) + └─────┬─────┘ └─────┬────┘ + │ │ + ┴ ┴ + Phone(450$) Headphones(30$) + +``` diff --git a/patterns/composite/products_and_boxes/diagram/convert_product_to_render_element.dart b/patterns/composite/products_and_boxes/diagram/convert_product_to_render_element.dart new file mode 100644 index 0000000..a8882b8 --- /dev/null +++ b/patterns/composite/products_and_boxes/diagram/convert_product_to_render_element.dart @@ -0,0 +1,24 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../products/product.dart'; +import '../products/product_leaf.dart'; +import '../render_elements/render_element.dart'; +import '../render_elements/render_text.dart'; + + +extension ConvertProductToDiagram on Product { + RenderElement toRenderElement() { + return RenderText( + content, + borderStyle: borderStyleBySize(), + ); + } + + BorderStyle borderStyleBySize() { + if (this is ProductLeaf) { + return BorderStyle.empty; + } else { + return BorderStyle.single; + } + } +} diff --git a/patterns/composite/products_and_boxes/diagram/diagram.dart b/patterns/composite/products_and_boxes/diagram/diagram.dart new file mode 100644 index 0000000..2d40c65 --- /dev/null +++ b/patterns/composite/products_and_boxes/diagram/diagram.dart @@ -0,0 +1,44 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../products/product.dart'; +import '../render_elements/render_connecting_lines.dart'; +import '../render_elements/render_element.dart'; +import 'convert_product_to_render_element.dart'; + +class Diagram { + Diagram(this.rootRenderElement); + + final RenderElement rootRenderElement; + + String renderToText() { + const pixelWidth = 3; + final width = (rootRenderElement.width / pixelWidth).ceil(); + final height = rootRenderElement.height; + final dc = Canvas( + width, + height, + lineStretch: pixelWidth, + fillColor: Color.white, + ); + rootRenderElement.render(dc); + + return dc.toString(); + } + + factory Diagram.node(Product product) { + return Diagram( + product.toRenderElement(), + ); + } + + factory Diagram.parentNode(Product product, List children) { + return Diagram( + RenderConnectingLines( + parent: product.toRenderElement(), + children: [ + for (final child in children) child.toDiagram().rootRenderElement, + ], + ), + ); + } +} diff --git a/patterns/composite/products_and_boxes/main.dart b/patterns/composite/products_and_boxes/main.dart new file mode 100644 index 0000000..37d1595 --- /dev/null +++ b/patterns/composite/products_and_boxes/main.dart @@ -0,0 +1,36 @@ +import 'products/box.dart'; +import 'products/product.dart'; +import 'products/product_leaf.dart'; + +void main() { + final diagram = createParcel().toDiagram(); + print(diagram.renderToText()); +} + +Product createParcel() { + return Box( + children: [ + Box.single( + ProductLeaf('Hammer', 9), + ), + Box( + children: [ + Box( + children: [ + Box.single( + ProductLeaf('Phone', 450), + ), + Box.single( + ProductLeaf('Headphones', 30), + ), + ], + ), + Box.single( + ProductLeaf('Charger', 25), + ), + ], + ), + ProductLeaf('Receipt', 0), + ], + ); +} diff --git a/patterns/composite/products_and_boxes/products/box.dart b/patterns/composite/products_and_boxes/products/box.dart new file mode 100644 index 0000000..35e091b --- /dev/null +++ b/patterns/composite/products_and_boxes/products/box.dart @@ -0,0 +1,35 @@ +import '../diagram/diagram.dart'; +import 'product.dart'; + +class Box implements Product { + Box({required this.children}); + + final List children; + + @override + String get content { + final places = size > 1 ? "places: $size, " : ""; + final localPrice = size > 1 ? "price: $price\$" : "$price\$"; + return 'Box($places$localPrice)'; + } + + @override + Diagram toDiagram() { + return Diagram.parentNode( + this, + children, + ); + } + + @override + int get price => children.fold(0, (sum, product) => sum + product.price); + + @override + int get size => children.fold(0, (sum, product) => sum + product.size); + + factory Box.single(Product productLeaf) { + return Box( + children: [productLeaf], + ); + } +} diff --git a/patterns/composite/products_and_boxes/products/product.dart b/patterns/composite/products_and_boxes/products/product.dart new file mode 100644 index 0000000..d5970fd --- /dev/null +++ b/patterns/composite/products_and_boxes/products/product.dart @@ -0,0 +1,11 @@ +import '../diagram/diagram.dart'; + +abstract class Product { + String get content; + + int get size; + + int get price; + + Diagram toDiagram(); +} diff --git a/patterns/composite/products_and_boxes/products/product_leaf.dart b/patterns/composite/products_and_boxes/products/product_leaf.dart new file mode 100644 index 0000000..30ca1eb --- /dev/null +++ b/patterns/composite/products_and_boxes/products/product_leaf.dart @@ -0,0 +1,22 @@ +import '../diagram/diagram.dart'; +import 'product.dart'; + +class ProductLeaf implements Product { + ProductLeaf(this.name, this.price); + + @override + String get content => '$name($price\$)'; + + final String name; + + @override + final int price; + + @override + int get size => 1; + + @override + Diagram toDiagram() { + return Diagram.node(this); + } +} diff --git a/patterns/composite/products_and_boxes/render_elements/render_column.dart b/patterns/composite/products_and_boxes/render_elements/render_column.dart new file mode 100644 index 0000000..d40b85b --- /dev/null +++ b/patterns/composite/products_and_boxes/render_elements/render_column.dart @@ -0,0 +1,36 @@ +import 'render_element.dart'; +import 'render_layout.dart'; +import 'render_position.dart'; + +class RenderColumn extends RenderLayout { + RenderColumn({ + required List children, + int space = 1, + }) : super(children: children, space: space); + + @override + int get width => children.reduce((a, b) => a.width > b.width ? a : b).width; + + @override + int get height => childHeight + spacesSum; + + @override + List compute() { + final result = []; + var y = 0; + + for (final child in children) { + final xCenter = (width - child.width) ~/ 2; + result.add( + RenderPosition( + x: xCenter, + y: y, + child: child, + ), + ); + + y += child.height + space; + } + return result; + } +} diff --git a/patterns/composite/products_and_boxes/render_elements/render_connecting_lines.dart b/patterns/composite/products_and_boxes/render_elements/render_connecting_lines.dart new file mode 100644 index 0000000..e1e7ce5 --- /dev/null +++ b/patterns/composite/products_and_boxes/render_elements/render_connecting_lines.dart @@ -0,0 +1,97 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import '../render_elements/render_column.dart'; +import 'render_element.dart'; +import 'render_position.dart'; +import 'render_row.dart'; +import 'render_layout.dart'; +import 'render_text.dart'; + +class RenderConnectingLines extends RenderElement { + RenderConnectingLines({ + required RenderElement parent, + required List children, + }) { + final row = RenderRow( + children: children, + ); + + _child = RenderColumn( + children: [ + parent, + row, + ], + ); + _lines = _Lines(_child.positions.first, row.positions); + } + + late final RenderLayout _child; + late final _Lines _lines; + + @override + int get height => _child.height; + + @override + int get width => _child.width; + + @override + void render(Canvas dc) { + _child.render(dc); + _lines.render(dc); + } +} + +class _Lines { + final RenderPosition parent; + final List childPos; + + _Lines(this.parent, this.childPos); + + void render(Canvas dc) { + if (parent.child is RenderText) { + final x = parent.x + parent.width ~/ 2; + var y = parent.y + parent.height - 1; + dc.char(x, y, Color('┬')); + + if (childPos.length == 1) { + dc.char(x, ++y, Color('│')); + dc.char(x, ++y, Color('┴')); + return; + } + + if (childPos.length > 2) { + dc.char(x, ++y, Color('┼')); + } + + dc.char(x, ++y, Color('┴')); + + if (childPos.length == 2) { + y++; + } + + drawLeftLine(x, y, dc); + drawRightLine(x, y, dc); + } + } + + void drawLeftLine(int parentCenterX, int y, Canvas dc) { + var x = childPos.first.x + (childPos.first.width ~/ 2); + dc.char(x, y, Color('┴')); + dc.char(x, --y, Color('┌')); + + while (x < parentCenterX - 1) { + dc.char(++x, y, Color('─')); + } + } + + void drawRightLine(int parentCenterX, int y, Canvas dc) { + final currChild = childPos.length == 2 ? childPos[1] : childPos[2]; + var x = currChild.x + (currChild.width ~/ 2); + dc.char(x, y, Color('┴')); + dc.char(x, --y, Color('┐')); + + while (x > parentCenterX + 1) { + dc.char(--x, y, Color('─')); + } + } +} diff --git a/patterns/composite/products_and_boxes/render_elements/render_element.dart b/patterns/composite/products_and_boxes/render_elements/render_element.dart new file mode 100644 index 0000000..1f56d27 --- /dev/null +++ b/patterns/composite/products_and_boxes/render_elements/render_element.dart @@ -0,0 +1,9 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +abstract class RenderElement { + int get width; + + int get height; + + void render(Canvas dc); +} diff --git a/patterns/composite/products_and_boxes/render_elements/render_layout.dart b/patterns/composite/products_and_boxes/render_elements/render_layout.dart new file mode 100644 index 0000000..7fa4706 --- /dev/null +++ b/patterns/composite/products_and_boxes/render_elements/render_layout.dart @@ -0,0 +1,35 @@ +import 'dart:math'; + +import 'package:design_patterns_dart/text_canvas.dart'; + +import 'render_element.dart'; +import 'render_position.dart'; + +abstract class RenderLayout extends RenderElement { + RenderLayout({ + required this.children, + this.space = 3, + }); + + late final List positions = compute(); + + List compute(); + + final List children; + final int space; + + int get childWidth => children.fold( + 0, (width, renderElement) => width + renderElement.width); + + int get childHeight => children.fold( + 0, (height, renderElement) => height + renderElement.height); + + int get spacesSum => max(0, (children.length - 1) * space); + + @override + void render(Canvas dc) { + for (final child in positions) { + child.render(dc); + } + } +} diff --git a/patterns/composite/products_and_boxes/render_elements/render_position.dart b/patterns/composite/products_and_boxes/render_elements/render_position.dart new file mode 100644 index 0000000..3c90b88 --- /dev/null +++ b/patterns/composite/products_and_boxes/render_elements/render_position.dart @@ -0,0 +1,31 @@ +import 'dart:math'; + +import 'package:design_patterns_dart/text_canvas/canvas.dart'; + +import 'render_element.dart'; + +class RenderPosition extends RenderElement { + RenderPosition({ + required this.x, + required this.y, + required this.child, + }); + + final int x; + final int y; + final RenderElement child; + + @override + int get height => child.height; + + @override + int get width => child.width; + + @override + void render(Canvas dc) { + final oldTranslate = dc.translate; + dc.translate = Point(oldTranslate.x + x, oldTranslate.y + y); + child.render(dc); + dc.translate = oldTranslate; + } +} diff --git a/patterns/composite/products_and_boxes/render_elements/render_row.dart b/patterns/composite/products_and_boxes/render_elements/render_row.dart new file mode 100644 index 0000000..a363819 --- /dev/null +++ b/patterns/composite/products_and_boxes/render_elements/render_row.dart @@ -0,0 +1,33 @@ +import 'render_element.dart'; +import 'render_layout.dart'; +import 'render_position.dart'; + +class RenderRow extends RenderLayout { + RenderRow({ + required List children, + int space = 3, + }) : super(children: children, space: space); + + @override + int get width => childWidth + spacesSum; + + @override + int get height => + children.reduce((a, b) => a.height > b.height ? a : b).height; + + @override + List compute() { + final result = []; + var x = 0; + final y = 0; + + for (final child in children) { + result.add( + RenderPosition(x: x, y: y, child: child), + ); + x += child.width + space; + } + + return result; + } +} diff --git a/patterns/composite/products_and_boxes/render_elements/render_text.dart b/patterns/composite/products_and_boxes/render_elements/render_text.dart new file mode 100644 index 0000000..78a60d7 --- /dev/null +++ b/patterns/composite/products_and_boxes/render_elements/render_text.dart @@ -0,0 +1,24 @@ +import 'package:design_patterns_dart/text_canvas.dart'; + +import 'render_element.dart'; + +class RenderText extends RenderElement { + RenderText(this.text, {required this.borderStyle}); + + final String text; + final BorderStyle borderStyle; + + @override + int get width => text.length + 2 + 2; + + @override + int get height => 3; + + @override + void render(Canvas dc) { + if (borderStyle != BorderStyle.empty) { + dc.border(width, height, borderStyle); + } + dc.text(text, widthCenter: width, heightCenter: height); + } +} diff --git a/patterns/decorator/data_source_decoder/README.md b/patterns/decorator/data_source_decoder/README.md new file mode 100644 index 0000000..95bf53c --- /dev/null +++ b/patterns/decorator/data_source_decoder/README.md @@ -0,0 +1,50 @@ +# Pattern Decorator +Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors. + +## Data source decoder example +In this example, the Decorator pattern lets you compress and encrypt sensitive data independently +from the code that actually uses this data. + +Full description can be found [here](https://refactoring.guru/design-patterns/decorator?#pseudocode) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/148954932-edc22d7b-becd-4e2f-bae8-d0d8200d8918.png) + +### Client code: +```dart +void main() { + final records = 'Name,Salary\nJohn Smith,100000\nSteven Jobs,912000'; + + // Create encrypt file + DataSourceDecorator encoded = CompressionDecorator( + EncryptionDecorator( + FileDataSource('Secret.txt'), + ), + ); + encoded.writeData(records); + + DataSource plain = FileDataSource('Secret.txt'); + print("- Input ----------------"); + print(records); + print("\n- Encoded --------------"); + print(plain.readData()); + print("\n- Decoded --------------"); + print(encoded.readData()); +} +``` + +**Output:** +``` +- Input ---------------- +Name,Salary +John Smith,100000 +Steven Jobs,912000 + +- Encoded -------------- +STV0SkJCQkJCQkJCRHdPTXtGNFdEVjhOVFR6cjZRTUx7OWlVRE44T01Ob1JOVVJCQmI4aGx1VHoyRXhHczB6bFppMk1SelBoRkJCOHhaTzlOQkJCQkI+Pg== + +- Decoded -------------- +Name,Salary +John Smith,100000 +Steven Jobs,912000 +``` diff --git a/patterns/decorator/data_source_decoder/Secret.txt b/patterns/decorator/data_source_decoder/Secret.txt new file mode 100644 index 0000000..b175c13 --- /dev/null +++ b/patterns/decorator/data_source_decoder/Secret.txt @@ -0,0 +1 @@ +STV0SkJCQkJCQkJCRHdPTXtGNFdEVjhOVFR6cjZRTUx7OWlVRE44T01Ob1JOVVJCQmI4aGx1VHoyRXhHczB6bFppMk1SelBoRkJCOHhaTzlOQkJCQkI+Pg== \ No newline at end of file diff --git a/patterns/decorator/data_source_decoder/main.dart b/patterns/decorator/data_source_decoder/main.dart new file mode 100644 index 0000000..24dd92e --- /dev/null +++ b/patterns/decorator/data_source_decoder/main.dart @@ -0,0 +1,25 @@ +import 'src/compression_decorator.dart'; +import 'src/data_source.dart'; +import 'src/data_source_decorator.dart'; +import 'src/encryption_decorator.dart'; +import 'src/file_data_source.dart'; + +void main() { + final records = 'Name,Salary\nJohn Smith,100000\nSteven Jobs,912000'; + + // Create encrypt file + DataSourceDecorator encoded = CompressionDecorator( + EncryptionDecorator( + FileDataSource('Secret.txt'), + ), + ); + encoded.writeData(records); + + DataSource plain = FileDataSource('Secret.txt'); + print("- Input ----------------"); + print(records); + print("\n- Encoded --------------"); + print(plain.readData()); + print("\n- Decoded --------------"); + print(encoded.readData()); +} diff --git a/patterns/decorator/data_source_decoder/src/compression_decorator.dart b/patterns/decorator/data_source_decoder/src/compression_decorator.dart new file mode 100644 index 0000000..c9a6131 --- /dev/null +++ b/patterns/decorator/data_source_decoder/src/compression_decorator.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'data_source.dart'; +import 'data_source_decorator.dart'; + +class CompressionDecorator extends DataSourceDecorator { + var compressionLevel = 6; + + CompressionDecorator(DataSource source) : super(source); + + @override + void writeData(String data) { + super.writeData(_compress(data)); + } + + @override + String readData() { + return _decompress(super.readData()); + } + + String _compress(String stringData) { + final gzip = GZipCodec(level: compressionLevel); + final encodeStr = base64.encode( + gzip.encode(stringData.codeUnits), + ); + return encodeStr; + } + + String _decompress(String stringData) { + final gzip = GZipCodec(level: compressionLevel); + final decodeStr = gzip.decode( + base64.decode(stringData), + ); + return String.fromCharCodes(decodeStr); + } +} diff --git a/patterns/decorator/data_source_decoder/src/data_source.dart b/patterns/decorator/data_source_decoder/src/data_source.dart new file mode 100644 index 0000000..bdd5270 --- /dev/null +++ b/patterns/decorator/data_source_decoder/src/data_source.dart @@ -0,0 +1,5 @@ +abstract class DataSource { + void writeData(String data); + + String readData(); +} diff --git a/patterns/decorator/data_source_decoder/src/data_source_decorator.dart b/patterns/decorator/data_source_decoder/src/data_source_decorator.dart new file mode 100644 index 0000000..e057cda --- /dev/null +++ b/patterns/decorator/data_source_decoder/src/data_source_decorator.dart @@ -0,0 +1,17 @@ +import 'data_source.dart'; + +class DataSourceDecorator implements DataSource { + final DataSource _wrapper; + + DataSourceDecorator(this._wrapper); + + @override + void writeData(String data) { + _wrapper.writeData(data); + } + + @override + String readData() { + return _wrapper.readData(); + } +} diff --git a/patterns/decorator/data_source_decoder/src/encryption_decorator.dart b/patterns/decorator/data_source_decoder/src/encryption_decorator.dart new file mode 100644 index 0000000..f276324 --- /dev/null +++ b/patterns/decorator/data_source_decoder/src/encryption_decorator.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'data_source.dart'; +import 'data_source_decorator.dart'; + +class EncryptionDecorator extends DataSourceDecorator { + EncryptionDecorator(DataSource source) : super(source); + + @override + void writeData(String data) { + super.writeData(_encode(data)); + } + + @override + String readData() { + return _decode(super.readData()); + } + + String _encode(String data) { + final encrypt = data.codeUnits.map((e) => e + 1).toList(); + return base64.encode(encrypt); + } + + String _decode(String data) { + final encrypt = base64.decode(data); + final decrypt = encrypt.map((e) => e - 1).toList(); + return String.fromCharCodes(decrypt); + } +} diff --git a/patterns/decorator/data_source_decoder/src/file_data_source.dart b/patterns/decorator/data_source_decoder/src/file_data_source.dart new file mode 100644 index 0000000..90d6198 --- /dev/null +++ b/patterns/decorator/data_source_decoder/src/file_data_source.dart @@ -0,0 +1,26 @@ +import 'dart:io'; + +import 'data_source.dart'; + +class FileDataSource implements DataSource { + final String _name; + + FileDataSource(String name) : _name = thisPath(name); + + @override + void writeData(String data) { + File(_name).writeAsStringSync(data); + } + + @override + String readData() { + return File(_name).readAsStringSync(); + } +} + +thisPath(name) => + Platform.script.pathSegments + .sublist(0, Platform.script.pathSegments.length - 1) + .join(Platform.pathSeparator) + + Platform.pathSeparator + + name; diff --git a/patterns/facade/conceptual/README.md b/patterns/facade/conceptual/README.md new file mode 100644 index 0000000..283887b --- /dev/null +++ b/patterns/facade/conceptual/README.md @@ -0,0 +1,30 @@ +# Facade pattern +Facade is a structural design pattern that provides a simplified interface to a library, a +framework, or any other complex set of classes. + +Tutorial: [here](https://refactoring.guru/design-patterns/facade). + +### About example. +This the very conceptual example rewrite from original source code [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/facade/example) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/183629745-a62d81be-f640-48e8-b70c-00d6cf59aa5f.png) + +### Client code: +```dart +void main() { + final converter = VideoConversionFacade(); + final mp4Video = converter.convertVideo("youtubevideo.ogg", "mp4"); + // ...; +} +``` + +### Output: +``` +VideoConversionFacade: conversion started. +CodecFactory: extracting ogg audio... +BitrateReader: reading file... +BitrateReader: writing file... +AudioMixer: fixing audio... +VideoConversionFacade: conversion completed. +``` diff --git a/patterns/facade/conceptual/main.dart b/patterns/facade/conceptual/main.dart new file mode 100644 index 0000000..c95a690 --- /dev/null +++ b/patterns/facade/conceptual/main.dart @@ -0,0 +1,9 @@ +// ignore_for_file: unused_local_variable + +import 'pattern/video_conversion_facade.dart'; + +void main() { + final converter = VideoConversionFacade(); + final mp4Video = converter.convertVideo("youtubevideo.ogg", "mp4"); + // ...; +} diff --git a/patterns/facade/conceptual/pattern/video_conversion_facade.dart b/patterns/facade/conceptual/pattern/video_conversion_facade.dart new file mode 100644 index 0000000..14f8d38 --- /dev/null +++ b/patterns/facade/conceptual/pattern/video_conversion_facade.dart @@ -0,0 +1,31 @@ +import 'dart:io'; + +import '../some_complex_media_library/audio_mixer.dart'; +import '../some_complex_media_library/bitrate_reader.dart'; +import '../some_complex_media_library/codec.dart'; +import '../some_complex_media_library/codec_factory.dart'; +import '../some_complex_media_library/mpeg4_compression_codec.dart'; +import '../some_complex_media_library/ogg_compression_codec.dart'; +import '../some_complex_media_library/video_file.dart'; + +class VideoConversionFacade { + File convertVideo(String fileName, String format) { + print('VideoConversionFacade: conversion started.'); + final file = VideoFile(fileName); + final sourceCodec = CodecFactory.extract(file); + late final Codec destinationCodec; + + if (format == "mp4") { + destinationCodec = MPEG4CompressionCodec(); + } else { + destinationCodec = OggCompressionCodec(); + } + + final buffer = BitrateReader.read(file, sourceCodec); + final intermediateResult = BitrateReader.convert(buffer, destinationCodec); + final result = AudioMixer().fix(intermediateResult); + print('VideoConversionFacade: conversion completed.'); + + return result; + } +} diff --git a/patterns/facade/conceptual/some_complex_media_library/audio_mixer.dart b/patterns/facade/conceptual/some_complex_media_library/audio_mixer.dart new file mode 100644 index 0000000..a334458 --- /dev/null +++ b/patterns/facade/conceptual/some_complex_media_library/audio_mixer.dart @@ -0,0 +1,10 @@ +import 'dart:io'; + +import 'video_file.dart'; + +class AudioMixer { + File fix(VideoFile result) { + print('AudioMixer: fixing audio...'); + return File('tmp'); + } +} diff --git a/patterns/facade/conceptual/some_complex_media_library/bitrate_reader.dart b/patterns/facade/conceptual/some_complex_media_library/bitrate_reader.dart new file mode 100644 index 0000000..a8efd17 --- /dev/null +++ b/patterns/facade/conceptual/some_complex_media_library/bitrate_reader.dart @@ -0,0 +1,14 @@ +import 'codec.dart'; +import 'video_file.dart'; + +class BitrateReader { + static VideoFile read(VideoFile file, Codec codec) { + print('BitrateReader: reading file...'); + return file; + } + + static VideoFile convert(VideoFile buffer, Codec codec) { + print('BitrateReader: writing file...'); + return buffer; + } +} diff --git a/patterns/facade/conceptual/some_complex_media_library/codec.dart b/patterns/facade/conceptual/some_complex_media_library/codec.dart new file mode 100644 index 0000000..14ea0da --- /dev/null +++ b/patterns/facade/conceptual/some_complex_media_library/codec.dart @@ -0,0 +1 @@ +abstract class Codec {} diff --git a/patterns/facade/conceptual/some_complex_media_library/codec_factory.dart b/patterns/facade/conceptual/some_complex_media_library/codec_factory.dart new file mode 100644 index 0000000..ba58ae1 --- /dev/null +++ b/patterns/facade/conceptual/some_complex_media_library/codec_factory.dart @@ -0,0 +1,17 @@ +import 'codec.dart'; +import 'mpeg4_compression_codec.dart'; +import 'ogg_compression_codec.dart'; +import 'video_file.dart'; + +class CodecFactory { + static Codec extract(VideoFile file) { + String type = file.codecType; + if (type == 'mp4') { + print('CodecFactory: extracting mpeg audio...'); + return MPEG4CompressionCodec(); + } else { + print('CodecFactory: extracting ogg audio...'); + return OggCompressionCodec(); + } + } +} diff --git a/patterns/facade/conceptual/some_complex_media_library/mpeg4_compression_codec.dart b/patterns/facade/conceptual/some_complex_media_library/mpeg4_compression_codec.dart new file mode 100644 index 0000000..d61d3a5 --- /dev/null +++ b/patterns/facade/conceptual/some_complex_media_library/mpeg4_compression_codec.dart @@ -0,0 +1,5 @@ +import 'codec.dart'; + +class MPEG4CompressionCodec implements Codec { + String type = 'mp4'; +} diff --git a/patterns/facade/conceptual/some_complex_media_library/ogg_compression_codec.dart b/patterns/facade/conceptual/some_complex_media_library/ogg_compression_codec.dart new file mode 100644 index 0000000..facf376 --- /dev/null +++ b/patterns/facade/conceptual/some_complex_media_library/ogg_compression_codec.dart @@ -0,0 +1,5 @@ +import 'codec.dart'; + +class OggCompressionCodec implements Codec { + final type = 'ogg'; +} diff --git a/patterns/facade/conceptual/some_complex_media_library/video_file.dart b/patterns/facade/conceptual/some_complex_media_library/video_file.dart new file mode 100644 index 0000000..74553e8 --- /dev/null +++ b/patterns/facade/conceptual/some_complex_media_library/video_file.dart @@ -0,0 +1,6 @@ +class VideoFile { + final String name; + final String codecType; + + VideoFile(this.name) : codecType = name.substring(name.indexOf('.') + 1); +} diff --git a/patterns/factory_method/conceptual_platform_dialog/README.md b/patterns/factory_method/conceptual_platform_dialog/README.md new file mode 100644 index 0000000..a67da5c --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/README.md @@ -0,0 +1,39 @@ +# Abstract Factory pattern +Factory Method is a creational design pattern that provides an interface for creating objects in a +superclass, but allows subclasses to alter the type of objects that will be created. + +Tutorial: [here](https://refactoring.guru/design-patterns/factory-method). + +### About example. +This the very conceptual example rewrite from original source code [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/factory_method/example) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/166105090-a2b490fe-3e3e-44f1-a781-9777023020fb.png) + +### Client code: +```dart +late Dialog dialog; + +void main() { + configure(); + runBusinessLogic(); +} + +void configure() { + if (Platform.isWindows) { + dialog = WindowsDialog(); + } else { + dialog = HtmlDialog(); + } +} + +void runBusinessLogic() { + dialog.renderWindow(); +} +``` + +### Output: +``` +Windows Button +Click! Button says - "Hello World!" +``` diff --git a/patterns/factory_method/conceptual_platform_dialog/buttons/button.dart b/patterns/factory_method/conceptual_platform_dialog/buttons/button.dart new file mode 100644 index 0000000..bdffd72 --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/buttons/button.dart @@ -0,0 +1,10 @@ +/// EN: Common interface for all buttons. +/// +/// RU: Общий интерфейс для всех продуктов. +abstract class Button { + final void Function() onClick; + + Button(this.onClick); + + void render(); +} diff --git a/patterns/factory_method/conceptual_platform_dialog/buttons/html_button.dart b/patterns/factory_method/conceptual_platform_dialog/buttons/html_button.dart new file mode 100644 index 0000000..6619972 --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/buttons/html_button.dart @@ -0,0 +1,14 @@ +import 'button.dart'; + +/// EN: HTML button implementation. +/// +/// RU: Реализация HTML кнопок. +class HtmlButton extends Button { + HtmlButton(void Function() onClick) : super(onClick); + + @override + void render() { + print(''); + onClick(); + } +} diff --git a/patterns/factory_method/conceptual_platform_dialog/buttons/windows_button.dart b/patterns/factory_method/conceptual_platform_dialog/buttons/windows_button.dart new file mode 100644 index 0000000..4a6c4f9 --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/buttons/windows_button.dart @@ -0,0 +1,14 @@ +import 'button.dart'; + +/// EN: Windows button implementation. +/// +/// RU: Реализация нативных кнопок операционной системы. +class WindowsButton extends Button { + WindowsButton(void Function() onClick) : super(onClick); + + @override + void render() { + print('Windows Button'); + onClick(); + } +} diff --git a/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/dialog.dart b/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/dialog.dart new file mode 100644 index 0000000..360a0d2 --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/dialog.dart @@ -0,0 +1,21 @@ +import '../buttons/button.dart'; + +abstract class Dialog { + void renderWindow() { + /// EN: ... other code ... + /// + /// RU: ... остальной код диалога ... + + Button okButton = createButton(() { + print('Click! Button says - "Hello World!"'); + }); + okButton.render(); + } + + /// EN: Subclasses will override this method in order to create specific + /// button objects. + /// + /// RU: Подклассы будут переопределять этот метод, чтобы создавать конкретные + /// объекты продуктов, разные для каждой фабрики. + Button createButton(void Function() onClick); +} diff --git a/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/html_dialog.dart b/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/html_dialog.dart new file mode 100644 index 0000000..1af9d7e --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/html_dialog.dart @@ -0,0 +1,11 @@ +import '../buttons/button.dart'; +import '../buttons/html_button.dart'; +import 'dialog.dart'; + +/// EN: HTML Dialog will produce HTML buttons. +/// +/// RU: HTML-диалог. +class HtmlDialog extends Dialog { + @override + Button createButton(void Function() onClick) => HtmlButton(onClick); +} diff --git a/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/windows_dialog.dart b/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/windows_dialog.dart new file mode 100644 index 0000000..5445e66 --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/dialogs_factory/windows_dialog.dart @@ -0,0 +1,11 @@ +import '../buttons/button.dart'; +import '../buttons/windows_button.dart'; +import 'dialog.dart'; + +/// EN: Windows Dialog will produce Windows buttons. +/// +/// RU: Диалог на элементах операционной системы. +class WindowsDialog extends Dialog { + @override + Button createButton(void Function() onClick) => WindowsButton(onClick); +} diff --git a/patterns/factory_method/conceptual_platform_dialog/main.dart b/patterns/factory_method/conceptual_platform_dialog/main.dart new file mode 100644 index 0000000..2ea39f6 --- /dev/null +++ b/patterns/factory_method/conceptual_platform_dialog/main.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'dialogs_factory/dialog.dart'; +import 'dialogs_factory/html_dialog.dart'; +import 'dialogs_factory/windows_dialog.dart'; + +late Dialog dialog; + +void main() { + configure(); + runBusinessLogic(); +} + +/// EN: The concrete factory is usually chosen depending on configuration or +/// environment options. +/// +/// RU: Приложение создаёт определённую фабрику в зависимости от конфигурации +/// или окружения. +void configure() { + if (Platform.isWindows) { + dialog = WindowsDialog(); + } else { + dialog = HtmlDialog(); + } +} + +/// EN: All of the client code should work with factories and products +/// through abstract interfaces. This way it does not care which factory it +/// works with and what kind of product it returns. +/// +/// RU: Весь остальной клиентский код работает с фабрикой и продуктами только +/// через общий интерфейс, поэтому для него неважно какая фабрика была +/// создана. +void runBusinessLogic() { + dialog.renderWindow(); +} diff --git a/patterns/flyweight/conceptual/README.md b/patterns/flyweight/conceptual/README.md new file mode 100644 index 0000000..4862949 --- /dev/null +++ b/patterns/flyweight/conceptual/README.md @@ -0,0 +1,50 @@ +# Flyweight Pattern +Flyweight is a structural design pattern that lets you fit more objects into the available amount of + RAM by sharing common parts of state between multiple objects instead of keeping all of the data in + each object. + +Tutorial: [here](https://refactoring.guru/design-patterns/flyweight). + +### Conceptual Diagram: +![Flyweight Pattern Diagram](https://user-images.githubusercontent.com/8049534/174476638-007f3179-495f-499d-8f35-e33956d4890b.png) + +### Client code: +```dart +void main() { + final factory = FlyweightFactory(); + + final one = factory.create('one'); + one.draw(); + + final ONE = factory.create('ONE'); + ONE.draw(); + + final OnE = factory.create('OnE'); + OnE.draw(); + + final two = factory.create('two'); + two.draw(); + + final Two = factory.create('Two'); + Two.draw(); +} +``` + +### Output: +``` +Flyweight( + localParam: "one", shareParam: (61, "b", 0.19) +) +Flyweight( + localParam: "ONE", shareParam: (61, "b", 0.19) +) +Flyweight( + localParam: "OnE", shareParam: (61, "b", 0.19) +) +Flyweight( + localParam: "two", shareParam: (53, "e", 0.55) +) +Flyweight( + localParam: "Two", shareParam: (53, "e", 0.55) +) +``` diff --git a/patterns/flyweight/conceptual/main.dart b/patterns/flyweight/conceptual/main.dart new file mode 100644 index 0000000..68a4d9b --- /dev/null +++ b/patterns/flyweight/conceptual/main.dart @@ -0,0 +1,22 @@ +// ignore_for_file: non_constant_identifier_names + +import 'pattern/flyweight_factory.dart'; + +void main() { + final factory = FlyweightFactory(); + + final one = factory.create('one'); + one.draw(); + + final ONE = factory.create('ONE'); + ONE.draw(); + + final OnE = factory.create('OnE'); + OnE.draw(); + + final two = factory.create('two'); + two.draw(); + + final Two = factory.create('Two'); + Two.draw(); +} diff --git a/patterns/flyweight/conceptual/params/share_params.dart b/patterns/flyweight/conceptual/params/share_params.dart new file mode 100644 index 0000000..3202f51 --- /dev/null +++ b/patterns/flyweight/conceptual/params/share_params.dart @@ -0,0 +1,7 @@ +class ShareParams { + final int param1; + final String param2; + final double param3; + + ShareParams(this.param1, this.param2, this.param3); +} diff --git a/patterns/flyweight/conceptual/pattern/flyweight.dart b/patterns/flyweight/conceptual/pattern/flyweight.dart new file mode 100644 index 0000000..33e02ca --- /dev/null +++ b/patterns/flyweight/conceptual/pattern/flyweight.dart @@ -0,0 +1,17 @@ +import '../params/share_params.dart'; + +class Flyweight { + final String localParam; + final ShareParams shareParams; + + Flyweight(this.localParam, this.shareParams); + + void draw() { + print('Flyweight('); + print('\tlocalParam: "$localParam", shareParam: (' + '${shareParams.param1}, ' + '"${shareParams.param2}", ' + '${shareParams.param3})'); + print(')'); + } +} diff --git a/patterns/flyweight/conceptual/pattern/flyweight_factory.dart b/patterns/flyweight/conceptual/pattern/flyweight_factory.dart new file mode 100644 index 0000000..96cbd5a --- /dev/null +++ b/patterns/flyweight/conceptual/pattern/flyweight_factory.dart @@ -0,0 +1,20 @@ +import 'flyweight.dart'; +import '../params/share_params.dart'; +import '../utils/fake_value.dart'; + +class FlyweightFactory { + Flyweight create(String localParam) { + final shareParams = _shares.putIfAbsent( + localParam.toLowerCase(), + () => ShareParams( + fakeInt, + fakeString, + fakeDouble, + ), + ); + + return Flyweight(localParam, shareParams); + } + + final _shares = {}; +} diff --git a/patterns/flyweight/conceptual/utils/fake_value.dart b/patterns/flyweight/conceptual/utils/fake_value.dart new file mode 100644 index 0000000..ae29e2c --- /dev/null +++ b/patterns/flyweight/conceptual/utils/fake_value.dart @@ -0,0 +1,12 @@ +import 'dart:math'; + +String str = 'abcdef'; +get fakeString { + final char = str[Random().nextInt(str.length - 1)]; + str = str.replaceFirst(char, ''); + return char; +} + +get fakeInt => Random().nextInt(100); + +get fakeDouble => Random().nextInt(100) / 100; diff --git a/patterns/interpreter/conceptual/README.md b/patterns/interpreter/conceptual/README.md new file mode 100644 index 0000000..b7e1217 --- /dev/null +++ b/patterns/interpreter/conceptual/README.md @@ -0,0 +1,42 @@ +# Interpreter Pattern +In computer programming, the interpreter pattern is a design pattern that specifies how to evaluate +sentences in a language. + +## Diagram: +![Interpreter Diagram](https://user-images.githubusercontent.com/8049534/176169636-4c8eb3ba-d5e8-4ecb-81a8-96f1a30f6339.png) + +## Client code: +```dart +void main() { + final context = Context(); + final variable1 = BoolVariable('var1'); + final variable2 = BoolVariable('var2'); + final variable3 = BoolVariable('var3'); + final variable4 = BoolVariable('var4'); + + context.assign(variable1, true); + context.assign(variable2, false); + context.assign(variable3, true); + context.assign(variable4, false); + + final expression = And( + variable1, // true + Xor( + variable2, // false + Or( + variable3, // true + variable4, // false + ), + ), + ); + + print(expression.evaluate(context)); + print(expression.toDebugString(context)); +} +``` + +### Output: +``` +var4(false) Or var3(true) Xor var2(false) And var1(true) +result: true +``` diff --git a/patterns/interpreter/conceptual/main.dart b/patterns/interpreter/conceptual/main.dart new file mode 100644 index 0000000..82e0394 --- /dev/null +++ b/patterns/interpreter/conceptual/main.dart @@ -0,0 +1,32 @@ +import 'operations/and.dart'; +import 'operations/or.dart'; +import 'operations/xor.dart'; +import 'pattern/context.dart'; +import 'variable/bool_variable.dart'; + +void main() { + final context = Context(); + final variable1 = BoolVariable('var1'); + final variable2 = BoolVariable('var2'); + final variable3 = BoolVariable('var3'); + final variable4 = BoolVariable('var4'); + + context.assign(variable1, true); + context.assign(variable2, false); + context.assign(variable3, true); + context.assign(variable4, false); + + final expression = And( + variable1, // true + Xor( + variable2, // false + Or( + variable3, // true + variable4, // false + ), + ), + ); + + print(expression.toDebugString(context)); + print('result: ${expression.evaluate(context)}'); +} diff --git a/patterns/interpreter/conceptual/operations/and.dart b/patterns/interpreter/conceptual/operations/and.dart new file mode 100644 index 0000000..fdecf7a --- /dev/null +++ b/patterns/interpreter/conceptual/operations/and.dart @@ -0,0 +1,8 @@ +import 'operation.dart'; + +class And extends Operation { + And(super.expression1, super.expression2); + + @override + bool operation(bool a, bool b) => a && b; +} diff --git a/patterns/interpreter/conceptual/operations/operation.dart b/patterns/interpreter/conceptual/operations/operation.dart new file mode 100644 index 0000000..7fdfb70 --- /dev/null +++ b/patterns/interpreter/conceptual/operations/operation.dart @@ -0,0 +1,25 @@ +import '../pattern/context.dart'; +import '../pattern/expression.dart'; + +abstract class Operation implements Expression { + final Expression expression1; + final Expression expression2; + + Operation(this.expression1, this.expression2); + + bool operation(bool a, bool b); + + @override + bool evaluate(Context context) { + final a = expression1.evaluate(context); + final b = expression2.evaluate(context); + return operation(a, b); + } + + @override + String toDebugString(Context context) { + final a = expression1.toDebugString(context); + final b = expression2.toDebugString(context); + return '$b $runtimeType $a'; + } +} diff --git a/patterns/interpreter/conceptual/operations/or.dart b/patterns/interpreter/conceptual/operations/or.dart new file mode 100644 index 0000000..3322a4c --- /dev/null +++ b/patterns/interpreter/conceptual/operations/or.dart @@ -0,0 +1,8 @@ +import 'operation.dart'; + +class Or extends Operation { + Or(super.expression1, super.expression2); + + @override + bool operation(bool a, bool b) => a || b; +} diff --git a/patterns/interpreter/conceptual/operations/xor.dart b/patterns/interpreter/conceptual/operations/xor.dart new file mode 100644 index 0000000..915877a --- /dev/null +++ b/patterns/interpreter/conceptual/operations/xor.dart @@ -0,0 +1,8 @@ +import 'operation.dart'; + +class Xor extends Operation { + Xor(super.expression1, super.expression2); + + @override + bool operation(bool a, bool b) => a ^ b; +} diff --git a/patterns/interpreter/conceptual/pattern/context.dart b/patterns/interpreter/conceptual/pattern/context.dart new file mode 100644 index 0000000..6f17393 --- /dev/null +++ b/patterns/interpreter/conceptual/pattern/context.dart @@ -0,0 +1,13 @@ +import '../variable/bool_variable.dart'; + +class Context { + void assign(BoolVariable variable, bool value) { + _values.putIfAbsent(variable.name, () => value); + } + + bool lookup(String name) { + return _values[name]!; + } + + final _values = {}; +} diff --git a/patterns/interpreter/conceptual/pattern/expression.dart b/patterns/interpreter/conceptual/pattern/expression.dart new file mode 100644 index 0000000..dd74e8e --- /dev/null +++ b/patterns/interpreter/conceptual/pattern/expression.dart @@ -0,0 +1,7 @@ +import 'context.dart'; + +abstract class Expression { + bool evaluate(Context context); + + String toDebugString(Context context); +} diff --git a/patterns/interpreter/conceptual/variable/bool_variable.dart b/patterns/interpreter/conceptual/variable/bool_variable.dart new file mode 100644 index 0000000..1472bc7 --- /dev/null +++ b/patterns/interpreter/conceptual/variable/bool_variable.dart @@ -0,0 +1,19 @@ +import '../pattern/context.dart'; +import '../pattern/expression.dart'; + +class BoolVariable implements Expression { + BoolVariable(this.name); + + final String name; + + @override + bool evaluate(Context context) { + return context.lookup(name); + } + + @override + String toDebugString(Context context) { + final value = context.lookup(name); + return '$name($value)'; + } +} diff --git a/patterns/iterator/github_commit/README.md b/patterns/iterator/github_commit/README.md new file mode 100644 index 0000000..93e6ee8 --- /dev/null +++ b/patterns/iterator/github_commit/README.md @@ -0,0 +1,44 @@ +# Iterator pattern +Iterator is a behavioral design pattern that lets you traverse elements of a collection without +exposing its underlying representation (list, stack, tree, etc.). + +Tutorial: [here](https://refactoring.guru/design-patterns/iterator). + +## Diagram: +![image](https://user-images.githubusercontent.com/8049534/183165928-7274e761-09e3-48ce-b9c1-41d552fa1f1a.png) + + +### Client code: +```dart +void main() async { + final GitHubRepo gitHubRepo = await GitHubLoader.get( + userName: 'RefactoringGuru', + repoName: 'design-patterns-dart', + ); + + print( + 'Iterate last 10 commits.' + '\n----------------------------', + ); + + for (Commit commit in gitHubRepo.commitIterator()) { + print(commit.message); + } +} +``` + +### Output: +``` +Iterate last 10 commits. +---------------------------- +Merge pull request #74 from ilopX/fix-conceptual-command-pattern-folder-name Fix conceptual command pattern folder name +Fix conceptual command pattern folder name. +Merge pull request #73 from ilopX/add-conceptual-command-pattern Add conceptual command pattern +Bump version 0.35.0. +Add README. +Impl conceptual command pattern. +Merge pull request #72 from ilopX/add-singleton-pattern Add singleton pattern. +Bump version 0.34.0. +Add README. +``` + diff --git a/patterns/iterator/github_commit/github/commit.dart b/patterns/iterator/github_commit/github/commit.dart new file mode 100644 index 0000000..88e7e43 --- /dev/null +++ b/patterns/iterator/github_commit/github/commit.dart @@ -0,0 +1,5 @@ +class Commit { + String message; + + Commit(this.message); +} diff --git a/patterns/iterator/github_commit/github/github_loader.dart b/patterns/iterator/github_commit/github/github_loader.dart new file mode 100644 index 0000000..8a40889 --- /dev/null +++ b/patterns/iterator/github_commit/github/github_loader.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; +import 'dart:io'; +import '../pattern/github_repo.dart'; + +class GitHubLoader { + static Future get({required userName, required repoName}) async { + final url = Uri.http( + 'api.github.com', + 'repos/$userName/$repoName/commits', + {'per_page': '10'}, + ); + final json = await _loadCommits(url); + return GitHubRepo(json); + } + + static Future> _loadCommits(Uri url) async { + final client = HttpClient(); + try { + final response = await client.getUrl(url); + final request = await response.close(); + final content = await request.transform(utf8.decoder).join(); + return jsonDecode(content); + } finally { + client.close(); + } + } +} diff --git a/patterns/iterator/github_commit/main.dart b/patterns/iterator/github_commit/main.dart new file mode 100644 index 0000000..9df54e9 --- /dev/null +++ b/patterns/iterator/github_commit/main.dart @@ -0,0 +1,19 @@ +import 'github/commit.dart'; +import 'github/github_loader.dart'; +import 'pattern/github_repo.dart'; + +void main() async { + final GitHubRepo gitHubRepo = await GitHubLoader.get( + userName: 'RefactoringGuru', + repoName: 'design-patterns-dart', + ); + + print( + 'Iterate last 10 commits.' + '\n----------------------------', + ); + + for (Commit commit in gitHubRepo.commitIterator()) { + print(commit.message); + } +} diff --git a/patterns/iterator/github_commit/pattern/github_repo.dart b/patterns/iterator/github_commit/pattern/github_repo.dart new file mode 100644 index 0000000..df8377c --- /dev/null +++ b/patterns/iterator/github_commit/pattern/github_repo.dart @@ -0,0 +1,16 @@ +import '../github/commit.dart'; + +class GitHubRepo { + GitHubRepo(this._json); + + Iterable commitIterator() sync* { + for (final jsonCommit in _json) { + var message = jsonCommit['commit']['message'] as String; + message = message.replaceAll(RegExp(r'\n+'), ' '); + + yield Commit(message); + } + } + + final List _json; +} diff --git a/patterns/iterator/word_iterator/README.md b/patterns/iterator/word_iterator/README.md new file mode 100644 index 0000000..ff1775c --- /dev/null +++ b/patterns/iterator/word_iterator/README.md @@ -0,0 +1,47 @@ +# Iterator pattern +Iterator is a behavioral design pattern that lets you traverse elements of a collection without +exposing its underlying representation (list, stack, tree, etc.). + +Tutorial: [here](https://refactoring.guru/design-patterns/iterator). + +### Client code: +```dart +void main() { + final text = Text( + 'Iterator is a behavioral design pattern that lets you traverse elements ' + 'of a collection without exposing its underlying representation ' + '(list, stack, tree, etc.).', + ); + + for (final s in text) { + print(s); + } +} +``` + +**Output:** +``` +Iterator +is +a +behavioral +design +pattern +that +lets +you +traverse +elements +of +a +collection +without +exposing +its +underlying +representation +list +stack +tree +etc +``` diff --git a/patterns/iterator/word_iterator/main.dart b/patterns/iterator/word_iterator/main.dart new file mode 100644 index 0000000..55b8f33 --- /dev/null +++ b/patterns/iterator/word_iterator/main.dart @@ -0,0 +1,13 @@ +import 'text/text.dart'; + +void main() { + final text = Text( + 'Iterator is a behavioral design pattern that lets you traverse elements ' + 'of a collection without exposing its underlying representation ' + '(list, stack, tree, etc.).', + ); + + for (final s in text) { + print(s); + } +} diff --git a/patterns/iterator/word_iterator/pattern/word_iterator.dart b/patterns/iterator/word_iterator/pattern/word_iterator.dart new file mode 100644 index 0000000..bd97f8f --- /dev/null +++ b/patterns/iterator/word_iterator/pattern/word_iterator.dart @@ -0,0 +1,48 @@ +import '../text/text.dart'; + +class WordIterator extends Iterator { + WordIterator(this._text); + + @override + String get current => _currWord!; + + @override + bool moveNext() { + final start = _lastIndex; + + while (_searchNextSpaceChar(' ')) { + // ++ + } + + _currWord = _getWord(start, _lastIndex); + return _currWord!.isNotEmpty; + } + + final Text _text; + int _lastIndex = 0; + String? _currWord; + + bool _searchNextSpaceChar(String char) { + final isTextEnd = _lastIndex >= _text.text.length; + + if (isTextEnd) { + return false; + } + + final isNotSpaceChar = _text.text[_lastIndex++] != char; + return isNotSpaceChar; + } + + String _getWord(int start, int end) { + final noWordChars = RegExp(r'\W'); + return _text.text + .substring( + start, + _lastIndex, + ) + .replaceAll( + noWordChars, + '', + ); + } +} diff --git a/patterns/iterator/word_iterator/text/text.dart b/patterns/iterator/word_iterator/text/text.dart new file mode 100644 index 0000000..00bb63d --- /dev/null +++ b/patterns/iterator/word_iterator/text/text.dart @@ -0,0 +1,10 @@ +import '../pattern/word_iterator.dart'; + +class Text extends Iterable { + final String text; + + Text(this.text); + + @override + Iterator get iterator => WordIterator(this); +} diff --git a/patterns/mediator/conceptual/README.md b/patterns/mediator/conceptual/README.md new file mode 100644 index 0000000..20eece8 --- /dev/null +++ b/patterns/mediator/conceptual/README.md @@ -0,0 +1,37 @@ +# Mediator Pattern +Mediator is a behavioral design pattern that lets you reduce chaotic dependencies between objects. +The pattern restricts direct communications between the objects and forces them to collaborate only +via a mediator object. + +Tutorial: [here](https://refactoring.guru/design-patterns/mediator). + +## Conceptual diagram: +![image](https://user-images.githubusercontent.com/8049534/173237874-971dd4e7-2e74-4cac-bcea-77b88255adad.png) + +### Client code: +```dart +void main() { + final component1 = Component1(); + final component2 = Component2(); + + ConcreteMediator(component1, component2); + + component1.doOne(); + print(''); + component2.doTwo(); +} +``` + +### Output: +``` +call Component1.doOne() +ConcreteMediator.notify(event: "doOne") +ConcreteMediator.reactComponentOne() +use component2.name = "Two" + +call Component2.doTwo() +ConcreteMediator.notify(event: "doTwo") +ConcreteMediator.reactComponentTwo() +use component1.sate = "Cmp1" +``` + diff --git a/patterns/mediator/conceptual/components/component1.dart b/patterns/mediator/conceptual/components/component1.dart new file mode 100644 index 0000000..6b4ee05 --- /dev/null +++ b/patterns/mediator/conceptual/components/component1.dart @@ -0,0 +1,10 @@ +import '../pattern/mediator.dart'; + +class Component1 extends Component { + final sate = 'Cmp1'; + + void doOne() { + print('call Component1.doOne()'); + mediator?.notify(this, 'doOne'); + } +} diff --git a/patterns/mediator/conceptual/components/component2.dart b/patterns/mediator/conceptual/components/component2.dart new file mode 100644 index 0000000..07708cf --- /dev/null +++ b/patterns/mediator/conceptual/components/component2.dart @@ -0,0 +1,10 @@ +import '../pattern/mediator.dart'; + +class Component2 extends Component { + final name = 'Two'; + + void doTwo() { + print('call Component2.doTwo()'); + mediator?.notify(this, 'doTwo'); + } +} diff --git a/patterns/mediator/conceptual/concrete_mediator/concrete_mediator.dart b/patterns/mediator/conceptual/concrete_mediator/concrete_mediator.dart new file mode 100644 index 0000000..fd29d5e --- /dev/null +++ b/patterns/mediator/conceptual/concrete_mediator/concrete_mediator.dart @@ -0,0 +1,34 @@ +import '../components/component1.dart'; +import '../components/component2.dart'; +import '../pattern/mediator.dart'; + +class ConcreteMediator extends Mediator { + final Component1 component1; + final Component2 component2; + + ConcreteMediator(this.component1, this.component2) { + applyThisMediator(component1); + applyThisMediator(component2); + } + + @override + void notify(Component component, String event) { + print('ConcreteMediator.notify(event: "$event")'); + + if (component == component1) { + reactComponentOne(); + } else if (component == component2) { + reactComponentTwo(); + } + } + + void reactComponentOne() { + print('ConcreteMediator.reactComponentOne()'); + print('use component2.name = "${component2.name}"'); + } + + void reactComponentTwo() { + print('ConcreteMediator.reactComponentTwo()'); + print('use component1.sate = "${component1.sate}"'); + } +} diff --git a/patterns/mediator/conceptual/main.dart b/patterns/mediator/conceptual/main.dart new file mode 100644 index 0000000..b17458f --- /dev/null +++ b/patterns/mediator/conceptual/main.dart @@ -0,0 +1,14 @@ +import 'components/component1.dart'; +import 'components/component2.dart'; +import 'concrete_mediator/concrete_mediator.dart'; + +void main() { + final component1 = Component1(); + final component2 = Component2(); + + ConcreteMediator(component1, component2); + + component1.doOne(); + print(''); + component2.doTwo(); +} diff --git a/patterns/mediator/conceptual/pattern/component.dart b/patterns/mediator/conceptual/pattern/component.dart new file mode 100644 index 0000000..f9fc286 --- /dev/null +++ b/patterns/mediator/conceptual/pattern/component.dart @@ -0,0 +1,7 @@ +part of mediator; + +class Component { + get mediator => _mediator; + + Mediator? _mediator; +} diff --git a/patterns/mediator/conceptual/pattern/mediator.dart b/patterns/mediator/conceptual/pattern/mediator.dart new file mode 100644 index 0000000..17fa41f --- /dev/null +++ b/patterns/mediator/conceptual/pattern/mediator.dart @@ -0,0 +1,11 @@ +library mediator; + +part 'component.dart'; + +abstract class Mediator { + void notify(Component component, String event); + + void applyThisMediator(Component component) { + component._mediator = this; + } +} diff --git a/patterns/memento/conceptual/README.md b/patterns/memento/conceptual/README.md new file mode 100644 index 0000000..45811db --- /dev/null +++ b/patterns/memento/conceptual/README.md @@ -0,0 +1,39 @@ +# Memento pattern +Memento is a behavioral design pattern that lets you save and restore the previous state of an +object without revealing the details of its implementation. + +Tutorial: [here](https://refactoring.guru/design-patterns/memento). + +## Conceptual Editor example +This example uses the Memento pattern alongside the Command pattern for storing snapshots of the +complex text editor’s state and restoring an earlier state from these snapshots when needed. + +More detailed explanation on [RefactoringGuru](https://refactoring.guru/design-patterns/memento?#pseudocode). + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/151352367-c97db094-fc87-4eb2-9210-581914c57ced.png) + +### Client code: +```dart +void main() { + final editor = Editor('New Document'); + final firstState = Command.makeBackup(editor); + editor.text += ' add text'; + final secondState = Command.makeBackup(editor); + + print('Current state: "${editor.text}"'); + + firstState.undo(); + print('First state: "${editor.text}"'); + + secondState.undo(); + print('Second state: "${editor.text}"'); +} +``` + +**Output:** +``` +Current state: "New Document add text" +First state: "New Document" +Second state: "New Document add text" +``` diff --git a/patterns/memento/conceptual/app/command.dart b/patterns/memento/conceptual/app/command.dart new file mode 100644 index 0000000..61730b5 --- /dev/null +++ b/patterns/memento/conceptual/app/command.dart @@ -0,0 +1,25 @@ +import 'editor.dart'; +import 'snapshot.dart'; + +/// EN: A command object can act as a caretaker. In that case, the +/// command gets a memento just before it changes the +/// originator's state. When undo is requested, it restores the +/// originator's state from a memento. +/// +/// RU: Опекуном может выступать класс команд (см. паттерн Команда). +/// В этом случае команда сохраняет снимок состояния объекта- +/// получателя, перед тем как передать ему своё действие. А в +/// случае отмены команда вернёт объект в прежнее состояние. +class Command { + Snapshot _backup; + + Command._(this._backup); + + factory Command.makeBackup(Editor editor) { + return Command._(editor.createSnapshot()); + } + + void undo() { + _backup.restore(); + } +} diff --git a/patterns/memento/conceptual/app/editor.dart b/patterns/memento/conceptual/app/editor.dart new file mode 100644 index 0000000..e0c3226 --- /dev/null +++ b/patterns/memento/conceptual/app/editor.dart @@ -0,0 +1,28 @@ +import 'snapshot.dart'; + +/// EN: The originator holds some important data that may change over +/// time. It also defines a method for saving its state inside a +/// memento and another method for restoring the state from it. +/// +/// RU: Класс создателя должен иметь специальный метод, который. +/// сохраняет состояние создателя в новом объекте-снимке. +class Editor { + var text = ''; + var curX = 0; + var curY = 0; + var selectionWidth = 0; + + Editor(this.text); + + /// EN: Saves the current state inside a memento. + Snapshot createSnapshot() { + /// EN: Memento is an immutable object; that's why the + /// originator passes its state to the memento's + /// constructor parameters. + /// + /// RU: Снимок — неизменяемый объект, поэтому Создатель + /// передаёт все своё состояние через параметры + /// конструктора. + return Snapshot(this, text, curX, curY, selectionWidth); + } +} diff --git a/patterns/memento/conceptual/app/snapshot.dart b/patterns/memento/conceptual/app/snapshot.dart new file mode 100644 index 0000000..a53e596 --- /dev/null +++ b/patterns/memento/conceptual/app/snapshot.dart @@ -0,0 +1,33 @@ +import 'editor.dart'; + +/// EN: The memento class stores the past state of the editor. +/// +/// RU: Снимок хранит прошлое состояние редактора. +class Snapshot { + final Editor _editor; + final String _text; + final int _curX; + final int _curY; + final int _selectionWidth; + + Snapshot( + this._editor, + this._text, + this._curX, + this._curY, + this._selectionWidth, + ); + + /// EN: At some point, a previous state of the editor can be + /// restored using a memento object. + /// + /// RU: В нужный момент владелец снимка может восстановить + /// состояние редактора. + void restore() { + _editor + ..text = _text + ..curX = _curX + ..curY = _curY + ..selectionWidth = _selectionWidth; + } +} diff --git a/patterns/memento/conceptual/main.dart b/patterns/memento/conceptual/main.dart new file mode 100644 index 0000000..07a0486 --- /dev/null +++ b/patterns/memento/conceptual/main.dart @@ -0,0 +1,17 @@ +import 'app/command.dart'; +import 'app/editor.dart'; + +void main() { + final editor = Editor('New Document'); + final firstState = Command.makeBackup(editor); + editor.text += ' add text'; + final secondState = Command.makeBackup(editor); + + print('Current state: "${editor.text}"'); + + firstState.undo(); + print('First state: "${editor.text}"'); + + secondState.undo(); + print('Second state: "${editor.text}"'); +} diff --git a/patterns/memento/memento_editor/README.md b/patterns/memento/memento_editor/README.md new file mode 100644 index 0000000..6fc92a7 --- /dev/null +++ b/patterns/memento/memento_editor/README.md @@ -0,0 +1,47 @@ +# Memento pattern +Memento is a behavioral design pattern that lets you save and restore the previous state of an +object without revealing the details of its implementation. + +Tutorial: [here](https://refactoring.guru/design-patterns/memento). + +### Online demo: +Click on the picture to see the [demo](https://RefactoringGuru.github.io/design-patterns-dart/#/memento/flutter_memento_editor). + +[![image](https://user-images.githubusercontent.com/8049534/165401175-88bc4593-4624-45b4-8c03-6f1390ed771a.png)](https://refactoringguru.github.io/design-patterns-dart/#/memento/flutter_memento_editor) + +### Dependency Patterns +This complex example includes these implementations: +- [[AppObserver](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/app_observer)] +- [[SubscriberWidget](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/subscriber_flutter_widget)] + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/165758516-1de543f5-666d-4e07-958d-2d8fceb73af9.png) + +### Client code: +```dart +class MementoEditorApplication { + final editor = Editor(); + final caretaker = Caretaker(); + + void createDefaultShapes() {/*...*/} + + void saveState() { + final snapshot = editor.backup(); + + if (caretaker.isSnapshotExists(snapshot)) { + return; + } + + final memento = Memento(DateTime.now(), snapshot); + caretaker.addMemento(memento); + editor.events.notify(MementoCreateEvent()); + } + + void restoreState(Memento memento) { + editor + ..unSelect() + ..restore(memento.snapshot) + ..repaint(); + } +} +``` diff --git a/patterns/memento/memento_editor/application.dart b/patterns/memento/memento_editor/application.dart new file mode 100644 index 0000000..a98d3cc --- /dev/null +++ b/patterns/memento/memento_editor/application.dart @@ -0,0 +1,44 @@ +import 'dart:math'; + +import 'editor/memento_create_event.dart'; +import 'editor/editor.dart'; +import 'memento_pattern/caretaker.dart'; +import 'memento_pattern/memento.dart'; +import 'shapes/shape.dart'; + +class MementoEditorApplication { + final editor = Editor(); + final caretaker = Caretaker(); + + MementoEditorApplication() { + createDefaultShapes(); + } + + void createDefaultShapes() { + const radius = 300.0; + for (var i = 0; i < 7; i++) { + final x = 60 + radius + cos(i / 1.15) * radius; + final y = 60 + radius + sin(i / 1.15) * radius; + editor.shapes.add(Shape(x, y)); + } + } + + void saveState() { + final snapshot = editor.backup(); + + if (caretaker.isSnapshotExists(snapshot)) { + return; + } + + final memento = Memento(DateTime.now(), snapshot); + caretaker.addMemento(memento); + editor.events.notify(MementoCreateEvent()); + } + + void restoreState(Memento memento) { + editor + ..unSelect() + ..restore(memento.snapshot) + ..repaint(); + } +} diff --git a/patterns/memento/memento_editor/editor/editor.dart b/patterns/memento/memento_editor/editor/editor.dart new file mode 100644 index 0000000..6455cb7 --- /dev/null +++ b/patterns/memento/memento_editor/editor/editor.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import '../../../adapter/flutter_adapter/classic_app/classic_app.dart'; +import '../memento_pattern/originator.dart'; +import '../shapes/shapes.dart'; +import 'manipulator.dart'; + +class Editor extends ClassicApp + with Manipulator, Shapes, BackupOriginator, RecoveryOriginator { + @override + void onPaint(Canvas canvas, Size canvasSize) { + _paintBackground(canvas, canvasSize); + paintShapes(canvas); + activeShape?.paintSelectionBox(canvas); + } + + void _paintBackground(Canvas canvas, Size canvasSize) { + canvas.drawRect( + Offset.zero & canvasSize, + Paint()..color = Color(0xff404040), + ); + } +} diff --git a/patterns/memento/memento_editor/editor/manipulator.dart b/patterns/memento/memento_editor/editor/manipulator.dart new file mode 100644 index 0000000..4124062 --- /dev/null +++ b/patterns/memento/memento_editor/editor/manipulator.dart @@ -0,0 +1,28 @@ +import '../shapes/shapes.dart'; + +mixin Manipulator implements Shapes { + var _isMouseDown = false; + + @override + void onMouseDown(double x, double y) { + _isMouseDown = true; + select(x, y); + } + + @override + void onMouseMove(double x, double y) { + if (_isMouseDown) { + activeShape?.dragTo(x, y); + } + } + + @override + void onPointerWheel(double deltaX, double deltaY) { + activeShape?.changeSize(deltaY / 5); + } + + @override + void onMouseUp() { + _isMouseDown = false; + } +} diff --git a/patterns/memento/memento_editor/editor/memento_create_event.dart b/patterns/memento/memento_editor/editor/memento_create_event.dart new file mode 100644 index 0000000..67e459a --- /dev/null +++ b/patterns/memento/memento_editor/editor/memento_create_event.dart @@ -0,0 +1,3 @@ +import '../../../observer/app_observer/observer/event.dart'; + +class MementoCreateEvent extends Event {} diff --git a/patterns/memento/memento_editor/main.dart b/patterns/memento/memento_editor/main.dart new file mode 100644 index 0000000..dc13777 --- /dev/null +++ b/patterns/memento/memento_editor/main.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../adapter/flutter_adapter/adapter/classic_app_adapter_widget.dart' + as adapter; +import 'application.dart'; +import 'widgets/right_panel_widget.dart'; + + +class FlutterMementoEditorApp extends StatefulWidget { + @override + State createState() => + _FlutterMementoEditorAppState(); +} + +class _FlutterMementoEditorAppState extends State { + final app = MementoEditorApplication(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + Expanded( + child: adapter.ClassicAppAdapterWidget( + classicApp: app.editor, + ), + ), + RightPanelWidget(app: app), + ], + ), + ); + } +} diff --git a/patterns/memento/memento_editor/memento_pattern/caretaker.dart b/patterns/memento/memento_editor/memento_pattern/caretaker.dart new file mode 100644 index 0000000..e736f00 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/caretaker.dart @@ -0,0 +1,18 @@ +import 'memento.dart'; +import 'snapshot.dart'; + +class Caretaker { + final _mementoList = []; + + List get list => List.unmodifiable(_mementoList); + + void addMemento(Memento memento) { + _mementoList.add(memento); + } + + bool isSnapshotExists(Snapshot snapshot) { + return list.any( + (e) => e.snapshot == snapshot, + ); + } +} diff --git a/patterns/memento/memento_editor/memento_pattern/memento.dart b/patterns/memento/memento_editor/memento_pattern/memento.dart new file mode 100644 index 0000000..2bfb0b7 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/memento.dart @@ -0,0 +1,9 @@ + +import 'snapshot.dart'; + +class Memento { + final DateTime time; + final Snapshot snapshot; + + Memento(this.time, this.snapshot); +} diff --git a/patterns/memento/memento_editor/memento_pattern/originator.dart b/patterns/memento/memento_editor/memento_pattern/originator.dart new file mode 100644 index 0000000..36ed585 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/originator.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:ui'; + +import '../shapes/shapes.dart'; +import '../shapes/shape.dart'; +import 'snapshot.dart'; + +const _shapeByteSize = 16; +const _selectedIndexByteSize = 4; + +mixin BackupOriginator implements Shapes { + Snapshot backup() { + final buffer = _allocateBuffer(); + _writeShapes(buffer); + _writeSelectedIndex(buffer); + return _toSnapshot(buffer); + } + + ByteData _allocateBuffer() { + final byteSize = shapes.length * _shapeByteSize + _selectedIndexByteSize; + return ByteData(byteSize); + } + + void _writeSelectedIndex(ByteData data) { + late final int selectedIndex; + + if (activeShape == null) { + selectedIndex = -1; + } else { + selectedIndex = shapes.indexOf(activeShape!.shape); + } + + final byteOffset = data.lengthInBytes - _selectedIndexByteSize; + data.setInt32(byteOffset, selectedIndex); + } + + int _writeShapes(ByteData data) { + var byteOffset = 0; + + for (final shape in shapes) { + data + ..setFloat32(byteOffset, shape.x) + ..setFloat32(byteOffset + 4, shape.y) + ..setInt32(byteOffset + 8, shape.color.value) + ..setFloat32(byteOffset + 12, shape.size); + byteOffset += _shapeByteSize; + } + + return byteOffset; + } + + Snapshot _toSnapshot(ByteData data) { + return Base64Encoder().convert( + data.buffer.asUint8List(), + ); + } +} + +mixin RecoveryOriginator implements Shapes { + void restore(Snapshot snapshot) { + final byteData = _fromSnapshotToByteData(snapshot); + final newShapes = _readShapes(byteData); + final selectedIndex = _readSelectedIndex(byteData); + shapes.clear(); + shapes.addAll(newShapes); + selectByIndex(selectedIndex); + } + + ByteData _fromSnapshotToByteData(Snapshot snapshot) { + final unBase = Base64Decoder().convert(snapshot); + final byteData = ByteData.sublistView(unBase); + return byteData; + } + + int _getNumberOfShapes(ByteData byteData) { + return (byteData.lengthInBytes - _selectedIndexByteSize) ~/ _shapeByteSize; + } + + List _readShapes(ByteData byteData) { + final shapeCount = _getNumberOfShapes(byteData); + var byteOffset = 0; + final shapes = []; + + for (var i = 0; i < shapeCount; i++) { + final shape = Shape( + byteData.getFloat32(byteOffset), + byteData.getFloat32(byteOffset + 4), + Color(byteData.getInt32(byteOffset + 8)), + byteData.getFloat32(byteOffset + 12), + ); + shapes.add(shape); + byteOffset += _shapeByteSize; + } + + return shapes; + } + + int _readSelectedIndex(ByteData byteData) { + return byteData.getInt32(byteData.lengthInBytes - _selectedIndexByteSize); + } +} diff --git a/patterns/memento/memento_editor/memento_pattern/snapshot.dart b/patterns/memento/memento_editor/memento_pattern/snapshot.dart new file mode 100644 index 0000000..e71d8a3 --- /dev/null +++ b/patterns/memento/memento_editor/memento_pattern/snapshot.dart @@ -0,0 +1 @@ +typedef Snapshot = String; diff --git a/patterns/memento/memento_editor/shapes/active_shape.dart b/patterns/memento/memento_editor/shapes/active_shape.dart new file mode 100644 index 0000000..2892e90 --- /dev/null +++ b/patterns/memento/memento_editor/shapes/active_shape.dart @@ -0,0 +1,61 @@ +part of 'shape.dart'; + +class ActiveShape { + final Shape shape; + final void Function() repaint; + + ActiveShape(this.shape, this._xStart, this._yStart, this.repaint) { + repaint(); + } + + void changeSize(double delta) { + final currentSize = shape.size; + var newSize = currentSize - delta; + + if (newSize == shape.size) { + return; + } + + if (newSize < 10) { + newSize = 10; + } else if (newSize > 200) { + newSize = 200; + } + + shape._size = newSize; + repaint(); + } + + final double _xStart; + final double _yStart; + + void dragTo(double x, double y) { + shape._x = x + _xStart; + shape._y = y + _yStart; + repaint(); + } + + void changeColor(Color newColor) { + if (shape.color == newColor) { + return; + } + + shape._color = newColor; + repaint(); + } + + void paintSelectionBox(Canvas canvas) { + final x = (shape.x - shape.size).roundToDouble() - 1.5; + final y = (shape.y - shape.size).roundToDouble() - 1.5; + canvas.drawRect( + Rect.fromLTWH(x, y, shape.size * 2 + 3, shape.size * 2 + 3), + Paint() + ..style = PaintingStyle.stroke + ..color = Color(0xff26e6ff), + ); + } + + void dispose() { + repaint(); + } +} diff --git a/patterns/memento/memento_editor/shapes/shape.dart b/patterns/memento/memento_editor/shapes/shape.dart new file mode 100644 index 0000000..fa27539 --- /dev/null +++ b/patterns/memento/memento_editor/shapes/shape.dart @@ -0,0 +1,50 @@ +// ignore_for_file: prefer_final_fields + +import 'dart:ui'; + +part 'active_shape.dart'; + +class Shape { + double _x; + + double get x => _x; + + double _y; + + double get y => _y; + + Color _color; + + Color get color => _color; + + double _size; + + double get size => _size; + + Shape( + this._x, + this._y, [ + this._color = const Color(0xFFFFFFFF), + this._size = 60.0, + ]); + + static final _paintStroke = Paint() + ..style = PaintingStyle.stroke + ..color = Color(0xFFD81B60) + ..strokeWidth = 2; + + void paint(Canvas canvas) { + final paintFill = Paint() + ..style = PaintingStyle.fill + ..color = color; + + final offset = Offset(x, y); + canvas.drawCircle(offset, _size, paintFill); + canvas.drawCircle(offset, _size, _paintStroke); + } + + bool isBounded(double x, double y) { + return ((x - this.x) * (x - this.x) + (y - this.y) * (y - this.y) <= + _size * _size); + } +} diff --git a/patterns/memento/memento_editor/shapes/shapes.dart b/patterns/memento/memento_editor/shapes/shapes.dart new file mode 100644 index 0000000..deaa151 --- /dev/null +++ b/patterns/memento/memento_editor/shapes/shapes.dart @@ -0,0 +1,56 @@ +import 'dart:ui'; +import '../../../adapter/flutter_adapter/classic_app/classic_app.dart'; +import 'shape.dart'; + +mixin Shapes implements ClassicApp { + final shapes = []; + + ActiveShape? _activeShape; + + ActiveShape? get activeShape => _activeShape; + + void select(double x, double y) { + final shape = findShape(x, y); + + if (shape != null) { + _activeShape = ActiveShape(shape, shape.x - x, shape.y - y, repaint); + } else { + unSelect(); + } + } + + void selectByIndex(int index) { + if (index == -1) { + return; + } + + if (index <= shapes.length - 1) { + _activeShape = ActiveShape(shapes[index], 0, 0, repaint); + } + } + + void unSelect() { + if (_activeShape == null) { + return; + } + + _activeShape?.dispose(); + _activeShape = null; + } + + Shape? findShape(double x, double y) { + for (final shape in shapes.reversed) { + if (shape.isBounded(x, y)) { + return shape; + } + } + + return null; + } + + void paintShapes(Canvas canvas) { + for (final shape in shapes) { + shape.paint(canvas); + } + } +} diff --git a/patterns/memento/memento_editor/widgets/composite/colors_widget.dart b/patterns/memento/memento_editor/widgets/composite/colors_widget.dart new file mode 100644 index 0000000..829fff6 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/composite/colors_widget.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +class ColorsWidget extends StatelessWidget { + final Color? currentColor; + final List colors; + final void Function(Color color) onColorSelect; + + const ColorsWidget({ + Key? key, + required this.currentColor, + required this.colors, + required this.onColorSelect, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: currentColor == null ? 0.2 : 1.0, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black87), + ), + child: Row( + children: colors.map(_buildColorButton).toList(), + ), + ), + ); + } + + Widget _buildColorButton(Color color) { + final isColorSelect = (color == currentColor); + return GestureDetector( + onTap: () { + onColorSelect(color); + }, + child: Container( + width: 20, + height: 20, + color: color, + child: isColorSelect ? _buildSelectColorIcon() : null, + ), + ); + } + + Widget _buildSelectColorIcon() { + return Center( + child: Container( + width: 5, + height: 5, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.8), + borderRadius: BorderRadius.all(Radius.circular(2)), + border: Border.all( + color: Colors.black.withOpacity(0.2), + ), + ), + ), + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/composite/named_panel.dart b/patterns/memento/memento_editor/widgets/composite/named_panel.dart new file mode 100644 index 0000000..a6637d4 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/composite/named_panel.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class NamedPanel extends StatelessWidget { + final String name; + final List children; + + NamedPanel({ + Key? key, + required this.name, + required this.children, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 15), + ...children, + ], + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/composite/snapshot_list_widget.dart b/patterns/memento/memento_editor/widgets/composite/snapshot_list_widget.dart new file mode 100644 index 0000000..33968f2 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/composite/snapshot_list_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import '../../memento_pattern/memento.dart'; + +class SnapshotListWidget extends StatelessWidget { + final List mementoList; + final void Function(Memento) onMementoRestore; + + const SnapshotListWidget({ + Key? key, + required this.mementoList, + required this.onMementoRestore, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ColoredBox( + color: Colors.white, + child: Material( + type: MaterialType.transparency, + child: ListView( + padding: EdgeInsets.all(5), + children: mementoList.map((e) => _buildItem('Snapshot', e)).toList(), + ), + ), + ); + } + + Widget _buildItem(String name, Memento memento) { + return Container( + margin: EdgeInsets.only(bottom: 4), + color: Colors.black.withOpacity(0.02), + child: ListTile( + leading: Container( + color: Colors.grey.shade200, + width: 50, + height: double.infinity, + child: Icon(Icons.backup), + ), + title: Text(name ), + subtitle: SingleChildScrollView( + child: Text(memento.time.toIso8601String()), + scrollDirection: Axis.horizontal, + ), + onTap: () { + onMementoRestore(memento); + }, + ), + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/panels/memento_widget.dart b/patterns/memento/memento_editor/widgets/panels/memento_widget.dart new file mode 100644 index 0000000..c61dc67 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/panels/memento_widget.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +import '../../../../observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart'; +import '../../application.dart'; +import '../../editor/memento_create_event.dart'; +import '../composite/named_panel.dart'; +import '../composite/snapshot_list_widget.dart'; + +class MementoWidget extends StatelessWidget { + final MementoEditorApplication app; + + const MementoWidget({Key? key, required this.app}) : super(key: key); + + @override + Widget build(BuildContext context) { + return NamedPanel( + name: 'MEMENTO', + children: [ + ..._buildDescription(Theme.of(context).textTheme.bodyMedium!), + SizedBox(height: 20), + _buildSaveStateButton(), + SizedBox(height: 5), + Expanded( + child: SubscriberWidget( + observer: app.editor.events, + builder: (buildContext, event) { + return SnapshotListWidget( + mementoList: app.caretaker.list, + onMementoRestore: app.restoreState, + ); + }, + ), + ), + ], + ); + } + + List _buildDescription(TextStyle style) { + return [ + Text( + '1. Select the shape.', + style: style, + ), + SizedBox(height: 5), + Text( + '2. Change color, size or position.', + style: style, + ), + SizedBox(height: 5), + Text( + '3. Click the "save state" button.', + style: style, + ), + SizedBox(height: 5), + Text( + 'Now you can restore states by selecting them from the list.', + style: style, + ), + ]; + } + + Widget _buildSaveStateButton() { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + OutlinedButton( + child: Text('Save state'), + onPressed: app.saveState, + ), + ], + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/panels/shape_properties_widget.dart b/patterns/memento/memento_editor/widgets/panels/shape_properties_widget.dart new file mode 100644 index 0000000..8671de9 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/panels/shape_properties_widget.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +import '../../../../adapter/flutter_adapter/classic_app/repaint_event.dart'; +import '../../../../observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart'; +import '../../application.dart'; +import '../composite/colors_widget.dart'; +import '../composite/named_panel.dart'; + +class ShapePropertiesWidget extends StatelessWidget { + final MementoEditorApplication app; + final List colors; + + const ShapePropertiesWidget({ + Key? key, + required this.app, + required this.colors, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SubscriberWidget( + observer: app.editor.events, + builder: (buildContext, event) { + return NamedPanel( + name: 'SHAPE PROPERTIES', + children: [ + Row( + children: [ + _buildNumberField('x:', app.editor.activeShape?.shape.x), + SizedBox(width: 20), + _buildNumberField('y:', app.editor.activeShape?.shape.y), + ], + ), + SizedBox(height: 20), + _buildNumberField( + 'size:', + app.editor.activeShape?.shape.size, + ), + SizedBox(height: 20), + Row( + children: [ + Text( + 'color:', + style: TextStyle( + color: Colors.black.withOpacity( + app.editor.activeShape == null ? 0.5 : 1.0, + ), + ), + ), + SizedBox(width: 10), + ColorsWidget( + currentColor: app.editor.activeShape?.shape.color, + colors: colors, + onColorSelect: (newColor) { + app.editor.activeShape?.changeColor(newColor); + }, + ), + ], + ), + ], + ); + }, + ); + } + + Widget _buildNumberField(String name, double? value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + name, + style: TextStyle( + color: Colors.black.withOpacity(value == null ? 0.5 : 1.0), + ), + ), + SizedBox(width: 10), + SizedBox( + width: 60, + child: TextField( + enabled: value != null, + controller: TextEditingController( + text: value == null ? '' : value.toStringAsFixed(0), + ), + decoration: InputDecoration( + filled: value != null, + fillColor: Colors.white, + ), + ), + ), + ], + ); + } +} diff --git a/patterns/memento/memento_editor/widgets/right_panel_widget.dart b/patterns/memento/memento_editor/widgets/right_panel_widget.dart new file mode 100644 index 0000000..6c29bb9 --- /dev/null +++ b/patterns/memento/memento_editor/widgets/right_panel_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../application.dart'; +import 'panels/shape_properties_widget.dart'; +import 'panels/memento_widget.dart'; + +class RightPanelWidget extends StatelessWidget { + final MementoEditorApplication app; + + RightPanelWidget({Key? key, required this.app}) : super(key: key); + + final colors = [ + Color(0xFF000000), + Color(0xFFD81B60), + Color(0xFF5E35B1), + Color(0xFF1E88E5), + Color(0xFF43A047), + Color(0xFFFFFFFF), + ]; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(20), + width: 300, + child: Column( + children: [ + ShapePropertiesWidget(app: app, colors: colors), + Container( + margin: EdgeInsets.symmetric(vertical: 20), + height: 2, + color: Colors.black.withOpacity(.2), + ), + Expanded( + child: MementoWidget(app: app), + ), + ], + ), + ); + } +} diff --git a/patterns/observer/app_observer/README.md b/patterns/observer/app_observer/README.md new file mode 100644 index 0000000..b04b276 --- /dev/null +++ b/patterns/observer/app_observer/README.md @@ -0,0 +1,58 @@ +# Observer pattern +Observer is a behavioral design pattern that lets you define a subscription mechanism to notify +multiple objects about any events that happen to the object they’re observing. + +Tutorial: [here](https://refactoring.guru/design-patterns/observer). + +## AppObserver example +This example was created to be used in a more complex examples: +- [[SubscriberWidget](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/subscriber_flutter_widget)] +- [[FlutterAdapter](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/adapter/flutter_adapter)] +- [[MementoEditor](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/memento/memento_editor)] + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/152783537-0c39c119-2e5b-44fb-9be1-a88840c7f7a1.png) + +### Sequence +![image](https://user-images.githubusercontent.com/8049534/152049996-72131655-402d-4b92-b5d0-10e3f2dd0e79.png) + +### Client code: +```dart +void main() { + final observer = AppObserver(); + + observer.subscribe((e) { + print('First'); + }); + + + observer.subscribe((SecondEvent e) { + print('Second'); + }); + + final saveThirdEvent = observer.subscribe((ThirdEvent e) { + print('Third'); + }); + + observer.notify(FirstEvent()); + observer.notify(SecondEvent()); + observer.notify(ThirdEvent()); + + print('---unsubscribe "ThirdEvent"---'); + observer.unsubscribe(saveThirdEvent); + + observer.notify(FirstEvent()); + observer.notify(SecondEvent()); + observer.notify(ThirdEvent()); +} +``` + +**Output:** +``` +First +Second +Third +---unsubscribe "ThirdEvent"--- +First +Second +``` diff --git a/patterns/observer/app_observer/main.dart b/patterns/observer/app_observer/main.dart new file mode 100644 index 0000000..459bb9e --- /dev/null +++ b/patterns/observer/app_observer/main.dart @@ -0,0 +1,35 @@ +import 'observer/app_observer.dart'; +import 'observer/event.dart'; + +class FirstEvent extends Event {} + +class SecondEvent extends Event {} + +class ThirdEvent extends Event {} + +void main() { + final observer = AppObserver(); + + observer.subscribe((e) { + print('First'); + }); + + observer.subscribe((SecondEvent e) { + print('Second'); + }); + + final saveThirdEvent = observer.subscribe((ThirdEvent e) { + print('Third'); + }); + + observer.notify(FirstEvent()); + observer.notify(SecondEvent()); + observer.notify(ThirdEvent()); + + print('---unsubscribe "ThirdEvent"---'); + observer.unsubscribe(saveThirdEvent); + + observer.notify(FirstEvent()); + observer.notify(SecondEvent()); + observer.notify(ThirdEvent()); +} diff --git a/patterns/observer/app_observer/observer/app_observer.dart b/patterns/observer/app_observer/observer/app_observer.dart new file mode 100644 index 0000000..d78848b --- /dev/null +++ b/patterns/observer/app_observer/observer/app_observer.dart @@ -0,0 +1,46 @@ +import 'event.dart'; + +typedef EventFunction = void Function(T e); + +class AppObserver { + final _subscribers = >{}; + + EventFunction subscribe(EventFunction func) { + assert( + Class() is Class, + '\n\nThe callback argument must implement the "Event" class.\n' + 'Correct use: \n' + '\tobserver.subscribe((MyEvent e) {}); \n' + 'Mistaken usage: \n' + '\tobserver.subscribe((String e) {});\n' + '\tobserver.subscribe((e) {});\n', + ); + + _subscribers.putIfAbsent(T.hashCode, () => []).add(func); + return func; + } + + void unsubscribe(EventFunction func) { + final isDeleted = _subscribers[T.hashCode]?.remove(func) ?? false; + + if (isDeleted) { + return; + } + + throw Exception('Subscriber not found.'); + } + + void notify(T event) { + final subscribers = _subscribers[T.hashCode]; + + if (subscribers == null) { + return; + } + + for (var sub in subscribers) { + sub.call(event); + } + } +} + +class Class {} diff --git a/patterns/observer/app_observer/observer/event.dart b/patterns/observer/app_observer/observer/event.dart new file mode 100644 index 0000000..dbd3bf8 --- /dev/null +++ b/patterns/observer/app_observer/observer/event.dart @@ -0,0 +1,3 @@ +abstract class Event { + +} diff --git a/patterns/observer/open_close_editor_events/README.md b/patterns/observer/open_close_editor_events/README.md new file mode 100644 index 0000000..613f1dc --- /dev/null +++ b/patterns/observer/open_close_editor_events/README.md @@ -0,0 +1,44 @@ +# Observer pattern +Observer is a behavioral design pattern that lets you define a subscription mechanism to notify +multiple objects about any events that happen to the object they’re observing. + +## Editor example +In this example, the Observer pattern lets the text editor object notify other service objects about +changes in its state. + +More detailed explanation on [RefactoringGuru](https://refactoring.guru/design-patterns/observer?#pseudocode). + +### Origin source code: +This example rewrite from [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/observer/example). + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/150955865-7fbb29f3-ed48-4317-a356-cbb9cc79ed11.png) + +### Client code: +```dart +void main() { + final editor = Editor(); + editor.events + ..subscribe( + 'open', + LogOpenListener('log.txt'), + ) + ..subscribe( + 'save', + EmailNotificationListener('admin@example.com'), + ); + + try { + editor.openFile('test.txt'); + editor.saveFile(); + } catch (e) { + print(e); + } +} +``` + +**Output:** +``` +Save to log "log.txt": Someone has performed "open" operation with the following file: "test.txt" +Email to "admin@example.com": Someone has performed "save" operation with the following file: "test.txt" +``` diff --git a/patterns/observer/open_close_editor_events/editor/editor.dart b/patterns/observer/open_close_editor_events/editor/editor.dart new file mode 100644 index 0000000..3411655 --- /dev/null +++ b/patterns/observer/open_close_editor_events/editor/editor.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +import '../event_manager/event_manager.dart'; + +class Editor { + final events = EventManager(['open', 'save']); + + File? _file; + + void openFile(String filePath) { + _file = File(filePath); + events.notify("open", _file!); + } + + void saveFile() { + if (_file == null) { + throw Exception('Please open a file first.'); + } + + events.notify('save', _file!); + } +} diff --git a/patterns/observer/open_close_editor_events/event_manager/event_manager.dart b/patterns/observer/open_close_editor_events/event_manager/event_manager.dart new file mode 100644 index 0000000..50d2371 --- /dev/null +++ b/patterns/observer/open_close_editor_events/event_manager/event_manager.dart @@ -0,0 +1,38 @@ +import 'dart:io'; + +import '../listeners/event_listener.dart'; + +class EventManager { + final _listeners = >{}; + + EventManager(List operations) { + for (final operation in operations) { + _listeners[operation] = []; + } + } + + void subscribe(String eventType, EventListener listener) { + _usersBy(eventType).add(listener); + } + + void unsubscribe(String eventType, EventListener listener) { + _usersBy(eventType).remove(listener); + } + + void notify(String eventType, File file) { + final users = _usersBy(eventType); + for (final listener in users) { + listener.update(eventType, file); + } + } + + List _usersBy(String eventType) { + final users = _listeners[eventType]; + + if (users == null) { + throw UnsupportedError('Event type "$eventType" do not support.'); + } + + return users; + } +} diff --git a/patterns/observer/open_close_editor_events/listeners/email_notification_listener.dart b/patterns/observer/open_close_editor_events/listeners/email_notification_listener.dart new file mode 100644 index 0000000..0936e4f --- /dev/null +++ b/patterns/observer/open_close_editor_events/listeners/email_notification_listener.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'event_listener.dart'; + +class EmailNotificationListener implements EventListener { + String email; + + EmailNotificationListener(this.email); + + @override + void update(String eventType, File file) { + print('Email to "$email": ' + 'Someone has performed "$eventType" ' + 'operation with the following file: "${file.path}"'); + } +} diff --git a/patterns/observer/open_close_editor_events/listeners/event_listener.dart b/patterns/observer/open_close_editor_events/listeners/event_listener.dart new file mode 100644 index 0000000..252d64a --- /dev/null +++ b/patterns/observer/open_close_editor_events/listeners/event_listener.dart @@ -0,0 +1,5 @@ +import 'dart:io'; + +abstract class EventListener { + void update(String eventType, File file); +} diff --git a/patterns/observer/open_close_editor_events/listeners/log_open_listener.dart b/patterns/observer/open_close_editor_events/listeners/log_open_listener.dart new file mode 100644 index 0000000..e44b1ae --- /dev/null +++ b/patterns/observer/open_close_editor_events/listeners/log_open_listener.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'event_listener.dart'; + +class LogOpenListener implements EventListener { + File logFile; + + LogOpenListener(String logFileName) : logFile = File(logFileName); + + @override + void update(String eventType, File file) { + print('Save to log "${logFile.path}": ' + 'Someone has performed "$eventType" ' + 'operation with the following file: "${file.path}"'); + } +} diff --git a/patterns/observer/open_close_editor_events/main.dart b/patterns/observer/open_close_editor_events/main.dart new file mode 100644 index 0000000..9951d3c --- /dev/null +++ b/patterns/observer/open_close_editor_events/main.dart @@ -0,0 +1,23 @@ +import 'editor/editor.dart'; +import 'listeners/email_notification_listener.dart'; +import 'listeners/log_open_listener.dart'; + +void main() { + final editor = Editor(); + editor.events + ..subscribe( + 'open', + LogOpenListener('log.txt'), + ) + ..subscribe( + 'save', + EmailNotificationListener('admin@example.com'), + ); + + try { + editor.openFile('test.txt'); + editor.saveFile(); + } catch (e) { + print(e); + } +} diff --git a/patterns/observer/subscriber_flutter_widget/README.md b/patterns/observer/subscriber_flutter_widget/README.md new file mode 100644 index 0000000..06fb727 --- /dev/null +++ b/patterns/observer/subscriber_flutter_widget/README.md @@ -0,0 +1,42 @@ +# Observer pattern +**Observer** is a behavioral design pattern that lets you define a subscription mechanism to notify +multiple objects about any events that happen to the object they’re observing. + +Tutorial: [here](https://refactoring.guru/design-patterns/observer). + +## Subscriber flutter widget example +This is a complex example of an Observer pattern, connected to a Flutter application. The example includes, +the previously implemented **AppObserver** example pattern, +which you can see [here](https://github.com/RefactoringGuru/design-patterns-dart/tree/main/patterns/observer/app_observer). + +### Online demo: +Click on the picture to see a [demo](https://RefactoringGuru.github.io/design-patterns-dart/#/observer/subscriber_flutter_widget). + +[![image](https://user-images.githubusercontent.com/8049534/152419178-f40a07fd-728d-4f99-befa-0935bbdd7b71.png)](https://refactoringguru.github.io/design-patterns-dart/#/observer/subscriber_flutter_widget) + +### Client code: +```dart +void main() { + // generates a new event + void onHashGenerate() { + final hash = '...'; + observer.notify(NewHashEvent(hash)); + } + + // widget event consumer NewHashEvent + SubscriberWidget( + observer: observer, + builder: (context, event) { + return Text( + event?.newHash ?? 'Hash no generated' , + ); + }, + ); +} +``` + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/152789070-cc53b0c0-bb5c-4191-ac96-f542ce75c1d7.png) + +### Sequence +![image](https://user-images.githubusercontent.com/8049534/152791540-d6ec1d24-a3a1-4340-8805-10df6de12067.png) diff --git a/patterns/observer/subscriber_flutter_widget/events/new_hash_event.dart b/patterns/observer/subscriber_flutter_widget/events/new_hash_event.dart new file mode 100644 index 0000000..0cbf2f5 --- /dev/null +++ b/patterns/observer/subscriber_flutter_widget/events/new_hash_event.dart @@ -0,0 +1,7 @@ +import '../../app_observer/observer/event.dart'; + +class NewHashEvent extends Event { + final String newHash; + + NewHashEvent(this.newHash); +} diff --git a/patterns/observer/subscriber_flutter_widget/main.dart b/patterns/observer/subscriber_flutter_widget/main.dart new file mode 100644 index 0000000..4423ccf --- /dev/null +++ b/patterns/observer/subscriber_flutter_widget/main.dart @@ -0,0 +1,36 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import '../app_observer/observer/app_observer.dart'; +import 'events/new_hash_event.dart'; +import 'widgets/hash_generator_widget.dart'; +import 'widgets/hash_user_widget.dart'; + +class SubscriberFlutterApp extends StatefulWidget { + @override + State createState() => _SubscriberFlutterAppState(); +} + +class _SubscriberFlutterAppState extends State { + final observer = AppObserver(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HashUserWidget(observer: observer), + HashGeneratorWidget(onHashGenerate: onHashGenerate), + ], + ), + ), + ); + } + + void onHashGenerate() { + final hash = Random().nextDouble().hashCode.toString(); + observer.notify(NewHashEvent(hash)); + } +} diff --git a/patterns/observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart b/patterns/observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart new file mode 100644 index 0000000..daef892 --- /dev/null +++ b/patterns/observer/subscriber_flutter_widget/subscriber/subscriber_widget.dart @@ -0,0 +1,46 @@ +import 'package:flutter/widgets.dart'; + +import '../../app_observer/observer/app_observer.dart'; +import '../../app_observer/observer/event.dart'; + +class SubscriberWidget extends StatefulWidget { + final AppObserver observer; + final Widget Function(BuildContext buildContext, T? event) builder; + + const SubscriberWidget({ + Key? key, + required this.builder, + required this.observer, + }) : super(key: key); + + @override + State> createState() { + return _SubscriberWidgetState(); + } +} + +class _SubscriberWidgetState + extends State> { + T? _event; + + @override + void initState() { + widget.observer.subscribe(update); + super.initState(); + } + + @override + void dispose() { + widget.observer.unsubscribe(update); + super.dispose(); + } + + void update(T event) { + setState(() => _event = event); + } + + @override + Widget build(BuildContext context) { + return widget.builder(context, _event); + } +} diff --git a/patterns/observer/subscriber_flutter_widget/widgets/hash_generator_widget.dart b/patterns/observer/subscriber_flutter_widget/widgets/hash_generator_widget.dart new file mode 100644 index 0000000..cc65ac7 --- /dev/null +++ b/patterns/observer/subscriber_flutter_widget/widgets/hash_generator_widget.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class HashGeneratorWidget extends StatelessWidget { + final void Function() onHashGenerate; + + const HashGeneratorWidget({ + Key? key, + required this.onHashGenerate, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text('Generate new hash'), + onPressed: onHashGenerate, + ); + } +} diff --git a/patterns/observer/subscriber_flutter_widget/widgets/hash_user_widget.dart b/patterns/observer/subscriber_flutter_widget/widgets/hash_user_widget.dart new file mode 100644 index 0000000..0b6458c --- /dev/null +++ b/patterns/observer/subscriber_flutter_widget/widgets/hash_user_widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/widgets.dart'; + +import '../../app_observer/observer/app_observer.dart'; +import '../events/new_hash_event.dart'; +import '../subscriber/subscriber_widget.dart'; + +class HashUserWidget extends StatelessWidget { + final AppObserver observer; + + const HashUserWidget({Key? key, required this.observer}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SubscriberWidget( + observer: observer, + builder: (context, event) { + return Text( + event?.newHash ?? 'Hash no generated', + ); + }, + ); + } +} diff --git a/patterns/prototype/shapes/README.md b/patterns/prototype/shapes/README.md new file mode 100644 index 0000000..f5f794c --- /dev/null +++ b/patterns/prototype/shapes/README.md @@ -0,0 +1,51 @@ +# Prototype pattern +Prototype is a creational design pattern that lets you copy existing objects without making your +code dependent on their classes. + +## Example +Copies of geometric objects. + +**Description:** +https://refactoring.guru/design-patterns/prototype?#pseudocode + +### Class Diagram: +![image](https://user-images.githubusercontent.com/8049534/150781031-52c8c32a-e94b-4f1b-86b9-eb0012427682.png) + + +### Client code: +```dart +void main() { + final originalShapes = [ + Rectangle( + x: 100, + y: 100, + width: 500, + height: 100, + color: '0xfff', + ), + Circle( + x: 20, + y: 30, + radius: 100, + color: '0xf0f', + ), + ]; + + businessLogic(originalShapes); +} + +void businessLogic(List originalShapes) { + final cloningShapes = [ + for (final shape in originalShapes) shape.clone(), + ]; + + print('Origin shapes: $originalShapes'); + print('Cloning shapes: $cloningShapes'); +} +``` + +**Output:** +``` +Origin shapes: [Rectangle(address: 0x2f7f544c), Circle(address: 0x3e59b487)] +Cloning shapes: [Rectangle(address: 0x3eb0a110), Circle(address: 0x75e3636)] +``` diff --git a/patterns/prototype/shapes/main.dart b/patterns/prototype/shapes/main.dart new file mode 100644 index 0000000..078411f --- /dev/null +++ b/patterns/prototype/shapes/main.dart @@ -0,0 +1,32 @@ +import 'shape/circle.dart'; +import 'shape/rectangle.dart'; +import 'shape/shape.dart'; + +void main() { + final originalShapes = [ + Rectangle( + x: 100, + y: 100, + width: 500, + height: 100, + color: '0xfff', + ), + Circle( + x: 20, + y: 30, + radius: 100, + color: '0xf0f', + ), + ]; + + businessLogic(originalShapes); +} + +void businessLogic(List originalShapes) { + final cloningShapes = [ + for (final shape in originalShapes) shape.clone(), + ]; + + print('Origin shapes: $originalShapes'); + print('Cloning shapes: $cloningShapes'); +} diff --git a/patterns/prototype/shapes/shape/circle.dart b/patterns/prototype/shapes/shape/circle.dart new file mode 100644 index 0000000..8bae257 --- /dev/null +++ b/patterns/prototype/shapes/shape/circle.dart @@ -0,0 +1,26 @@ +import 'shape.dart'; + +class Circle extends Shape { + final int radius; + final String color; + + Circle({ + required int x, + required int y, + required this.radius, + required this.color, + }) : super( + x: x, + y: y, + ); + + @override + Circle clone() { + return Circle( + x: x, + y: y, + radius: radius, + color: color, + ); + } +} diff --git a/patterns/prototype/shapes/shape/cloneable.dart b/patterns/prototype/shapes/shape/cloneable.dart new file mode 100644 index 0000000..393e7cf --- /dev/null +++ b/patterns/prototype/shapes/shape/cloneable.dart @@ -0,0 +1,3 @@ +abstract class Cloneable { + T clone(); +} diff --git a/patterns/prototype/shapes/shape/rectangle.dart b/patterns/prototype/shapes/shape/rectangle.dart new file mode 100644 index 0000000..2631cbd --- /dev/null +++ b/patterns/prototype/shapes/shape/rectangle.dart @@ -0,0 +1,26 @@ +import 'shape.dart'; + +class Rectangle extends Shape { + final int width; + final int height; + final String color; + + Rectangle({ + required int x, + required int y, + required this.width, + required this.height, + required this.color, + }) : super(x: x, y: y); + + @override + Rectangle clone() { + return Rectangle( + x: x, + y: y, + width: width, + height: height, + color: color, + ); + } +} diff --git a/patterns/prototype/shapes/shape/shape.dart b/patterns/prototype/shapes/shape/shape.dart new file mode 100644 index 0000000..020c885 --- /dev/null +++ b/patterns/prototype/shapes/shape/shape.dart @@ -0,0 +1,15 @@ +import 'cloneable.dart'; + +abstract class Shape implements Cloneable { + final int x; + final int y; + + Shape({ + required this.x, + required this.y, + }); + + + @override + String toString() => '$runtimeType(address: 0x${hashCode.toRadixString(16)})'; +} diff --git a/patterns/proxy/conceptual/README.md b/patterns/proxy/conceptual/README.md new file mode 100644 index 0000000..3560140 --- /dev/null +++ b/patterns/proxy/conceptual/README.md @@ -0,0 +1,29 @@ +# Proxy Pattern +Proxy is a structural design pattern that lets you provide a substitute or placeholder for another +object. A proxy controls access to the original object, allowing you to perform something either +before or after the request gets through to the original object. + +Tutorial: [here](https://refactoring.guru/design-patterns/proxy). + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/175926828-d4fed7c6-ea82-4717-a24b-8ad2b23910ba.png) + +### Client code: +```dart +void main() async { + final subject = Proxy(); + print(subject.request()); // print "Proxy data" + + print('Wait for 2 seconds...'); + await Future.delayed(Duration(seconds: 2)); + + print(subject.request()); // print "Real data" +} +``` + +### Output: +``` +Proxy data. +Wait 2 seconds... +Real data. +``` diff --git a/patterns/proxy/conceptual/main.dart b/patterns/proxy/conceptual/main.dart new file mode 100644 index 0000000..66f2528 --- /dev/null +++ b/patterns/proxy/conceptual/main.dart @@ -0,0 +1,16 @@ +import 'pattern/proxy.dart'; +import 'pattern/subject.dart'; + +void main() async { + final subject = Proxy(); + client(subject); // print "Proxy data" + + print('Wait 2 seconds...'); + await Future.delayed(Duration(seconds: 2)); + + client(subject); // print "Real data" +} + +void client(Subject subject) { + print(subject.request()); +} diff --git a/patterns/proxy/conceptual/pattern/proxy.dart b/patterns/proxy/conceptual/pattern/proxy.dart new file mode 100644 index 0000000..e01e59d --- /dev/null +++ b/patterns/proxy/conceptual/pattern/proxy.dart @@ -0,0 +1,24 @@ +import 'subject.dart'; +import 'real_subject.dart'; + +class Proxy implements Subject { + @override + String request() { + if (isSubjectLoaded) { + return _subject!.request(); + } + + _load(); + return 'Proxy data.'; + } + + bool get isSubjectLoaded => _subject != null; + + void _load() async { + Future.delayed(Duration(seconds: 1), () { + _subject = RealSubject(); + }); + } + + Subject? _subject; +} diff --git a/patterns/proxy/conceptual/pattern/real_subject.dart b/patterns/proxy/conceptual/pattern/real_subject.dart new file mode 100644 index 0000000..a5dac68 --- /dev/null +++ b/patterns/proxy/conceptual/pattern/real_subject.dart @@ -0,0 +1,8 @@ +import 'subject.dart'; + +class RealSubject implements Subject { + @override + String request() { + return 'Real data.'; + } +} diff --git a/patterns/proxy/conceptual/pattern/subject.dart b/patterns/proxy/conceptual/pattern/subject.dart new file mode 100644 index 0000000..23a343d --- /dev/null +++ b/patterns/proxy/conceptual/pattern/subject.dart @@ -0,0 +1,3 @@ +abstract class Subject { + String request(); +} diff --git a/patterns/singleton/conceptual/README.md b/patterns/singleton/conceptual/README.md new file mode 100644 index 0000000..5876387 --- /dev/null +++ b/patterns/singleton/conceptual/README.md @@ -0,0 +1,28 @@ +# Singleton Pattern +Singleton is a creational design pattern that lets you ensure that a class has only one instance, +while providing a global access point to this instance. + +Tutorial: [here](https://refactoring.guru/design-patterns/singleton). + +### Diagram: +![Singleton Pattern Diagram](https://user-images.githubusercontent.com/8049534/182938119-78a21534-5751-4dea-afa3-8acaec46eed9.png) + +### Client code: +```dart +void main() { + // dart style + Singleton().doSome(); + Singleton().doSome(); + + // standard style + Singleton.instance.doSome(); +} +``` + +### Output: +``` +Create singleton once. +doSome() +doSome() +doSome() +``` diff --git a/patterns/singleton/conceptual/main.dart b/patterns/singleton/conceptual/main.dart new file mode 100644 index 0000000..e54f386 --- /dev/null +++ b/patterns/singleton/conceptual/main.dart @@ -0,0 +1,10 @@ +import 'pattern/singleton.dart'; + +void main() { + // dart style + Singleton().doSome(); + Singleton().doSome(); + + // standard style + Singleton.instance.doSome(); +} diff --git a/patterns/singleton/conceptual/pattern/singleton.dart b/patterns/singleton/conceptual/pattern/singleton.dart new file mode 100644 index 0000000..7096950 --- /dev/null +++ b/patterns/singleton/conceptual/pattern/singleton.dart @@ -0,0 +1,23 @@ +abstract class Singleton { + factory Singleton() { + if (_instance == null) { + print('Create singleton once.'); + _instance = ConcreteSingleton(); + } + + return _instance!; + } + + static Singleton get instance => Singleton(); + + void doSome(); + + static Singleton? _instance; +} + +class ConcreteSingleton implements Singleton { + @override + void doSome() { + print('doSome()'); + } +} diff --git a/patterns/state/manipulator_state/README.md b/patterns/state/manipulator_state/README.md new file mode 100644 index 0000000..93d1e38 --- /dev/null +++ b/patterns/state/manipulator_state/README.md @@ -0,0 +1,76 @@ +# State Pattern +State is a behavioral design pattern that lets an object alter its behavior when its internal state +changes. It appears as if the object changed its class. + +Tutorial: [here](https://refactoring.guru/design-patterns/state). + +### Online demo: +Click on the picture to see the [demo](https://RefactoringGuru.github.io/design-patterns-dart/#/state/manipulator_state). + +[![image](https://user-images.githubusercontent.com/8049534/171070341-1decb58f-033b-4eb5-89d4-355aafa6b680.png)](https://refactoringguru.github.io/design-patterns-dart/#/state/manipulator_state) + +### Video +https://user-images.githubusercontent.com/8049534/171499203-1400c3ae-d5cd-4e48-a0b6-0252f4345d19.mp4 + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/171740942-659d3ec9-8355-4078-a7d6-b4a338b41187.png) + +## Client code: +### Change FreeState to MoveState: +```dart +class FreeState extends ManipulationState { + @override + void mouseDown(double x, double y) { + tryToSelectAndStartMovingShape(x, y); + } + + bool tryToSelectAndStartMovingShape(double x, double y) { + final selectedShape = context.shapes.findShapeByCoordinates(x, y); + + context.changeState( + MoveState( + startX: x, + startY: y, + selectedShape: selectedShape, + ), + ); + + return true; + } +} +``` + +### Change MoveState to ResizableState: +```dart +class MoveState extends SelectionState { + @override + void mouseMove(double x, double y) { + selectedShape.move(x, y); + context.update(); + } + + @override + void mouseUp() { + context.changeState( + selectedShape.createSelectionState(), + ); + } +} +``` + +### Each shape has its own state manipulator: +```dart +class RectangleShape extends BaseShape { + @override + SelectionState createSelectionState() { + return ResizableState(selectedShape: this); + } +} + +class CircleShape extends BaseShape { + @override + SelectionState createSelectionState() { + return InnerRadiusState(selectedShape: this); + } +} +``` diff --git a/patterns/state/manipulator_state/app/app.dart b/patterns/state/manipulator_state/app/app.dart new file mode 100644 index 0000000..2e754d1 --- /dev/null +++ b/patterns/state/manipulator_state/app/app.dart @@ -0,0 +1,15 @@ +import '../pattern/manipulator.dart'; +import 'shapes.dart'; +import 'tool.dart'; + +class App { + final Shapes shapes; + final Manipulator manipulator; + final List tools; + + App({ + required this.shapes, + required this.manipulator, + required this.tools, + }); +} diff --git a/patterns/state/manipulator_state/app/base_manipulation.dart b/patterns/state/manipulator_state/app/base_manipulation.dart new file mode 100644 index 0000000..85db133 --- /dev/null +++ b/patterns/state/manipulator_state/app/base_manipulation.dart @@ -0,0 +1,87 @@ +part of manipulator; + +class BaseManipulator implements Manipulator { + BaseManipulator({ + required this.shapes, + required ManipulationState initState, + required this.paintStyle, + }) : _state = initState { + _state._context = this; + } + + @override + ManipulationState get state => _state; + + @override + final Shapes shapes; + + @override + final onStateChange = Event(); + + @override + final onUpdate = Event(); + + @override + var cursor = MouseCursor.defer; + + @override + final PaintStyle paintStyle; + + @override + void changeState(ManipulationState newState) { + if (_state == newState) { + return; + } + + _state = newState; + _state._context = this; + _state.init(); + onStateChange._emit(); + } + + @override + void update() { + onUpdate._emit(); + } + + @override + void mouseMove(double x, double y) { + _state.mouseMove(x, y); + } + + @override + void mouseDown(double x, double y) { + _state.mouseDown(x, y); + } + + @override + void mouseUp() { + _state.mouseUp(); + } + + @override + void mouseDoubleClick(double x, double y) { + _state.mouseDoubleClick(x, y); + } + + @override + void keyDown(KeyEvent keyEvent) { + _state.keyDown(keyEvent); + } + + @override + void paint(Canvas canvas) { + _state.paint(canvas); + } + + @override + String toString() { + return _state.toString(); + } + + ManipulationState _state; +} + +class Event extends ChangeNotifier { + void _emit() => notifyListeners(); +} diff --git a/patterns/state/manipulator_state/app/shapes.dart b/patterns/state/manipulator_state/app/shapes.dart new file mode 100644 index 0000000..381b96c --- /dev/null +++ b/patterns/state/manipulator_state/app/shapes.dart @@ -0,0 +1,36 @@ +import 'dart:collection'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; + +import '../shapes/shape.dart'; + +class Shapes with IterableMixin { + final List _shapes; + + Shapes(List shapes) : _shapes = shapes; + + void add(Shape shape) { + _shapes.add(shape); + onChange._emit(); + } + + @override + Iterator get iterator => _shapes.iterator; + + final onChange = Event(); + + Shape? findShapeByCoordinates(x, y) { + for (final shape in _shapes.reversed) { + if (shape.rect.contains(Offset(x, y))) { + return shape; + } + } + + return null; + } +} + +class Event extends ChangeNotifier { + void _emit() => notifyListeners(); +} diff --git a/patterns/state/manipulator_state/app/tool.dart b/patterns/state/manipulator_state/app/tool.dart new file mode 100644 index 0000000..a3e7ca6 --- /dev/null +++ b/patterns/state/manipulator_state/app/tool.dart @@ -0,0 +1,13 @@ +import 'package:flutter/widgets.dart'; + +import '../pattern/manipulator.dart'; + +class Tool { + final Icon icon; + final ManipulationState state; + + Tool({ + required this.icon, + required this.state, + }); +} diff --git a/patterns/state/manipulator_state/main.dart b/patterns/state/manipulator_state/main.dart new file mode 100644 index 0000000..f235a2f --- /dev/null +++ b/patterns/state/manipulator_state/main.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +import 'app/app.dart'; +import 'app/shapes.dart'; +import 'app/tool.dart'; +import 'pattern/manipulator.dart'; +import 'states/creations/circle_creation_state.dart'; +import 'states/creations/rectangle_creation_state.dart'; +import 'states/creations/text_creation_state.dart'; +import 'states/free_sate.dart'; +import 'states/_/paint_style.dart'; +import 'widgets/current_state.dart'; +import 'widgets/drawing_board.dart'; +import 'widgets/tool_bar.dart'; + +class ManipulatorStateApp extends StatefulWidget { + const ManipulatorStateApp({Key? key}) : super(key: key); + + @override + State createState() => _ManipulatorStateAppState(); +} + +class _ManipulatorStateAppState extends State { + late final App app; + + @override + void initState() { + final shapes = Shapes([]); + app = App( + shapes: shapes, + manipulator: BaseManipulator( + initState: FreeState(), + shapes: shapes, + paintStyle: PaintStyle(Colors.pink), + ), + tools: [ + Tool( + icon: Icon(MdiIcons.cursorDefaultOutline), + state: FreeState(), + ), + Tool( + icon: Icon(MdiIcons.rectangleOutline), + state: RectangleCreationState(), + ), + Tool( + icon: Icon(MdiIcons.circleOutline), + state: CircleCreationState(), + ), + Tool( + icon: Icon(MdiIcons.formatTextVariant), + state: TextCreationState(), + ), + ], + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SizedBox.expand( + child: Stack( + children: [ + DrawingBoard(app: app), + ToolBar(app: app), + CurrentState(manipulator: app.manipulator), + ], + ), + ), + ); + } +} diff --git a/patterns/state/manipulator_state/pattern/manipulation_state.dart b/patterns/state/manipulator_state/pattern/manipulation_state.dart new file mode 100644 index 0000000..49221a2 --- /dev/null +++ b/patterns/state/manipulator_state/pattern/manipulation_state.dart @@ -0,0 +1,21 @@ +part of manipulator; + +class ManipulationState { + Manipulator get context => _context; + + void init() {} + + void mouseMove(double x, double y) {} + + void mouseDown(double x, double y) {} + + void mouseUp() {} + + void mouseDoubleClick(double x, double y) {} + + void keyDown(KeyEvent keyEvent) {} + + void paint(Canvas canvas) {} + + late Manipulator _context; +} diff --git a/patterns/state/manipulator_state/pattern/manipulator.dart b/patterns/state/manipulator_state/pattern/manipulator.dart new file mode 100644 index 0000000..b7df969 --- /dev/null +++ b/patterns/state/manipulator_state/pattern/manipulator.dart @@ -0,0 +1,40 @@ +library manipulator; + +import 'package:flutter/material.dart'; +import '../states/_/paint_style.dart'; +import '../app/shapes.dart'; + +part 'manipulation_state.dart'; +part '../app/base_manipulation.dart'; + +abstract class Manipulator { + ManipulationState get state; + + Shapes get shapes; + + MouseCursor get cursor; + + set cursor(MouseCursor cursor); + + PaintStyle get paintStyle; + + Event get onStateChange; + + Event get onUpdate; + + void changeState(ManipulationState newState); + + void update(); + + void mouseMove(double x, double y); + + void mouseDown(double x, double y); + + void mouseUp(); + + void mouseDoubleClick(double x, double y); + + void keyDown(KeyEvent keyEvent); + + void paint(Canvas canvas); +} diff --git a/patterns/state/manipulator_state/shapes/base_shape.dart b/patterns/state/manipulator_state/shapes/base_shape.dart new file mode 100644 index 0000000..c14eae8 --- /dev/null +++ b/patterns/state/manipulator_state/shapes/base_shape.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import 'shape.dart'; + +abstract class BaseShape implements Shape { + BaseShape(double x, double y, double width, double height) + : _x = x, + _y = y, + _width = width, + _height = height; + + @override + double get x => _x; + + @override + double get y => _y; + + @override + double get height => _height; + + @override + double get width => _width; + + @override + Rect get rect => Rect.fromLTWH( + _width < 0 ? x + _width : x, + _height < 0 ? y + _height : y, + _width.abs(), + _height.abs(), + ); + + @override + void move(double x, double y) { + _x = x; + _y = y; + } + + @override + void resize(double width, double height) { + _width = width; + _height = height; + } + + double _x; + double _y; + double _width; + double _height; +} diff --git a/patterns/state/manipulator_state/shapes/circle_shape.dart b/patterns/state/manipulator_state/shapes/circle_shape.dart new file mode 100644 index 0000000..6efb1a5 --- /dev/null +++ b/patterns/state/manipulator_state/shapes/circle_shape.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +import '../states/selections/inner_radius_state.dart'; +import '../states/selections/selection_state.dart'; +import 'base_shape.dart'; + +class CircleShape extends BaseShape { + CircleShape( + super.x, + super.y, + super.width, + super.height, + double innerRadius, + ) { + this.innerRadius = innerRadius; + } + + double get innerRadius => + _turnOnInnerRadius && _drawInnerRadius ? _innerRadius : width / 2; + + set innerRadius(double newValue) { + if (newValue > width / 2) { + _turnOnInnerRadius = false; + _innerRadius = newValue; + _buildPath(); + return; + } else if (newValue < 1) { + newValue = 1; + } + + _drawInnerRadius = true; + _turnOnInnerRadius = _drawInnerRadius; + _innerRadius = newValue; + _buildPath(); + } + + @override + void resize(double width, double height) { + super.resize(width, height); + _drawInnerRadius = width > _innerRadius * 2; + _buildPath(); + } + + @override + void move(double x, double y) { + super.move(x, y); + _buildPath(); + } + + @override + SelectionState createSelectionState() { + return InnerRadiusState(selectedShape: this); + } + + @override + void paint(Canvas canvas) { + canvas.drawPath( + _path, + Paint()..color = Colors.white, + ); + } + + void _buildPath() { + _path = Path() + ..fillType = PathFillType.evenOdd + ..addOval(rect); + + if (_drawInnerRadius) { + final fixHeight = height / width; + final doubleRadius = innerRadius * 2; + _path.addOval( + Rect.fromLTWH( + x + innerRadius, + y + innerRadius * fixHeight, + width - doubleRadius, + height - doubleRadius * fixHeight, + ), + ); + } + } + + late double _innerRadius; + late Path _path; + bool _drawInnerRadius = true; + bool _turnOnInnerRadius = true; +} diff --git a/patterns/state/manipulator_state/shapes/marker_shape.dart b/patterns/state/manipulator_state/shapes/marker_shape.dart new file mode 100644 index 0000000..aef7acf --- /dev/null +++ b/patterns/state/manipulator_state/shapes/marker_shape.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import '../states/selections/selection_state.dart'; +import 'base_shape.dart'; +import 'shape.dart'; + +class MarkerShape extends BaseShape { + MarkerShape(double size) : super(0, 0, size, -1); + + @override + Rect get rect => Rect.fromLTWH(x - width, y - width, width * 2, width * 2); + + @override + void paint(Canvas canvas) => throw UnimplementedError(); + + @override + SelectionState createSelectionState() => throw UnimplementedError(); +} diff --git a/patterns/state/manipulator_state/shapes/rectangle_shape.dart b/patterns/state/manipulator_state/shapes/rectangle_shape.dart new file mode 100644 index 0000000..c6eeedd --- /dev/null +++ b/patterns/state/manipulator_state/shapes/rectangle_shape.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +import '../states/selections/resizable_state.dart'; +import '../states/selections/selection_state.dart'; +import 'base_shape.dart'; + +class RectangleShape extends BaseShape { + RectangleShape(super.x, super.y, super.width, super.height); + + @override + SelectionState createSelectionState() { + return ResizableState(selectedShape: this); + } + + @override + void paint(Canvas canvas) { + canvas.drawRect( + rect, + Paint()..color = Colors.white, + ); + } +} diff --git a/patterns/state/manipulator_state/shapes/shape.dart b/patterns/state/manipulator_state/shapes/shape.dart new file mode 100644 index 0000000..4484439 --- /dev/null +++ b/patterns/state/manipulator_state/shapes/shape.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import '../states/selections/selection_state.dart'; + +abstract class Shape { + double get x; + + double get y; + + double get width; + + double get height; + + Rect get rect; + + void move(double x, double y); + + void resize(double width, double height); + + void paint(Canvas canvas); + + SelectionState createSelectionState(); +} diff --git a/patterns/state/manipulator_state/shapes/text_shape.dart b/patterns/state/manipulator_state/shapes/text_shape.dart new file mode 100644 index 0000000..9c8d51f --- /dev/null +++ b/patterns/state/manipulator_state/shapes/text_shape.dart @@ -0,0 +1,74 @@ +import 'dart:ui'; + +import '../states/selections/selection_state.dart'; +import '../states/selections/text_resize_state.dart'; +import 'base_shape.dart'; + +class TextShape extends BaseShape { + TextShape(double x, double y, double height) : super(x, y, 0, height) { + _buildParagraph(height); + } + + String get text => _text; + + set text(String newText) { + _text = newText; + _buildParagraph(_textHeight); + } + + double get userHeight => _textHeight; + + Paragraph get paragraph => _paragraph; + + @override + Rect get rect { + final fixY = height - _textHeight; + return Rect.fromLTWH(x, y + fixY, width, height - fixY); + } + + @override + void paint(Canvas can) { + can.drawParagraph(_paragraph, Offset(x, y)); + } + + @override + SelectionState createSelectionState() { + return TextResizeState(selectedShape: this); + } + + @override + void resize(double _, double newHeight) { + if (newHeight < 2) { + newHeight = 2; + } + + final oldWidth = width; + _buildParagraph(newHeight); + final centerX = x - (width - oldWidth) / 2; + move(centerX, y); + } + + void _buildParagraph(double newHeight) { + _textHeight = newHeight; + + final style = ParagraphStyle( + textDirection: TextDirection.ltr, + ); + final tStyle = TextStyle( + fontFamily: 'Arial', + color: Color(0xffffffff), + fontSize: newHeight, + ); + _paragraph = (ParagraphBuilder(style) + ..pushStyle(tStyle) + ..addText(_text)) + .build(); + _paragraph.layout(ParagraphConstraints(width: double.infinity)); + + super.resize(_paragraph.maxIntrinsicWidth, _paragraph.height); + } + + late Paragraph _paragraph; + String _text = 'Text'; + late double _textHeight; +} diff --git a/patterns/state/manipulator_state/states/_/creation_state.dart b/patterns/state/manipulator_state/states/_/creation_state.dart new file mode 100644 index 0000000..3485c86 --- /dev/null +++ b/patterns/state/manipulator_state/states/_/creation_state.dart @@ -0,0 +1,75 @@ +import 'dart:ui'; + +import '../../pattern/manipulator.dart'; +import '../../shapes/shape.dart'; + +abstract class CreationState extends ManipulationState { + Shape createShape(double x, double y); + + @override + void mouseDown(double x, double y) { + _startCreatingShape(x, y); + } + + @override + void mouseMove(double x, double y) { + if (_isCreatingNotStart) { + return; + } + + _resizeNewShape(x, y); + } + + @override + void mouseUp() { + if (_isCreatingNotStart) { + return; + } + + _repositionNewShape(); + context.shapes.add(_newShape!); + _finishCreatingShape(); + } + + @override + void paint(Canvas canvas) { + _newShape?.paint(canvas); + } + + bool get _isCreatingNotStart => _newShape == null; + + void _startCreatingShape(double x, double y) { + _startX = x; + _startY = y; + _newShape = createShape(x, y); + } + + void _resizeNewShape(double x, double y) { + _isDragged = true; + _newShape!.resize(x - _startX, y - _startY); + context.update(); + } + + void _repositionNewShape() { + if (!_isDragged) { + _newShape!.resize(100, 100); + final rect = _newShape!.rect; + _newShape!.move( + _startX - rect.width / 2, + _startY - rect.height / 2, + ); + } + } + + void _finishCreatingShape() { + final selectedShapeState = _newShape!.createSelectionState(); + context.changeState(selectedShapeState); + _isDragged = false; + _newShape = null; + } + + var _startX = 0.0; + var _startY = 0.0; + Shape? _newShape; + var _isDragged = false; +} diff --git a/patterns/state/manipulator_state/states/_/marker.dart b/patterns/state/manipulator_state/states/_/marker.dart new file mode 100644 index 0000000..ea9a188 --- /dev/null +++ b/patterns/state/manipulator_state/states/_/marker.dart @@ -0,0 +1,35 @@ +import 'package:flutter/rendering.dart'; + +import '../../shapes/marker_shape.dart'; +import '../../shapes/shape.dart'; +import 'sub_states/child_state.dart'; +import 'sub_states/parent_state.dart'; + +abstract class Marker extends ChildState { + Marker({ + required ParentState parentState, + }) : super( + parentState: parentState, + markerShape: MarkerShape(5), + ); + + @override + MouseCursor get hoverCursor { + final rect = parentState.selectedShape.rect; + final corner = Offset(markerShape.x, markerShape.y); + + if (corner == rect.topLeft) { + return SystemMouseCursors.resizeUpLeft; + } else if (corner == rect.topRight) { + return SystemMouseCursors.resizeUpRight; + } else if (corner == rect.bottomLeft) { + return SystemMouseCursors.resizeDownLeft; + } else if (corner == rect.bottomRight) { + return SystemMouseCursors.resizeDownRight; + } + + return SystemMouseCursors.move; + } + + T get selectedShape => parentState.selectedShape; +} diff --git a/patterns/state/manipulator_state/states/_/mixins/hover_shape_mixin.dart b/patterns/state/manipulator_state/states/_/mixins/hover_shape_mixin.dart new file mode 100644 index 0000000..6e5679f --- /dev/null +++ b/patterns/state/manipulator_state/states/_/mixins/hover_shape_mixin.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import '../../../pattern/manipulator.dart'; +import '../../../shapes/shape.dart'; + +mixin HoverShapeMixin implements ManipulationState { + Shape? findShapeByCoordinates(double x, double y) { + return context.shapes.findShapeByCoordinates(x, y); + } + + Shape? get hoverShape => _hoverShape; + + bool get isHover => _hoverShape != null; + + @override + void mouseMove(double x, double y) { + final newHover = findShapeByCoordinates(x, y); + + if (newHover == _hoverShape) { + return; + } + + _hoverShape = newHover; + + if (newHover == null) { + onMouseLeave(); + } else { + onHover(); + } + + context.update(); + } + + void onHover() {} + + void onMouseLeave() {} + + @override + void paint(Canvas canvas) { + if (_hoverShape == null) { + return; + } + + context.paintStyle.paintHover(canvas, _hoverShape!); + } + + Shape? _hoverShape; +} diff --git a/patterns/state/manipulator_state/states/_/paint_style.dart b/patterns/state/manipulator_state/states/_/paint_style.dart new file mode 100644 index 0000000..a26d70e --- /dev/null +++ b/patterns/state/manipulator_state/states/_/paint_style.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../../shapes/circle_shape.dart'; +import '../../shapes/shape.dart'; +import '../../shapes/text_shape.dart'; +import '../selections/text/text_cursor.dart'; + +class PaintStyle { + PaintStyle(this.color) + : _selectStroke = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0, + _markerStroke = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5, + _markerFill = Paint() + ..style = PaintingStyle.fill + ..color = Colors.black; + + final Color color; + + void paintHover(Canvas canvas, Shape shape) { + canvas.drawRect( + shape.rect.deflate(1), + _selectStroke, + ); + } + + void paintSelection(Canvas canvas, Shape shape) { + paintHover(canvas, shape); + } + + void paintMarker(Canvas canvas, Shape markerShape) { + final point = Offset(markerShape.x, markerShape.y); + canvas.drawCircle( + point, + markerShape.width, + _markerFill, + ); + canvas.drawCircle( + point, + markerShape.width, + _markerStroke, + ); + } + + void paintRadiusLine(CircleShape selectedShape, Canvas canvas) { + canvas.save(); + canvas.translate(selectedShape.x, selectedShape.y); + final x = selectedShape.width - selectedShape.innerRadius; + final y = selectedShape.height / 2; + canvas.drawLine( + Offset(x, y), + Offset(selectedShape.width, y), + Paint() + ..color = color + ..strokeWidth = 1.5, + ); + + canvas.restore(); + } + + void paintSelectedText(TextShape selectedShape, Canvas canvas) { + canvas.drawRect( + selectedShape.rect, + Paint()..color = color.withOpacity(0.3), + ); + } + + void paintTextCursor( + TextCursor cursor, + TextShape selectedShape, + Canvas canvas, + ) { + canvas.drawLine( + Offset( + cursor.xCoordinate, + selectedShape.y + (selectedShape.height - selectedShape.userHeight) + 2, + ), + Offset( + cursor.xCoordinate, + selectedShape.y + (selectedShape.height) - 2, + ), + Paint() + ..strokeWidth = 2.2 + ..color = Colors.white, + ); + } + + final Paint _selectStroke; + final Paint _markerStroke; + final Paint _markerFill; +} diff --git a/patterns/state/manipulator_state/states/_/sub_states/child_state.dart b/patterns/state/manipulator_state/states/_/sub_states/child_state.dart new file mode 100644 index 0000000..b1a1747 --- /dev/null +++ b/patterns/state/manipulator_state/states/_/sub_states/child_state.dart @@ -0,0 +1,88 @@ +import 'dart:ui'; + +import 'package:flutter/services.dart'; + +import '../../../shapes/shape.dart'; +import '../../../pattern/manipulator.dart'; +import '../../_/mixins/hover_shape_mixin.dart'; +import 'parent_state.dart'; + +abstract class ChildState extends ManipulationState + with HoverShapeMixin { + final ParentState parentState; + final Shape markerShape; + + ChildState({ + required this.parentState, + required this.markerShape, + }) { + updatePosition(); + } + + void updatePosition(); + + void mouseDragAction(double x, double y); + + MouseCursor get hoverCursor; + + @override + void onHover() { + context.cursor = hoverCursor; + } + + @override + void onMouseLeave() { + context.cursor = SystemMouseCursors.basic; + } + + @override + void mouseDown(double x, double y) { + if (isHover) { + _isDown = true; + context.changeState(this); + } + } + + @override + void mouseMove(double x, double y) { + super.mouseMove(x, y); + if (_isDown) { + mouseDragAction(x, y); + parentState.updateKids(); + context.cursor = hoverCursor; + context.update(); + } + } + + @override + void mouseUp() { + if (!_isDown) { + return; + } + + context.changeState(parentState); + _isDown = false; + + if (!isHover) { + context.cursor = SystemMouseCursors.basic; + context.update(); + } + } + + @override + void paint(Canvas canvas) { + parentState.paint(canvas); + } + + @override + Manipulator get context => parentState.context; + + @override + Shape? findShapeByCoordinates(double x, double y) { + return markerShape.rect.contains(Offset(x, y)) ? markerShape : null; + } + + bool get isDown => _isDown; + + bool _isDown = false; +} diff --git a/patterns/state/manipulator_state/states/_/sub_states/parent_state.dart b/patterns/state/manipulator_state/states/_/sub_states/parent_state.dart new file mode 100644 index 0000000..ba0638f --- /dev/null +++ b/patterns/state/manipulator_state/states/_/sub_states/parent_state.dart @@ -0,0 +1,56 @@ +import 'dart:ui'; + +import '../../selections/selection_state.dart'; +import 'child_state.dart'; +import '../../../shapes/shape.dart'; + +abstract class ParentState extends SelectionState { + ParentState({ + required super.selectedShape, + }); + + void addChildren(List markers) { + _children.addAll(markers); + } + + void updateKids() { + for (final child in _children) { + child.updatePosition(); + } + } + + @override + void mouseDown(double x, double y) { + for (final child in _children) { + child.mouseDown(x, y); + if (context.state == child) { + return; + } + } + + super.mouseDown(x, y); + } + + @override + void mouseMove(double x, double y) { + super.mouseMove(x, y); + + for (final child in _children) { + child.mouseMove(x, y); + if (child.isHover) { + return; + } + } + } + + @override + void paint(Canvas canvas) { + super.paint(canvas); + + for (final child in _children) { + context.paintStyle.paintMarker(canvas, child.markerShape); + } + } + + final _children = []; +} diff --git a/patterns/state/manipulator_state/states/creations/circle_creation_state.dart b/patterns/state/manipulator_state/states/creations/circle_creation_state.dart new file mode 100644 index 0000000..dabf811 --- /dev/null +++ b/patterns/state/manipulator_state/states/creations/circle_creation_state.dart @@ -0,0 +1,15 @@ +import '../../shapes/circle_shape.dart'; +import '../../shapes/shape.dart'; +import '../_/creation_state.dart'; + +class CircleCreationState extends CreationState { + @override + Shape createShape(double x, double y) { + return CircleShape(x, y, 100, 100, 25); + } + + @override + String toString() { + return 'Circle Creation State'; + } +} diff --git a/patterns/state/manipulator_state/states/creations/rectangle_creation_state.dart b/patterns/state/manipulator_state/states/creations/rectangle_creation_state.dart new file mode 100644 index 0000000..4a61891 --- /dev/null +++ b/patterns/state/manipulator_state/states/creations/rectangle_creation_state.dart @@ -0,0 +1,15 @@ +import '../../shapes/rectangle_shape.dart'; +import '../../shapes/shape.dart'; +import '../_/creation_state.dart'; + +class RectangleCreationState extends CreationState { + @override + Shape createShape(double x, double y) { + return RectangleShape(x, y, 0, 0); + } + + @override + String toString() { + return 'Rectangle Creation State'; + } +} diff --git a/patterns/state/manipulator_state/states/creations/text_creation_state.dart b/patterns/state/manipulator_state/states/creations/text_creation_state.dart new file mode 100644 index 0000000..4425a84 --- /dev/null +++ b/patterns/state/manipulator_state/states/creations/text_creation_state.dart @@ -0,0 +1,15 @@ +import '../../shapes/shape.dart'; +import '../../shapes/text_shape.dart'; +import '../_/creation_state.dart'; + +class TextCreationState extends CreationState { + @override + Shape createShape(double x, double y) { + return TextShape(x, y, 2); + } + + @override + String toString() { + return 'Text Creation State'; + } +} diff --git a/patterns/state/manipulator_state/states/free_sate.dart b/patterns/state/manipulator_state/states/free_sate.dart new file mode 100644 index 0000000..e001070 --- /dev/null +++ b/patterns/state/manipulator_state/states/free_sate.dart @@ -0,0 +1,33 @@ +import '../pattern/manipulator.dart'; +import '_/mixins/hover_shape_mixin.dart'; +import 'selections/move_state.dart'; + +class FreeState extends ManipulationState with HoverShapeMixin { + @override + void mouseDown(double x, double y) { + tryToSelectAndStartMovingShape(x, y); + } + + bool tryToSelectAndStartMovingShape(double x, double y) { + final selectedShape = context.shapes.findShapeByCoordinates(x, y); + + if (selectedShape == null) { + return false; + } + + context.changeState( + MoveState( + startX: x, + startY: y, + selectedShape: selectedShape, + ), + ); + + return true; + } + + @override + String toString() { + return 'Free State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/inner_radius_markers/inner_radius_marker_state.dart b/patterns/state/manipulator_state/states/selections/inner_radius_markers/inner_radius_marker_state.dart new file mode 100644 index 0000000..ae60e1b --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/inner_radius_markers/inner_radius_marker_state.dart @@ -0,0 +1,32 @@ +import 'package:flutter/src/services/mouse_cursor.dart'; + +import '../../../shapes/circle_shape.dart'; +import '../../_/marker.dart'; + +class InnerRadiusMarkerState extends Marker { + InnerRadiusMarkerState({required super.parentState}); + + @override + void mouseDragAction(double x, double y) { + selectedShape.innerRadius = selectedShape.rect.right - x; + } + + @override + void updatePosition() { + final y = selectedShape.y + selectedShape.height / 2; + final x = selectedShape.x + selectedShape.width; + + markerShape.move( + x - selectedShape.innerRadius, + y, + ); + } + + @override + MouseCursor get hoverCursor => SystemMouseCursors.resizeLeftRight; + + @override + String toString() { + return '${parentState.toString()} + Inner Radius Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/inner_radius_state.dart b/patterns/state/manipulator_state/states/selections/inner_radius_state.dart new file mode 100644 index 0000000..898b5b5 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/inner_radius_state.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import '../../shapes/circle_shape.dart'; +import 'inner_radius_markers/inner_radius_marker_state.dart'; +import 'resizable_state.dart'; + +class InnerRadiusState extends ResizableState { + InnerRadiusState({required super.selectedShape}) { + addChildren([ + InnerRadiusMarkerState(parentState: this), + ]); + } + + @override + void paint(Canvas canvas) { + context.paintStyle.paintRadiusLine(selectedShape, canvas); + super.paint(canvas); + } + + @override + String toString() { + return '${super.toString()} + Inner Radius State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/move_state.dart b/patterns/state/manipulator_state/states/selections/move_state.dart new file mode 100644 index 0000000..8f0773c --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/move_state.dart @@ -0,0 +1,31 @@ +import 'selection_state.dart'; + +class MoveState extends SelectionState { + final double startX; + final double startY; + + MoveState({ + required double startX, + required double startY, + required super.selectedShape, + }) : startX = startX - selectedShape.x, + startY = startY - selectedShape.y; + + @override + void mouseMove(double x, double y) { + selectedShape.move(x - startX, y - startY); + context.update(); + } + + @override + void mouseUp() { + context.changeState( + selectedShape.createSelectionState(), + ); + } + + @override + String toString() { + return '${super.toString()} + Moving State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_left_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_left_marker_state.dart new file mode 100644 index 0000000..9d2a1f8 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_left_marker_state.dart @@ -0,0 +1,30 @@ +import '../../_/marker.dart'; + +class BottomLeftMarkerState extends Marker { + BottomLeftMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + final newX = selectedShape.width + selectedShape.x - x; + final newY = y - selectedShape.y; + + selectedShape + ..resize(newX, newY) + ..move(x, selectedShape.y); + } + + @override + void updatePosition() { + markerShape.move( + selectedShape.x, + selectedShape.y + selectedShape.height, + ); + } + + @override + String toString() { + return '${parentState.toString()} + Bottom Left Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_right_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_right_marker_state.dart new file mode 100644 index 0000000..5db6e10 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/bottom_right_marker_state.dart @@ -0,0 +1,27 @@ +import '../../_/marker.dart'; + +class BottomRightMarkerState extends Marker { + BottomRightMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + selectedShape.resize( + x - selectedShape.x, + y - selectedShape.y, + ); + } + + @override + void updatePosition() { + final width = selectedShape.x + selectedShape.width; + final height = selectedShape.y + selectedShape.height; + markerShape.move(width, height); + } + + @override + String toString() { + return '${parentState.toString()} + Bottom Right Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/top_left_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/top_left_marker_state.dart new file mode 100644 index 0000000..74bd92d --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/top_left_marker_state.dart @@ -0,0 +1,30 @@ +import '../../_/marker.dart'; + +class TopLeftMarkerState extends Marker { + TopLeftMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + final newWidth = selectedShape.width + selectedShape.x - x; + final newHeight = selectedShape.height + selectedShape.y - y; + + selectedShape + ..resize(newWidth, newHeight) + ..move(x, y); + } + + @override + void updatePosition() { + markerShape.move( + selectedShape.x, + selectedShape.y, + ); + } + + @override + String toString() { + return '${parentState.toString()} + Top Left Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_markers/top_right_marker_state.dart b/patterns/state/manipulator_state/states/selections/resizable_markers/top_right_marker_state.dart new file mode 100644 index 0000000..f03f484 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_markers/top_right_marker_state.dart @@ -0,0 +1,30 @@ +import '../../_/marker.dart'; + +class TopRightMarkerState extends Marker { + TopRightMarkerState({ + required super.parentState, + }); + + @override + void mouseDragAction(double x, double y) { + final width = x - selectedShape.x; + final height = selectedShape.height + selectedShape.y - y; + + selectedShape + ..resize(width, height) + ..move(selectedShape.x, y); + } + + @override + void updatePosition() { + markerShape.move( + selectedShape.x + selectedShape.width, + selectedShape.y, + ); + } + + @override + String toString() { + return '${parentState.toString()} + Top Right Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/resizable_state.dart b/patterns/state/manipulator_state/states/selections/resizable_state.dart new file mode 100644 index 0000000..92236b0 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/resizable_state.dart @@ -0,0 +1,22 @@ +import 'resizable_markers/bottom_left_marker_state.dart'; +import 'resizable_markers/bottom_right_marker_state.dart'; +import 'resizable_markers/top_left_marker_state.dart'; +import 'resizable_markers/top_right_marker_state.dart'; +import '../_/sub_states/parent_state.dart'; +import '../../shapes/shape.dart'; + +class ResizableState extends ParentState { + ResizableState({required super.selectedShape}) { + addChildren([ + TopLeftMarkerState(parentState: this), + TopRightMarkerState(parentState: this), + BottomRightMarkerState(parentState: this), + BottomLeftMarkerState(parentState: this), + ]); + } + + @override + String toString() { + return '${super.toString()} + Resizable State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/selection_state.dart b/patterns/state/manipulator_state/states/selections/selection_state.dart new file mode 100644 index 0000000..26fb82d --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/selection_state.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../../shapes/shape.dart'; +import '../free_sate.dart'; + +class SelectionState extends FreeState { + final TShape selectedShape; + + SelectionState({required this.selectedShape}); + + @override + void mouseDown(double x, double y) { + final isShapeNotSelected = !tryToSelectAndStartMovingShape(x, y); + + if (isShapeNotSelected) { + context.changeState(FreeState()); + } + } + + @override + void paint(Canvas canvas) { + super.paint(canvas); + context.paintStyle.paintSelection(canvas, selectedShape); + } + + @override + String toString() { + return '${super.toString()} + Selection State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/text/keyboard_actions.dart b/patterns/state/manipulator_state/states/selections/text/keyboard_actions.dart new file mode 100644 index 0000000..ed0f11b --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/keyboard_actions.dart @@ -0,0 +1,31 @@ +import 'package:flutter/services.dart'; + +class KeyboardActions { + final Map actions; + final void Function(String) inputCharAction; + + KeyboardActions({ + required this.actions, + required this.inputCharAction, + }); + + void keyDown(KeyEvent keyEvent) { + final isNotKeyDown = + !(keyEvent is KeyDownEvent || keyEvent is KeyRepeatEvent); + + if (isNotKeyDown) { + return; + } + + final foundEvent = actions[keyEvent.physicalKey]; + + if (foundEvent != null) { + foundEvent.call(); + return; + } + + if (keyEvent.character != null) { + inputCharAction.call(keyEvent.character!); + } + } +} diff --git a/patterns/state/manipulator_state/states/selections/text/text_cursor.dart b/patterns/state/manipulator_state/states/selections/text/text_cursor.dart new file mode 100644 index 0000000..b09f9b8 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/text_cursor.dart @@ -0,0 +1,89 @@ +import 'dart:ui'; + +import '../../../shapes/text_shape.dart'; + +class TextCursor { + TextCursor(this._textShape) : _xPosition = _textShape.x; + + double get xCoordinate => _xPosition; + + void changePosition(double x) { + x = x - _textShape.x; + + final pos = + _textShape.paragraph.getPositionForOffset(Offset(x, _textShape.y)); + + _charIndex = pos.offset; + _xPosition = _textShape.x; + + final range = _textShape.paragraph.getBoxesForRange( + pos.offset - 1, + pos.offset, + ); + + if (range.isNotEmpty) { + _xPosition += range.first.right; + } + } + + void inputText(String char) { + _changeText(char: char); + moveRight(); + } + + void backspace() { + if (_charIndex <= 0) { + return; + } + + _changeText(removeChars: -1); + moveLeft(); + } + + void moveLeft() { + _charIndex--; + _xPosition = _textShape.x; + + if (_charIndex <= 0) { + _charIndex = 0; + return; + } + + final range = + _textShape.paragraph.getBoxesForRange(_charIndex - 1, _charIndex); + + if (range.isNotEmpty) { + _xPosition += range.first.right; + } + } + + void moveRight() { + _charIndex++; + _xPosition = _textShape.x; + + if (_charIndex >= _textShape.text.length) { + _charIndex = _textShape.text.length; + _xPosition += _textShape.width; + return; + } + + final range = + _textShape.paragraph.getBoxesForRange(_charIndex - 1, _charIndex); + + if (range.isNotEmpty) { + _xPosition += range.first.right; + } + } + + void _changeText({String char = '', int removeChars = 0}) { + final start = _textShape.text.substring(0, _charIndex + removeChars); + final end = _textShape.text.length > start.length + ? _textShape.text.substring(_charIndex) + : ''; + _textShape.text = '$start$char$end'; + } + + final TextShape _textShape; + int _charIndex = 0; + late double _xPosition = 0; +} diff --git a/patterns/state/manipulator_state/states/selections/text/text_cursor_animation.dart b/patterns/state/manipulator_state/states/selections/text/text_cursor_animation.dart new file mode 100644 index 0000000..22b6bee --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/text_cursor_animation.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +class TextCursorAnimation { + final Duration speed; + final void Function() onBlink; + + TextCursorAnimation({ + required this.speed, + required this.onBlink, + }) { + _timer = Timer.periodic(speed, (_) { + _isShowCursor = !_isShowCursor; + onBlink.call(); + }); + } + + bool get isVisible => _isShowCursor; + + void touch() { + _isShowCursor = true; + onBlink.call(); + } + + void dispose() { + _timer.cancel(); + } + + bool _isShowCursor = true; + late Timer _timer; +} diff --git a/patterns/state/manipulator_state/states/selections/text/text_size_marker_state.dart b/patterns/state/manipulator_state/states/selections/text/text_size_marker_state.dart new file mode 100644 index 0000000..05c250f --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text/text_size_marker_state.dart @@ -0,0 +1,30 @@ +import 'package:flutter/services.dart'; + +import '../../../shapes/text_shape.dart'; +import '../../_/marker.dart'; + +class TextSizeMarkerState extends Marker { + TextSizeMarkerState({required super.parentState}); + + @override + void mouseDragAction(double x, double y) { + final newHeight = + y - selectedShape.y - (selectedShape.height - selectedShape.userHeight); + + selectedShape.resize(0, newHeight); + } + + @override + void updatePosition() { + final bottomCenter = selectedShape.rect.bottomCenter; + markerShape.move(bottomCenter.dx, bottomCenter.dy); + } + + @override + MouseCursor get hoverCursor => SystemMouseCursors.resizeUpDown; + + @override + String toString() { + return '${parentState.toString()} + Text Size Marker State'; + } +} diff --git a/patterns/state/manipulator_state/states/selections/text_edit_state.dart b/patterns/state/manipulator_state/states/selections/text_edit_state.dart new file mode 100644 index 0000000..c11b288 --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text_edit_state.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../shapes/text_shape.dart'; +import 'selection_state.dart'; +import 'text/keyboard_actions.dart'; +import 'text/text_cursor.dart'; +import 'text/text_cursor_animation.dart'; + +class TextEditState extends SelectionState { + TextEditState({ + required Offset startPointer, + required super.selectedShape, + }) : _startPointer = startPointer; + + @override + void init() { + _textCursor = TextCursor(selectedShape)..changePosition(_startPointer.dx); + + _keyboardActions = KeyboardActions( + actions: { + PhysicalKeyboardKey.backspace: _textCursor.backspace, + PhysicalKeyboardKey.arrowLeft: _textCursor.moveLeft, + PhysicalKeyboardKey.arrowRight: _textCursor.moveRight, + }, + inputCharAction: _textCursor.inputText, + ); + + _animationCursor = TextCursorAnimation( + speed: Duration(milliseconds: 400), + onBlink: context.update, + ); + + context.cursor = SystemMouseCursors.text; + context.update(); + } + + @override + void mouseDown(double x, double y) { + if (selectedShape.rect.contains(Offset(x, y))) { + _textCursor.changePosition(x); + _animationCursor.touch(); + return; + } + + _animationCursor.dispose(); + super.mouseDown(x, y); + } + + @override + void keyDown(KeyEvent keyEvent) { + _keyboardActions.keyDown(keyEvent); + _animationCursor.touch(); + } + + @override + void paint(Canvas canvas) { + context.paintStyle.paintSelectedText(selectedShape, canvas); + super.paint(canvas); + + if (_animationCursor.isVisible) { + context.paintStyle.paintTextCursor(_textCursor, selectedShape, canvas); + } + } + + @override + void onHover() { + if (hoverShape == selectedShape) { + context.cursor = SystemMouseCursors.text; + } + } + + @override + void onMouseLeave() { + context.cursor = SystemMouseCursors.basic; + } + + @override + String toString() { + return '${super.toString()} + Text Edit State'; + } + + final Offset _startPointer; + late final TextCursor _textCursor; + late final KeyboardActions _keyboardActions; + late final TextCursorAnimation _animationCursor; +} diff --git a/patterns/state/manipulator_state/states/selections/text_resize_state.dart b/patterns/state/manipulator_state/states/selections/text_resize_state.dart new file mode 100644 index 0000000..c79e4cb --- /dev/null +++ b/patterns/state/manipulator_state/states/selections/text_resize_state.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +import '../../shapes/text_shape.dart'; +import '../_/sub_states/parent_state.dart'; +import 'text_edit_state.dart'; +import 'text/text_size_marker_state.dart'; + +class TextResizeState extends ParentState { + TextResizeState({required super.selectedShape}) { + addChildren([ + TextSizeMarkerState(parentState: this), + ]); + } + + @override + void mouseDoubleClick(double x, double y) { + context.changeState( + TextEditState( + startPointer: Offset(x, y), + selectedShape: selectedShape, + ), + ); + } + + @override + String toString() { + return '${super.toString()} + Text Resize State'; + } +} diff --git a/patterns/state/manipulator_state/widgets/current_state.dart b/patterns/state/manipulator_state/widgets/current_state.dart new file mode 100644 index 0000000..458c06d --- /dev/null +++ b/patterns/state/manipulator_state/widgets/current_state.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import '../../../abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart'; +import '../../../abstract_factory/tool_panel_factory/widgets/independent/panel.dart'; +import '../pattern/manipulator.dart'; + +class CurrentState extends StatelessWidget { + final Manipulator manipulator; + + const CurrentState({ + Key? key, + required this.manipulator, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + top: 12, + left: 300, + child: Panel( + thicknessHeight: 64, + direction: Axis.horizontal, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Center( + child: EventListenableBuilder( + event: manipulator.onStateChange, + builder: (context) { + return EventListenableBuilder( + event: manipulator.onUpdate, + builder: (context) { + return Text( + manipulator.toString(), + style: TextStyle(color: Colors.white, fontSize: 16), + ); + }, + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/patterns/state/manipulator_state/widgets/drawing_board.dart b/patterns/state/manipulator_state/widgets/drawing_board.dart new file mode 100644 index 0000000..2f25926 --- /dev/null +++ b/patterns/state/manipulator_state/widgets/drawing_board.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; + +import '../../../abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart'; +import '../app/app.dart'; + +class DrawingBoard extends StatefulWidget { + final App app; + + const DrawingBoard({Key? key, required this.app}) : super(key: key); + + @override + State createState() => _DrawingBoardState(); +} + +class _DrawingBoardState extends State { + late FocusNode focusNode; + late Offset _lastMouseDown; + + @override + void initState() { + focusNode = FocusNode(skipTraversal: true); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return KeyboardListener( + autofocus: true, + focusNode: focusNode, + onKeyEvent: widget.app.manipulator.keyDown, + child: GestureDetector( + onDoubleTap: () => widget.app.manipulator.mouseDoubleClick( + _lastMouseDown.dx, + _lastMouseDown.dy, + ), + child: Listener( + onPointerDown: (e) { + _lastMouseDown = e.localPosition; + FocusScope.of(context).requestFocus(focusNode); + widget.app.manipulator.mouseDown( + e.localPosition.dx, + e.localPosition.dy, + ); + }, + onPointerHover: (e) => widget.app.manipulator.mouseMove( + e.localPosition.dx, + e.localPosition.dy, + ), + onPointerMove: (e) => widget.app.manipulator.mouseMove( + e.localPosition.dx, + e.localPosition.dy, + ), + onPointerUp: (e) => widget.app.manipulator.mouseUp(), + child: Container( + constraints: BoxConstraints.expand(), + color: Color(0xff1f1f1f), + child: EventListenableBuilder( + event: widget.app.shapes.onChange, + builder: (_) { + return EventListenableBuilder( + event: widget.app.manipulator.onUpdate, + builder: (_) { + return MouseRegion( + cursor: widget.app.manipulator.cursor, + child: CustomPaint( + painter: _Painter(widget.app), + ), + ); + }, + ); + }, + ), + ), + ), + ), + ); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } +} + +class _Painter extends CustomPainter { + final App app; + + _Painter(this.app); + + @override + void paint(Canvas canvas, Size size) { + for (final shape in app.shapes) { + shape.paint(canvas); + } + + app.manipulator.paint(canvas); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/patterns/state/manipulator_state/widgets/tool_bar.dart b/patterns/state/manipulator_state/widgets/tool_bar.dart new file mode 100644 index 0000000..b40adaf --- /dev/null +++ b/patterns/state/manipulator_state/widgets/tool_bar.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import '../../../abstract_factory/tool_panel_factory/widgets/independent/event_listenable_builder.dart'; +import '../../../abstract_factory/tool_panel_factory/widgets/independent/panel.dart'; +import '../../../abstract_factory/tool_panel_factory/widgets/independent/tool_button.dart'; +import '../app/app.dart'; +import '../app/tool.dart'; +import '../states/free_sate.dart'; +import '../states/selections/selection_state.dart'; + +class ToolBar extends StatelessWidget { + final App app; + + const ToolBar({Key? key, required this.app}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Positioned( + top: 12, + left: 12, + child: Panel( + thicknessHeight: 64, + direction: Axis.horizontal, + child: EventListenableBuilder( + event: app.manipulator.onStateChange, + builder: (_) { + return Row( + children: [ + for (final tool in app.tools) buildButton(tool), + ], + ); + }, + ), + ), + ); + } + + Widget buildButton(Tool tool) { + return ToolButton( + active: isSelected(tool), + icon: Center(child: tool.icon), + onTap: () { + app.manipulator.changeState(tool.state); + }, + ); + } + + bool isSelected(Tool tool) { + final currentState = app.manipulator.state; + + if (currentState is SelectionState) { + if (tool.state is FreeState) { + return true; + } + } + + if (currentState.runtimeType == tool.state.runtimeType) { + return true; + } + + return false; + } +} diff --git a/patterns/state/three_state/README.md b/patterns/state/three_state/README.md new file mode 100644 index 0000000..ec3e52a --- /dev/null +++ b/patterns/state/three_state/README.md @@ -0,0 +1,30 @@ +# State Pattern +State is a behavioral design pattern that lets an object alter its behavior when its internal state +changes. It appears as if the object changed its class. + +Tutorial: [here](https://refactoring.guru/design-patterns/state). + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/172001291-8d87a3c8-b694-45c3-bd46-40211cd9ac45.png) + +### Client code: +```dart +void main() { + final switcher = Switcher( + initState: One(), + ); + + switcher.call(); // call(1): One + switcher.call(); // call(2): Two + switcher.call(); // call(3): Three + switcher.call(); // call:(4) One +} +``` + +### Output: +``` +call(1): One +call(2): Two +call(3): Three +call(4): One +``` diff --git a/patterns/state/three_state/main.dart b/patterns/state/three_state/main.dart new file mode 100644 index 0000000..f5990f1 --- /dev/null +++ b/patterns/state/three_state/main.dart @@ -0,0 +1,13 @@ +import 'pattern/switcher.dart'; +import 'states/one.dart'; + +void main() { + final switcher = Switcher( + initState: One(), + ); + + switcher.call(); // call(1): One + switcher.call(); // call(2): Two + switcher.call(); // call(3): Three + switcher.call(); // call:(4) One +} diff --git a/patterns/state/three_state/pattern/state.dart b/patterns/state/three_state/pattern/state.dart new file mode 100644 index 0000000..37e7f8e --- /dev/null +++ b/patterns/state/three_state/pattern/state.dart @@ -0,0 +1,9 @@ +part of switcher; + +abstract class State { + Switcher get context => _context; + + void call(); + + late Switcher _context; +} diff --git a/patterns/state/three_state/pattern/switcher.dart b/patterns/state/three_state/pattern/switcher.dart new file mode 100644 index 0000000..baf877d --- /dev/null +++ b/patterns/state/three_state/pattern/switcher.dart @@ -0,0 +1,24 @@ +library switcher; + +part 'state.dart'; + +class Switcher { + Switcher({required State initState}) { + changeState(initState); + } + + int get calls => _calls; + + void call() { + _calls++; + _state.call(); + } + + void changeState(State newState) { + _state = newState; + _state._context = this; + } + + late State _state; + int _calls = 0; +} diff --git a/patterns/state/three_state/states/one.dart b/patterns/state/three_state/states/one.dart new file mode 100644 index 0000000..bc1f61a --- /dev/null +++ b/patterns/state/three_state/states/one.dart @@ -0,0 +1,10 @@ +import '../pattern/switcher.dart'; +import 'two.dart'; + +class One extends State { + @override + void call() { + print('call(${context.calls}): One'); + context.changeState(Two()); + } +} diff --git a/patterns/state/three_state/states/three.dart b/patterns/state/three_state/states/three.dart new file mode 100644 index 0000000..d2c0bac --- /dev/null +++ b/patterns/state/three_state/states/three.dart @@ -0,0 +1,10 @@ +import '../pattern/switcher.dart'; +import 'one.dart'; + +class Three extends State { + @override + void call() { + print('call(${context.calls}): Three'); + context.changeState(One()); + } +} diff --git a/patterns/state/three_state/states/two.dart b/patterns/state/three_state/states/two.dart new file mode 100644 index 0000000..a171415 --- /dev/null +++ b/patterns/state/three_state/states/two.dart @@ -0,0 +1,10 @@ +import '../pattern/switcher.dart'; +import 'three.dart'; + +class Two extends State { + @override + void call() { + print('call(${context.calls}): Two'); + context.changeState(Three()); + } +} diff --git a/patterns/strategy/reservation_cargo_spaces/README.md b/patterns/strategy/reservation_cargo_spaces/README.md new file mode 100644 index 0000000..afeae66 --- /dev/null +++ b/patterns/strategy/reservation_cargo_spaces/README.md @@ -0,0 +1,43 @@ +# Strategy pattern +Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of +them into a separate class, and make their objects interchangeable. + +Tutorial: [here](https://refactoring.guru/design-patterns/strategy). + +## About example: Reservation cargo spaces. +This example is taken from the "Blue" book **"Domain-Driven Design" - Eric Evans. Chapter Once**. + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/166560051-6e392b01-6777-4eb1-ae20-fcd643b248ef.png) + +### Client code: +```dart +void main() { + final overbookingPolicy = OverbookingPolicy(); + final app = Application(overbookingPolicy); + final voyage = Voyage(); + + try { + app.makeBooking(Cargo(1000), voyage); + app.makeBooking(Cargo(500), voyage); + app.makeBooking(Cargo(800), voyage); // error + } catch (e) { + print(e); + } +} + +class Application { + void makeBooking(Cargo cargo, Voyage voyage) { + if (overbookingPolicy.isAllowed(cargo, voyage)) { + voyage.addCargo(cargo, confirmation); + } + } +} +``` + +**Output:** +``` +add Cargo(1000.0) to voyage. +add Cargo(500.0) to voyage. +The weight of the cargo exceeds the permissible norm. +``` diff --git a/patterns/strategy/reservation_cargo_spaces/application/application.dart b/patterns/strategy/reservation_cargo_spaces/application/application.dart new file mode 100644 index 0000000..d8aaa8f --- /dev/null +++ b/patterns/strategy/reservation_cargo_spaces/application/application.dart @@ -0,0 +1,20 @@ +import '../partners/cargo.dart'; +import '../partners/voyage.dart'; +import '../policy/overbooking_policy.dart'; +import 'order_confirmation_sequence.dart'; + +class Application { + final OverbookingPolicy overbookingPolicy; + final orderConfirmationSequence = OrderConfirmationSequence(); + + Application(this.overbookingPolicy); + + void makeBooking(Cargo cargo, Voyage voyage) { + if (overbookingPolicy.isAllowed(cargo, voyage)) { + final confirmation = orderConfirmationSequence.next(); + voyage.addCargo(cargo, confirmation); + } else { + throw 'The weight of the cargo exceeds the permissible norm.'; + } + } +} diff --git a/patterns/strategy/reservation_cargo_spaces/application/order_confirmation_sequence.dart b/patterns/strategy/reservation_cargo_spaces/application/order_confirmation_sequence.dart new file mode 100644 index 0000000..906793e --- /dev/null +++ b/patterns/strategy/reservation_cargo_spaces/application/order_confirmation_sequence.dart @@ -0,0 +1,5 @@ +class OrderConfirmationSequence { + var _index = 0; + + int next() => _index++; +} diff --git a/patterns/strategy/reservation_cargo_spaces/main.dart b/patterns/strategy/reservation_cargo_spaces/main.dart new file mode 100644 index 0000000..ddced6e --- /dev/null +++ b/patterns/strategy/reservation_cargo_spaces/main.dart @@ -0,0 +1,18 @@ +import 'application/application.dart'; +import 'partners/cargo.dart'; +import 'partners/voyage.dart'; +import 'policy/overbooking_policy.dart'; + +void main() { + final overbookingPolicy = OverbookingPolicy(); + final app = Application(overbookingPolicy); + final voyage = Voyage(); + + try { + app.makeBooking(Cargo(1000), voyage); + app.makeBooking(Cargo(500), voyage); + app.makeBooking(Cargo(800), voyage); // error + } catch (e) { + print(e); + } +} diff --git a/patterns/strategy/reservation_cargo_spaces/partners/cargo.dart b/patterns/strategy/reservation_cargo_spaces/partners/cargo.dart new file mode 100644 index 0000000..b6b4d23 --- /dev/null +++ b/patterns/strategy/reservation_cargo_spaces/partners/cargo.dart @@ -0,0 +1,5 @@ +class Cargo { + final double size; + + Cargo(this.size); +} diff --git a/patterns/strategy/reservation_cargo_spaces/partners/voyage.dart b/patterns/strategy/reservation_cargo_spaces/partners/voyage.dart new file mode 100644 index 0000000..13f42b7 --- /dev/null +++ b/patterns/strategy/reservation_cargo_spaces/partners/voyage.dart @@ -0,0 +1,16 @@ +import 'cargo.dart'; + +class Voyage { + final _cargo = {}; + + double get capacity => 2000.0; + + double bookedCargoSize() { + return _cargo.values.fold(0, (prev, cargo) => prev + cargo.size); + } + + void addCargo(Cargo cargo, int confirmation) { + _cargo.putIfAbsent(confirmation, () => cargo); + print('Add Cargo(${cargo.size}) to voyage.'); + } +} diff --git a/patterns/strategy/reservation_cargo_spaces/policy/overbooking_policy.dart b/patterns/strategy/reservation_cargo_spaces/policy/overbooking_policy.dart new file mode 100644 index 0000000..8061db5 --- /dev/null +++ b/patterns/strategy/reservation_cargo_spaces/policy/overbooking_policy.dart @@ -0,0 +1,13 @@ +import '../partners/cargo.dart'; +import '../partners/voyage.dart'; + +class OverbookingPolicy { + static const allowableRedundancy = 1.1; + + bool isAllowed(Cargo cargo, Voyage voyage) { + final maxBooking = voyage.capacity * allowableRedundancy; + final futureWeight = voyage.bookedCargoSize() + cargo.size; + + return futureWeight < maxBooking; + } +} diff --git a/patterns/strategy/view_strategy/README.md b/patterns/strategy/view_strategy/README.md new file mode 100644 index 0000000..ad2abd8 --- /dev/null +++ b/patterns/strategy/view_strategy/README.md @@ -0,0 +1,43 @@ +# Strategy Pattern +Strategy is a behavioral design pattern that lets you define a family of algorithms, put each of +them into a separate class, and make their objects interchangeable. + +Tutorial: [here](https://refactoring.guru/design-patterns/strategy). + +## Diagram: +![image](https://user-images.githubusercontent.com/8049534/175562829-c91fbb12-50ac-4373-a33f-900527383a6c.png) + +## Client code: +```dart +void main() { + final byteList = ByteContext() + ..add('Hello guru') + ..add(123456789) + ..add(3.1456564984); + + final strFormat = byteList.toStringView(StrViewStrategy()); + final hexFormat = byteList.toStringView(HexViewStrategy()); + final zipFormat = byteList.toStringView(ZipViewStrategy()); + + print(strFormat); + print(hexFormat); + print(zipFormat); +} +``` + +### Output: +``` +StrViewStrategy: +Hello guru, 123456789, 3.1456564984 + +HexViewStrategy: +Hello guru : 48 65 6c 6c 6f 20 67 75 72 75 +123456789 : 00 00 00 00 07 5b cd 15 +3.1456564984 : 40 09 2a 4d f4 48 9f 7e + +ZipViewStrategy: +1f 8b 08 00 00 00 00 00 00 0a f3 48 cd c9 c9 57 +48 2f 2d 2a d5 51 30 34 32 36 31 35 33 b7 b0 d4 +51 30 d6 33 04 32 4d cd 4c 2c 2d 4c b8 00 d4 70 +cf ee 24 00 00 00 +``` diff --git a/patterns/strategy/view_strategy/main.dart b/patterns/strategy/view_strategy/main.dart new file mode 100644 index 0000000..fe343f4 --- /dev/null +++ b/patterns/strategy/view_strategy/main.dart @@ -0,0 +1,19 @@ +import 'pattern/byte_context.dart'; +import 'strategies/hex_view_strategy.dart'; +import 'strategies/str_view_strategy.dart'; +import 'strategies/zip_view_strategy.dart'; + +void main() { + final byteList = ByteContext() + ..add('Hello guru') + ..add(123456789) + ..add(3.1456564984); + + final strFormat = byteList.toStringView(StrViewStrategy()); + final hexFormat = byteList.toStringView(HexViewStrategy()); + final zipFormat = byteList.toStringView(ZipViewStrategy()); + + print(strFormat); + print(hexFormat); + print(zipFormat); +} diff --git a/patterns/strategy/view_strategy/pattern/byte_context.dart b/patterns/strategy/view_strategy/pattern/byte_context.dart new file mode 100644 index 0000000..962d45c --- /dev/null +++ b/patterns/strategy/view_strategy/pattern/byte_context.dart @@ -0,0 +1,16 @@ +import 'view_strategy.dart'; + +class ByteContext { + String toStringView(ViewStrategy strategy) { + return '${strategy.runtimeType}:\n' + '${strategy.out(this)}'; + } + + void add(dynamic value) { + _buf.add(value); + } + + List toList() => _buf; + + final _buf = []; +} diff --git a/patterns/strategy/view_strategy/pattern/view_strategy.dart b/patterns/strategy/view_strategy/pattern/view_strategy.dart new file mode 100644 index 0000000..4b785ec --- /dev/null +++ b/patterns/strategy/view_strategy/pattern/view_strategy.dart @@ -0,0 +1,5 @@ +import 'byte_context.dart'; + +abstract class ViewStrategy { + String out(ByteContext byteList); +} diff --git a/patterns/strategy/view_strategy/strategies/hex_view_strategy.dart b/patterns/strategy/view_strategy/strategies/hex_view_strategy.dart new file mode 100644 index 0000000..408d276 --- /dev/null +++ b/patterns/strategy/view_strategy/strategies/hex_view_strategy.dart @@ -0,0 +1,47 @@ +import 'dart:typed_data'; + +import '../pattern/byte_context.dart'; +import '../pattern/view_strategy.dart'; + +class HexViewStrategy implements ViewStrategy { + @override + String out(ByteContext byteList) { + final buf = StringBuffer(); + + for (final val in byteList.toList()) { + buf.write('${val.toString().padRight(15)}: '); + + if (val is String) { + buf.writeln(_stringToHex(val)); + } else { + buf.writeln(_valueToHex(val, size: 8)); + } + } + + return buf.toString(); + } + + String _stringToHex(String value) { + return value.codeUnits + .map((charCode) => _valueToHex(size: 1, charCode)) + .join(' '); + } + + String _valueToHex(T value, {required int size}) { + late ByteData byteData; + + if (size == 1) { + byteData = ByteData(1)..setInt8(0, value as int); + } else { + byteData = ByteData(size); + if (value is double) { + byteData.setFloat64(0, value); + } else if (value is int) { + byteData.setInt64(0, value); + } + } + + final bytes = byteData.buffer.asUint8List(); + return bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' '); + } +} diff --git a/patterns/strategy/view_strategy/strategies/str_view_strategy.dart b/patterns/strategy/view_strategy/strategies/str_view_strategy.dart new file mode 100644 index 0000000..b9aa9bb --- /dev/null +++ b/patterns/strategy/view_strategy/strategies/str_view_strategy.dart @@ -0,0 +1,9 @@ +import '../pattern/byte_context.dart'; +import '../pattern/view_strategy.dart'; + +class StrViewStrategy implements ViewStrategy { + @override + String out(ByteContext byteList) { + return '${byteList.toList().join(', ')}\n'; + } +} diff --git a/patterns/strategy/view_strategy/strategies/zip_view_strategy.dart b/patterns/strategy/view_strategy/strategies/zip_view_strategy.dart new file mode 100644 index 0000000..7e78645 --- /dev/null +++ b/patterns/strategy/view_strategy/strategies/zip_view_strategy.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import '../pattern/byte_context.dart'; +import 'str_view_strategy.dart'; + +class ZipViewStrategy extends StrViewStrategy { + @override + String out(ByteContext byteList) { + final codes = super.out(byteList).codeUnits; + final bytes = GZipCodec().encode(codes); + final buf = StringBuffer(); + + var odd = 1; + for (final byte in bytes) { + final hexByte = byte.toRadixString(16).padLeft(2, '0'); + buf.write('$hexByte '); + + if (odd++ % 16 == 0) { + buf.writeln(); + } + } + + return buf.toString(); + } +} diff --git a/patterns/template_method/data_miner/README.md b/patterns/template_method/data_miner/README.md new file mode 100644 index 0000000..c3e1815 --- /dev/null +++ b/patterns/template_method/data_miner/README.md @@ -0,0 +1,73 @@ +# Template Method Pattern +Template Method is a behavioral design pattern that defines the skeleton of an algorithm in the +superclass but lets subclasses override specific steps of the algorithm without changing its +structure. + +Tutorial: [here](https://refactoring.guru/design-patterns/template-method). + +## Diagram: +![image](https://user-images.githubusercontent.com/8049534/174060908-402eea9b-a030-4e45-8e95-54cfcac61bb6.png) + +### Client code: +```dart +void main() { + print('HTMLMiner'); + HTMLMiner('google_workers.html').mine(); + + print('CsvMiner'); + CsvMiner('twitter_workers.csv').mine(); + + print('GuruMiner'); + ZipMiner('refactoring_guru_workers.zip').mine(); +} +``` + +### Output: +``` +HTMLMiner + Name Index +------------------------------- + Ayana White 33.92 + Dillan Ewing 6.21 + Kimora Arroyo 84.7 + Aspen Simmons 93.08 + Giana Perry 67.56 + Luz Carson 69.88 + Kaleb Pineda 47.87 + Kianna Bautista 3.09 + Jorge Reeves 16.52 + Leroy Morrow 51.24 + Ashlyn Crawford 7.25 + Yasmine Hampton 38.42 +------------------------------- + sum: 519.74 + len: 12 + +CsvMiner + Name Index +------------------------------- + Laylah Whitehead 22.1 + Karissa Arellano 2.0 + Maggie Mcmahon 12.25 + Ariel Bartlett 8.1 + Rashad Herman 5.8 + Rachael Hickman 5.22 + Tara Spence 9.48 + Kiera Cervantes 4.0 + Shane Robertson 9.0 + Perla Yoder 67.3 +------------------------------- + sum: 145.25 + len: 10 + +ZipMiner + Name Index +------------------------------- + Dmitry Zhart 62.4 + Alexander Shvets 44.6 + Fishchenko Ilya 1.4 +------------------------------- + sum: 108.4 + len: 3 +``` + diff --git a/patterns/template_method/data_miner/main.dart b/patterns/template_method/data_miner/main.dart new file mode 100644 index 0000000..879d0fa --- /dev/null +++ b/patterns/template_method/data_miner/main.dart @@ -0,0 +1,15 @@ +import 'miners/csv_miner.dart'; +import 'miners/zip_miner.dart'; +import 'miners/html_analyzer.dart'; + +void main() { + print('HTMLMiner'); + HTMLMiner('google_workers.html').mine(); + + print('CsvMiner'); + CsvMiner('twitter_workers.csv').mine(); + + print('GuruMiner'); + ZipMiner('refactoring_guru_workers.zip').mine(); +} + diff --git a/patterns/template_method/data_miner/miners/csv_miner.dart b/patterns/template_method/data_miner/miners/csv_miner.dart new file mode 100644 index 0000000..efb849c --- /dev/null +++ b/patterns/template_method/data_miner/miners/csv_miner.dart @@ -0,0 +1,11 @@ +import '../pattern/data_miner.dart'; +import '../utils/raw_data.dart'; + +class CsvMiner extends DataMiner { + CsvMiner(super.fileName); + + @override + StringTable parseData(RawData raw) { + return raw.split('\n').map((line) => line.split(',')).toList(); + } +} diff --git a/patterns/template_method/data_miner/miners/html_analyzer.dart b/patterns/template_method/data_miner/miners/html_analyzer.dart new file mode 100644 index 0000000..674e872 --- /dev/null +++ b/patterns/template_method/data_miner/miners/html_analyzer.dart @@ -0,0 +1,23 @@ +import '../pattern/data_miner.dart'; +import '../utils/raw_data.dart'; + +class HTMLMiner extends DataMiner { + HTMLMiner(super.fileName); + + @override + StringTable parseData(RawData raw) { + String regexString = r']+>(.+)<\/td>' + r'\s+' + r']+>(.+)<\/td>'; + final regExp = RegExp(regexString); + final matches = regExp.allMatches(raw); + + return [ + for (final employer in matches) + [ + employer.group(1)!, + employer.group(2)!, + ] + ]; + } +} diff --git a/patterns/template_method/data_miner/miners/zip_miner.dart b/patterns/template_method/data_miner/miners/zip_miner.dart new file mode 100644 index 0000000..b98d6b3 --- /dev/null +++ b/patterns/template_method/data_miner/miners/zip_miner.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import '../pattern/data_miner.dart'; +import '../utils/raw_data.dart'; + +class ZipMiner extends DataMiner { + ZipMiner(super.fileName); + + @override + RawData extractData(File file) { + final content = File('${reportPath}refactoring_guru_workers.zip') + .readAsBytesSync() + .toList(); + final unzip = GZipCodec().decode(content); + return String.fromCharCodes(unzip); + } + + @override + StringTable parseData(RawData raw) { + final lines = raw.split('\n'); + final divIndex = lines.length ~/ 2; + final names = lines.sublist(0, divIndex); + final indexes = lines.sublist(divIndex); + + return [ + for (var i = 0; i < divIndex; i++) + [ + names[i].trim(), + indexes[i].trim(), + ] + ]; + } +} diff --git a/patterns/template_method/data_miner/pattern/data_miner.dart b/patterns/template_method/data_miner/pattern/data_miner.dart new file mode 100644 index 0000000..1600972 --- /dev/null +++ b/patterns/template_method/data_miner/pattern/data_miner.dart @@ -0,0 +1,71 @@ +import 'dart:io'; + +import '../utils/analysis.dart'; +import '../utils/formatted_table.dart'; +import '../utils/raw_data.dart'; + +abstract class DataMiner { + final String fileName; + + DataMiner(this.fileName); + + void mine() { + final file = openFile(); + final raw = extractData(file); + final table = parseData(raw); + final analysis = analyzeData(table); + sendReport(analysis, table); + } + + File openFile() { + final file = File(reportPath + fileName); + + if (!file.existsSync()) { + throw 'File not exists.'; + } + + return file; + } + + RawData extractData(File file) { + return file.readAsStringSync(); + } + + StringTable parseData(RawData raw); + + Analysis analyzeData(StringTable table) { + return Analysis(table); + } + + void sendReport(Analysis analysis, StringTable table) { + final formattedTable = FormattedTable(); + + formattedTable + ..addRow('Name', 'Index') + ..addLine(); + + for (final row in table) { + final name = row[0]; + final index = double.parse(row[1]); + + formattedTable.addRow(name, index); + } + + formattedTable + ..addLine() + ..addRow('sum:', analysis.sum) + ..addRow('len:', analysis.len); + + print(formattedTable.toString()); + } + + String get reportPath => '${Platform.script.pathSegments.sublist( + 0, + Platform.script.pathSegments.length - 1, + ).join( + Platform.pathSeparator, + )}' + '${Platform.pathSeparator}' + 'reports' + '${Platform.pathSeparator}'; +} diff --git a/patterns/template_method/data_miner/reports/google_workers.html b/patterns/template_method/data_miner/reports/google_workers.html new file mode 100644 index 0000000..660be3a --- /dev/null +++ b/patterns/template_method/data_miner/reports/google_workers.html @@ -0,0 +1,102 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AB
+
1
+
Ayana White33.92
+
2
+
Dillan Ewing6.21
+
3
+
Kimora Arroyo84.7
+
4
+
Aspen Simmons93.08
+
5
+
Giana Perry67.56
+
6
+
Luz Carson69.88
+
7
+
Kaleb Pineda47.87
+
8
+
Kianna Bautista3.09
+
9
+
Jorge Reeves16.52
+
10
+
Leroy Morrow51.24
+
11
+
Ashlyn Crawford7.25
+
12
+
Yasmine Hampton38.42
+
diff --git a/patterns/template_method/data_miner/reports/refactoring_guru_workers.zip b/patterns/template_method/data_miner/reports/refactoring_guru_workers.zip new file mode 100644 index 0000000..9d5afd4 Binary files /dev/null and b/patterns/template_method/data_miner/reports/refactoring_guru_workers.zip differ diff --git a/patterns/template_method/data_miner/reports/twitter_workers.csv b/patterns/template_method/data_miner/reports/twitter_workers.csv new file mode 100644 index 0000000..29796d9 --- /dev/null +++ b/patterns/template_method/data_miner/reports/twitter_workers.csv @@ -0,0 +1,10 @@ +Laylah Whitehead,22.1 +Karissa Arellano,2 +Maggie Mcmahon,12.25 +Ariel Bartlett,8.1 +Rashad Herman,5.8 +Rachael Hickman,5.22 +Tara Spence,9.48 +Kiera Cervantes,4 +Shane Robertson,9 +Perla Yoder,67.3 \ No newline at end of file diff --git a/patterns/template_method/data_miner/utils/analysis.dart b/patterns/template_method/data_miner/utils/analysis.dart new file mode 100644 index 0000000..e8f4e81 --- /dev/null +++ b/patterns/template_method/data_miner/utils/analysis.dart @@ -0,0 +1,24 @@ +import 'raw_data.dart'; + +class Analysis { + Analysis(StringTable table) { + _analyze(table); + } + + void _analyze(StringTable table) { + int len = 0; + double sum = 0.0; + + for (final row in table) { + final index = double.parse(row[1]); + sum += index; + len++; + } + + this.len = len; + this.sum = sum; + } + + late final int len; + late final double sum; +} diff --git a/patterns/template_method/data_miner/utils/formatted_table.dart b/patterns/template_method/data_miner/utils/formatted_table.dart new file mode 100644 index 0000000..0d1461d --- /dev/null +++ b/patterns/template_method/data_miner/utils/formatted_table.dart @@ -0,0 +1,18 @@ +class FormattedTable { + void addRow(a, b) { + _buff.writeln( + '${' ' * 1}${a.padRight(20)}${b.toString().padLeft(7)}', + ); + } + + void addLine() { + _buff.writeln('-' * 31); + } + + @override + String toString() { + return _buff.toString(); + } + + final _buff = StringBuffer(); +} diff --git a/patterns/template_method/data_miner/utils/raw_data.dart b/patterns/template_method/data_miner/utils/raw_data.dart new file mode 100644 index 0000000..2143126 --- /dev/null +++ b/patterns/template_method/data_miner/utils/raw_data.dart @@ -0,0 +1,2 @@ +typedef RawData = String; +typedef StringTable = List>; diff --git a/patterns/visitor/conceptual/README.md b/patterns/visitor/conceptual/README.md new file mode 100644 index 0000000..fef872e --- /dev/null +++ b/patterns/visitor/conceptual/README.md @@ -0,0 +1,82 @@ +# Visitor pattern +Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which +they operate. + +Tutorial: [here](https://refactoring.guru/design-patterns/visitor). + +### About example. + +## Client code: +### Before: +```dart +void main() { + final list = createElements(); + list.forEach(operation1); +} + +Iterable createElements() { + return [ + One(), + Two(), + Three(), + ]; +} + +void operation1(Object obj) { + if (obj is One) { + print('operation1: one (param1 = ${obj.param1})'); + } else if (obj is Two) { + print('operation1: two (param2 = ${obj.param2})'); + } else if (obj is Three) { + print('operation1: two (param3 = ${obj.param3})'); + } +} +``` + +### After: +```dart +void main() { + final list = createElements(); + + for (final e in elements) { + e.accept(visitor); + } +} + +Iterable createElements() { + return [ + One(), + Two(), + Three(), + ]; +} + +class ConcreteVisitor1 implements Visitor { + @override + void visitOne(One one) { + print('operation1: one (param1 = ${one.param1})'); + } + + @override + void visitTwo(Two two) { + print('operation1: two (param2 = ${two.param2})'); + } + + @override + void visitThree(Three three) { + print('operation1: three (param3 = ${three.param3})'); + } +} +``` +#### Diagram: +![image](https://user-images.githubusercontent.com/8049534/174583542-5f57463c-148b-4113-acd3-2814ec017ecc.png) + +### Output: +``` +operation1: one (param1 = 1) +operation1: two (param2 = 2) +operation1: three (param3 = 3) +operation2: one (param1 = 1) +operation2: two (param2 = 2) +operation2: three (param3 = 3) +``` diff --git a/patterns/visitor/conceptual/after/elements/one.dart b/patterns/visitor/conceptual/after/elements/one.dart new file mode 100644 index 0000000..3d174a6 --- /dev/null +++ b/patterns/visitor/conceptual/after/elements/one.dart @@ -0,0 +1,11 @@ +import '../pattern/element.dart'; +import '../pattern/visitor.dart'; + +class One implements Element { + final String param1 = '1'; + + @override + void accept(Visitor visitor) { + visitor.visitOne(this); + } +} diff --git a/patterns/visitor/conceptual/after/elements/three.dart b/patterns/visitor/conceptual/after/elements/three.dart new file mode 100644 index 0000000..28a8559 --- /dev/null +++ b/patterns/visitor/conceptual/after/elements/three.dart @@ -0,0 +1,11 @@ +import '../pattern/element.dart'; +import '../pattern/visitor.dart'; + +class Three implements Element { + final String param3 = '3'; + + @override + void accept(Visitor visitor) { + visitor.visitThree(this); + } +} diff --git a/patterns/visitor/conceptual/after/elements/two.dart b/patterns/visitor/conceptual/after/elements/two.dart new file mode 100644 index 0000000..9937c09 --- /dev/null +++ b/patterns/visitor/conceptual/after/elements/two.dart @@ -0,0 +1,11 @@ +import '../pattern/element.dart'; +import '../pattern/visitor.dart'; + +class Two implements Element { + final String param2 = '2'; + + @override + void accept(Visitor visitor) { + visitor.visitTwo(this); + } +} diff --git a/patterns/visitor/conceptual/after/main.dart b/patterns/visitor/conceptual/after/main.dart new file mode 100644 index 0000000..3bb0215 --- /dev/null +++ b/patterns/visitor/conceptual/after/main.dart @@ -0,0 +1,27 @@ +import 'elements/one.dart'; +import 'elements/three.dart'; +import 'elements/two.dart'; +import 'operations/concrete_visitor1.dart'; +import 'operations/concrete_visitor2.dart'; +import 'pattern/element.dart'; +import 'pattern/visitor.dart'; + +void main() { + final list = createElements(); + operation(list, ConcreteVisitor1()); + operation(list, ConcreteVisitor2()); +} + +Iterable createElements() { + return [ + One(), + Two(), + Three(), + ]; +} + +void operation(Iterable elements, Visitor visitor) { + for (final e in elements) { + e.accept(visitor); + } +} diff --git a/patterns/visitor/conceptual/after/operations/concrete_visitor1.dart b/patterns/visitor/conceptual/after/operations/concrete_visitor1.dart new file mode 100644 index 0000000..3784140 --- /dev/null +++ b/patterns/visitor/conceptual/after/operations/concrete_visitor1.dart @@ -0,0 +1,21 @@ +import '../elements/one.dart'; +import '../elements/three.dart'; +import '../elements/two.dart'; +import '../pattern/visitor.dart'; + +class ConcreteVisitor1 implements Visitor { + @override + void visitOne(One one) { + print('operation1: one (param1 = ${one.param1})'); + } + + @override + void visitTwo(Two two) { + print('operation1: two (param2 = ${two.param2})'); + } + + @override + void visitThree(Three three) { + print('operation1: three (param3 = ${three.param3})'); + } +} diff --git a/patterns/visitor/conceptual/after/operations/concrete_visitor2.dart b/patterns/visitor/conceptual/after/operations/concrete_visitor2.dart new file mode 100644 index 0000000..0dec8d3 --- /dev/null +++ b/patterns/visitor/conceptual/after/operations/concrete_visitor2.dart @@ -0,0 +1,21 @@ +import '../elements/one.dart'; +import '../elements/three.dart'; +import '../elements/two.dart'; +import '../pattern/visitor.dart'; + +class ConcreteVisitor2 implements Visitor { + @override + void visitOne(One one) { + print('operation2: one (param1 = ${one.param1})'); + } + + @override + void visitTwo(Two two) { + print('operation2: two (param2 = ${two.param2})'); + } + + @override + void visitThree(Three three) { + print('operation2: three (param3 = ${three.param3})'); + } +} diff --git a/patterns/visitor/conceptual/after/pattern/element.dart b/patterns/visitor/conceptual/after/pattern/element.dart new file mode 100644 index 0000000..5a7026d --- /dev/null +++ b/patterns/visitor/conceptual/after/pattern/element.dart @@ -0,0 +1,5 @@ +import 'visitor.dart'; + +abstract class Element { + void accept(Visitor visitor); +} diff --git a/patterns/visitor/conceptual/after/pattern/visitor.dart b/patterns/visitor/conceptual/after/pattern/visitor.dart new file mode 100644 index 0000000..0556fca --- /dev/null +++ b/patterns/visitor/conceptual/after/pattern/visitor.dart @@ -0,0 +1,11 @@ +import '../elements/one.dart'; +import '../elements/three.dart'; +import '../elements/two.dart'; + +abstract class Visitor { + void visitOne(One one); + + void visitTwo(Two two); + + void visitThree(Three three); +} diff --git a/patterns/visitor/conceptual/before/elements/elements.dart b/patterns/visitor/conceptual/before/elements/elements.dart new file mode 100644 index 0000000..5e844c9 --- /dev/null +++ b/patterns/visitor/conceptual/before/elements/elements.dart @@ -0,0 +1,11 @@ +class One { + final String param1 = '1'; +} + +class Two { + final String param2 = '2'; +} + +class Three { + final String param3 = '3'; +} diff --git a/patterns/visitor/conceptual/before/main.dart b/patterns/visitor/conceptual/before/main.dart new file mode 100644 index 0000000..a75fdd6 --- /dev/null +++ b/patterns/visitor/conceptual/before/main.dart @@ -0,0 +1,36 @@ +import 'elements/elements.dart'; + +void main() { + final list = createElements(); + + list.forEach(operation1); + list.forEach(operation2); +} + +Iterable createElements() { + return [ + One(), + Two(), + Three(), + ]; +} + +void operation1(Object obj) { + if (obj is One) { + print('operation1: one (param1 = ${obj.param1})'); + } else if (obj is Two) { + print('operation1: two (param2 = ${obj.param2})'); + } else if (obj is Three) { + print('operation1: two (param3 = ${obj.param3})'); + } +} + +void operation2(Object obj) { + if (obj is One) { + print('operation2: one (param1 = ${obj.param1})'); + } else if (obj is Two) { + print('operation2: two (param2 = ${obj.param2})'); + } else if (obj is Three) { + print('operation2: two (param3 = ${obj.param3})'); + } +} diff --git a/patterns/visitor/shapes_exporter/README.md b/patterns/visitor/shapes_exporter/README.md new file mode 100644 index 0000000..11ebdc4 --- /dev/null +++ b/patterns/visitor/shapes_exporter/README.md @@ -0,0 +1,78 @@ +# Visitor pattern +Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which +they operate. + +Tutorial: [here](https://refactoring.guru/design-patterns/visitor). + +### About example: Shape XML Exporter. +In this example, the Visitor pattern adds XML export support to the class hierarchy of geometric +shapes [More info](https://refactoring.guru/design-patterns/visitor#pseudocode). + +This example rewrite from original source code [java example](https://github.com/RefactoringGuru/design-patterns-java/tree/main/src/refactoring_guru/visitor/example) + +### Diagram: +![image](https://user-images.githubusercontent.com/8049534/167304227-04237030-879e-4d7f-be32-4c815a3e1cbf.png) + +### Client code: +```dart + final compoundShape = CompoundShape( + x: 30, + y: 45, + children: [ + Rectangle(x: 10, y: 10, width: 100, height: 100), + Circle(xCenter: 300, yCenter: 20, radius: 35), + Dot(x: 60, y: 60), + CompoundShape( + x: 5, + y: 5, + children: [ + Dot(x: 15, y: 15), + Dot(x: 20, y: 20), + ], + ), + ], + ); + + final xml = XMLExportVisitor().export(compoundShape); + print(xml); +``` + +### Output: +```xml + + + 30 + 45 + + + 10 + 10 + 100 + 100 + + + 300 + 20 + 35 + + + 60 + 60 + + + 5 + 5 + + + 15 + 15 + + + 20 + 20 + + + + + +``` diff --git a/patterns/visitor/shapes_exporter/main.dart b/patterns/visitor/shapes_exporter/main.dart new file mode 100644 index 0000000..bf0c2df --- /dev/null +++ b/patterns/visitor/shapes_exporter/main.dart @@ -0,0 +1,28 @@ +import 'shapes/circle.dart'; +import 'shapes/compound_shape.dart'; +import 'shapes/dot.dart'; +import 'shapes/rectangle.dart'; +import 'visitor/xml_export_visitor.dart'; + +void main() { + final compoundShape = CompoundShape( + x: 30, + y: 45, + children: [ + Rectangle(x: 10, y: 10, width: 100, height: 100), + Circle(xCenter: 300, yCenter: 20, radius: 35), + Dot(x: 60, y: 60), + CompoundShape( + x: 5, + y: 5, + children: [ + Dot(x: 15, y: 15), + Dot(x: 20, y: 20), + ], + ), + ], + ); + + final xml = XMLExportVisitor().export(compoundShape); + print(xml); +} diff --git a/patterns/visitor/shapes_exporter/shapes/circle.dart b/patterns/visitor/shapes_exporter/shapes/circle.dart new file mode 100644 index 0000000..739c99e --- /dev/null +++ b/patterns/visitor/shapes_exporter/shapes/circle.dart @@ -0,0 +1,24 @@ +import '../visitor/visitor.dart'; +import 'shape.dart'; + +class Circle implements Shape { + final int xCenter; + final int yCenter; + final int radius; + + Circle({ + required this.xCenter, + required this.yCenter, + required this.radius, + }); + + @override + void accept(Visitor visitor) { + visitor.visitCircle(this); + } + + @override + void draw() { + // ... + } +} diff --git a/patterns/visitor/shapes_exporter/shapes/compound_shape.dart b/patterns/visitor/shapes_exporter/shapes/compound_shape.dart new file mode 100644 index 0000000..d35338e --- /dev/null +++ b/patterns/visitor/shapes_exporter/shapes/compound_shape.dart @@ -0,0 +1,24 @@ +import '../visitor/visitor.dart'; +import 'shape.dart'; + +class CompoundShape implements Shape { + final int x; + final int y; + final List children; + + CompoundShape({ + required this.x, + required this.y, + required this.children, + }); + + @override + void accept(Visitor visitor) { + visitor.visitCompoundShape(this); + } + + @override + void draw() { + // ... + } +} diff --git a/patterns/visitor/shapes_exporter/shapes/dot.dart b/patterns/visitor/shapes_exporter/shapes/dot.dart new file mode 100644 index 0000000..5d5811a --- /dev/null +++ b/patterns/visitor/shapes_exporter/shapes/dot.dart @@ -0,0 +1,22 @@ +import '../visitor/visitor.dart'; +import 'shape.dart'; + +class Dot implements Shape { + final int x; + final int y; + + Dot({ + required this.x, + required this.y, + }); + + @override + void accept(Visitor visitor) { + visitor.visitDot(this); + } + + @override + void draw() { + // ... + } +} diff --git a/patterns/visitor/shapes_exporter/shapes/rectangle.dart b/patterns/visitor/shapes_exporter/shapes/rectangle.dart new file mode 100644 index 0000000..bd0bb4e --- /dev/null +++ b/patterns/visitor/shapes_exporter/shapes/rectangle.dart @@ -0,0 +1,26 @@ +import '../visitor/visitor.dart'; +import 'shape.dart'; + +class Rectangle implements Shape { + final int x; + final int y; + final int width; + final int height; + + Rectangle({ + required this.x, + required this.y, + required this.width, + required this.height, + }); + + @override + void accept(Visitor visitor) { + visitor.visitRectangle(this); + } + + @override + void draw() { + // ... + } +} diff --git a/patterns/visitor/shapes_exporter/shapes/shape.dart b/patterns/visitor/shapes_exporter/shapes/shape.dart new file mode 100644 index 0000000..aecdf09 --- /dev/null +++ b/patterns/visitor/shapes_exporter/shapes/shape.dart @@ -0,0 +1,7 @@ +import '../visitor/visitor.dart'; + +abstract class Shape { + void accept(Visitor visitor); + + void draw(); +} diff --git a/patterns/visitor/shapes_exporter/visitor/visitor.dart b/patterns/visitor/shapes_exporter/visitor/visitor.dart new file mode 100644 index 0000000..3d6ba3b --- /dev/null +++ b/patterns/visitor/shapes_exporter/visitor/visitor.dart @@ -0,0 +1,14 @@ +import '../shapes/circle.dart'; +import '../shapes/compound_shape.dart'; +import '../shapes/dot.dart'; +import '../shapes/rectangle.dart'; + +abstract class Visitor { + void visitCompoundShape(CompoundShape compound); + + void visitDot(Dot dot); + + void visitCircle(Circle circle); + + void visitRectangle(Rectangle rectangle); +} diff --git a/patterns/visitor/shapes_exporter/visitor/xml_export_visitor.dart b/patterns/visitor/shapes_exporter/visitor/xml_export_visitor.dart new file mode 100644 index 0000000..be6acd3 --- /dev/null +++ b/patterns/visitor/shapes_exporter/visitor/xml_export_visitor.dart @@ -0,0 +1,73 @@ +import '../shapes/compound_shape.dart'; +import '../shapes/dot.dart'; +import '../shapes/rectangle.dart'; +import '../shapes/shape.dart'; +import 'visitor.dart'; + +class XMLExportVisitor implements Visitor { + final buff = StringBuffer(); + + String export(Shape shape) { + buff.clear(); + _write('?'); + shape.accept(this); + return buff.toString().trim(); + } + + @override + void visitCompoundShape(CompoundShape compound) { + _write('', openTag: true); + _write('${compound.x}'); + _write('${compound.y}'); + _write('', openTag: true); + + for (final shape in compound.children) { + shape.accept(this); + } + + _write('', closeTag: true); + _write('', closeTag: true); + } + + @override + void visitCircle(circle) { + _write('', openTag: true); + _write('${circle.xCenter}'); + _write('${circle.yCenter}'); + _write('${circle.radius}'); + _write('', closeTag: true); + } + + @override + void visitDot(Dot dot) { + _write('', openTag: true); + _write('${dot.x}'); + _write('${dot.y}'); + _write('', closeTag: true); + } + + @override + void visitRectangle(Rectangle rectangle) { + _write('', openTag: true); + _write('${rectangle.x}'); + _write('${rectangle.y}'); + _write('${rectangle.width}'); + _write('${rectangle.height}'); + _write('', closeTag: true); + } + + int _openTags = 0; + + void _write(String str, {openTag = false, closeTag = false}) { + if (closeTag) { + _openTags--; + } + + final tab = ' ' * _openTags; + buff.writeln(tab + str); + + if (openTag) { + _openTags++; + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..8703754 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,22 @@ +name: design_patterns_dart +description: Dart examples for all classic GoF design patterns. +version: 0.38.0 +homepage: https://refactoring.guru/design-patterns +repository: https://github.com/RefactoringGuru/design-patterns-dart +issue_tracker: https://github.com/RefactoringGuru/design-patterns-dart/issue + +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + collection: ^1.15.0 + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + material_design_icons_flutter: ^5.0.6595 + +dev_dependencies: + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..a8339d0 --- /dev/null +++ b/web/index.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + flutter_launcher + + + + + + + + + + + diff --git a/web/logo-screen-ukraine.png b/web/logo-screen-ukraine.png new file mode 100644 index 0000000..5f7bebc Binary files /dev/null and b/web/logo-screen-ukraine.png differ diff --git a/web/logo-screen.png b/web/logo-screen.png new file mode 100644 index 0000000..d5c9c2f Binary files /dev/null and b/web/logo-screen.png differ diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..7bab0e5 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "flutter_launcher", + "short_name": "flutter_launcher", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}