хорошо, что касается как , я бы указал вам на мои 5 заповедей . :)
Для этого вопроса действительно важны только 3:
- одиночная ответственность (SRP)
- интерфейс разделения (ISP)
- инверсия зависимостей (DIP)
Начиная с SRP , вы должны задать себе вопрос: "Какова ответственность класса X?".
Экран входа в систему отвечает за предоставление пользователю интерфейса для заполнения и отправки его данных для входа. Таким образом
- имеет смысл зависеть от класса кнопки, потому что ей нужна кнопка.
- не имеет смысла, он делает все сети и т. Д.
Прежде всего, давайте рассмотрим сервис входа в систему:
interface ILoginService {
function login(user:String, pwd:String, onDone:LoginResult->Void):Void;
//Rather than using signalers and what-not, I'll just rely on haXe's support for functional style,
//which renders these cumbersome idioms from more classic languages quite obsolete.
}
enum Result<T> {//this is a generic enum to return results from basically any kind of actions, that may fail
Fail(error:Int, reason:String);
Success(user:T);
}
typedef LoginResult = Result<IUser>;//IUser basically represent an authenticated user
С точки зрения основного класса экран входа выглядит так:
interface ILoginInterface {
function show(inputHandler:String->String->Void):Void;
function hide():Void;
function error(reason:String):Void;
}
выполнение входа в систему:
var server:ILoginService = ... //where ever it comes from. I will say a word about that later
var login:ILoginInterface = ... //same thing as with the service
login.show(function (user, pwd):Void {
server.login(user, pwd, function (result) {
switch (result) {
case Fail(_, reason):
login.error(reason);
case Success(user):
login.hide();
//proceed with the resulting user
}
});
});//for the sake of conciseness I used an anonymous function but usually, you'd put a method here of course
Теперь ILoginService
выглядит немного дурно. Но, если честно, он делает все, что нужно. Теперь он может быть эффективно реализован классом Server
, который инкапсулирует все сети в одном классе, имея метод для каждого из N вызовов, которые обеспечивает ваш реальный сервер, но прежде всего ISP предполагает, что многие клиентские интерфейсы лучше, чем один универсальный интерфейс . По той же причине ILoginInterface
действительно сведено к минимуму.
Независимо от того, как эти два действительно реализованы, вам не нужно менять Main
(если, конечно, интерфейс не изменяется). Это DIP применяется. Main
не зависит от конкретной реализации, только от очень краткой абстракции.
Теперь давайте представим несколько реализаций:
class LoginScreen implements ILoginInterface {
public function show(inputHandler:String->String->Void):Void {
//render the UI on the screen
//wait for the button to be clicked
//when done, call inputHandler with the input values from the respective fields
}
public function hide():Void {
//hide UI
}
public function error(reason:String):Void {
//display error message
}
public static function getInstance():LoginScreen {
//classical singleton instantiation
}
}
class Server implements ILoginService {
function new(host:String, port:Int) {
//init connection here for example
}
public static function getInstance():Server {
//classical singleton instantiation
}
public function login(user:String, pwd:String, onDone:LoginResult->Void) {
//issue login over the connection
//invoke the handler with the retrieved result
}
//... possibly other methods here, that are used by other classes
}
Хорошо, это было довольно просто, я полагаю. Но просто для удовольствия, давайте сделаем что-то действительно идиотское:
class MailLogin implements ILoginInterface {
public function new(mail:String) {
//save address
}
public function show(inputHandler:String->String->Void):Void {
//print some sort of "waiting for authentication"-notification on screen
//send an email to the given address: "please respond with username:password"
//keep polling you mail server for a response, parse it and invoke the input handler
}
public function hide():Void {
//remove the "waiting for authentication"-notification
//send an email to the given address: "login successful"
}
public function error(reason:String):Void {
//send an email to the given address: "login failed. reason: [reason] please retry."
}
}
Каким бы ни был этот пешеход, с точки зрения основного класса,
это ничего не меняет и, следовательно, будет работать так же хорошо.
Более вероятным сценарием является то, что ваша служба входа в систему находится на другом сервере (возможно, HTTP-сервере), который выполняет аутентификацию и в случае успеха создает сеанс на реальном сервере приложений. В плане дизайна это можно отразить в двух отдельных классах.
Теперь давайте поговорим о "...", который я оставил в Main. Ну, я ленивый, поэтому я могу сказать вам, что в моем коде вы, вероятно, увидите
var server:ILoginService = Server.getInstance();
var login:ILoginInterface = LoginScreen.getInstance();
Конечно, это далеко не чистый способ сделать это. По правде говоря, это самый простой способ, и зависимость ограничена одним вхождением, которое впоследствии можно удалить с помощью внедрения зависимостей .
Так же, как простой пример для IoC -контейнера в haXe:
class Injector {
static var providers = new Hash < Void->Dynamic > ;
public static function setProvider<T>(type:Class<T>, provider:Void->T):Void {
var name = Type.getClassName(type);
if (providers.exists(name))
throw "duplicate provider for " + name;
else
providers.set(name, provider);
}
public static function get<T>(type:Class<T>):T {
var name = Type.getClassName(type);
return
if (providers.exists(name))
providers.get(name);
else
throw "no provider for " + name;
}
}
элегантное использование (с ключевым словом using
):
using Injector;
//wherever you would like to wire it up:
ILoginService.setProvider(Server.getInstance);
ILoginInterface.setProvider(LoginScreen.getInstance);
//and in Main:
var server = ILoginService.get();
var login = ILoginInterface.get();
Таким образом, у вас практически нет связи между отдельными классами.
Что касается вопроса о том, как передавать события между кнопкой и экраном входа в систему:
это всего лишь вопрос вкуса и реализации.
Смысл программирования, управляемого событиями, заключается в том, что источник и наблюдатель связаны только в том смысле,
что источник должен отправлять какое-то уведомление, и цель должна быть в состоянии обработать его.
someButton.onClick = handler;
в основном делает именно это, но это так элегантно и лаконично, что вы не суетитесь в этом.
someButton.onClick(handler);
, вероятно, немного лучше, поскольку вы можете иметь несколько обработчиков, хотя это редко требуется для компонентов пользовательского интерфейса. Но, в конце концов, если вы хотите, чтобы сигнализаторы, пошли с сигнализаторами.
Теперь, когда дело доходит до АОП, это не правильный подход в этой ситуации. Это не умный хак для соединения компонентов друг с другом, но для решения сквозных проблем , таких как добавление журнала, истории или даже вещей в качестве слоя персистентности во множестве модулей.
В общем, старайтесь не модулировать и не разбивать мелкие части вашего приложения.Это нормально иметь спагетти в кодовой базе, пока
- сегменты спагетти хорошо инкапсулированы
- сегменты спагетти достаточно малы, чтобы их можно было понять или иным образом реорганизовать / переписать в течение разумного промежутка времени, не ломая приложение (что должно гарантироваться пунктом 1)
Попробуйте скорее разбить все приложение на автономные части, которые взаимодействуют через лаконичные интерфейсы. Если деталь становится слишком большой, выполните ее рефакторинг точно так же.
редактирование:
В ответ на вопросы Тома:
- это вопрос вкуса. в некоторых средах люди заходят так далеко, что используют внешние файлы конфигурации, но это не имеет особого смысла в haXe, поскольку вам нужно указать компилятору принудительно компилировать зависимости, которые вы вводите во время выполнения. Настройка зависимости в вашем коде, в центральном файле, так же трудоемка и намного проще. Для большей структуры вы можете разделить приложение на «модули», каждый из которых имеет класс загрузчика, отвечающий за регистрацию реализаций, которые он предоставляет. В ваш основной файл вы загружаете модули.
- Это зависит. Я склонен объявлять их в пакете класса в зависимости от них, а затем реорганизовывать их в дополнительный пакет на случай, если они окажутся необходимыми в другом месте. Используя анонимные типы, вы также можете полностью отделить вещи, но у вас будет небольшое снижение производительности на платформах, таких как flash9.
- Я бы не абстрагировал кнопку, а затем внедрил бы реализацию через IoC, но не стесняйтесь. Я бы точно создал его, потому что в конце концов, это просто кнопка. Он имеет стиль, заголовок, положение и размер экрана и запускает события клика. Я думаю, что это ненужная модуляризация, как указано выше.
- Придерживайтесь SRP. Если вы это сделаете, ни один класс не станет излишне большим. Роль класса Main заключается в инициализации приложения. Когда это сделано, он должен передать управление контроллеру входа в систему, а когда этот контроллер получает объект пользователя, он может передать его главному контроллеру фактического приложения и так далее. Я предлагаю вам прочитать немного о шаблонах поведения , чтобы получить некоторые идеи.
Greetz
back2dos